From 22a7acb33a0bd11601f96d02df5a01a27d50f4c5 Mon Sep 17 00:00:00 2001 From: gabe Date: Tue, 15 Aug 2023 17:48:37 -0700 Subject: [PATCH 1/2] cred manifest/application example --- doc/howto/credential_manifest_test.go | 232 ++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 doc/howto/credential_manifest_test.go diff --git a/doc/howto/credential_manifest_test.go b/doc/howto/credential_manifest_test.go new file mode 100644 index 000000000..8448313e0 --- /dev/null +++ b/doc/howto/credential_manifest_test.go @@ -0,0 +1,232 @@ +package howto + +import ( + "context" + gocrypto "crypto" + "fmt" + "testing" + "time" + + "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/exchange" + "github.com/TBD54566975/ssi-sdk/credential/integrity" + "github.com/TBD54566975/ssi-sdk/credential/manifest" + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/crypto/jwx" + "github.com/TBD54566975/ssi-sdk/did/key" + "github.com/goccy/go-json" + "github.com/lestrrat-go/jwx/jwt" + "github.com/stretchr/testify/require" +) + +func TestCredentialApplication(t *testing.T) { + // create an issuer DID for the manifest + issuerPrivKey, issuerDID, err := key.GenerateDIDKey(crypto.Ed25519) + require.NoError(t, err) + + // Get a Credential Manifest that we're going to apply to + manifestJWT := CreateCredentialManifest(t, issuerPrivKey, *issuerDID) + + // create a holder DID for the application + holderPrivKey, holderDID, err := key.GenerateDIDKey(crypto.Ed25519) + require.NoError(t, err) + + // self sign a credential with a first name to respond to the manifest with + credJWT := CreateNameCredential(t, holderPrivKey, *holderDID) + + // create a credential application against the manifest using the credential + credAppJWT := CreateCredentialApplication(t, holderPrivKey, *holderDID, manifestJWT, credJWT) + fmt.Printf("Credential Application JWT: %s\n", string(credAppJWT)) + + // submit and process the credential application to get a credential response + success := ProcessCredentialApplication(t, manifestJWT, credAppJWT) + require.True(t, success) +} + +func ProcessCredentialApplication(t *testing.T, manifestJWT, credAppJWT []byte) bool { + // TODO we won't actually issue a credential here, but you can see what validation looks like + + // decode the manifest + manifestToken, err := jwt.Parse(manifestJWT) + require.NoError(t, err) + + var m manifest.CredentialManifest + manifestClaim, ok := manifestToken.Get("credential_manifest") + if !ok { + t.Fatal("credential_manifest claim not found") + } + manifestClaimBytes, err := json.Marshal(manifestClaim) + require.NoError(t, err) + err = json.Unmarshal(manifestClaimBytes, &m) + require.NoError(t, err) + + // decode the application to JSON + token, err := jwt.Parse(credAppJWT) + require.NoError(t, err) + tokenMap, err := token.AsMap(context.Background()) + require.NoError(t, err) + + _, err = manifest.IsValidCredentialApplicationForManifest(m, tokenMap) + return err == nil +} + +func CreateCredentialApplication(t *testing.T, privKey gocrypto.PrivateKey, holderDID key.DIDKey, manifestJWT, credJWT []byte) []byte { + // decode the manifest + manifestToken, err := jwt.Parse(manifestJWT) + require.NoError(t, err) + + var m manifest.CredentialManifest + manifestClaim, ok := manifestToken.Get("credential_manifest") + if !ok { + t.Fatal("credential_manifest claim not found") + } + manifestClaimBytes, err := json.Marshal(manifestClaim) + require.NoError(t, err) + err = json.Unmarshal(manifestClaimBytes, &m) + require.NoError(t, err) + + // TODO: we could validate the manifest here, but skipped for simplicity + + credAppBuilder := manifest.NewCredentialApplicationBuilder(m.ID) + err = credAppBuilder.SetApplicantID(holderDID.String()) + require.NoError(t, err) + + err = credAppBuilder.SetApplicationClaimFormat(exchange.ClaimFormat{ + JWT: &exchange.JWTType{Alg: []crypto.SignatureAlgorithm{crypto.EdDSA}}, + }) + require.NoError(t, err) + + err = credAppBuilder.SetPresentationSubmission(exchange.PresentationSubmission{ + ID: "test-submission", + DefinitionID: m.PresentationDefinition.ID, + DescriptorMap: []exchange.SubmissionDescriptor{ + { + ID: m.PresentationDefinition.InputDescriptors[0].ID, + Format: exchange.JWTVC.String(), + Path: "$.verifiableCredentials[0]", + }, + }, + }) + require.NoError(t, err) + credApplication, err := credAppBuilder.Build() + require.NoError(t, err) + + // wrap the credential application in a JSON structure that's expected with a top-level credential_application claim + credApplicationWrapper := manifest.CredentialApplicationWrapper{ + CredentialApplication: *credApplication, + Credentials: []any{string(credJWT)}, + } + credAppBytes, err := json.Marshal(credApplicationWrapper) + require.NoError(t, err) + + // sign the cred app as a JWT + didDoc, err := holderDID.Expand() + require.NoError(t, err) + signer, err := jwx.NewJWXSigner(holderDID.String(), didDoc.VerificationMethod[0].ID, privKey) + require.NoError(t, err) + + credAppJWT, err := signer.SignJWS(credAppBytes) + require.NoError(t, err) + + return credAppJWT +} + +func CreateNameCredential(t *testing.T, privKey gocrypto.PrivateKey, holderDID key.DIDKey) []byte { + credBuilder := credential.NewVerifiableCredentialBuilder() + err := credBuilder.SetIssuer(holderDID.String()) + require.NoError(t, err) + + err = credBuilder.SetIssuanceDate(time.Now().Format(time.RFC3339)) + require.NoError(t, err) + + err = credBuilder.SetCredentialSubject(map[string]interface{}{ + credential.VerifiableCredentialIDProperty: holderDID.String(), + "firstName": "Alice", + }) + require.NoError(t, err) + + cred, err := credBuilder.Build() + require.NoError(t, err) + + // sign the cred as a JWT + didDoc, err := holderDID.Expand() + require.NoError(t, err) + signer, err := jwx.NewJWXSigner(holderDID.String(), didDoc.VerificationMethod[0].ID, privKey) + require.NoError(t, err) + + credJWT, err := integrity.SignVerifiableCredentialJWT(*signer, *cred) + require.NoError(t, err) + + return credJWT +} + +func CreateCredentialManifest(t *testing.T, privKey gocrypto.PrivateKey, issuerDID key.DIDKey) []byte { + manifestBuilder := manifest.NewCredentialManifestBuilder() + did := issuerDID.String() + err := manifestBuilder.SetIssuer(manifest.Issuer{ + ID: did, + Name: "Test Issuer", + }) + require.NoError(t, err) + + err = manifestBuilder.SetName("Test Credential Manifest") + require.NoError(t, err) + + descriptors := []manifest.OutputDescriptor{ + { + ID: "name-cred", + Schema: "https://test.com/schema", + Name: "Name Credential", + }, + } + err = manifestBuilder.SetOutputDescriptors(descriptors) + require.NoError(t, err) + + // only accept JWTs signed with EdDSA + err = manifestBuilder.SetClaimFormat(exchange.ClaimFormat{ + JWT: &exchange.JWTType{Alg: []crypto.SignatureAlgorithm{crypto.EdDSA}}, + }) + + // accept a VC with a first name as an input + err = manifestBuilder.SetPresentationDefinition(exchange.PresentationDefinition{ + ID: "require-name-credential", + InputDescriptors: []exchange.InputDescriptor{ + { + ID: "name", + Constraints: &exchange.Constraints{ + Fields: []exchange.Field{ + { + Path: []string{"$.vc.credentialSubject.firstName"}, + Filter: &exchange.Filter{ + Type: "string", + MinLength: 3, + }, + }, + }, + }, + }, + }, + }) + + m, err := manifestBuilder.Build() + require.NoError(t, err) + + // sign the manifest as a JWT + didDoc, err := issuerDID.Expand() + require.NoError(t, err) + + signer, err := jwx.NewJWXSigner(did, didDoc.VerificationMethod[0].ID, privKey) + require.NoError(t, err) + + // marshal the manifest into JSON before signing over it as a JWS + manifestWrapper := struct { + Manifest manifest.CredentialManifest `json:"credential_manifest"` + }{ + Manifest: *m, + } + manifestBytes, err := json.Marshal(manifestWrapper) + require.NoError(t, err) + manifestJWT, err := signer.SignJWS(manifestBytes) + require.NoError(t, err) + return manifestJWT +} From 14a1cd25838a55ce380a955f79dc827ef9ab899b Mon Sep 17 00:00:00 2001 From: gabe Date: Mon, 28 Aug 2023 12:14:42 -0400 Subject: [PATCH 2/2] valid presentation submission example --- doc/howto/presentation_submission_test.go | 149 ++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 doc/howto/presentation_submission_test.go diff --git a/doc/howto/presentation_submission_test.go b/doc/howto/presentation_submission_test.go new file mode 100644 index 000000000..86cb96203 --- /dev/null +++ b/doc/howto/presentation_submission_test.go @@ -0,0 +1,149 @@ +package howto + +import ( + gocrypto "crypto" + "fmt" + "testing" + + "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/exchange" + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/crypto/jwx" + "github.com/TBD54566975/ssi-sdk/did/key" + "github.com/TBD54566975/ssi-sdk/util" + "github.com/goccy/go-json" + "github.com/lestrrat-go/jwx/jwt" + "github.com/stretchr/testify/require" +) + +func TestPresentationSubmission(t *testing.T) { + // create an issuer DID for the manifest + issuerPrivKey, issuerDID, err := key.GenerateDIDKey(crypto.Ed25519) + require.NoError(t, err) + + // Get a Presentation Request that we're going to respond to + presentationRequestJWT := CreatePresentationRequest(t, issuerPrivKey, *issuerDID) + + // create a holder DID for the submission + holderPrivKey, holderDID, err := key.GenerateDIDKey(crypto.Ed25519) + require.NoError(t, err) + + // self sign a credential with a first name to respond to the request with + credJWT := CreateNameCredential(t, holderPrivKey, *holderDID) + + // create a presentation submission against the request using the credential + presentationSubmissionJWT := CreatePresentationSubmission(t, holderPrivKey, *holderDID, presentationRequestJWT, credJWT) + fmt.Printf("Presentation Submission JWT: %s\n", string(presentationSubmissionJWT)) + + // submit and process the credential application to get a credential response + success := ProcessPresentationSubmission(t, presentationRequestJWT, presentationSubmissionJWT) + require.True(t, success) +} + +func ProcessPresentationSubmission(t *testing.T, requestJWT, submissionJWT []byte) bool { + // decode the presentation request + requestToken, err := jwt.Parse(requestJWT) + require.NoError(t, err) + + var d exchange.PresentationDefinition + definitionClaim, ok := requestToken.Get("presentation_definition") + if !ok { + t.Fatal("presentation_definition claim not found") + } + definitionClaimBytes, err := json.Marshal(definitionClaim) + require.NoError(t, err) + err = json.Unmarshal(definitionClaimBytes, &d) + require.NoError(t, err) + + // decode the submission to JSON + submissionToken, err := jwt.Parse(submissionJWT) + require.NoError(t, err) + vpToken, ok := submissionToken.Get("vp") + if !ok { + t.Fatal("vp claim not found") + } + vpBytes, err := json.Marshal(vpToken) + require.NoError(t, err) + var vp credential.VerifiablePresentation + err = json.Unmarshal(vpBytes, &vp) + require.NoError(t, err) + + _, err = exchange.VerifyPresentationSubmissionVP(d, vp) + return err == nil +} + +func CreatePresentationSubmission(t *testing.T, privKey gocrypto.PrivateKey, submitterDID key.DIDKey, requestJWT, credJWT []byte) []byte { + // TODO(gabe) we could verify the presentation request here, but we won't for now + requestToken, err := jwt.Parse(requestJWT) + require.NoError(t, err) + + requester := requestToken.Issuer() + var d exchange.PresentationDefinition + definitionClaim, ok := requestToken.Get("presentation_definition") + if !ok { + t.Fatal("presentation_definition claim not found") + } + definitionClaimBytes, err := json.Marshal(definitionClaim) + require.NoError(t, err) + err = json.Unmarshal(definitionClaimBytes, &d) + require.NoError(t, err) + + // construct signer for the submitter + signer, err := jwx.NewJWXSigner(submitterDID.String(), submitterDID.String()+"#+"+submitterDID.String(), privKey) + require.NoError(t, err) + + presentationSubmissionVPJWT, err := exchange.BuildPresentationSubmission(*signer, requester, d, []exchange.PresentationClaim{ + { + Token: util.StringPtr(string(credJWT)), + JWTFormat: exchange.JWTVC.Ptr(), + SignatureAlgorithmOrProofType: string(crypto.EdDSA), + }, + }, exchange.JWTVPTarget) + require.NoError(t, err) + + return presentationSubmissionVPJWT +} + +func CreatePresentationRequest(t *testing.T, privKey gocrypto.PrivateKey, issuerDID key.DIDKey) []byte { + definitionBuilder := exchange.NewPresentationDefinitionBuilder() + did := issuerDID.String() + err := definitionBuilder.SetName("Test Presentation Definition") + require.NoError(t, err) + + err = definitionBuilder.SetInputDescriptors([]exchange.InputDescriptor{ + { + ID: "name", + Constraints: &exchange.Constraints{ + Fields: []exchange.Field{ + { + Path: []string{"$.vc.credentialSubject.firstName"}, + Filter: &exchange.Filter{ + Type: "string", + MinLength: 3, + }, + }, + }, + }, + Format: &exchange.ClaimFormat{ + JWTVC: &exchange.JWTType{Alg: []crypto.SignatureAlgorithm{crypto.EdDSA}}, + }, + }, + }) + require.NoError(t, err) + + // build the presentation definition + d, err := definitionBuilder.Build() + require.NoError(t, err) + + // sign the definition as a JWT + didDoc, err := issuerDID.Expand() + require.NoError(t, err) + + signer, err := jwx.NewJWXSigner(did, didDoc.VerificationMethod[0].ID, privKey) + require.NoError(t, err) + + presentationRequestBytes, err := exchange.BuildPresentationRequest(*signer, exchange.JWTRequest, *d) + require.NoError(t, err) + + return presentationRequestBytes +}