Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

One step signature and verification #552

Merged
merged 5 commits into from
Jun 25, 2024

Conversation

timothee-haudebourg
Copy link
Contributor

@timothee-haudebourg timothee-haudebourg commented Jun 25, 2024

This PR is about simplifying how ssi process a set of claims for signature and/or verification.

How it works currently

Currently signature and verification are both done in two steps.

Signature

  • Start from the unsecured claims of type T and use a security-specific function to build a Verifiable<T, P> where P is a proof type. For JWS the proof type is ssi_jws::Signature. For Data-Integrity the proof type is ssi_data_integrity::Proof<S> where S is a cryptosuite type (S: CryptographicSuite). The Data-Integrity-specific signature function is CryptographicSuite::sign.
  • Use Verifiable::unprepare to discard byproducts of the signature, what we call the proof "preparation". This will merge the claims and proof into a single type determined by <P as MergeProof>::Merged. In the case of JWS this type is DecodedJWS<T>. In the case of Data-Integrity proofs this type is DataIntegrity<T, S>.
        ┌───┐                                                                                             
        │   │                                                                                             
        │ T │                                                                                             
        │   │                                                                                             
        └─┬─┘              ┌─────────────────────────┐                             ┌─────────────────────┐
          │                │                         │                             │                     │
          ├───► S::sign ─► │ Verifiable<T, Proof<S>> │ ─► Verifiable::unprepare ─► │ DataIntegrity<T, S> │
          │                │                         │                             │                     │
┌─────────┴──────────────┐ └─────────────────────────┘                             └─────────────────────┘
│                        │                                                                                
│ S: CryptographicSuite  │                                                                                
│                        │                                                                                
└────────────────────────┘                                                                                

Here is how it looks from the user point of view:

cryptosuite.sign(
  credential,
  context,
  &vm_resolver,
  &signer,
  proof_options
).await.unwrap().unprepare();

Here the context holds all the parameters required by the proof preparation algorithm.

Verification

  • Start from a secured claim type, implementing VerifiableClaims. This can be DecodedJWS<T> or DataIntegrity<T, S>.
  • Use VerifiableClaims::into_verifiable to create a Verifiable<T, P> type "preparing" the proof.
  • Use Verifiable::verify to verify the proof.
┌─────────────────────┐                                         ┌─────────────────────────┐                          ┌────────┐
│                     │                                         │                         │                          │        │
│ DataIntegrity<T, S> │ ─► VerifiableClaims::into_verifiable ─► │ Verifiable<T, Proof<S>> │ ─► Verifiable::verify ─► │ Result │
│                     │                                         │                         │                          │        │
└─────────────────────┘                                         └─────────────────────────┘                          └────────┘

Here is how it looks from the user point of view:

vc.into_verifiable_with(context).await.unwrap().verify(&vm_resolver).await.unwrap()

Once again, context here holds all the parameters required by the proof preparation algorithm (although in this case we provide good defaults by using into_verifiable() instead of into_verifiable_with(context)).

The Verifiable type

The Verifiable<T, P> type is here to hold the "prepared" proof. The preparation holds all the intermediate data common to signature and verification. For instance for Data-Integrity suites using Linked-Data, preparing the proof will mean doing the JSON-LD expansion and RDF canonicalization, required by both signature and verification.

In theory this has a number of advantages:

  • No code duplication for common stages
  • For Linked-Data processing, we currently need to pass a Linked-Data environment to the proof preparation (an RDF vocabulary and interpretation). With the proof preparation mechanism we can do that as early as possible and never care about it later.
  • Increase performance: we often sign then verify claims, in which case proof preparation allows us to do the common work once (which can be very expensive in the case of Linked-Data) and use it for both signature and verification.

The issue

After using this design for a while, I've found a lot of issue that I think are not worth the benefits:

  • Wrong assumptions: In the Data-Integrity spec, there is this notion of transformation and hashing algorithm that are common to signature and verification (this is where LD canonicalization occurs for instance). These algorithms are defined by all cryptosuites and are supposed to be unaware of if they are called for signature or verification. In theory we should be able to run those algorithms during proof preparation, but in practice cryptosuite specifications are more or less breaking this isolation principle by assuming access to a lot of parameters proper to signature and/or verification, so the abstraction is hard to maintain.
  • Annoying to use: Dealing with the Verifiable type and having to use into_verifiable is annoying. What if you have a Verifiable instance and want to change the claims or the proof? Then you have to call unprepare and into_verifiable again. There is an helper method called temper, but it's still annoying.
  • Lots of types, traits: Verifiable, Proof, PrepareProof, ExtractProof, MergeProof, etc. that's a lot we could get rid of.
  • Lots of bounds: this one is not really the current implementation's fault, but an issue inherited from the json-ld crate. Data-Integrity cryptosuites need to use the JSON-LD expansion algorithm, which requires a JSON-LD loader that we pass to the proof preparation environment using the context variable that should provide a loader. The issue is, the Loader trait expects a type parameter leaking the RDF vocabulary type used by the application. Which means the cryptosuite implementation cannot use any RDF vocabulary it wants, but one provided by the user so it is compatible with the loader. So now instead of just the loader, the context needs to provide an RDF vocabulary and interpretation with a lot of trait bounds to satisfy the requirements of the cryptosuite. Here is how this hell looks like (C is the environment type, and T the claim type):
