From 5d3dce172f5f1493b72d9e04ebf9322f297cd104 Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Tue, 18 Apr 2023 08:33:21 -0700 Subject: [PATCH] Add signature checks for Verifiable Presentations (#353) * generic signers and verifiers * progress * more tests * bad renames * disabling limited disclosure * give the university case a glow up * lints * fix apt application * remove schemas * bad renames * test for verifying jwt cred * tests * pr comments * fix lint --- credential/exchange/request.go | 8 +- credential/exchange/request_test.go | 4 +- credential/exchange/submission.go | 95 ++++--- credential/exchange/submission_test.go | 131 ++++++---- credential/exchange/verification.go | 27 +- credential/exchange/verification_test.go | 85 ++++-- credential/{signing => }/jws.go | 11 +- credential/{signing => }/jws_test.go | 9 +- credential/{signing => }/jwt.go | 83 ++++-- credential/jwt_test.go | 244 ++++++++++++++++++ credential/manifest/validation.go | 4 +- credential/manifest/validation_test.go | 3 +- credential/model_test.go | 4 +- credential/signature.go | 94 +++++++ credential/signature_test.go | 244 ++++++++++++++++++ credential/signing/jwt_test.go | 132 ---------- credential/util.go | 128 +++++++++ credential/util/util.go | 99 ------- credential/{util => }/util_test.go | 23 +- credential/verification/verification.go | 11 +- credential/verification/verification_test.go | 12 +- credential/verification/verifiers.go | 43 +-- crypto/jwt.go | 6 +- crypto/jwt_test.go | 6 +- .../bbsplussignatureproofsuite_test.go | 4 +- cryptosuite/bbsplussignaturesuite.go | 2 +- cryptosuite/bbsplussignaturesuite_test.go | 2 +- cryptosuite/testdata/case16_reveal_doc.jsonld | 2 +- cryptosuite/testdata/case16_revealed.jsonld | 2 +- cryptosuite/testdata/case16_vc.jsonld | 2 +- cryptosuite/testdata/case18_reveal_doc.jsonld | 2 +- cryptosuite/testdata/case18_vc.jsonld | 2 +- cryptosuite/testdata/vc_test_vector.jsonld | 2 +- did/key_fuzz_test.go | 9 +- did/util.go | 19 ++ did/util_test.go | 61 +++++ .../apartment_application.go | 34 ++- .../usecase/employer_university_flow/main.go | 87 ++++--- .../employer_university_flow/pkg/issuer.go | 33 ++- .../employer_university_flow/pkg/trust.go | 16 -- .../employer_university_flow/pkg/util.go | 37 ++- .../employer_university_flow/pkg/verifier.go | 28 +- example/usecase/steel_thread/steel_thread.go | 2 +- example/wallet.go | 120 ++++++--- schema/loader.go | 42 +-- 45 files changed, 1394 insertions(+), 620 deletions(-) rename credential/{signing => }/jws.go (87%) rename credential/{signing => }/jws_test.go (92%) rename credential/{signing => }/jwt.go (76%) create mode 100644 credential/jwt_test.go create mode 100644 credential/signature.go create mode 100644 credential/signature_test.go delete mode 100644 credential/signing/jwt_test.go create mode 100644 credential/util.go delete mode 100644 credential/util/util.go rename credential/{util => }/util_test.go (84%) delete mode 100644 example/usecase/employer_university_flow/pkg/trust.go diff --git a/credential/exchange/request.go b/credential/exchange/request.go index c2cecd7d..d32e07e1 100644 --- a/credential/exchange/request.go +++ b/credential/exchange/request.go @@ -90,14 +90,18 @@ func BuildJWTPresentationRequest(signer crypto.JWTSigner, def PresentationDefini // VerifyPresentationRequest finds the correct verifier and parser for a given presentation request type, // verifying the signature on the request, and returning the parsed Presentation Definition object. -func VerifyPresentationRequest(verifier crypto.JWTVerifier, pt PresentationRequestType, request []byte) (*PresentationDefinition, error) { +func VerifyPresentationRequest(verifier any, pt PresentationRequestType, request []byte) (*PresentationDefinition, error) { err := fmt.Errorf("cannot verify unsupported presentation request type: %s", pt) if !IsSupportedPresentationRequestType(pt) { return nil, err } switch pt { case JWTRequest: - return VerifyJWTPresentationRequest(verifier, request) + jwtVerifier, ok := verifier.(crypto.JWTVerifier) + if !ok { + return nil, fmt.Errorf("verifier<%T> is not a JWTVerifier", verifier) + } + return VerifyJWTPresentationRequest(jwtVerifier, request) default: return nil, err } diff --git a/credential/exchange/request_test.go b/credential/exchange/request_test.go index 8be6e2d2..cd8ba54e 100644 --- a/credential/exchange/request_test.go +++ b/credential/exchange/request_test.go @@ -21,7 +21,7 @@ func TestBuildPresentationRequest(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, requestJWTBytes) - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(signer.ID) assert.NoError(t, err) headers, parsed, err := verifier.VerifyAndParse(string(requestJWTBytes)) @@ -48,7 +48,7 @@ func TestBuildPresentationRequest(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, requestJWTBytes) - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(signer.ID) assert.NoError(t, err) headers, parsed, err := verifier.VerifyAndParse(string(requestJWTBytes)) diff --git a/credential/exchange/submission.go b/credential/exchange/submission.go index 5f7b4860..f9e990d1 100644 --- a/credential/exchange/submission.go +++ b/credential/exchange/submission.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" @@ -36,21 +35,36 @@ const ( // https://identity.foundation/presentation-exchange/#claim-format-designations // This object must be constructed for each claim before processing of a Presentation Definition type PresentationClaim struct { + // Data Integrity Claim // If we have a Credential or Presentation value, we assume we have a LDP_VC or LDP_VP respectively Credential *credential.VerifiableCredential Presentation *credential.VerifiablePresentation LDPFormat *LinkedDataFormat - // If we have a token, we assume we have a JWT format value - TokenBytes []byte - JWTFormat *JWTFormat + // JWT claims + Token *string + JWTFormat *JWTFormat + + // Always required // The algorithm or Linked Data proof type by which the claim was signed must be present SignatureAlgorithmOrProofType string } +// GetClaim returns the claim value as a generic type. Since PresentationClaim is a union type, the value returned is +// the first non-nil value in the following order: Credential, Presentation, Token +func (pc *PresentationClaim) GetClaim() any { + if pc.Credential != nil { + return pc.Credential + } + if pc.Presentation != nil { + return pc.Presentation + } + return pc.Token +} + func (pc *PresentationClaim) IsEmpty() bool { - if pc == nil || (pc.Credential == nil && pc.Presentation == nil && len(pc.TokenBytes) == 0) { + if pc == nil || (pc.Credential == nil && pc.Presentation == nil && pc.Token == nil) { return true } return reflect.DeepEqual(pc, &PresentationClaim{}) @@ -65,10 +79,10 @@ func (pc *PresentationClaim) GetClaimValue() (any, error) { if pc.Presentation != nil { return *pc.Presentation, nil } - if pc.TokenBytes != nil { + if pc.Token != nil { switch pc.JWTFormat.String() { case JWT.String(), JWTVC.String(), JWTVP.String(): - return jwt.Parse(pc.TokenBytes, jwt.WithValidate(false), jwt.WithVerify(false)) + return jwt.Parse([]byte(*pc.Token), jwt.WithValidate(false), jwt.WithVerify(false)) default: return nil, fmt.Errorf("unsupported JWT format: %s", pc.JWTFormat) } @@ -92,7 +106,7 @@ func (pc *PresentationClaim) GetClaimFormat() (string, error) { } return string(*pc.LDPFormat), nil } - if pc.TokenBytes != nil { + if pc.Token != nil { if pc.JWTFormat == nil { return "", errors.New("JWT claim has no JWT format set") } @@ -130,7 +144,7 @@ func (pc *PresentationClaim) GetClaimJSON() (map[string]any, error) { // https://identity.foundation/presentation-exchange/#presentation-submission // Note: this method does not support LD cryptosuites, and prefers JWT representations. Future refactors // may include an analog method for LD suites. -func BuildPresentationSubmission(signer crypto.JWTSigner, requester string, def PresentationDefinition, claims []PresentationClaim, et EmbedTarget) ([]byte, error) { +func BuildPresentationSubmission(signer any, requester string, def PresentationDefinition, claims []PresentationClaim, et EmbedTarget) ([]byte, error) { if !IsSupportedEmbedTarget(et) { return nil, fmt.Errorf("unsupported presentation submission embed target type: %s", et) } @@ -143,11 +157,15 @@ func BuildPresentationSubmission(signer crypto.JWTSigner, requester string, def } switch et { case JWTVPTarget: - vpSubmission, err := BuildPresentationSubmissionVP(signer.ID, def, normalizedClaims) + jwtSigner, ok := signer.(crypto.JWTSigner) + if !ok { + return nil, fmt.Errorf("signer<%T> is not a JWTSigner", signer) + } + vpSubmission, err := BuildPresentationSubmissionVP(jwtSigner.ID, def, normalizedClaims) if err != nil { return nil, errors.Wrap(err, "unable to fulfill presentation definition with given credentials") } - return signing.SignVerifiablePresentationJWT(signer, signing.JWTVVPParameters{Audience: requester}, *vpSubmission) + return credential.SignVerifiablePresentationJWT(jwtSigner, credential.JWTVVPParameters{Audience: requester}, *vpSubmission) default: return nil, fmt.Errorf("presentation submission embed target <%s> is not implemented", et) } @@ -156,8 +174,10 @@ func BuildPresentationSubmission(signer crypto.JWTSigner, requester string, def type NormalizedClaim struct { // id for the claim ID string - // go-json representation of the claim + // json representation of the claim Data map[string]any + // claim in its original format (e.g. Verifiable Credential, token string, etc.) + RawClaim any // JWT_VC, JWT_VP, LDP_VC, LDP_VP, etc. Format string // Signing algorithm used for the claim (e.g. EdDSA, ES256, PS256, etc.). @@ -188,10 +208,13 @@ func normalizePresentationClaims(claims []PresentationClaim) ([]NormalizedClaim, var id string if claimID, ok := claimJSON["id"]; ok { id = claimID.(string) + } else if claimID, ok := claimJSON["jti"]; ok { + id = claimID.(string) } normalizedClaims = append(normalizedClaims, NormalizedClaim{ ID: id, Data: claimJSON, + RawClaim: claim.GetClaim(), Format: claimFormat, AlgOrProofType: claim.SignatureAlgorithmOrProofType, }) @@ -202,7 +225,7 @@ func normalizePresentationClaims(claims []PresentationClaim) ([]NormalizedClaim, // processedClaim represents a claim that has been processed for an input descriptor along with relevant // information for building a valid descriptor_map in the resulting presentation submission type processedClaim struct { - claim map[string]any + claim any SubmissionDescriptor } @@ -235,31 +258,31 @@ func BuildPresentationSubmissionVP(submitter string, def PresentationDefinition, // keep track of claims we've already added, to avoid duplicates seenClaims := make(map[string]int) for _, id := range def.InputDescriptors { - processedID, err := processInputDescriptor(id, claims) + processedDescriptor, err := processInputDescriptor(id, claims) if err != nil { return nil, errors.Wrapf(err, "error processing input descriptor: %s", id.ID) } - if processedID == nil { + if processedDescriptor == nil { return nil, fmt.Errorf("input descrpitor<%s> could not be fulfilled; could not build a valid presentation submission", id.ID) } // check if claim already exists. if it has, we won't duplicate the claim var currIndex int - var claim map[string]any - claimID := processedID.ClaimID + var claim any + claimID := processedDescriptor.ClaimID if seen, ok := seenClaims[claimID]; ok { currIndex = seen } else { currIndex = claimIndex claimIndex++ - claim = processedID.Claim + claim = processedDescriptor.Claim seenClaims[claimID] = currIndex } processedClaims = append(processedClaims, processedClaim{ claim: claim, SubmissionDescriptor: SubmissionDescriptor{ - ID: processedID.ID, - Format: processedID.Format, + ID: processedDescriptor.ID, + Format: processedDescriptor.Format, Path: fmt.Sprintf("$.verifiableCredential[%d]", currIndex), }, }) @@ -269,10 +292,10 @@ func BuildPresentationSubmissionVP(submitter string, def PresentationDefinition, var descriptorMap []SubmissionDescriptor for _, claim := range processedClaims { descriptorMap = append(descriptorMap, claim.SubmissionDescriptor) - // on the case we've seen the claim, we need to check as to not add a nil claim value - if len(claim.claim) > 0 { + // in the case where we've seen the claim, we need to check as to not add a nil claim value + if claim.claim != nil { if err := builder.AddVerifiableCredentials(claim.claim); err != nil { - return nil, errors.Wrap(err, "could not add claim value to verifiable presentation") + return nil, errors.Wrap(err, "could not add claim to verifiable presentation") } } } @@ -294,7 +317,7 @@ type processedInputDescriptor struct { // ID of the claim ClaimID string // generic claim - Claim map[string]any + Claim any // claim format Format string } @@ -319,16 +342,17 @@ func processInputDescriptor(id InputDescriptor, claims []NormalizedClaim) (*proc // bookkeeping to check whether we've fulfilled all required fields, and whether we need to limit disclosure fieldsToProcess := len(fields) - limitDisclosure := false disclosure := constraints.LimitDisclosure - if disclosure != nil && (*disclosure == Required || *disclosure == Preferred) { - limitDisclosure = true + if disclosure != nil && *disclosure == Required { + // TODO(gabe) enable limiting disclosure for ZKP/SD creds https://github.com/TBD54566975/ssi-sdk/issues/354 + // otherwise, we won't be able to send back a claim with a signature attached + return nil, errors.New("requiring limit disclosure is not supported") } // first, reduce the set of claims that conform with the format required by the input descriptor filteredClaims := filterClaimsByFormat(claims, id.Format) if len(filteredClaims) == 0 { - return nil, fmt.Errorf("no claims match the required format, and signing alg/proof type requirements "+ + return nil, fmt.Errorf("no claims match the required format, and jwt alg/proof type requirements "+ "for input descriptor: %s", id.ID) } @@ -337,35 +361,24 @@ func processInputDescriptor(id InputDescriptor, claims []NormalizedClaim) (*proc // if we find a match for each field, we know a claim can fulfill the given input descriptor. for _, claim := range filteredClaims { fieldsProcessed := 0 - var limited []limitedInputDescriptor claimValue := claim.Data for _, field := range fields { // apply the field to the claim, and return the processed value, which we only care about for // filtering and/or limit_disclosure settings - limitedClaim, fulfilled := processInputDescriptorField(field, claimValue) - if !fulfilled { + if _, fulfilled := processInputDescriptorField(field, claimValue); !fulfilled { // we know this claim is not sufficient to fulfill the input descriptor break } // we've fulfilled the field, so note it fieldsProcessed++ - if limitDisclosure { - limited = append(limited, *limitedClaim) - } } // if a claim has matched all fields, we can fulfill the input descriptor with this claim if fieldsProcessed == fieldsToProcess { - // because the `limit_disclosure` property is present, we must merge the limited fields - resultClaim := claimValue - if limitDisclosure { - limitedClaim := constructLimitedClaim(limited) - resultClaim = limitedClaim - } return &processedInputDescriptor{ ID: id.ID, ClaimID: claim.ID, - Claim: resultClaim, + Claim: claim.RawClaim, Format: claim.Format, }, nil } diff --git a/credential/exchange/submission_test.go b/credential/exchange/submission_test.go index f6ef3840..f79415aa 100644 --- a/credential/exchange/submission_test.go +++ b/credential/exchange/submission_test.go @@ -1,8 +1,11 @@ package exchange import ( + "context" "testing" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/oliveagle/jsonpath" @@ -10,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/cryptosuite" ) @@ -22,7 +24,7 @@ func TestBuildPresentationSubmission(t *testing.T) { assert.Contains(tt, err.Error(), "unsupported presentation submission embed target type") }) - t.Run("Supported embed target", func(tt *testing.T) { + t.Run("Supported embed target, unsigned credential", func(tt *testing.T) { def := PresentationDefinition{ ID: "test-id", InputDescriptors: []InputDescriptor{ @@ -43,17 +45,63 @@ func TestBuildPresentationSubmission(t *testing.T) { assert.NoError(tt, def.IsValid()) signer, verifier := getJWKSignerVerifier(tt) - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential(signer.ID, signer.ID) presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), SignatureAlgorithmOrProofType: string(cryptosuite.JSONWebSignature2020), } - submissionBytes, err := BuildPresentationSubmission(*signer, "requester", def, []PresentationClaim{presentationClaim}, JWTVPTarget) + submissionBytes, err := BuildPresentationSubmission(*signer, signer.ID, def, []PresentationClaim{presentationClaim}, JWTVPTarget) assert.NoError(tt, err) assert.NotEmpty(tt, submissionBytes) - _, _, vp, err := signing.VerifyVerifiablePresentationJWT(*verifier, string(submissionBytes)) + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + _, _, _, err = credential.VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(submissionBytes)) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential must have a proof") + }) + + t.Run("Supported embed target with JWT credential", func(tt *testing.T) { + def := PresentationDefinition{ + ID: "test-id", + InputDescriptors: []InputDescriptor{ + { + ID: "id-1", + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.vc.issuer", "$.issuer"}, + ID: "issuer-input-descriptor", + Purpose: "need to check the issuer", + }, + }, + }, + }, + }, + } + assert.NoError(tt, def.IsValid()) + + signer, verifier := getJWKSignerVerifier(tt) + testVC := getTestVerifiableCredential(signer.ID, signer.ID) + + credJWT, err := credential.SignVerifiableCredentialJWT(*signer, testVC) + assert.NoError(tt, err) + assert.NotEmpty(tt, credJWT) + presentationClaim := PresentationClaim{ + Token: util.StringPtr(string(credJWT)), + JWTFormat: JWTVC.Ptr(), + SignatureAlgorithmOrProofType: signer.GetSigningAlgorithm(), + } + submissionBytes, err := BuildPresentationSubmission(*signer, signer.ID, def, []PresentationClaim{presentationClaim}, JWTVPTarget) + assert.NoError(tt, err) + assert.NotEmpty(tt, submissionBytes) + + println(string(submissionBytes)) + + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + _, _, vp, err := credential.VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(submissionBytes)) assert.NoError(tt, err) assert.NoError(tt, vp.IsValid()) @@ -82,7 +130,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { } assert.NoError(tt, def.IsValid()) - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -140,7 +188,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { assert.NoError(tt, def.IsValid()) vp, err := BuildPresentationSubmissionVP("submitter", def, nil) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "no claims match the required format, and signing alg/proof type requirements for input descriptor") + assert.Contains(tt, err.Error(), "no claims match the required format, and jwt alg/proof type requirements for input descriptor") assert.Empty(tt, vp) }) @@ -176,7 +224,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { } assert.NoError(tt, def.IsValid()) - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -247,7 +295,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { } assert.NoError(tt, def.IsValid()) - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -255,7 +303,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { } testVCJWT := getTestJWTVerifiableCredential() presentationClaimJWT := PresentationClaim{ - TokenBytes: testVCJWT, + Token: util.StringPtr(string(testVCJWT)), JWTFormat: JWTVC.Ptr(), SignatureAlgorithmOrProofType: string(crypto.EdDSA), } @@ -288,15 +336,13 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { assert.Equal(tt, "test-verifiable-credential", asVC.ID) assert.Equal(tt, "Block", asVC.CredentialSubject["company"]) - vcBytesJWT, err := json.Marshal(vp.VerifiableCredential[1]) - assert.NoError(tt, err) - var asVCJWT map[string]any - err = json.Unmarshal(vcBytesJWT, &asVCJWT) + _, vcJWTToken, asVCJWT, err := credential.ParseVerifiableCredentialFromJWT(*(vp.VerifiableCredential[1].(*string))) assert.NoError(tt, err) + assert.NotEmpty(tt, vcJWTToken) assert.NotEmpty(tt, asVCJWT) - assert.Equal(tt, "did:example:456", asVCJWT["sub"]) - assert.Equal(tt, "JimBobertson", asVCJWT["vc"].(map[string]any)["credentialSubject"].(map[string]any)["name"]) + assert.Equal(tt, "did:example:456", vcJWTToken.Subject()) + assert.Equal(tt, "JimBobertson", asVCJWT.CredentialSubject["name"]) }) } @@ -315,7 +361,7 @@ func TestProcessInputDescriptor(t *testing.T) { }, }, } - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -329,9 +375,11 @@ func TestProcessInputDescriptor(t *testing.T) { assert.Equal(tt, id.ID, processed.ID) // make sure it's not limited disclosure - assert.Equal(tt, "test-verifiable-credential", processed.Claim["id"]) + vc := processed.Claim.(*credential.VerifiableCredential) + assert.Equal(tt, "test-verifiable-credential", vc.ID) }) + // TODO(gabe): update with https://github.com/TBD54566975/ssi-sdk/issues/354 t.Run("Simple Descriptor with One VC Claim and Limited Disclosure", func(tt *testing.T) { id := InputDescriptor{ ID: "id-1", @@ -346,7 +394,7 @@ func TestProcessInputDescriptor(t *testing.T) { }, }, } - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -354,20 +402,16 @@ func TestProcessInputDescriptor(t *testing.T) { } normalized, err := normalizePresentationClaims([]PresentationClaim{presentationClaim}) assert.NoError(tt, err) - processed, err := processInputDescriptor(id, normalized) - assert.NoError(tt, err) - assert.NotEmpty(tt, processed) - assert.Equal(tt, id.ID, processed.ID) - - // make sure it's limited disclosure - assert.NotEqual(tt, "test-verifiable-credential", processed.Claim["id"]) + _, err = processInputDescriptor(id, normalized) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "requiring limit disclosure is not supported") }) t.Run("Descriptor with no matching paths", func(tt *testing.T) { id := InputDescriptor{ ID: "id-1", Constraints: &Constraints{ - LimitDisclosure: Required.Ptr(), + LimitDisclosure: Preferred.Ptr(), Fields: []Field{ { Path: []string{"$.vc.issuer"}, @@ -377,7 +421,7 @@ func TestProcessInputDescriptor(t *testing.T) { }, }, } - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -387,14 +431,13 @@ func TestProcessInputDescriptor(t *testing.T) { assert.NoError(tt, err) _, err = processInputDescriptor(id, normalized) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "no claims could fulfill the input descriptor") + assert.Contains(tt, err.Error(), "no claims could fulfill the input descriptor: id-1") }) t.Run("Descriptor with no matching format", func(tt *testing.T) { id := InputDescriptor{ ID: "id-1", Constraints: &Constraints{ - LimitDisclosure: Required.Ptr(), Fields: []Field{ { Path: []string{"$.issuer"}, @@ -409,7 +452,7 @@ func TestProcessInputDescriptor(t *testing.T) { }, }, } - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -419,14 +462,13 @@ func TestProcessInputDescriptor(t *testing.T) { assert.NoError(tt, err) _, err = processInputDescriptor(id, normalized) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "no claims match the required format, and signing alg/proof type requirements") + assert.Contains(tt, err.Error(), "no claims match the required format, and jwt alg/proof type requirements") }) t.Run("Descriptor with matching format", func(tt *testing.T) { id := InputDescriptor{ ID: "id-1", Constraints: &Constraints{ - LimitDisclosure: Required.Ptr(), Fields: []Field{ { Path: []string{"$.issuer"}, @@ -441,7 +483,7 @@ func TestProcessInputDescriptor(t *testing.T) { }, }, } - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -648,16 +690,16 @@ func TestConstructLimitedClaim(t *testing.T) { }) } -func getTestVerifiableCredential() credential.VerifiableCredential { +func getTestVerifiableCredential(issuerDID, subjectDID string) credential.VerifiableCredential { return credential.VerifiableCredential{ Context: []any{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, ID: "test-verifiable-credential", Type: []string{"VerifiableCredential"}, - Issuer: "test-issuer", + Issuer: issuerDID, IssuanceDate: "2021-01-01T19:23:24Z", CredentialSubject: map[string]any{ - "id": "test-vc-id", + "id": subjectDID, "company": "Block", "website": "https://block.xyz", }, @@ -693,7 +735,7 @@ func TestNormalizePresentationClaims(t *testing.T) { assert.NotEmpty(tt, jwtVC) presentationClaim := PresentationClaim{ - TokenBytes: jwtVC, + Token: util.StringPtr(string(jwtVC)), JWTFormat: JWTVC.Ptr(), SignatureAlgorithmOrProofType: string(crypto.EdDSA), } @@ -727,7 +769,7 @@ func TestNormalizePresentationClaims(t *testing.T) { }) t.Run("Normalize VC Claim", func(tt *testing.T) { - vcClaim := getTestVerifiableCredential() + vcClaim := getTestVerifiableCredential("test-issuer", "test-subject") assert.NotEmpty(tt, vcClaim) presentationClaim := PresentationClaim{ @@ -804,18 +846,19 @@ func getGenericTestClaim() map[string]any { } func getJWKSignerVerifier(t *testing.T) (*crypto.JWTSigner, *crypto.JWTVerifier) { - _, privKey, err := crypto.GenerateEd25519Key() + privKey, didKey, err := did.GenerateDIDKey(crypto.Ed25519) require.NoError(t, err) key, err := jwk.FromRaw(privKey) require.NoError(t, err) - id := "test-id" - kid := "test-key" - signer, err := crypto.NewJWTSignerFromKey(id, kid, key) + expanded, err := didKey.Expand() + require.NoError(t, err) + kid := expanded.VerificationMethod[0].ID + signer, err := crypto.NewJWTSignerFromKey(didKey.String(), kid, key) require.NoError(t, err) - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(didKey.String()) require.NoError(t, err) return signer, verifier diff --git a/credential/exchange/verification.go b/credential/exchange/verification.go index 067eee6e..5c9a66b9 100644 --- a/credential/exchange/verification.go +++ b/credential/exchange/verification.go @@ -1,14 +1,14 @@ package exchange import ( + "context" "fmt" "strings" - credutil "github.com/TBD54566975/ssi-sdk/credential/util" + "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/schema" "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" @@ -18,10 +18,15 @@ import ( // VerifyPresentationSubmission verifies a presentation submission for both signature validity and correctness // with the specification. It is assumed that the caller knows the submission embed target, and the corresponding -// presentation definition, and has access to the public key of the signer. +// presentation definition, and has access to the public key of the signer. A DID resolver is required to resolve +// the DID and keys of the signer for each credential in the presentation, whose signatures also need to be verified. // Note: this method does not support LD cryptosuites, and prefers JWT representations. Future refactors // may include an analog method for LD suites. -func VerifyPresentationSubmission(verifier crypto.JWTVerifier, et EmbedTarget, def PresentationDefinition, submission []byte) error { +// TODO(gabe) remove embed target, have it detected from the submission +func VerifyPresentationSubmission(ctx context.Context, verifier any, resolver did.Resolver, et EmbedTarget, def PresentationDefinition, submission []byte) error { //revive:disable-line + if resolver == nil { + return errors.New("resolver cannot be empty") + } if err := canProcessDefinition(def); err != nil { return errors.Wrap(err, "not able to verify submission; feature not supported") } @@ -30,7 +35,12 @@ func VerifyPresentationSubmission(verifier crypto.JWTVerifier, et EmbedTarget, d } switch et { case JWTVPTarget: - _, _, vp, err := signing.VerifyVerifiablePresentationJWT(verifier, string(submission)) + jwtVerifier, ok := verifier.(crypto.JWTVerifier) + if !ok { + return fmt.Errorf("verifier<%T> is not a JWT verifier", verifier) + } + // verify the VP, which in turn verifies all credentials in it + _, _, vp, err := credential.VerifyVerifiablePresentationJWT(ctx, jwtVerifier, resolver, string(submission)) if err != nil { return errors.Wrap(err, "verification of the presentation submission failed") } @@ -41,8 +51,7 @@ func VerifyPresentationSubmission(verifier crypto.JWTVerifier, et EmbedTarget, d } // VerifyPresentationSubmissionVP verifies whether a verifiable presentation is a valid presentation submission -// for a given presentation definition. -// TODO(gabe) handle signature validation of submission claims https://github.com/TBD54566975/ssi-sdk/issues/71 +// for a given presentation definition. No signature verification happens here. func VerifyPresentationSubmissionVP(def PresentationDefinition, vp credential.VerifiablePresentation) error { if err := vp.IsValid(); err != nil { return errors.Wrap(err, "presentation submission does not contain a valid VP") @@ -102,7 +111,7 @@ func VerifyPresentationSubmissionVP(def PresentationDefinition, vp credential.Ve } // TODO(gabe) add in signature verification of claims here https://github.com/TBD54566975/ssi-sdk/issues/71 - cred, err := credutil.ToCredential(claim) + _, _, cred, err := credential.ToCredential(claim) if err != nil { return errors.Wrapf(err, "getting claim as json: <%s>", claim) } @@ -117,7 +126,7 @@ func VerifyPresentationSubmissionVP(def PresentationDefinition, vp credential.Ve // TODO(gabe) consider enforcing limited disclosure if present // for each field we need to verify at least one path matches - credJSON, err := credutil.ToCredentialJSONMap(claim) + credJSON, err := credential.ToCredentialJSONMap(claim) if err != nil { return errors.Wrapf(err, "getting credential as json: %v", cred) } diff --git a/credential/exchange/verification_test.go b/credential/exchange/verification_test.go index 95189415..6d75bd8f 100644 --- a/credential/exchange/verification_test.go +++ b/credential/exchange/verification_test.go @@ -1,20 +1,24 @@ package exchange import ( + "context" "testing" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/util" "github.com/stretchr/testify/assert" "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/cryptosuite" ) func TestVerifyPresentationSubmission(t *testing.T) { t.Run("Unsupported embed target", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) verifier := crypto.JWTVerifier{} - err := VerifyPresentationSubmission(verifier, "badEmbedTarget", PresentationDefinition{}, nil) + err = VerifyPresentationSubmission(context.Background(), verifier, resolver, "badEmbedTarget", PresentationDefinition{}, nil) assert.Error(tt, err) assert.Contains(tt, err.Error(), "unsupported presentation submission embed target type") }) @@ -37,13 +41,16 @@ func TestVerifyPresentationSubmission(t *testing.T) { }, }, } + + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) _, verifier := getJWKSignerVerifier(tt) - err := VerifyPresentationSubmission(*verifier, JWTVPTarget, def, nil) + err = VerifyPresentationSubmission(context.Background(), *verifier, resolver, JWTVPTarget, def, nil) assert.Error(tt, err) assert.Contains(tt, err.Error(), "verification of the presentation submission failed") }) - t.Run("Supported embed target, valid submission", func(tt *testing.T) { + t.Run("Supported embed target, valid submission, invalid credential format", func(tt *testing.T) { def := PresentationDefinition{ ID: "test-id", InputDescriptors: []InputDescriptor{ @@ -63,18 +70,62 @@ func TestVerifyPresentationSubmission(t *testing.T) { } assert.NoError(tt, def.IsValid()) + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + signer, verifier := getJWKSignerVerifier(tt) - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential(signer.ID, signer.ID) presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), SignatureAlgorithmOrProofType: string(cryptosuite.JSONWebSignature2020), } - submissionBytes, err := BuildPresentationSubmission(*signer, "requester", def, []PresentationClaim{presentationClaim}, JWTVPTarget) + submissionBytes, err := BuildPresentationSubmission(*signer, verifier.ID, def, []PresentationClaim{presentationClaim}, JWTVPTarget) + assert.NoError(tt, err) + assert.NotEmpty(tt, submissionBytes) + + err = VerifyPresentationSubmission(context.Background(), *verifier, resolver, JWTVPTarget, def, submissionBytes) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential must have a proof") + }) + + t.Run("Supported embed target, valid submission", func(tt *testing.T) { + def := PresentationDefinition{ + ID: "test-id", + InputDescriptors: []InputDescriptor{ + { + ID: "id-1", + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.vc.issuer", "$.issuer"}, + ID: "issuer-input-descriptor", + Purpose: "need to check the issuer", + }, + }, + }, + }, + }, + } + assert.NoError(tt, def.IsValid()) + + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + signer, verifier := getJWKSignerVerifier(tt) + testVC := getTestVerifiableCredential(signer.ID, signer.ID) + credJWT, err := credential.SignVerifiableCredentialJWT(*signer, testVC) + assert.NoError(tt, err) + presentationClaim := PresentationClaim{ + Token: util.StringPtr(string(credJWT)), + JWTFormat: JWTVC.Ptr(), + SignatureAlgorithmOrProofType: signer.GetSigningAlgorithm(), + } + submissionBytes, err := BuildPresentationSubmission(*signer, verifier.ID, def, []PresentationClaim{presentationClaim}, JWTVPTarget) assert.NoError(tt, err) assert.NotEmpty(tt, submissionBytes) - err = VerifyPresentationSubmission(*verifier, JWTVPTarget, def, submissionBytes) + err = VerifyPresentationSubmission(context.Background(), *verifier, resolver, JWTVPTarget, def, submissionBytes) assert.NoError(tt, err) }) } @@ -101,7 +152,7 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { assert.NoError(tt, def.IsValid()) signer, _ := getJWKSignerVerifier(tt) - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") presentationClaim := PresentationClaim{ Credential: &testVC, LDPFormat: LDPVC.Ptr(), @@ -111,7 +162,7 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { assert.NoError(tt, err) assert.NotEmpty(tt, submissionBytes) - _, _, verifiablePresentation, err := signing.ParseVerifiablePresentationFromJWT(string(submissionBytes)) + _, _, verifiablePresentation, err := credential.ParseVerifiablePresentationFromJWT(string(submissionBytes)) assert.NoError(tt, err) err = VerifyPresentationSubmissionVP(def, *verifiablePresentation) @@ -241,7 +292,7 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { }, }, VerifiableCredential: []any{ - getTestVerifiableCredential(), + getTestVerifiableCredential("test-issuer", "test-subject"), }, } @@ -291,7 +342,7 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { }, }, VerifiableCredential: []any{ - getTestVerifiableCredential(), + getTestVerifiableCredential("test-issuer", "test-subject"), }, } @@ -346,16 +397,16 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { }, }, VerifiableCredential: []any{ - getTestVerifiableCredential(), + getTestVerifiableCredential("test-issuer", "test-subject"), }, } err := VerifyPresentationSubmissionVP(def, presentation) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "subject is not the same as issuer") + assert.Contains(tt, err.Error(), "subject is not the same as issuer") // modify the VC to have the same issuer and subject - testVC := getTestVerifiableCredential() + testVC := getTestVerifiableCredential("test-issuer", "test-subject") testVC.CredentialSubject[credential.VerifiableCredentialIDProperty] = "test-issuer" presentation.VerifiableCredential = []any{testVC} err = VerifyPresentationSubmissionVP(def, presentation) @@ -410,7 +461,7 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { }, }, VerifiableCredential: []any{ - getTestVerifiableCredential(), + getTestVerifiableCredential("test-issuer", "test-subject"), }, } @@ -437,8 +488,8 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { }, } signer, _ := getJWKSignerVerifier(t) - testVC := getTestVerifiableCredential() - vcData, err := signing.SignVerifiableCredentialJWT(*signer, testVC) + testVC := getTestVerifiableCredential("test-issuer", "test-subject") + vcData, err := credential.SignVerifiableCredentialJWT(*signer, testVC) assert.NoError(t, err) b := NewPresentationSubmissionBuilder(def.ID) assert.NoError(t, b.SetDescriptorMap([]SubmissionDescriptor{ diff --git a/credential/signing/jws.go b/credential/jws.go similarity index 87% rename from credential/signing/jws.go rename to credential/jws.go index 528bad86..6a046055 100644 --- a/credential/signing/jws.go +++ b/credential/jws.go @@ -1,7 +1,6 @@ -package signing +package credential import ( - "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/goccy/go-json" "github.com/lestrrat-go/jwx/v2/jwa" @@ -15,7 +14,7 @@ const ( // SignVerifiableCredentialJWS is prepared according to https://transmute-industries.github.io/vc-jws/. // This is currently an experimental. It's unstable and subject to change. Use at your own peril. -func SignVerifiableCredentialJWS(signer crypto.JWTSigner, cred credential.VerifiableCredential) ([]byte, error) { +func SignVerifiableCredentialJWS(signer crypto.JWTSigner, cred VerifiableCredential) ([]byte, error) { payload, err := json.Marshal(cred) if err != nil { return nil, errors.Wrap(err, "marshalling credential") @@ -37,7 +36,7 @@ func SignVerifiableCredentialJWS(signer crypto.JWTSigner, cred credential.Verifi // ParseVerifiableCredentialFromJWS parses a JWS. Depending on the `cty` header value, it parses as a JWT or simply // decodes the payload. // This is currently an experimental. It's unstable and subject to change. Use at your own peril. -func ParseVerifiableCredentialFromJWS(token string) (*jws.Message, *credential.VerifiableCredential, error) { +func ParseVerifiableCredentialFromJWS(token string) (*jws.Message, *VerifiableCredential, error) { parsed, err := jws.Parse([]byte(token)) if err != nil { return nil, nil, errors.Wrap(err, "parsing JWS") @@ -54,7 +53,7 @@ func ParseVerifiableCredentialFromJWS(token string) (*jws.Message, *credential.V return parsed, cred, err } - var cred credential.VerifiableCredential + var cred VerifiableCredential if err = json.Unmarshal(parsed.Payload(), &cred); err != nil { return nil, nil, errors.Wrap(err, "reconstructing Verifiable Credential") } @@ -65,7 +64,7 @@ func ParseVerifiableCredentialFromJWS(token string) (*jws.Message, *credential.V // VerifyVerifiableCredentialJWS verifies the signature validity on the token and parses // the token in a verifiable credential. // This is currently an experimental. It's unstable and subject to change. Use at your own peril. -func VerifyVerifiableCredentialJWS(verifier crypto.JWTVerifier, token string) (*jws.Message, *credential.VerifiableCredential, error) { +func VerifyVerifiableCredentialJWS(verifier crypto.JWTVerifier, token string) (*jws.Message, *VerifiableCredential, error) { if err := verifier.VerifyJWS(token); err != nil { return nil, nil, errors.Wrap(err, "verifying JWS") } diff --git a/credential/signing/jws_test.go b/credential/jws_test.go similarity index 92% rename from credential/signing/jws_test.go rename to credential/jws_test.go index 218fc1df..005d2e61 100644 --- a/credential/signing/jws_test.go +++ b/credential/jws_test.go @@ -1,15 +1,14 @@ -package signing +package credential import ( "testing" - "github.com/TBD54566975/ssi-sdk/credential" "github.com/lestrrat-go/jwx/v2/jws" "github.com/stretchr/testify/assert" ) func TestVerifiableCredentialJWS(t *testing.T) { - testCredential := credential.VerifiableCredential{ + testCredential := VerifiableCredential{ Context: []any{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, Type: []any{"VerifiableCredential"}, Issuer: "did:example:123", @@ -43,7 +42,7 @@ func TestVerifiableCredentialJWS(t *testing.T) { signed, err := SignVerifiableCredentialJWT(signer, testCredential) assert.NoError(t, err) - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(signer.ID) assert.NoError(t, err) token := string(signed) @@ -55,7 +54,7 @@ func TestVerifiableCredentialJWS(t *testing.T) { signed, err := SignVerifiableCredentialJWS(signer, testCredential) assert.NoError(t, err) - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(signer.ID) assert.NoError(t, err) token := string(signed) diff --git a/credential/signing/jwt.go b/credential/jwt.go similarity index 76% rename from credential/signing/jwt.go rename to credential/jwt.go index d0bb35ed..298d158f 100644 --- a/credential/signing/jwt.go +++ b/credential/jwt.go @@ -1,11 +1,12 @@ -package signing +package credential import ( + "context" "fmt" "time" - "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" "github.com/goccy/go-json" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jws" @@ -21,7 +22,7 @@ const ( // SignVerifiableCredentialJWT is prepared according to https://w3c.github.io/vc-jwt/#version-1.1 // which will soon be deprecated by https://w3c.github.io/vc-jwt/ see: https://github.com/TBD54566975/ssi-sdk/issues/191 -func SignVerifiableCredentialJWT(signer crypto.JWTSigner, cred credential.VerifiableCredential) ([]byte, error) { +func SignVerifiableCredentialJWT(signer crypto.JWTSigner, cred VerifiableCredential) ([]byte, error) { if cred.IsEmpty() { return nil, errors.New("credential cannot be empty") } @@ -89,7 +90,9 @@ func SignVerifiableCredentialJWT(signer crypto.JWTSigner, cred credential.Verifi // VerifyVerifiableCredentialJWT verifies the signature validity on the token and parses // the token in a verifiable credential. -func VerifyVerifiableCredentialJWT(verifier crypto.JWTVerifier, token string) (jws.Headers, jwt.Token, *credential.VerifiableCredential, error) { +// TODO(gabe) modify this to add additional verification steps such as credential status, expiration, etc. +// related to https://github.com/TBD54566975/ssi-service/issues/122 +func VerifyVerifiableCredentialJWT(verifier crypto.JWTVerifier, token string) (jws.Headers, jwt.Token, *VerifiableCredential, error) { if err := verifier.Verify(token); err != nil { return nil, nil, nil, errors.Wrap(err, "verifying JWT") } @@ -100,14 +103,14 @@ func VerifyVerifiableCredentialJWT(verifier crypto.JWTVerifier, token string) (j // https://www.w3.org/TR/vc-data-model/#jwt-decoding // If there are any issues during decoding, an error is returned. As a result, a successfully // decoded VerifiableCredential object is returned. -func ParseVerifiableCredentialFromJWT(token string) (jws.Headers, jwt.Token, *credential.VerifiableCredential, error) { +func ParseVerifiableCredentialFromJWT(token string) (jws.Headers, jwt.Token, *VerifiableCredential, error) { parsed, err := jwt.Parse([]byte(token), jwt.WithValidate(false), jwt.WithVerify(false)) if err != nil { return nil, nil, nil, errors.Wrap(err, "parsing credential token") } // get headers - headers, err := getJWTHeaders([]byte(token)) + headers, err := GetJWTHeaders([]byte(token)) if err != nil { return nil, nil, nil, errors.Wrap(err, "could not get JWT headers") } @@ -122,7 +125,7 @@ func ParseVerifiableCredentialFromJWT(token string) (jws.Headers, jwt.Token, *cr } // ParseVerifiableCredentialFromToken takes a JWT object and parses it into a VerifiableCredential -func ParseVerifiableCredentialFromToken(token jwt.Token) (*credential.VerifiableCredential, error) { +func ParseVerifiableCredentialFromToken(token jwt.Token) (*VerifiableCredential, error) { // parse remaining JWT properties and set in the credential vcClaim, ok := token.Get(VCJWTProperty) if !ok { @@ -132,7 +135,7 @@ func ParseVerifiableCredentialFromToken(token jwt.Token) (*credential.Verifiable if err != nil { return nil, errors.Wrap(err, "marshalling credential claim") } - var cred credential.VerifiableCredential + var cred VerifiableCredential if err = json.Unmarshal(vcBytes, &cred); err != nil { return nil, errors.Wrap(err, "reconstructing Verifiable Credential") } @@ -168,7 +171,7 @@ func ParseVerifiableCredentialFromToken(token jwt.Token) (*credential.Verifiable if cred.CredentialSubject == nil { cred.CredentialSubject = make(map[string]any) } - cred.CredentialSubject[credential.VerifiableCredentialIDProperty] = subStr + cred.CredentialSubject[VerifiableCredentialIDProperty] = subStr } return &cred, nil @@ -184,7 +187,7 @@ type JWTVVPParameters struct { // SignVerifiablePresentationJWT transforms a VP into a VP JWT and signs it // According to https://w3c.github.io/vc-jwt/#version-1.1 -func SignVerifiablePresentationJWT(signer crypto.JWTSigner, parameters JWTVVPParameters, presentation credential.VerifiablePresentation) ([]byte, error) { +func SignVerifiablePresentationJWT(signer crypto.JWTSigner, parameters JWTVVPParameters, presentation VerifiablePresentation) ([]byte, error) { if parameters.Audience == "" { return nil, errors.New("audience cannot be empty") } @@ -245,23 +248,60 @@ func SignVerifiablePresentationJWT(signer crypto.JWTSigner, parameters JWTVVPPar return signed, nil } -// VerifyVerifiablePresentationJWT verifies the signature validity on the token. -// After signature validation, the JWT is decoded according to the specification. -// https://www.w3.org/TR/vc-data-model/#jwt-decoding -// If there are any issues during decoding, an error is returned. As a result, a successfully -// decoded VerifiablePresentation object is returned. -func VerifyVerifiablePresentationJWT(verifier crypto.JWTVerifier, token string) (jws.Headers, jwt.Token, *credential.VerifiablePresentation, error) { +// VerifyVerifiablePresentationJWT verifies the signature validity on the token. Then, the JWT is decoded according +// to the specification: https://www.w3.org/TR/vc-data-model/#jwt-decoding +// After decoding the signature of each credential in the presentation is verified. If there are any issues during +// decoding or signature validation, an error is returned. As a result, a successfully decoded VerifiablePresentation +// object is returned. +func VerifyVerifiablePresentationJWT(ctx context.Context, verifier crypto.JWTVerifier, resolver did.Resolver, token string) (jws.Headers, jwt.Token, *VerifiablePresentation, error) { + if resolver == nil { + return nil, nil, nil, errors.New("resolver cannot be empty") + } + + // verify outer signature on the token if err := verifier.Verify(token); err != nil { return nil, nil, nil, errors.Wrap(err, "verifying JWT and its signature") } - return ParseVerifiablePresentationFromJWT(token) + + // parse the token into its parts (header, jwt, vp) + headers, vpToken, vp, err := ParseVerifiablePresentationFromJWT(token) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "parsing VP from JWT") + } + + // make sure the audience matches the verifier + audMatch := false + for _, aud := range vpToken.Audience() { + if aud == verifier.ID || aud == verifier.KeyID() { + audMatch = true + break + } + } + if !audMatch { + return nil, nil, nil, errors.Errorf("audience mismatch: expected [%s] or [%s], got %s", verifier.ID, verifier.KeyID(), vpToken.Audience()) + } + + // verify signature for each credential in the vp + for i, cred := range vp.VerifiableCredential { + // verify the signature on the credential + verified, err := VerifyCredentialSignature(ctx, cred, resolver) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "verifying credential %d", i) + } + if !verified { + return nil, nil, nil, errors.Errorf("credential %d failed signature verification", i) + } + } + + // return if successful + return headers, vpToken, vp, nil } // ParseVerifiablePresentationFromJWT the JWT is decoded according to the specification. // https://www.w3.org/TR/vc-data-model/#jwt-decoding // If there are any issues during decoding, an error is returned. As a result, a successfully // decoded VerifiablePresentation object is returned. -func ParseVerifiablePresentationFromJWT(token string) (jws.Headers, jwt.Token, *credential.VerifiablePresentation, error) { +func ParseVerifiablePresentationFromJWT(token string) (jws.Headers, jwt.Token, *VerifiablePresentation, error) { parsed, err := jwt.Parse([]byte(token), jwt.WithValidate(false), jwt.WithVerify(false)) if err != nil { return nil, nil, nil, errors.Wrap(err, "parsing vp token") @@ -274,13 +314,13 @@ func ParseVerifiablePresentationFromJWT(token string) (jws.Headers, jwt.Token, * if err != nil { return nil, nil, nil, errors.Wrap(err, "could not marshalling vp claim") } - var pres credential.VerifiablePresentation + var pres VerifiablePresentation if err = json.Unmarshal(vpBytes, &pres); err != nil { return nil, nil, nil, errors.Wrap(err, "reconstructing Verifiable Presentation") } // get headers - headers, err := getJWTHeaders([]byte(token)) + headers, err := GetJWTHeaders([]byte(token)) if err != nil { return nil, nil, nil, errors.Wrap(err, "could not get JWT headers") } @@ -305,7 +345,8 @@ func ParseVerifiablePresentationFromJWT(token string) (jws.Headers, jwt.Token, * return headers, parsed, &pres, nil } -func getJWTHeaders(token []byte) (jws.Headers, error) { +// GetJWTHeaders returns the headers of a JWT token, assuming there is only one signature. +func GetJWTHeaders(token []byte) (jws.Headers, error) { msg, err := jws.Parse(token) if err != nil { return nil, err diff --git a/credential/jwt_test.go b/credential/jwt_test.go new file mode 100644 index 00000000..e4cd3e98 --- /dev/null +++ b/credential/jwt_test.go @@ -0,0 +1,244 @@ +//go:build jwx_es256k + +package credential + +import ( + "context" + "testing" + "time" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVerifiableCredentialJWT(t *testing.T) { + testCredential := VerifiableCredential{ + ID: "http://example.edu/credentials/1872", + Context: []any{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, + Type: []string{"VerifiableCredential"}, + Issuer: "did:example:123", + IssuanceDate: "2021-01-01T19:23:24Z", + CredentialSubject: map[string]any{ + "id": "did:example:456", + "name": "JimBobertson", + }, + } + + t.Run("Known JWK Signer", func(t *testing.T) { + signer := getTestVectorKey0Signer(t) + signed, err := SignVerifiableCredentialJWT(signer, testCredential) + assert.NoError(t, err) + + verifier, err := signer.ToVerifier(signer.ID) + assert.NoError(t, err) + + token := string(signed) + err = verifier.Verify(token) + assert.NoError(t, err) + + parsedHeaders, parsedJWT, parsedCred, err := ParseVerifiableCredentialFromJWT(token) + assert.NoError(t, err) + assert.NotEmpty(t, parsedJWT) + assert.NotEmpty(t, parsedCred) + assert.NotEmpty(t, parsedHeaders) + + headers, verifiedJWT, cred, err := VerifyVerifiableCredentialJWT(*verifier, token) + assert.NoError(t, err) + assert.NotEmpty(t, verifiedJWT) + assert.NotEmpty(t, cred) + assert.NotEmpty(t, headers) + assert.Equal(t, parsedJWT, verifiedJWT) + assert.Equal(t, parsedCred, cred) + assert.Equal(t, parsedHeaders, headers) + }) + + t.Run("Generated Private Key For Signer", func(tt *testing.T) { + _, privKey, err := crypto.GenerateEd25519Key() + assert.NoError(tt, err) + + signer, err := crypto.NewJWTSigner("test-id", "test-kid", privKey) + assert.NoError(tt, err) + + signed, err := SignVerifiableCredentialJWT(*signer, testCredential) + assert.NoError(tt, err) + + verifier, err := signer.ToVerifier(signer.ID) + assert.NoError(tt, err) + + token := string(signed) + err = verifier.Verify(token) + assert.NoError(tt, err) + + parsedHeaders, parsedJWT, parsedCred, err := ParseVerifiableCredentialFromJWT(token) + assert.NoError(tt, err) + assert.NotEmpty(tt, parsedJWT) + assert.NotEmpty(tt, parsedHeaders) + assert.NotEmpty(tt, parsedCred) + + verifiedHeaders, verifiedJWT, cred, err := VerifyVerifiableCredentialJWT(*verifier, token) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifiedJWT) + assert.Equal(tt, parsedJWT, verifiedJWT) + assert.Equal(tt, parsedCred, cred) + assert.Equal(tt, parsedHeaders, verifiedHeaders) + }) +} + +func TestVerifiablePresentationJWT(t *testing.T) { + t.Run("bad audience", func(tt *testing.T) { + testPresentation := VerifiablePresentation{ + Context: []string{"https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/jws-2020/v1"}, + Type: []string{"VerifiablePresentation"}, + Holder: "did:example:123", + } + + signer := getTestVectorKey0Signer(tt) + signed, err := SignVerifiablePresentationJWT(signer, JWTVVPParameters{Audience: "bad-audience"}, testPresentation) + assert.NoError(tt, err) + + verifier, err := signer.ToVerifier(signer.ID) + assert.NoError(tt, err) + + token := string(signed) + err = verifier.Verify(token) + assert.NoError(tt, err) + + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + require.NoError(tt, err) + require.NotEmpty(tt, resolver) + + _, _, _, err = VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, token) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "audience mismatch") + }) + + t.Run("no VCs", func(tt *testing.T) { + testPresentation := VerifiablePresentation{ + Context: []string{"https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/jws-2020/v1"}, + Type: []string{"VerifiablePresentation"}, + Holder: "did:example:123", + } + + signer := getTestVectorKey0Signer(tt) + signed, err := SignVerifiablePresentationJWT(signer, JWTVVPParameters{Audience: signer.ID}, testPresentation) + assert.NoError(tt, err) + + verifier, err := signer.ToVerifier(signer.ID) + assert.NoError(tt, err) + + token := string(signed) + err = verifier.Verify(token) + assert.NoError(tt, err) + + parsedHeaders, parsedJWT, parsedPres, err := ParseVerifiablePresentationFromJWT(token) + assert.NoError(tt, err) + assert.NotEmpty(tt, parsedJWT) + assert.NotEmpty(tt, parsedHeaders) + assert.NotEmpty(tt, parsedPres) + + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + require.NoError(tt, err) + require.NotEmpty(tt, resolver) + + parsedHeaders, verifiedJWT, pres, err := VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, token) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifiedJWT) + assert.NotEmpty(tt, parsedHeaders) + assert.Equal(tt, parsedJWT, verifiedJWT) + assert.Equal(tt, parsedPres, pres) + }) + + t.Run("with VC a single valid VC JWT", func(tt *testing.T) { + issuerPrivKey, issuerDID, err := did.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerPrivKey) + assert.NotEmpty(tt, issuerDID) + expandedIssuerDID, err := issuerDID.Expand() + assert.NoError(tt, err) + assert.NotEmpty(tt, expandedIssuerDID) + issuerKID := expandedIssuerDID.VerificationMethod[0].ID + assert.NotEmpty(tt, issuerKID) + + subjectPrivKey, subjectDID, err := did.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, subjectPrivKey) + assert.NotEmpty(tt, subjectDID) + expandedSubjectDID, err := subjectDID.Expand() + assert.NoError(tt, err) + assert.NotEmpty(tt, expandedSubjectDID) + subjectKID := expandedSubjectDID.VerificationMethod[0].ID + assert.NotEmpty(tt, subjectKID) + + testCredential := VerifiableCredential{ + ID: uuid.NewString(), + Context: []any{"https://www.w3.org/2018/credentials/v1"}, + Type: []string{"VerifiableCredential"}, + Issuer: issuerDID.String(), + IssuanceDate: time.Now().Format(time.RFC3339), + CredentialSubject: map[string]any{ + "id": subjectDID.String(), + "name": "Toshi", + }, + } + + issuerSigner, err := crypto.NewJWTSigner(issuerDID.String(), issuerKID, issuerPrivKey) + assert.NoError(tt, err) + signedVC, err := SignVerifiableCredentialJWT(*issuerSigner, testCredential) + assert.NoError(t, err) + + testPresentation := VerifiablePresentation{ + Context: []string{"https://www.w3.org/2018/credentials/v1"}, + Type: []string{"VerifiablePresentation"}, + Holder: subjectDID.String(), + VerifiableCredential: []any{ + string(signedVC), + }, + } + + // sign the presentation from the subject to the issuer + subjectSigner, err := crypto.NewJWTSigner(subjectDID.String(), subjectKID, subjectPrivKey) + assert.NoError(tt, err) + signed, err := SignVerifiablePresentationJWT(*subjectSigner, JWTVVPParameters{Audience: issuerDID.String()}, testPresentation) + assert.NoError(tt, err) + + // parse the VP + parsedHeaders, parsedJWT, parsedPres, err := ParseVerifiablePresentationFromJWT(string(signed)) + assert.NoError(tt, err) + assert.NotEmpty(tt, parsedJWT) + assert.NotEmpty(tt, parsedHeaders) + assert.NotEmpty(tt, parsedPres) + + // Verify the VP JWT + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + require.NoError(tt, err) + require.NotEmpty(tt, resolver) + + verifier, err := subjectSigner.ToVerifier(issuerDID.String()) + assert.NoError(tt, err) + parsedHeaders, verifiedJWT, pres, err := VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(signed)) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifiedJWT) + assert.NotEmpty(tt, parsedHeaders) + assert.Equal(tt, parsedJWT, verifiedJWT) + assert.Equal(tt, parsedPres, pres) + }) +} + +func getTestVectorKey0Signer(t *testing.T) crypto.JWTSigner { + // https://github.com/decentralized-identity/JWS-Test-Suite/blob/main/data/keys/key-0-ed25519.json + knownJWK := crypto.PrivateKeyJWK{ + KTY: "OKP", + CRV: "Ed25519", + X: "JYCAGl6C7gcDeKbNqtXBfpGzH0f5elifj7L6zYNj_Is", + D: "pLMxJruKPovJlxF3Lu_x9Aw3qe2wcj5WhKUAXYLBjwE", + } + + signer, err := crypto.NewJWTSignerFromJWK("signer-id", knownJWK.KID, knownJWK) + assert.NoError(t, err) + return *signer +} diff --git a/credential/manifest/validation.go b/credential/manifest/validation.go index 45356138..f618bc70 100644 --- a/credential/manifest/validation.go +++ b/credential/manifest/validation.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + credutil "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/credential/exchange" - credutil "github.com/TBD54566975/ssi-sdk/credential/util" errresp "github.com/TBD54566975/ssi-sdk/error" "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" @@ -177,7 +177,7 @@ func IsValidCredentialApplicationForManifest(cm CredentialManifest, applicationA } // convert submitted claim vc to map[string]any - cred, credErr := credutil.ToCredential(submittedClaim) + _, _, cred, credErr := credutil.ToCredential(submittedClaim) if credErr != nil { unfulfilledInputDescriptors[inputDescriptor.ID] = "failed to extract credential from json" continue diff --git a/credential/manifest/validation_test.go b/credential/manifest/validation_test.go index 8d67def9..70a452ef 100644 --- a/credential/manifest/validation_test.go +++ b/credential/manifest/validation_test.go @@ -5,7 +5,6 @@ import ( "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/credential/exchange" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/cryptosuite" "github.com/goccy/go-json" @@ -412,7 +411,7 @@ func getValidTestCredManifestCredApplicationJWTCred(t *testing.T) (CredentialMan require.NoError(t, err) signer, err := crypto.NewJWTSigner("test-id", "test-kid", privKey) require.NoError(t, err) - jwt, err := signing.SignVerifiableCredentialJWT(*signer, vc) + jwt, err := credential.SignVerifiableCredentialJWT(*signer, vc) require.NoError(t, err) require.NotEmpty(t, jwt) diff --git a/credential/model_test.go b/credential/model_test.go index 8c6965bd..a6613dcd 100644 --- a/credential/model_test.go +++ b/credential/model_test.go @@ -42,7 +42,7 @@ func TestVCVectors(t *testing.T) { vcBytes, err := json.Marshal(vc) assert.NoError(t, err) - assert.JSONEqf(t, gotTestVector, string(vcBytes), "error message %s") + assert.JSONEq(t, gotTestVector, string(vcBytes)) } } @@ -61,7 +61,7 @@ func TestVPVectors(t *testing.T) { vpBytes, err := json.Marshal(vp) assert.NoError(t, err) - assert.JSONEqf(t, gotTestVector, string(vpBytes), "error message %s") + assert.JSONEq(t, gotTestVector, string(vpBytes)) } } diff --git a/credential/signature.go b/credential/signature.go new file mode 100644 index 00000000..4c36226f --- /dev/null +++ b/credential/signature.go @@ -0,0 +1,94 @@ +package credential + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/pkg/errors" +) + +// VerifyCredentialSignature verifies the signature of a credential of any type +// TODO(gabe) support other types of credentials https://github.com/TBD54566975/ssi-sdk/issues/352 +func VerifyCredentialSignature(ctx context.Context, genericCred any, resolver did.Resolver) (bool, error) { + if genericCred == nil { + return false, errors.New("credential cannot be empty") + } + if resolver == nil { + return false, errors.New("resolver cannot be empty") + } + switch genericCred.(type) { + case *VerifiableCredential, VerifiableCredential, map[string]any: + _, token, cred, err := ToCredential(genericCred) + if err != nil { + return false, errors.Wrap(err, "error converting credential from generic type") + } + if token != nil { + return false, errors.New("JWT credentials must include a signature to be verified") + } + if cred.IsEmpty() { + return false, errors.New("credential cannot be empty") + } + if cred.GetProof() == nil { + return false, errors.New("credential must have a proof") + } + return false, errors.New("data integrity signature verification not yet implemented") + case []byte: + // turn it into a string and try again + return VerifyCredentialSignature(ctx, string(genericCred.([]byte)), resolver) + case string: + // could be a Data Integrity credential + var cred VerifiableCredential + if err := json.Unmarshal([]byte(genericCred.(string)), &cred); err == nil { + return VerifyCredentialSignature(ctx, cred, resolver) + } + + // could be a JWT + return VerifyJWTCredential(genericCred.(string), resolver) + } + return false, fmt.Errorf("invalid credential type: %s", reflect.TypeOf(genericCred).Kind().String()) +} + +// VerifyJWTCredential verifies the signature of a JWT credential after parsing it to resolve the issuer DID +// The issuer DID is resolver from the provided resolver, and used to find the issuer's public key matching +// the KID in the JWT header. +func VerifyJWTCredential(cred string, resolver did.Resolver) (bool, error) { + if cred == "" { + return false, errors.New("credential cannot be empty") + } + if resolver == nil { + return false, errors.New("resolver cannot be empty") + } + headers, token, _, err := ParseVerifiableCredentialFromJWT(cred) + if err != nil { + return false, errors.Wrap(err, "parsing JWT") + } + + // get key to verify the credential with + issuerKID := headers.KeyID() + if issuerKID == "" { + return false, errors.Errorf("missing kid in header of credential<%s>", token.JwtID()) + } + issuerDID, err := resolver.Resolve(context.Background(), token.Issuer()) + if err != nil { + return false, errors.Wrapf(err, "error getting issuer DID<%s> to verify credential<%s>", token.Issuer(), token.JwtID()) + } + issuerKey, err := did.GetKeyFromVerificationMethod(issuerDID.Document, issuerKID) + if err != nil { + return false, errors.Wrapf(err, "error getting key to verify credential<%s>", token.JwtID()) + } + + // construct a verifier + credVerifier, err := crypto.NewJWTVerifier(issuerDID.ID, issuerKey) + if err != nil { + return false, errors.Wrapf(err, "error constructing verifier for credential<%s>", token.JwtID()) + } + // verify the signature + if err = credVerifier.Verify(cred); err != nil { + return false, errors.Wrapf(err, "error verifying credential<%s>", token.JwtID()) + } + return true, nil +} diff --git a/credential/signature_test.go b/credential/signature_test.go new file mode 100644 index 00000000..74580dca --- /dev/null +++ b/credential/signature_test.go @@ -0,0 +1,244 @@ +package credential + +import ( + "context" + "testing" + "time" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/goccy/go-json" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVerifyCredentialSignature(t *testing.T) { + t.Run("empty credential", func(tt *testing.T) { + _, err := VerifyCredentialSignature(context.Background(), nil, nil) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential cannot be empty") + }) + + t.Run("empty resolver", func(tt *testing.T) { + _, err := VerifyCredentialSignature(context.Background(), "not-empty", nil) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "resolver cannot be empty") + }) + + t.Run("invalid credential type - int", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + _, err = VerifyCredentialSignature(context.Background(), 5, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "invalid credential type: int") + }) + + t.Run("empty map credential type", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + _, err = VerifyCredentialSignature(context.Background(), map[string]any{"a": "test"}, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "converting credential from generic type: parsing generic credential as either VC or JWT") + }) + + t.Run("data integrity map credential type missing proof", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + credential := getTestCredential() + credMap, err := ToCredentialJSONMap(credential) + assert.NoError(tt, err) + + _, err = VerifyCredentialSignature(context.Background(), credMap, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential must have a proof") + }) + + t.Run("data integrity credential - no proof", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + credential := getTestCredential() + _, err = VerifyCredentialSignature(context.Background(), credential, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential must have a proof") + + // test with a pointer + _, err = VerifyCredentialSignature(context.Background(), &credential, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential must have a proof") + }) + + t.Run("data integrity credential - as bytes and string", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + credential := getTestCredential() + credBytes, err := json.Marshal(credential) + assert.NoError(tt, err) + _, err = VerifyCredentialSignature(context.Background(), credBytes, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential must have a proof") + + // test with a string + _, err = VerifyCredentialSignature(context.Background(), string(credBytes), resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential must have a proof") + }) + + t.Run("jwt credential - as bytes and string", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + privKey, didKey, err := did.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + expanded, err := didKey.Expand() + assert.NoError(tt, err) + kid := expanded.VerificationMethod[0].ID + signer, err := crypto.NewJWTSigner(didKey.String(), kid, privKey) + assert.NoError(tt, err) + + jwtCred := getTestJWTCredential(tt, *signer) + verified, err := VerifyCredentialSignature(context.Background(), jwtCred, resolver) + assert.NoError(tt, err) + assert.True(tt, verified) + + // test with bytes + verified, err = VerifyCredentialSignature(context.Background(), []byte(jwtCred), resolver) + assert.NoError(tt, err) + assert.True(tt, verified) + }) +} + +func TestVerifyJWTCredential(t *testing.T) { + t.Run("empty credential", func(tt *testing.T) { + _, err := VerifyJWTCredential("", nil) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential cannot be empty") + }) + + t.Run("empty resolver", func(tt *testing.T) { + _, err := VerifyJWTCredential("not-empty", nil) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "resolver cannot be empty") + }) + + t.Run("invalid credential", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + _, err = VerifyJWTCredential("not-empty", resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "invalid JWT") + }) + + t.Run("valid credential, not signed by DID", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + _, privKey, err := crypto.GenerateEd25519Key() + assert.NoError(tt, err) + signer, err := crypto.NewJWTSigner("test-id", "test-kid", privKey) + assert.NoError(tt, err) + + jwtCred := getTestJWTCredential(tt, *signer) + _, err = VerifyJWTCredential(jwtCred, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "error getting issuer DID to verify credential") + }) + + t.Run("valid credential, signed by DID the resolver can't resolve", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.WebResolver{}}...) + assert.NoError(tt, err) + + privKey, didKey, err := did.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + expanded, err := didKey.Expand() + assert.NoError(tt, err) + kid := expanded.VerificationMethod[0].ID + signer, err := crypto.NewJWTSigner(didKey.String(), kid, privKey) + assert.NoError(tt, err) + + jwtCred := getTestJWTCredential(tt, *signer) + _, err = VerifyJWTCredential(jwtCred, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "unsupported method: key") + }) + + t.Run("valid credential, kid not found", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + privKey, didKey, err := did.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + signer, err := crypto.NewJWTSigner(didKey.String(), "missing", privKey) + assert.NoError(tt, err) + + jwtCred := getTestJWTCredential(tt, *signer) + _, err = VerifyJWTCredential(jwtCred, resolver) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "has no verification methods with kid: missing") + }) + + t.Run("valid credential, bad signature", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + privKey, didKey, err := did.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + expanded, err := didKey.Expand() + assert.NoError(tt, err) + kid := expanded.VerificationMethod[0].ID + signer, err := crypto.NewJWTSigner(didKey.String(), kid, privKey) + assert.NoError(tt, err) + + jwtCred := getTestJWTCredential(tt, *signer) + + // modify the signature to make it invalid + jwtCred = jwtCred[:len(jwtCred)-1] + "a" + + verified, err := VerifyJWTCredential(jwtCred, resolver) + assert.Error(tt, err) + assert.False(tt, verified) + }) + + t.Run("valid credential", func(tt *testing.T) { + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + assert.NoError(tt, err) + + privKey, didKey, err := did.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + expanded, err := didKey.Expand() + assert.NoError(tt, err) + kid := expanded.VerificationMethod[0].ID + signer, err := crypto.NewJWTSigner(didKey.String(), kid, privKey) + assert.NoError(tt, err) + + jwtCred := getTestJWTCredential(tt, *signer) + verified, err := VerifyJWTCredential(jwtCred, resolver) + assert.NoError(tt, err) + assert.True(tt, verified) + }) +} + +func getTestJWTCredential(t *testing.T, signer crypto.JWTSigner) string { + cred := VerifiableCredential{ + ID: uuid.NewString(), + Context: []any{"https://www.w3.org/2018/credentials/v1"}, + Type: []string{"VerifiableCredential"}, + Issuer: signer.ID, + IssuanceDate: time.Now().Format(time.RFC3339), + CredentialSubject: map[string]any{ + "id": "did:example:123", + "favoriteColor": "green", + "favoriteFood": "pizza", + }, + } + + signed, err := SignVerifiableCredentialJWT(signer, cred) + require.NoError(t, err) + require.NotEmpty(t, signed) + return string(signed) +} diff --git a/credential/signing/jwt_test.go b/credential/signing/jwt_test.go deleted file mode 100644 index b466b40a..00000000 --- a/credential/signing/jwt_test.go +++ /dev/null @@ -1,132 +0,0 @@ -//go:build jwx_es256k - -package signing - -import ( - "testing" - - "github.com/TBD54566975/ssi-sdk/crypto" - "github.com/stretchr/testify/assert" - - "github.com/TBD54566975/ssi-sdk/credential" -) - -func TestVerifiableCredentialJWT(t *testing.T) { - testCredential := credential.VerifiableCredential{ - ID: "http://example.edu/credentials/1872", - Context: []any{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, - Type: []string{"VerifiableCredential"}, - Issuer: "did:example:123", - IssuanceDate: "2021-01-01T19:23:24Z", - CredentialSubject: map[string]any{ - "id": "did:example:456", - "name": "JimBobertson", - }, - } - - t.Run("Known JWK Signer", func(t *testing.T) { - signer := getTestVectorKey0Signer(t) - signed, err := SignVerifiableCredentialJWT(signer, testCredential) - assert.NoError(t, err) - - verifier, err := signer.ToVerifier() - assert.NoError(t, err) - - token := string(signed) - err = verifier.Verify(token) - assert.NoError(t, err) - - parsedHeaders, parsedJWT, parsedCred, err := ParseVerifiableCredentialFromJWT(token) - assert.NoError(t, err) - assert.NotEmpty(t, parsedJWT) - assert.NotEmpty(t, parsedCred) - assert.NotEmpty(t, parsedHeaders) - - headers, verifiedJWT, cred, err := VerifyVerifiableCredentialJWT(*verifier, token) - assert.NoError(t, err) - assert.NotEmpty(t, verifiedJWT) - assert.NotEmpty(t, cred) - assert.NotEmpty(t, headers) - assert.Equal(t, parsedJWT, verifiedJWT) - assert.Equal(t, parsedCred, cred) - assert.Equal(t, parsedHeaders, headers) - }) - - t.Run("Generated Private Key For Signer", func(tt *testing.T) { - _, privKey, err := crypto.GenerateEd25519Key() - assert.NoError(tt, err) - - signer, err := crypto.NewJWTSigner("test-id", "test-kid", privKey) - assert.NoError(tt, err) - - signed, err := SignVerifiableCredentialJWT(*signer, testCredential) - assert.NoError(tt, err) - - verifier, err := signer.ToVerifier() - assert.NoError(tt, err) - - token := string(signed) - err = verifier.Verify(token) - assert.NoError(tt, err) - - parsedHeaders, parsedJWT, parsedCred, err := ParseVerifiableCredentialFromJWT(token) - assert.NoError(tt, err) - assert.NotEmpty(tt, parsedJWT) - assert.NotEmpty(tt, parsedHeaders) - assert.NotEmpty(tt, parsedCred) - - verifiedHeaders, verifiedJWT, cred, err := VerifyVerifiableCredentialJWT(*verifier, token) - assert.NoError(tt, err) - assert.NotEmpty(tt, verifiedJWT) - assert.Equal(tt, parsedJWT, verifiedJWT) - assert.Equal(tt, parsedCred, cred) - assert.Equal(tt, parsedHeaders, verifiedHeaders) - }) -} - -func TestVerifiablePresentationJWT(t *testing.T) { - testPresentation := credential.VerifiablePresentation{ - Context: []string{"https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1"}, - Type: []string{"VerifiablePresentation"}, - Holder: "did:example:123", - } - - signer := getTestVectorKey0Signer(t) - signed, err := SignVerifiablePresentationJWT(signer, JWTVVPParameters{Audience: "did:test:aud"}, testPresentation) - assert.NoError(t, err) - - verifier, err := signer.ToVerifier() - assert.NoError(t, err) - - token := string(signed) - err = verifier.Verify(token) - assert.NoError(t, err) - - parsedHeaders, parsedJWT, parsedPres, err := ParseVerifiablePresentationFromJWT(token) - assert.NoError(t, err) - assert.NotEmpty(t, parsedJWT) - assert.NotEmpty(t, parsedHeaders) - assert.NotEmpty(t, parsedPres) - - parsedHeaders, verifiedJWT, pres, err := VerifyVerifiablePresentationJWT(*verifier, token) - assert.NoError(t, err) - assert.NotEmpty(t, verifiedJWT) - assert.NotEmpty(t, parsedHeaders) - assert.Equal(t, parsedJWT, verifiedJWT) - assert.Equal(t, parsedPres, pres) -} - -func getTestVectorKey0Signer(t *testing.T) crypto.JWTSigner { - // https://github.com/decentralized-identity/JWS-Test-Suite/blob/main/data/keys/key-0-ed25519.json - knownJWK := crypto.PrivateKeyJWK{ - KTY: "OKP", - CRV: "Ed25519", - X: "JYCAGl6C7gcDeKbNqtXBfpGzH0f5elifj7L6zYNj_Is", - D: "pLMxJruKPovJlxF3Lu_x9Aw3qe2wcj5WhKUAXYLBjwE", - } - - signer, err := crypto.NewJWTSignerFromJWK("signer-id", knownJWK.KID, knownJWK) - assert.NoError(t, err) - return *signer -} diff --git a/credential/util.go b/credential/util.go new file mode 100644 index 00000000..1c65361d --- /dev/null +++ b/credential/util.go @@ -0,0 +1,128 @@ +package credential + +import ( + "fmt" + "reflect" + + "github.com/goccy/go-json" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/pkg/errors" +) + +// ToCredential turn a generic cred into its known object model +func ToCredential(genericCred any) (jws.Headers, jwt.Token, *VerifiableCredential, error) { + switch genericCred.(type) { + case []byte: + // could be a JWT + headers, token, vcFromJWT, err := ToCredential(string(genericCred.([]byte))) + if err == nil { + return headers, token, vcFromJWT, err + } + + // could also be a vc + var cred VerifiableCredential + if err = json.Unmarshal(genericCred.([]byte), &cred); err != nil { + return nil, nil, nil, errors.Wrap(err, "unmarshalling credential object") + } + return ToCredential(cred) + case *VerifiableCredential: + return nil, nil, genericCred.(*VerifiableCredential), nil + case VerifiableCredential: + verifiableCredential := genericCred.(VerifiableCredential) + return nil, nil, &verifiableCredential, nil + case string: + // JWT + return ParseVerifiableCredentialFromJWT(genericCred.(string)) + case map[string]any: + // VC or JWTVC JSON + credJSON := genericCred.(map[string]any) + credMapBytes, err := json.Marshal(credJSON) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "marshalling credential map") + } + + // first try as a VC object + var cred VerifiableCredential + if err = json.Unmarshal(credMapBytes, &cred); err == nil && !cred.IsEmpty() { + return nil, nil, &cred, nil + } + + // if that fails, try as a JWT + headers, token, vcFromJWT, err := VCJWTJSONToVC(credMapBytes) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "parsing generic credential as either VC or JWT") + } + return headers, token, vcFromJWT, nil + } + return nil, nil, nil, fmt.Errorf("invalid credential type: %s", reflect.TypeOf(genericCred).Kind().String()) +} + +// ToCredentialJSONMap turn a generic cred into a JSON object +func ToCredentialJSONMap(genericCred any) (map[string]any, error) { + switch genericCred.(type) { + case []byte: + // could be a JWT + credJSON, err := ToCredentialJSONMap(string(genericCred.([]byte))) + if err == nil { + return credJSON, err + } + + // could also be a vc + var cred VerifiableCredential + if err = json.Unmarshal(genericCred.([]byte), &cred); err != nil { + return nil, errors.Wrap(err, "unmarshalling credential object") + } + return ToCredentialJSONMap(cred) + case map[string]any: + return genericCred.(map[string]any), nil + case string: + // JWT + _, token, _, err := ParseVerifiableCredentialFromJWT(genericCred.(string)) + if err != nil { + return nil, errors.Wrap(err, "parsing credential from JWT") + } + // marshal it into a JSON map + tokenJSONBytes, err := json.Marshal(token) + if err != nil { + return nil, errors.Wrap(err, "marshaling credential JWT") + } + var credJSON map[string]any + if err := json.Unmarshal(tokenJSONBytes, &credJSON); err != nil { + return nil, errors.Wrap(err, "unmarshalling credential JWT") + } + return credJSON, nil + case VerifiableCredential, *VerifiableCredential: + credJSONBytes, err := json.Marshal(genericCred) + if err != nil { + return nil, errors.Wrap(err, "marshalling credential object") + } + var credJSON map[string]any + if err := json.Unmarshal(credJSONBytes, &credJSON); err != nil { + return nil, errors.Wrap(err, "unmarshalling credential object") + } + return credJSON, nil + } + return nil, fmt.Errorf("invalid credential type: %s", reflect.TypeOf(genericCred).Kind().String()) +} + +// VCJWTJSONToVC converts a JSON representation of a VC JWT into a VerifiableCredential +func VCJWTJSONToVC(vcJWTJSON []byte) (jws.Headers, jwt.Token, *VerifiableCredential, error) { + // next, try to turn it into a JWT to check if it's a VC JWT + token, err := jwt.Parse(vcJWTJSON, jwt.WithValidate(false), jwt.WithVerify(false)) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "coercing generic cred to JWT") + } + + // get headers + headers, err := GetJWTHeaders(vcJWTJSON) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "could not get JWT headers") + } + + cred, err := ParseVerifiableCredentialFromToken(token) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "parsing credential from token") + } + return headers, token, cred, nil +} diff --git a/credential/util/util.go b/credential/util/util.go deleted file mode 100644 index 671f368f..00000000 --- a/credential/util/util.go +++ /dev/null @@ -1,99 +0,0 @@ -package util - -import ( - "fmt" - "reflect" - - "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" - "github.com/goccy/go-json" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/pkg/errors" -) - -// ToCredential turn a generic cred into its known object model -func ToCredential(genericCred any) (*credential.VerifiableCredential, error) { - switch genericCred.(type) { - case *credential.VerifiableCredential: - return genericCred.(*credential.VerifiableCredential), nil - case credential.VerifiableCredential: - verifiableCredential := genericCred.(credential.VerifiableCredential) - return &verifiableCredential, nil - case string: - // JWT - _, _, parsedCred, err := signing.ParseVerifiableCredentialFromJWT(genericCred.(string)) - if err != nil { - return nil, errors.Wrap(err, "parsing credential from JWT") - } - return parsedCred, nil - case map[string]any: - // VC or JWTVC JSON - credJSON := genericCred.(map[string]any) - credMapBytes, marshalErr := json.Marshal(credJSON) - if marshalErr != nil { - return nil, errors.Wrap(marshalErr, "marshalling credential map") - } - - // first try as a VC object - var cred credential.VerifiableCredential - if err := json.Unmarshal(credMapBytes, &cred); err != nil || cred.IsEmpty() { - // if that fails, try as a JWT - _, vcFromJWT, err := VCJWTJSONToVC(credMapBytes) - if err != nil { - return nil, errors.Wrap(err, "parsing generic credential as either VC or JWT") - } - return vcFromJWT, nil - } - return &cred, nil - } - return nil, fmt.Errorf("invalid credential type: %s", reflect.TypeOf(genericCred).Kind().String()) -} - -// ToCredentialJSONMap turn a generic cred into a JSON object -func ToCredentialJSONMap(genericCred any) (map[string]any, error) { - switch genericCred.(type) { - case map[string]any: - return genericCred.(map[string]any), nil - case string: - // JWT - _, token, _, parseErr := signing.ParseVerifiableCredentialFromJWT(genericCred.(string)) - if parseErr != nil { - return nil, errors.Wrap(parseErr, "parsing credential from JWT") - } - // marshal it into a JSON map - tokenJSONBytes, marshalErr := json.Marshal(token) - if marshalErr != nil { - return nil, errors.Wrap(marshalErr, "marshaling credential JWT") - } - var credJSON map[string]any - if err := json.Unmarshal(tokenJSONBytes, &credJSON); err != nil { - return nil, errors.Wrap(err, "unmarshalling credential JWT") - } - return credJSON, nil - case credential.VerifiableCredential, *credential.VerifiableCredential: - credJSONBytes, marshalErr := json.Marshal(genericCred) - if marshalErr != nil { - return nil, errors.Wrap(marshalErr, "marshalling credential object") - } - var credJSON map[string]any - if err := json.Unmarshal(credJSONBytes, &credJSON); err != nil { - return nil, errors.Wrap(err, "unmarshalling credential object") - } - return credJSON, nil - } - return nil, fmt.Errorf("invalid credential type: %s", reflect.TypeOf(genericCred).Kind().String()) -} - -// VCJWTJSONToVC converts a JSON representation of a VC JWT into a VerifiableCredential -func VCJWTJSONToVC(vcJWTJSON []byte) (jwt.Token, *credential.VerifiableCredential, error) { - // next, try to turn it into a JWT to check if it's a VC JWT - token, err := jwt.Parse(vcJWTJSON, jwt.WithValidate(false), jwt.WithVerify(false)) - if err != nil { - return nil, nil, errors.Wrap(err, "coercing generic cred to JWT") - } - cred, err := signing.ParseVerifiableCredentialFromToken(token) - if err != nil { - return nil, nil, errors.Wrap(err, "parsing credential from token") - } - return token, cred, nil -} diff --git a/credential/util/util_test.go b/credential/util_test.go similarity index 84% rename from credential/util/util_test.go rename to credential/util_test.go index f30ffa2b..20b856b5 100644 --- a/credential/util/util_test.go +++ b/credential/util_test.go @@ -1,10 +1,8 @@ -package util +package credential import ( "testing" - "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/cryptosuite" "github.com/stretchr/testify/assert" @@ -12,7 +10,7 @@ import ( func TestCredentialsFromInterface(t *testing.T) { t.Run("Bad Cred", func(tt *testing.T) { - parsedCred, err := ToCredential("bad") + _, _, parsedCred, err := ToCredential("bad") assert.Error(tt, err) assert.Empty(tt, parsedCred) @@ -24,7 +22,7 @@ func TestCredentialsFromInterface(t *testing.T) { t.Run("Unsigned Cred", func(tt *testing.T) { testCred := getTestCredential() - parsedCred, err := ToCredential(testCred) + _, _, parsedCred, err := ToCredential(testCred) assert.NoError(tt, err) assert.NotEmpty(tt, parsedCred) assert.Equal(tt, testCred.Issuer, parsedCred.Issuer) @@ -60,7 +58,7 @@ func TestCredentialsFromInterface(t *testing.T) { err = suite.Sign(signer, &testCred) assert.NoError(t, err) - parsedCred, err := ToCredential(testCred) + _, _, parsedCred, err := ToCredential(testCred) assert.NoError(tt, err) assert.NotEmpty(tt, parsedCred) assert.Equal(tt, testCred.Issuer, parsedCred.Issuer) @@ -83,14 +81,19 @@ func TestCredentialsFromInterface(t *testing.T) { assert.NoError(tt, err) testCred := getTestCredential() - signed, err := signing.SignVerifiableCredentialJWT(*signer, testCred) + signed, err := SignVerifiableCredentialJWT(*signer, testCred) assert.NoError(tt, err) assert.NotEmpty(tt, signed) - parsedCred, err := ToCredential(string(signed)) + headers, token, parsedCred, err := ToCredential(string(signed)) assert.NoError(tt, err) assert.NotEmpty(tt, parsedCred) + assert.NotEmpty(tt, headers) + assert.NotEmpty(tt, token) assert.Equal(tt, parsedCred.Issuer, testCred.Issuer) + gotIss, ok := token.Get("iss") + assert.True(tt, ok) + assert.Equal(tt, gotIss.(string), testCred.Issuer) genericCred, err := ToCredentialJSONMap(string(signed)) assert.NoError(tt, err) @@ -99,8 +102,8 @@ func TestCredentialsFromInterface(t *testing.T) { }) } -func getTestCredential() credential.VerifiableCredential { - return credential.VerifiableCredential{ +func getTestCredential() VerifiableCredential { + return VerifiableCredential{ Context: []any{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, Type: []string{"VerifiableCredential"}, Issuer: "did:example:123", diff --git a/credential/verification/verification.go b/credential/verification/verification.go index 6130771f..f23e0c97 100644 --- a/credential/verification/verification.go +++ b/credential/verification/verification.go @@ -22,14 +22,14 @@ type ( OptionKey string ) -// VerificationOption represents a single option that may be required for a verifier -type VerificationOption struct { +// Option represents a single option that may be required for a verifier +type Option struct { ID OptionKey Option any } // GetVerificationOption returns a verification option given an ID -func GetVerificationOption(opts []VerificationOption, id OptionKey) (any, error) { +func GetVerificationOption(opts []Option, id OptionKey) (any, error) { for _, opt := range opts { if opt.ID == id { return opt.Option, nil @@ -38,9 +38,10 @@ func GetVerificationOption(opts []VerificationOption, id OptionKey) (any, error) return nil, errors.Errorf("option with id <%s> not found", id) } -type Verify func(cred credential.VerifiableCredential, opts ...VerificationOption) error +type Verify func(cred credential.VerifiableCredential, opts ...Option) error // NewCredentialVerifier creates a new credential verifier which executes in the order of the verifiers provided +// The verifiers introspect the contents of the credential, and do not handle signature verification. func NewCredentialVerifier(verifiers []Verifier) (*CredentialVerifier, error) { // dedupe var deduplicatedVerifiers []Verifier @@ -58,7 +59,7 @@ func NewCredentialVerifier(verifiers []Verifier) (*CredentialVerifier, error) { } // VerifyCredential verifies a credential given a credential verifier -func (cv *CredentialVerifier) VerifyCredential(cred credential.VerifiableCredential, opts ...VerificationOption) error { +func (cv *CredentialVerifier) VerifyCredential(cred credential.VerifiableCredential, opts ...Option) error { ae := util.NewAppendError() for _, verifier := range cv.verifiers { if err := verifier.VerifyFunc(cred, opts...); err != nil { diff --git a/credential/verification/verification_test.go b/credential/verification/verification_test.go index df9c9978..0790383a 100644 --- a/credential/verification/verification_test.go +++ b/credential/verification/verification_test.go @@ -98,7 +98,7 @@ func TestVerifier(t *testing.T) { }) } -func NoOpVerifier(_ credential.VerifiableCredential, _ ...VerificationOption) error { +func NoOpVerifier(_ credential.VerifiableCredential, _ ...Option) error { return nil } @@ -138,10 +138,8 @@ func getVCJSONSchema() string { "format": "email" } }, - "required": [ - "emailAddress" - ], - "additionalProperties": false - } -}` + "required": ["emailAddress"], + "additionalProperties": false + } + }` } diff --git a/credential/verification/verifiers.go b/credential/verification/verifiers.go index 868b6120..06b9bda8 100644 --- a/credential/verification/verifiers.go +++ b/credential/verification/verifiers.go @@ -13,30 +13,14 @@ const ( SchemaOption OptionKey = "schema" ) -var KnownVerifiers = []Verifier{ - { - ID: "Object Validation", - VerifyFunc: VerifyValidCredential, - }, - { - ID: "Expiry Check", - VerifyFunc: VerifyExpiry, - }, - { - ID: "VC JSON Schema", - VerifyFunc: VerifyJSONSchema, - }, -} - // VerifyValidCredential verifies a credential's object model depending on the struct tags used on VerifiableCredential -// TODO(gabe) add support for JSON schema validation of the VCDM after https://github.com/w3c/vc-data-model/issues/934 -func VerifyValidCredential(cred credential.VerifiableCredential, _ ...VerificationOption) error { +func VerifyValidCredential(cred credential.VerifiableCredential, _ ...Option) error { return cred.IsValid() } // VerifyExpiry verifies a credential's expiry date is not in the past. We assume the date is parseable as // an RFC3339 date time value. -func VerifyExpiry(cred credential.VerifiableCredential, _ ...VerificationOption) error { +func VerifyExpiry(cred credential.VerifiableCredential, _ ...Option) error { if cred.ExpirationDate == "" { return nil } @@ -51,8 +35,8 @@ func VerifyExpiry(cred credential.VerifiableCredential, _ ...VerificationOption) } // WithSchema provides a schema as a verification option -func WithSchema(schema string) VerificationOption { - return VerificationOption{ +func WithSchema(schema string) Option { + return Option{ ID: SchemaOption, Option: schema, } @@ -61,7 +45,7 @@ func WithSchema(schema string) VerificationOption { // VerifyJSONSchema verifies a credential's data against a Verifiable Credential JSON Schema: // https://w3c-ccg.github.io/vc-json-schemas/v2/index.html#credential_schema_definition // There is a required single option which is a string JSON value representing the Credential Schema Object -func VerifyJSONSchema(cred credential.VerifiableCredential, opts ...VerificationOption) error { +func VerifyJSONSchema(cred credential.VerifiableCredential, opts ...Option) error { hasSchemaProperty := cred.CredentialSchema != nil schema, err := GetVerificationOption(opts, SchemaOption) if err != nil { @@ -92,3 +76,20 @@ func optionToCredentialSchema(maybeSchema any) (*credschema.VCJSONSchema, error) } return credschema.StringToVCJSONCredentialSchema(schema) } + +func GetKnownVerifiers() []Verifier { + return []Verifier{ + { + ID: "Object Validation", + VerifyFunc: VerifyValidCredential, + }, + { + ID: "Expiry Check", + VerifyFunc: VerifyExpiry, + }, + { + ID: "VC JSON Schema", + VerifyFunc: VerifyJSONSchema, + }, + } +} diff --git a/crypto/jwt.go b/crypto/jwt.go index 85768ffb..991aae0d 100644 --- a/crypto/jwt.go +++ b/crypto/jwt.go @@ -54,12 +54,12 @@ func NewJWTSignerFromKey(id, kid string, key jwk.Key) (*JWTSigner, error) { return &JWTSigner{ID: id, SignatureAlgorithm: *alg, Key: gotJWK}, nil } -func (s *JWTSigner) ToVerifier() (*JWTVerifier, error) { +func (s *JWTSigner) ToVerifier(verifierID string) (*JWTVerifier, error) { key, err := s.Key.PublicKey() if err != nil { return nil, err } - return NewJWTVerifierFromKey(s.ID, key) + return NewJWTVerifierFromKey(verifierID, key) } // JWTVerifier is a struct that contains the key and algorithm used to verify JWTs @@ -280,7 +280,7 @@ func AlgFromKeyAndCurve(kty jwa.KeyType, crv jwa.EllipticCurveAlgorithm) (jwa.Si case jwa.Ed25519: return jwa.EdDSA, nil default: - return "", fmt.Errorf("unsupported OKP signing curve: %s", curve) + return "", fmt.Errorf("unsupported OKP jwt curve: %s", curve) } } diff --git a/crypto/jwt_test.go b/crypto/jwt_test.go index 3eb31a43..42439442 100644 --- a/crypto/jwt_test.go +++ b/crypto/jwt_test.go @@ -10,7 +10,7 @@ import ( func TestJsonWebSignature2020TestVectorJWT(t *testing.T) { // https://github.com/decentralized-identity/JWS-Test-Suite/blob/main/data/keys/key-0-ed25519.json signer := getTestVectorKey0Signer(t) - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(signer.ID) assert.NoError(t, err) // https://github.com/decentralized-identity/JWS-Test-Suite/blob/main/data/implementations/spruce/credential-0--key-0-ed25519.vc-jwt.json @@ -94,7 +94,7 @@ func TestSignVerifyJWTForEachSupportedKeyType(t *testing.T) { assert.NotEmpty(t, token) // verify - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(signer.ID) assert.NoError(t, err) assert.NotEmpty(t, verifier) @@ -110,7 +110,7 @@ func TestSignVerifyJWTForEachSupportedKeyType(t *testing.T) { func TestSignVerifyGenericJWT(t *testing.T) { signer := getTestVectorKey0Signer(t) - verifier, err := signer.ToVerifier() + verifier, err := signer.ToVerifier(signer.ID) assert.NoError(t, err) jwtData := map[string]any{ diff --git a/cryptosuite/bbsplussignatureproofsuite_test.go b/cryptosuite/bbsplussignatureproofsuite_test.go index be1f130a..30ac08f3 100644 --- a/cryptosuite/bbsplussignatureproofsuite_test.go +++ b/cryptosuite/bbsplussignatureproofsuite_test.go @@ -32,7 +32,7 @@ func TestBBSPlusSignatureProofSuite(t *testing.T) { suite := GetBBSPlusSignatureSuite() testCred := TestCredential{ Context: []any{"https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/bbs/v1"}, + "https://w3c.github.io/vc-di-bbs/contexts/v1"}, Type: []string{"VerifiableCredential"}, Issuer: "did:example:123", IssuanceDate: "2021-01-01T19:23:24Z", @@ -50,7 +50,7 @@ func TestBBSPlusSignatureProofSuite(t *testing.T) { proofSuite := GetBBSPlusSignatureProofSuite() revealDoc := map[string]any{ - "@context": []any{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/bbs/v1"}, + "@context": []any{"https://www.w3.org/2018/credentials/v1", "https://w3c.github.io/vc-di-bbs/contexts/v1"}, "type": "VerifiableCredential", "issuer": map[string]any{}, } diff --git a/cryptosuite/bbsplussignaturesuite.go b/cryptosuite/bbsplussignaturesuite.go index 463ef0fb..c66a1102 100644 --- a/cryptosuite/bbsplussignaturesuite.go +++ b/cryptosuite/bbsplussignaturesuite.go @@ -11,7 +11,7 @@ import ( ) const ( - BBSSecurityContext string = "https://w3id.org/security/bbs/v1" + BBSSecurityContext string = "https://w3c.github.io/vc-di-bbs/contexts/v1" BBSPlusSignature2020 SignatureType = "BbsBlsSignature2020" BBSPlusSignatureSuiteID string = "https://w3c-ccg.github.io/ldp-bbs2020/#the-bbs-signature-suite-2020" BBSPlusSignatureSuiteType LDKeyType = BLS12381G2Key2020 diff --git a/cryptosuite/bbsplussignaturesuite_test.go b/cryptosuite/bbsplussignaturesuite_test.go index b935a9eb..df1efcc0 100644 --- a/cryptosuite/bbsplussignaturesuite_test.go +++ b/cryptosuite/bbsplussignaturesuite_test.go @@ -18,7 +18,7 @@ func TestBBSPlusSignatureSuite(t *testing.T) { suite := GetBBSPlusSignatureSuite() testCred := TestCredential{ Context: []any{"https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/bbs/v1"}, + "https://w3c.github.io/vc-di-bbs/contexts/v1"}, Type: []string{"VerifiableCredential"}, Issuer: "did:example:123", IssuanceDate: "2021-01-01T19:23:24Z", diff --git a/cryptosuite/testdata/case16_reveal_doc.jsonld b/cryptosuite/testdata/case16_reveal_doc.jsonld index 47490031..6062892a 100644 --- a/cryptosuite/testdata/case16_reveal_doc.jsonld +++ b/cryptosuite/testdata/case16_reveal_doc.jsonld @@ -2,7 +2,7 @@ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/citizenship/v1", - "https://w3id.org/security/bbs/v1" + "https://w3c.github.io/vc-di-bbs/contexts/v1" ], "type": ["VerifiableCredential", "PermanentResidentCard"], "credentialSubject": { diff --git a/cryptosuite/testdata/case16_revealed.jsonld b/cryptosuite/testdata/case16_revealed.jsonld index 8e55515e..f2c7b62f 100644 --- a/cryptosuite/testdata/case16_revealed.jsonld +++ b/cryptosuite/testdata/case16_revealed.jsonld @@ -2,7 +2,7 @@ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/citizenship/v1", - "https://w3id.org/security/bbs/v1" + "https://w3c.github.io/vc-di-bbs/contexts/v1" ], "credentialSubject": { "familyName": "SMITH", diff --git a/cryptosuite/testdata/case16_vc.jsonld b/cryptosuite/testdata/case16_vc.jsonld index 888bad96..93785d1e 100644 --- a/cryptosuite/testdata/case16_vc.jsonld +++ b/cryptosuite/testdata/case16_vc.jsonld @@ -1,7 +1,7 @@ { "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/bbs/v1", + "https://w3c.github.io/vc-di-bbs/contexts/v1", "https://w3id.org/citizenship/v1" ], "id": "https://issuer.oidp.uscis.gov/credentials/83627465", diff --git a/cryptosuite/testdata/case18_reveal_doc.jsonld b/cryptosuite/testdata/case18_reveal_doc.jsonld index 2ce88cf2..7812507b 100644 --- a/cryptosuite/testdata/case18_reveal_doc.jsonld +++ b/cryptosuite/testdata/case18_reveal_doc.jsonld @@ -2,7 +2,7 @@ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1", - "https://w3id.org/security/bbs/v1" + "https://w3c.github.io/vc-di-bbs/contexts/v1" ], "type": ["UniversityDegreeCredential", "VerifiableCredential"], "@explicit": true, diff --git a/cryptosuite/testdata/case18_vc.jsonld b/cryptosuite/testdata/case18_vc.jsonld index 2a8a65e6..45d5d5a1 100644 --- a/cryptosuite/testdata/case18_vc.jsonld +++ b/cryptosuite/testdata/case18_vc.jsonld @@ -2,7 +2,7 @@ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1", - "https://w3id.org/security/bbs/v1" + "https://w3c.github.io/vc-di-bbs/contexts/v1" ], "id": "http://example.gov/credentials/3732", "type": [ diff --git a/cryptosuite/testdata/vc_test_vector.jsonld b/cryptosuite/testdata/vc_test_vector.jsonld index 4f2fc2b1..62296f68 100644 --- a/cryptosuite/testdata/vc_test_vector.jsonld +++ b/cryptosuite/testdata/vc_test_vector.jsonld @@ -2,7 +2,7 @@ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/citizenship/v1", - "https://w3id.org/security/bbs/v1" + "https://w3c.github.io/vc-di-bbs/contexts/v1" ], "id": "https://issuer.oidp.uscis.gov/credentials/83627465", "type": [ diff --git a/did/key_fuzz_test.go b/did/key_fuzz_test.go index dae8c747..c0c95e2e 100644 --- a/did/key_fuzz_test.go +++ b/did/key_fuzz_test.go @@ -31,18 +31,19 @@ func FuzzCreateAndDecode(f *testing.F) { } func FuzzCreateAndResolve(f *testing.F) { - keytypes := GetSupportedDIDKeyTypes() - ktLen := len(keytypes) + keyTypes := GetSupportedDIDKeyTypes() + ktLen := len(keyTypes) resolvers := []Resolver{KeyResolver{}, WebResolver{}, PKHResolver{}, PeerResolver{}} - resolver, _ := NewResolver(resolvers...) + resolver, err := NewResolver(resolvers...) + assert.NoError(f, err) for i, pk := range mockPubKeys { f.Add(i, []byte(pk)) } f.Fuzz(func(t *testing.T, ktSeed int, pubKey []byte) { - kt := keytypes[(ktSeed%ktLen+ktLen)%ktLen] + kt := keyTypes[(ktSeed%ktLen+ktLen)%ktLen] didKey, err := CreateDIDKey(kt, pubKey) assert.NoError(t, err) diff --git a/did/util.go b/did/util.go index c047d0cc..fda7b7ee 100644 --- a/did/util.go +++ b/did/util.go @@ -1,6 +1,7 @@ package did import ( + "context" gocrypto "crypto" "fmt" @@ -16,6 +17,24 @@ import ( "github.com/pkg/errors" ) +// ResolveKeyForDID resolves a public key from a DID for a given KID. +func ResolveKeyForDID(ctx context.Context, resolver Resolver, did, kid string) (gocrypto.PublicKey, error) { + if resolver == nil { + return nil, errors.New("resolver cannot be empty") + } + resolved, err := resolver.Resolve(ctx, did, nil) + if err != nil { + return nil, errors.Wrapf(err, "resolving DID: %s", did) + } + + // next, get the verification information (key) from the did document + pubKey, err := GetKeyFromVerificationMethod(resolved.Document, kid) + if err != nil { + return nil, errors.Wrapf(err, "getting verification information from DID Document: %s", did) + } + return pubKey, err +} + // GetKeyFromVerificationMethod resolves a DID and provides a kid and public key needed for data verification // it is possible that a DID has multiple verification methods, in which case a kid must be provided, otherwise // resolution will fail. diff --git a/did/util_test.go b/did/util_test.go index 0913d919..addd693f 100644 --- a/did/util_test.go +++ b/did/util_test.go @@ -1,14 +1,75 @@ package did import ( + "context" "crypto/ed25519" "testing" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestResolveKeyForDID(t *testing.T) { + resolver, err := NewResolver([]Resolver{KeyResolver{}}...) + require.NoError(t, err) + require.NotEmpty(t, resolver) + + t.Run("empty resolver", func(tt *testing.T) { + _, err := ResolveKeyForDID(context.Background(), nil, "did:test", "test-kid") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "resolver cannot be empty") + }) + + t.Run("empty did", func(tt *testing.T) { + _, err := ResolveKeyForDID(context.Background(), resolver, "", "test-kid") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "not a valid did") + }) + + t.Run("unresolveable did", func(tt *testing.T) { + _, err := ResolveKeyForDID(context.Background(), resolver, "did:example:test", "test-kid") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "unsupported method: example") + }) + + t.Run("unresolveable did", func(tt *testing.T) { + _, err := ResolveKeyForDID(context.Background(), resolver, "did:example:test", "test-kid") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "unsupported method: example") + }) + + t.Run("invalid did", func(tt *testing.T) { + _, err := ResolveKeyForDID(context.Background(), resolver, "did:key:test", "test-kid") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not expand did:key DID") + }) + + t.Run("valid did; no kid", func(tt *testing.T) { + _, didKey, err := GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + _, err = ResolveKeyForDID(context.Background(), resolver, didKey.String(), "test-kid") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "has no verification methods with kid: test-kid") + }) + + t.Run("valid did; valid kid", func(tt *testing.T) { + privKey, didKey, err := GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + expanded, err := didKey.Expand() + assert.NoError(tt, err) + kid := expanded.VerificationMethod[0].ID + + key, err := ResolveKeyForDID(context.Background(), resolver, didKey.String(), kid) + assert.NoError(tt, err) + assert.NotEmpty(tt, key) + + pubKey := privKey.(ed25519.PrivateKey).Public() + assert.Equal(tt, pubKey, key) + }) +} + func TestGetKeyFromVerificationInformation(t *testing.T) { t.Run("empty doc", func(tt *testing.T) { _, err := GetKeyFromVerificationMethod(Document{}, "test-kid") diff --git a/example/usecase/apartment_application/apartment_application.go b/example/usecase/apartment_application/apartment_application.go index f4114ae0..a7e7e05d 100644 --- a/example/usecase/apartment_application/apartment_application.go +++ b/example/usecase/apartment_application/apartment_application.go @@ -12,11 +12,11 @@ package main import ( + "context" "fmt" "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/credential/exchange" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/example" @@ -31,23 +31,30 @@ func main() { // User Holder holderDIDPrivateKey, holderDIDKey, err := did.GenerateDIDKey(crypto.Ed25519) example.HandleExampleError(err, "Failed to generate DID") - holderSigner, err := crypto.NewJWTSigner(holderDIDKey.String(), holderDIDKey.String(), holderDIDPrivateKey) + expanded, err := holderDIDKey.Expand() + example.HandleExampleError(err, "Failed to expand DID") + holderKID := expanded.VerificationMethod[0].ID + holderSigner, err := crypto.NewJWTSigner(holderDIDKey.String(), holderKID, holderDIDPrivateKey) example.HandleExampleError(err, "Failed to generate signer") - holderVerifier, err := holderSigner.ToVerifier() - example.HandleExampleError(err, "Failed to generate verifier") // Apt Verifier aptDIDPrivateKey, aptDIDKey, err := did.GenerateDIDKey(crypto.Ed25519) example.HandleExampleError(err, "Failed to generate DID key") - aptSigner, err := crypto.NewJWTSigner(aptDIDKey.String(), aptDIDKey.String(), aptDIDPrivateKey) + expanded, err = aptDIDKey.Expand() + example.HandleExampleError(err, "Failed to expand DID") + aptKID := expanded.VerificationMethod[0].ID + aptSigner, err := crypto.NewJWTSigner(aptDIDKey.String(), aptKID, aptDIDPrivateKey) example.HandleExampleError(err, "Failed to generate signer") - aptVerifier, err := aptSigner.ToVerifier() + aptVerifier, err := aptSigner.ToVerifier(aptSigner.ID) example.HandleExampleError(err, "Failed to generate verifier") // Government Issuer govtDIDPrivateKey, govtDIDKey, err := did.GenerateDIDKey(crypto.Ed25519) example.HandleExampleError(err, "Failed to generate DID key") - govtSigner, err := crypto.NewJWTSigner(govtDIDKey.String(), govtDIDKey.String(), govtDIDPrivateKey) + expanded, err = govtDIDKey.Expand() + example.HandleExampleError(err, "Failed to expand DID") + govKID := expanded.VerificationMethod[0].ID + govtSigner, err := crypto.NewJWTSigner(govtDIDKey.String(), govKID, govtDIDPrivateKey) example.HandleExampleError(err, "Failed to generate signer") _, _ = fmt.Print("\n\nStep 1: Create new DIDs for entities\n\n") @@ -79,7 +86,7 @@ func main() { example.HandleExampleError(err, "Failed to make verifiable credential") example.HandleExampleError(vc.IsValid(), "Verifiable credential is not valid") - signedVCBytes, err := signing.SignVerifiableCredentialJWT(*govtSigner, *vc) + signedVCBytes, err := credential.SignVerifiableCredentialJWT(*govtSigner, *vc) example.HandleExampleError(err, "Failed to sign vc") @@ -136,7 +143,7 @@ func main() { example.HandleExampleError(verifiedPresentationDefinition.IsValid(), "Verified presentation definition is not valid") presentationClaim := exchange.PresentationClaim{ - TokenBytes: signedVCBytes, + Token: util.StringPtr(string(signedVCBytes)), JWTFormat: exchange.JWTVC.Ptr(), SignatureAlgorithmOrProofType: string(crypto.EdDSA), } @@ -153,7 +160,14 @@ func main() { Step 5: The apartment will verify the presentation submission. This is done to make sure the presentation is in compliance with the definition. **/ - err = exchange.VerifyPresentationSubmission(*holderVerifier, exchange.JWTVPTarget, *presentationDefinition, presentationSubmissionBytes) + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}}...) + example.HandleExampleError(err, "Failed to build resolver") + + // Convert the holder signer to a verifier with the audience set as the apartment DID + holderVerifier, err := holderSigner.ToVerifier(aptVerifier.ID) + example.HandleExampleError(err, "Failed to generate verifier") + + err = exchange.VerifyPresentationSubmission(context.Background(), *holderVerifier, resolver, exchange.JWTVPTarget, *presentationDefinition, presentationSubmissionBytes) example.HandleExampleError(err, "Failed to verify presentation submission") _, _ = fmt.Print("\n\nStep 5: The apartment verifies that the presentation submission is valid and then can cryptographically verify that the birthdate of the tenant is authentic\n\n") diff --git a/example/usecase/employer_university_flow/main.go b/example/usecase/employer_university_flow/main.go index a636b409..e479f483 100644 --- a/example/usecase/employer_university_flow/main.go +++ b/example/usecase/employer_university_flow/main.go @@ -50,17 +50,17 @@ package main import ( + "context" "fmt" "os" - "github.com/goccy/go-json" - + "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/example" emp "github.com/TBD54566975/ssi-sdk/example/usecase/employer_university_flow/pkg" + "github.com/goccy/go-json" - "github.com/TBD54566975/ssi-sdk/credential/signing" - "github.com/TBD54566975/ssi-sdk/cryptosuite" "github.com/sirupsen/logrus" ) @@ -89,80 +89,91 @@ func main() { step := 0 example.WriteStep("Starting University Flow", step) - step += 1 + step++ // Wallet initialization example.WriteStep("Initializing Student", step) - step += 1 + step++ - student, err := emp.NewEntity("Student", "key") + student, err := emp.NewEntity("Student", did.KeyMethod) example.HandleExampleError(err, "failed to create student") + studentDID := student.GetWallet().GetDIDs()[0] + studentKeys, err := student.GetWallet().GetKeysForDID(studentDID) + studentKey := studentKeys[0].Key + studentKID := studentKeys[0].ID + example.HandleExampleError(err, "failed to get student key") example.WriteStep("Initializing Employer", step) - step += 1 + step++ employer, err := emp.NewEntity("Employer", "peer") example.HandleExampleError(err, "failed to make employer identity") - verifierDID, err := employer.GetWallet().GetDID("main") - example.HandleExampleError(err, "failed to create employer") + employerDID := employer.GetWallet().GetDIDs()[0] + employerKeys, err := employer.GetWallet().GetKeysForDID(employerDID) + employerKey := employerKeys[0].Key + employerKID := employerKeys[0].ID + example.HandleExampleError(err, "failed to get employer key") example.WriteStep("Initializing University", step) - step += 1 + step++ - university, err := emp.NewEntity("University", "peer") + university, err := emp.NewEntity("University", did.PeerMethod) example.HandleExampleError(err, "failed to create university") - vcDID, err := university.GetWallet().GetDID("main") + universityDID := university.GetWallet().GetDIDs()[0] + universityKeys, err := university.GetWallet().GetKeysForDID(universityDID) + universityKey := universityKeys[0].Key + universityKID := universityKeys[0].ID + example.HandleExampleError(err, "failed to get university key") - example.HandleExampleError(err, "failed to initialize verifier") - example.WriteNote(fmt.Sprintf("Initialized Verifier DID: %s and registered it", vcDID)) - emp.TrustedEntities.Issuers[vcDID] = true + example.WriteNote(fmt.Sprintf("Initialized University (Verifier) DID: %s and registered it", universityDID)) example.WriteStep("Example University Creates VC for Holder", step) - step += 1 - - example.WriteNote("DID is shared from holder") - holderDID, err := student.GetWallet().GetDID("main") - example.HandleExampleError(err, "failed to store did from university") + step++ - vc, err := emp.BuildExampleUniversityVC(vcDID, holderDID) + universitySigner, err := crypto.NewJWTSigner(universityDID, universityKID, universityKey) + example.HandleExampleError(err, "failed to build university signer") + vcID, vc, err := emp.BuildExampleUniversityVC(*universitySigner, universityDID, studentDID) example.HandleExampleError(err, "failed to build vc") - example.WriteStep("Example University Sends VC to Holder", step) - step += 1 + example.WriteStep("Example University Sends VC to Student (Holder)", step) + step++ - err = student.GetWallet().AddCredentials(*vc) + err = student.GetWallet().AddCredentialJWT(vcID, vc) example.HandleExampleError(err, "failed to add credentials to wallet") - msg := fmt.Sprintf("VC puts into wallet. Wallet size is now: %d", student.GetWallet().Size()) + msg := fmt.Sprintf("VC is stored in wallet. Wallet size is now: %d", student.GetWallet().Size()) example.WriteNote(msg) - example.WriteNote(fmt.Sprintf("initialized verifier DID: %v", verifierDID)) + example.WriteNote(fmt.Sprintf("initialized Employer (Verifier) DID: %v", employerDID)) example.WriteStep("Employer wants to verify student graduated from Example University. Sends a presentation request", step) - step += 1 + step++ - presentationData, err := emp.MakePresentationData("test-id", "id-1") + presentationData, err := emp.MakePresentationData("test-id", "id-1", universityDID) example.HandleExampleError(err, "failed to create pd") dat, err := json.Marshal(presentationData) example.HandleExampleError(err, "failed to marshal presentation data") logrus.Debugf("Presentation Data:\n%v", string(dat)) - jwk, err := cryptosuite.GenerateJSONWebKey2020(cryptosuite.OKP, cryptosuite.Ed25519) - example.HandleExampleError(err, "failed to generate json web key") - - presentationRequestJWT, _, err := emp.MakePresentationRequest(*jwk, presentationData, verifierDID, holderDID) + presentationRequestJWT, employerSigner, err := emp.MakePresentationRequest(employerKey, employerKID, presentationData, employerDID, studentDID) example.HandleExampleError(err, "failed to make presentation request") - signer, err := crypto.NewJWTSignerFromJWK("student-id", jwk.ID, jwk.PrivateKeyJWK) + studentSigner, err := crypto.NewJWTSigner(studentDID, studentKID, studentKey) example.HandleExampleError(err, "failed to build json web key signer") example.WriteNote("Student returns claims via a Presentation Submission") - submission, err := emp.BuildPresentationSubmission(string(presentationRequestJWT), *signer, *vc) + + employerVerifier, err := employerSigner.ToVerifier(studentDID) + example.HandleExampleError(err, "failed to build employer verifier") + submission, err := emp.BuildPresentationSubmission(string(presentationRequestJWT), *employerVerifier, *studentSigner, vc) example.HandleExampleError(err, "failed to build presentation submission") - verifier, err := signer.ToVerifier() + verifier, err := studentSigner.ToVerifier(employerDID) example.HandleExampleError(err, "failed to construct verifier") - _, _, vp, err := signing.VerifyVerifiablePresentationJWT(*verifier, string(submission)) + + resolver, err := did.NewResolver([]did.Resolver{did.KeyResolver{}, did.PeerResolver{}}...) + example.HandleExampleError(err, "failed to create DID resolver") + _, _, vp, err := credential.VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(submission)) example.HandleExampleError(err, "failed to verify jwt") dat, err = json.Marshal(vp) @@ -170,7 +181,7 @@ func main() { logrus.Debugf("Submission:\n%v", string(dat)) example.WriteStep(fmt.Sprintf("Employer Attempting to Grant Access"), step) - if err = emp.ValidateAccess(*verifier, submission); err == nil { + if err = emp.ValidateAccess(*verifier, resolver, submission); err == nil { example.WriteOK("Access Granted!") } else { example.WriteError(fmt.Sprintf("Access was not granted! Reason: %s", err)) diff --git a/example/usecase/employer_university_flow/pkg/issuer.go b/example/usecase/employer_university_flow/pkg/issuer.go index 8f2cbd60..bc0f9b2c 100644 --- a/example/usecase/employer_university_flow/pkg/issuer.go +++ b/example/usecase/employer_university_flow/pkg/issuer.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/TBD54566975/ssi-sdk/crypto" "github.com/goccy/go-json" "github.com/TBD54566975/ssi-sdk/credential" @@ -16,17 +17,17 @@ import ( // who issued it. Building a VC means using the CredentialBuilder as part of the credentials package in the ssk-sdk. // VerifiableCredential is the verifiable credential model outlined in the vc-data-model spec: // https://www.w3.org/TR/2021/REC-vc-data-model-20211109/#basic-concept -func BuildExampleUniversityVC(universityID, recipient string) (*credential.VerifiableCredential, error) { +func BuildExampleUniversityVC(signer crypto.JWTSigner, universityDID, recipientDID string) (credID string, cred string, err error) { knownContext := []string{"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"} // JSON-LD context statement knownID := "http://example.edu/credentials/1872" knownType := []string{"VerifiableCredential", "AlumniCredential"} - knownIssuer := "https://example.edu/issuers/565049" + knownIssuer := universityDID knownIssuanceDate := time.Now().Format(time.RFC3339) knownSubject := map[string]any{ - "id": universityID, // did:: + "id": recipientDID, // did:: "alumniOf": map[string]any{ // claims are here - "id": recipient, + "id": recipientDID, "name": []any{ map[string]any{"value": "Example University", "lang": "en", @@ -37,10 +38,6 @@ func BuildExampleUniversityVC(universityID, recipient string) (*credential.Verif }, }, } - // This is an embedded proof. - // For more information - // https://github.com/TBD54566975/ssi-sdk/blob/main/cryptosuite/jwssignaturesuite_test.go#L357 - // https://www.w3.org/TR/vc-data-model/#proofs-signatures // For more information on VC object, go to: // https://github.com/TBD54566975/ssi-sdk/blob/main/credential/model.go @@ -54,16 +51,28 @@ func BuildExampleUniversityVC(universityID, recipient string) (*credential.Verif } if err := knownCred.IsValid(); err != nil { - return nil, err + return "", "", err } dat, err := json.Marshal(knownCred) if err != nil { - return nil, err + return "", "", err } logrus.Debug(string(dat)) - example.WriteNote(fmt.Sprintf("VC issued from %s to %s", universityID, recipient)) + // sign the credential as a JWT + signedCred, err := credential.SignVerifiableCredentialJWT(signer, knownCred) + if err != nil { + return "", "", err + } + cred = string(signedCred) + _, credToken, _, err := credential.ParseVerifiableCredentialFromJWT(string(signedCred)) + if err != nil { + return "", "", err + } + credID = credToken.JwtID() + + example.WriteNote(fmt.Sprintf("VC issued from %s to %s", universityDID, recipientDID)) - return &knownCred, nil + return credID, cred, nil } diff --git a/example/usecase/employer_university_flow/pkg/trust.go b/example/usecase/employer_university_flow/pkg/trust.go deleted file mode 100644 index 52125b0d..00000000 --- a/example/usecase/employer_university_flow/pkg/trust.go +++ /dev/null @@ -1,16 +0,0 @@ -package pkg - -type trustedEntitiesStore struct { - Issuers map[string]bool -} - -func (t *trustedEntitiesStore) isTrusted(did string) bool { - if v, ok := t.Issuers[did]; ok { - return v - } - return false -} - -var TrustedEntities = trustedEntitiesStore{ - Issuers: make(map[string]bool), -} diff --git a/example/usecase/employer_university_flow/pkg/util.go b/example/usecase/employer_university_flow/pkg/util.go index de4c313c..70e68484 100644 --- a/example/usecase/employer_university_flow/pkg/util.go +++ b/example/usecase/employer_university_flow/pkg/util.go @@ -1,16 +1,14 @@ package pkg import ( + gocrypto "crypto" "fmt" - "github.com/goccy/go-json" - "github.com/pkg/errors" - - "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/credential/exchange" "github.com/TBD54566975/ssi-sdk/crypto" - "github.com/TBD54566975/ssi-sdk/cryptosuite" + "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/example" + "github.com/goccy/go-json" ) type Entity struct { @@ -21,12 +19,13 @@ type Entity struct { func (e *Entity) GetWallet() *example.SimpleWallet { return e.wallet } -func NewEntity(name string, keyType string) (*Entity, error) { + +func NewEntity(name string, didMethod did.Method) (*Entity, error) { e := Entity{ wallet: example.NewSimpleWallet(), Name: name, } - if err := e.wallet.Init(keyType); err != nil { + if err := e.wallet.Init(didMethod); err != nil { return nil, err } return &e, nil @@ -36,11 +35,11 @@ func NewEntity(name string, keyType string) (*Entity, error) { // over multiple mechanisms. For more information, please go to here: // https://identity.foundation/presentation-exchange/#presentation-request and for the source code with the sdk, // https://github.com/TBD54566975/ssi-sdk/blob/main/credential/exchange/request.go is appropriate to start off with. -func MakePresentationRequest(jwk cryptosuite.JSONWebKey2020, presentationData exchange.PresentationDefinition, requesterID, targetID string) (pr []byte, signer *crypto.JWTSigner, err error) { +func MakePresentationRequest(key gocrypto.PrivateKey, keyID string, presentationData exchange.PresentationDefinition, requesterID, targetID string) (pr []byte, signer *crypto.JWTSigner, err error) { example.WriteNote("Presentation Request (JWT) is created") - // Signer uses a JWK - signer, err = crypto.NewJWTSignerFromJWK(requesterID, jwk.ID, jwk.PrivateKeyJWK) + // Signer uses a private key + signer, err = crypto.NewJWTSigner(requesterID, keyID, key) if err != nil { return nil, nil, err } @@ -57,17 +56,13 @@ func MakePresentationRequest(jwk cryptosuite.JSONWebKey2020, presentationData ex // BuildPresentationSubmission builds a submission using... // https://github.com/TBD54566975/ssi-sdk/blob/d279ca2779361091a70b8aa3c685a388067409a9/credential/exchange/submission.go#L126 -func BuildPresentationSubmission(presentationRequestJWT string, signer crypto.JWTSigner, vc credential.VerifiableCredential) ([]byte, error) { +func BuildPresentationSubmission(presentationRequestJWT string, verifier crypto.JWTVerifier, signer crypto.JWTSigner, vc string) ([]byte, error) { presentationClaim := exchange.PresentationClaim{ - Credential: &vc, - LDPFormat: exchange.LDPVC.Ptr(), - SignatureAlgorithmOrProofType: string(cryptosuite.JSONWebSignature2020), + Token: &vc, + JWTFormat: exchange.JWTVC.Ptr(), + SignatureAlgorithmOrProofType: crypto.Ed25519.String(), } - verifier, err := signer.ToVerifier() - if err != nil { - return nil, errors.Wrap(err, "creating verifier from signer") - } _, parsedPresentationRequest, err := verifier.VerifyAndParse(presentationRequestJWT) if err != nil { return nil, err @@ -98,7 +93,7 @@ func BuildPresentationSubmission(presentationRequestJWT string, signer crypto.JW // MakePresentationData Makes a dummy presentation definition. These are eventually transported via Presentation Request. // For more information on presentation definitions view the spec here: // https://identity.foundation/presentation-exchange/#term:presentation-definition -func MakePresentationData(id string, inputID string) (exchange.PresentationDefinition, error) { +func MakePresentationData(id, inputID, trustedIssuer string) (exchange.PresentationDefinition, error) { // Input Descriptors: Describe the information the verifier requires of the holder // https://identity.foundation/presentation-exchange/#input-descriptor // Required fields: ID and Input Descriptors @@ -113,6 +108,10 @@ func MakePresentationData(id string, inputID string) (exchange.PresentationDefin Path: []string{"$.vc.issuer", "$.issuer"}, ID: "issuer-input-descriptor", Purpose: "need to check the issuer", + Filter: &exchange.Filter{ + Type: "string", + Pattern: trustedIssuer, + }, }, }, }, diff --git a/example/usecase/employer_university_flow/pkg/verifier.go b/example/usecase/employer_university_flow/pkg/verifier.go index a0dcccc6..c96c36a6 100644 --- a/example/usecase/employer_university_flow/pkg/verifier.go +++ b/example/usecase/employer_university_flow/pkg/verifier.go @@ -1,20 +1,21 @@ package pkg import ( - "github.com/goccy/go-json" + "context" "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" "github.com/pkg/errors" ) // ValidateAccess is a very simple validation process against a Presentation Submission // It checks: -// 1. That the VC is valid -// 2. That the VC was issued by a trusted entity -func ValidateAccess(verifier crypto.JWTVerifier, credBytes []byte) error { - _, _, vp, err := signing.VerifyVerifiablePresentationJWT(verifier, string(credBytes)) +// 1. That the VP is valid +// 2. All VCs in the VP are valid +// 3. That the VC was issued by a trusted entity (implied by the presentation, according to the Presentation Definition) +func ValidateAccess(verifier crypto.JWTVerifier, resolver did.Resolver, submissionBytes []byte) error { + _, _, vp, err := credential.VerifyVerifiablePresentationJWT(context.Background(), verifier, resolver, string(submissionBytes)) if err != nil { return errors.Wrap(err, "failed to validate VP signature") } @@ -22,20 +23,5 @@ func ValidateAccess(verifier crypto.JWTVerifier, credBytes []byte) error { if err = vp.IsValid(); err != nil { return errors.Wrap(err, "failed to validate VP") } - - for _, untypedCredential := range vp.VerifiableCredential { - credBytes, err = json.Marshal(untypedCredential) - if err != nil { - return errors.Wrap(err, "could not marshal credential in VP") - } - var vc credential.VerifiableCredential - if err = json.Unmarshal(credBytes, &vc); err != nil { - return errors.Wrap(err, "could not unmarshal credential in VP") - } - // validity check - if issuer, ok := vc.CredentialSubject["id"]; !ok || !TrustedEntities.isTrusted(issuer.(string)) { - return errors.New("insufficient claims provided") - } - } return nil } diff --git a/example/usecase/steel_thread/steel_thread.go b/example/usecase/steel_thread/steel_thread.go index 68037547..ee0fe7f4 100644 --- a/example/usecase/steel_thread/steel_thread.go +++ b/example/usecase/steel_thread/steel_thread.go @@ -44,7 +44,7 @@ func (t *Entity) GenerateWallet() { example.HandleExampleError(err, "Failed to generate DID") walletSigner, err := crypto.NewJWTSigner(walletDIDKey.String(), walletDIDKey.String(), walletDIDPrivateKey) example.HandleExampleError(err, "Failed to generate signer") - walletVerifier, err := walletSigner.ToVerifier() + walletVerifier, err := walletSigner.ToVerifier(walletSigner.ID) example.HandleExampleError(err, "Failed to generate verifier") t.didKey = *walletDIDKey diff --git a/example/wallet.go b/example/wallet.go index 0ea06315..3fffbf15 100644 --- a/example/wallet.go +++ b/example/wallet.go @@ -1,12 +1,12 @@ package example import ( + "context" gocrypto "crypto" "errors" "fmt" "sync" - "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/did" ) @@ -15,103 +15,151 @@ import ( // This would NOT be how it would be stored in production, but serves for demonstrative purposes // This holds the assigned DIDs, their associated private keys, and VCs type SimpleWallet struct { - vcs map[string]credential.VerifiableCredential - keys map[string]gocrypto.PrivateKey - dids map[string]string + vcs map[string]string + dids map[string][]WalletKeys mux *sync.Mutex } +type WalletKeys struct { + ID string + Key gocrypto.PrivateKey +} + func NewSimpleWallet() *SimpleWallet { return &SimpleWallet{ - vcs: make(map[string]credential.VerifiableCredential), + vcs: make(map[string]string), mux: new(sync.Mutex), - dids: make(map[string]string), - keys: make(map[string]gocrypto.PrivateKey), + dids: make(map[string][]WalletKeys), } } -// AddPrivateKey Adds a Private Key to a wallet -func (s *SimpleWallet) AddPrivateKey(k string, key gocrypto.PrivateKey) error { +func (s *SimpleWallet) AddDID(id string) error { s.mux.Lock() defer s.mux.Unlock() - if _, ok := s.keys[k]; ok { + if _, ok := s.dids[id]; ok { return errors.New("already an entry") } - s.keys[k] = key + s.dids[id] = make([]WalletKeys, 0) return nil } -func (s *SimpleWallet) AddDIDKey(k string, key string) error { +func (s *SimpleWallet) GetDIDs() []string { s.mux.Lock() defer s.mux.Unlock() - if _, ok := s.dids[k]; ok { - return errors.New("already an entry") + var dids []string + for d := range s.dids { + dids = append(dids, d) + } + return dids +} + +// AddPrivateKey Adds a Private Key to a wallet +func (s *SimpleWallet) AddPrivateKey(id, kid string, key gocrypto.PrivateKey) error { + s.mux.Lock() + defer s.mux.Unlock() + walletKeys, ok := s.dids[id] + if !ok { + return fmt.Errorf("did<%s> not found", id) } - s.dids[k] = key + for _, k := range walletKeys { + if k.ID == kid { + return fmt.Errorf("key<%s> already exists", kid) + } + } + walletKeys = append(walletKeys, WalletKeys{ + ID: kid, + Key: key, + }) + s.dids[id] = walletKeys return nil } -func (s *SimpleWallet) GetDID(k string) (string, error) { +func (s *SimpleWallet) GetKey(kid string) (string, gocrypto.PrivateKey, error) { + s.mux.Lock() + defer s.mux.Unlock() + for _, d := range s.dids { + for _, k := range d { + if k.ID == kid { + return k.ID, k.Key, nil + } + } + } + return "", nil, fmt.Errorf("key<%s> not found", kid) +} + +func (s *SimpleWallet) GetKeysForDID(id string) ([]WalletKeys, error) { s.mux.Lock() defer s.mux.Unlock() - if v, ok := s.dids[k]; ok { - return v, nil + if keys, ok := s.dids[id]; ok { + return keys, nil } - return "", errors.New("not found") + return nil, fmt.Errorf("id<%s> not found", id) } -func (s *SimpleWallet) AddCredentials(cred credential.VerifiableCredential) error { +func (s *SimpleWallet) AddCredentialJWT(credID, cred string) error { if s.mux == nil { return errors.New("no mux for wallet") } s.mux.Lock() defer s.mux.Unlock() - if _, ok := s.vcs[cred.ID]; ok { - return fmt.Errorf("duplicate credential<%s>; could not add", cred.ID) + if _, ok := s.vcs[credID]; ok { + return fmt.Errorf("duplicate credential<%s>; could not add", credID) } - s.vcs[cred.ID] = cred + s.vcs[credID] = cred return nil } // Init stores a DID for a particular user and adds it to the registry -func (s *SimpleWallet) Init(keyType string) error { +func (s *SimpleWallet) Init(didMethod did.Method) error { var privKey gocrypto.PrivateKey var pubKey gocrypto.PublicKey var didStr string + var kid string var err error - - if keyType == did.DIDPeerPrefix { + switch didMethod { + case did.PeerMethod: kt := crypto.Ed25519 pubKey, privKey, err = crypto.GenerateKeyByKeyType(kt) if err != nil { return err } - didk, err := did.PeerMethod0{}.Generate(kt, pubKey) + didPeer, err := did.PeerMethod0{}.Generate(kt, pubKey) + if err != nil { + return err + } + didStr = didPeer.String() + resolvedPeer, err := did.PeerResolver{}.Resolve(context.Background(), didPeer.String()) if err != nil { return err } - didStr = didk.String() - } else { + kid = resolvedPeer.VerificationMethod[0].ID + case did.KeyMethod: var didKey *did.DIDKey - privKey, didKey, err = did.GenerateDIDKey(crypto.SECP256k1) + privKey, didKey, err = did.GenerateDIDKey(crypto.Ed25519) + if err != nil { + return err + } + didStr = didKey.String() + expanded, err := didKey.Expand() if err != nil { return err } - didStr = string(*didKey) + kid = expanded.VerificationMethod[0].ID + default: + return fmt.Errorf("unsupported did method<%s>", didMethod) } WriteNote(fmt.Sprintf("DID for holder is: %s", didStr)) - if err := s.AddPrivateKey("main", privKey); err != nil { + if err = s.AddDID(didStr); err != nil { return err } - WriteNote(fmt.Sprintf("Private Key stored with wallet")) - if err := s.AddDIDKey("main", string(didStr)); err != nil { + WriteNote(fmt.Sprintf("DID stored in wallet")) + if err = s.AddPrivateKey(didStr, kid, privKey); err != nil { return err } - WriteNote(fmt.Sprintf("DID Key stored in wallet")) - + WriteNote(fmt.Sprintf("Private Key stored with wallet")) return nil } diff --git a/schema/loader.go b/schema/loader.go index cd745e65..b318f197 100644 --- a/schema/loader.go +++ b/schema/loader.go @@ -19,39 +19,41 @@ var ( ) type ( - SchemaFile string + File string ) const ( schemaDirectory = "known_schemas/" // Presentation Exchange Schemas - PresentationDefinitionSchema SchemaFile = "pe-presentation-definition.json" - PresentationDefinitionEnvelopeSchema SchemaFile = "pe-presentation-definition-envelope.json" - PresentationSubmissionSchema SchemaFile = "pe-presentation-submission.json" - PresentationClaimFormatDesignationsSchema SchemaFile = "pe-definition-claim-format-designations.json" - SubmissionClaimFormatDesignationsSchema SchemaFile = "pe-submission-claim-format-designations.json" - SubmissionRequirementSchema SchemaFile = "pe-submission-requirement.json" - SubmissionRequirementsSchema SchemaFile = "pe-submission-requirements.json" - PresentationClaimFormatDesignationFormatDefinition SchemaFile = "pe-submission-claim-format-designations.json" + PresentationDefinitionSchema File = "pe-presentation-definition.json" + PresentationDefinitionEnvelopeSchema File = "pe-presentation-definition-envelope.json" + PresentationSubmissionSchema File = "pe-presentation-submission.json" + PresentationClaimFormatDesignationsSchema File = "pe-definition-claim-format-designations.json" + SubmissionClaimFormatDesignationsSchema File = "pe-submission-claim-format-designations.json" + SubmissionRequirementSchema File = "pe-submission-requirement.json" + SubmissionRequirementsSchema File = "pe-submission-requirements.json" // Credential Manifest Schemas - CredentialManifestSchema SchemaFile = "cm-credential-manifest.json" - CredentialApplicationSchema SchemaFile = "cm-credential-application.json" - CredentialResponseSchema SchemaFile = "cm-credential-response.json" - OutputDescriptorsSchema SchemaFile = "cm-output-descriptors.json" + + CredentialManifestSchema File = "cm-credential-manifest.json" + CredentialApplicationSchema File = "cm-credential-application.json" + CredentialResponseSchema File = "cm-credential-response.json" + OutputDescriptorsSchema File = "cm-output-descriptors.json" // Wallet Rendering Schemas - DisplayMappingObjectSchema SchemaFile = "wr-display-mapping-object.json" - EntityStylesSchema SchemaFile = "wr-entity-styles.json" - LabeledDisplayMappingObjectSchema SchemaFile = "wr-labeled-display-mapping-object.json" + + DisplayMappingObjectSchema File = "wr-display-mapping-object.json" + EntityStylesSchema File = "wr-entity-styles.json" + LabeledDisplayMappingObjectSchema File = "wr-labeled-display-mapping-object.json" // VC JSON Schema Schemas - VerifiableCredentialJSONSchemaSchema SchemaFile = "vc-json-schema.json" + + VerifiableCredentialJSONSchemaSchema File = "vc-json-schema.json" ) -func (s SchemaFile) String() string { +func (s File) String() string { return string(s) } @@ -120,14 +122,14 @@ func (cl *CachingLoader) GetCachedSchemas() ([]string, error) { } // LoadSchema loads a schema from the embedded filesystem and returns its contents as a json string -func LoadSchema(schemaFile SchemaFile) (string, error) { +func LoadSchema(schemaFile File) (string, error) { b, err := knownSchemas.ReadFile(schemaDirectory + schemaFile.String()) return string(b), err } // GetAllLocalSchemas returns all locally cached schemas to be added to a CachingLoader func GetAllLocalSchemas() (map[string]string, error) { - localFiles := map[string]SchemaFile{ + localFiles := map[string]File{ "identity.foundation/presentation-exchange/schemas/presentation-definition.json": PresentationDefinitionSchema, "identity.foundation/presentation-exchange/schemas/presentation-definition-envelope.json": PresentationDefinitionEnvelopeSchema, "identity.foundation/presentation-exchange/schemas/presentation-submission.json": PresentationSubmissionSchema,