C: AnyJsonLdEnvironment<Vocabulary = V, Interpretation = I, Loader = L> + eip712::TypesProvider,
V: VocabularyMut,
V::Iri: Clone + Eq + Hash + LinkedDataSubject<I, V> + LinkedDataResource<I, V>,
V::BlankId: Clone + Eq + Hash + LinkedDataSubject<I, V> + LinkedDataResource<I, V>,
I: InterpretationMut<V> + ReverseTermInterpretation<Iri = V::Iri, BlankId = V::BlankId, Literal = V::Literal>,
I::Resource: Clone,
L: json_ld::Loader<V::Iri>, // if this trait had no type parameter, we could get rid of `V` and `I`.
L::Error: std::fmt::Display,
T: Serialize + Expandable<C> + JsonLdNodeObject,
T::Expanded: LinkedData<I, V>

Solution

The solution implemented by this PR is as follows:

  • I've released a new version of json-ld that removes the type parameter on the Loader trait. This allowed me to reduce the number of bounds above to
C: ContextLoaderEnvironment + Eip712TypesEnvironment,
T: Serialize + Expandable + JsonLdNodeObject,
  • I've remove the Verifier type and all the traits related to proof preparation.
    • For the signature part, this means that the sign function directly produces a type implementing VerifiableClaims such as DecodedJWS<T> or DataIntegrity<T, S>:
            ┌───┐                                     
            │   │                                     
            │ T │                                     
            │   │                                     
            └─┬─┘              ┌─────────────────────┐
              │                │                     │
              ├───► S::sign ─► │ DataIntegrity<T, S> │
              │                │                     │
    ┌─────────┴──────────────┐ └─────────────────────┘
    │                        │                        
    │ S: CryptographicSuite  │                        
    │                        │                        
    └────────────────────────┘                        
    
    For the user it looks like this:
    cryptosuite.sign( // NOTE: the `context` is optional now as we can provide a good default.
      credential,
      &vm_resolver,
      &signer,
      proof_options
    ).await.unwrap();
    • For the verification part, this means the verify method is now directly provided by the VerifiableClaims trait:
    ┌─────────────────────┐                                ┌────────┐
    │                     │                                │        │
    │ DataIntegrity<T, S> │ ─► VerifiableClaims::verify ─► │ Result │
    │                     │                                │        │
    └─────────────────────┘                                └────────┘
    
    For the user it looks like this:
    vc.verify(&vm_resolver).await.unwrap()

@timothee-haudebourg timothee-haudebourg changed the title Direct signature and verification One step signature and verification Jun 25, 2024
@timothee-haudebourg timothee-haudebourg marked this pull request as ready for review June 25, 2024 10:39
Copy link
Member

@sbihel sbihel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you push again to trigger the CI fully? Some jobs are skipped because it was a draft PR.

I didn't review in full details, but we discussed it and I'm fully in favour of simplifying the API.

A couple of question:

  • is CryptographicSuite the correct term? I know "cryptosuite" is often used in specs, should we use that instead?
  • how does one provide the context loader now? Is it as part of the VM/DID resolver?

@timothee-haudebourg
Copy link
Contributor Author

timothee-haudebourg commented Jun 25, 2024

is CryptographicSuite the correct term? I know "cryptosuite" is often used in specs, should we use that instead?

I often ask myself the same question. "Cryptographic Suite" is clearly the official name as you can see here, but "cryptosuite" is used everywhere as a shorthand, so much that its starting to feel like the de facto name. I don't have a strong opinion on which one we should use. My only argument is that my spell checker doesn't recognize "cryptosuite".

how does one provide the context loader now? Is it as part of the VM/DID resolver?

Instead of CruptographicSuite::sign you can use sign_with to provide a custom environment. The environment can be any type that provides a JSON-LD loader. The default type is SignatureEnvironment that you could reuse with a custom loader.

@timothee-haudebourg
Copy link
Contributor Author

I used the opportunity to remove the todo!() in ssi-status.

@timothee-haudebourg timothee-haudebourg merged commit b9e988d into main Jun 25, 2024
4 checks passed
@timothee-haudebourg timothee-haudebourg deleted the direct-signature-and-verification branch June 25, 2024 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants