diff --git a/credential/builder.go b/credential/builder.go index 5e7202b2..b173af27 100644 --- a/credential/builder.go +++ b/credential/builder.go @@ -187,10 +187,6 @@ func (vcb *VerifiableCredentialBuilder) SetCredentialSubject(subject CredentialS return errors.New(BuilderEmptyError) } - if subject.GetID() == "" { - return errors.New("credential subject must have an ID property") - } - vcb.CredentialSubject = subject return nil } diff --git a/credential/builder_test.go b/credential/builder_test.go index 2f0830eb..717bfd56 100644 --- a/credential/builder_test.go +++ b/credential/builder_test.go @@ -168,13 +168,12 @@ func TestCredentialBuilder(t *testing.T) { err = builder.SetCredentialStatus(status) assert.NoError(t, err) - // bad cred subject - no id - badSubject := CredentialSubject{ + // cred subject - no id + subjectWithMissingID := CredentialSubject{ "name": "Satoshi", } - err = builder.SetCredentialSubject(badSubject) - assert.Error(t, err) - assert.Contains(t, err.Error(), "credential subject must have an ID property") + err = builder.SetCredentialSubject(subjectWithMissingID) + assert.NoError(t, err) // good subject subject := CredentialSubject{ diff --git a/credential/exchange/submission.go b/credential/exchange/submission.go index c4c04895..68fc5c21 100644 --- a/credential/exchange/submission.go +++ b/credential/exchange/submission.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" @@ -165,7 +166,7 @@ func BuildPresentationSubmission(signer any, requester string, def PresentationD if err != nil { return nil, errors.Wrap(err, "unable to fulfill presentation definition with given credentials") } - return credential.SignVerifiablePresentationJWT(jwtSigner, credential.JWTVVPParameters{Audience: []string{requester}}, *vpSubmission) + return integrity.SignVerifiablePresentationJWT(jwtSigner, integrity.JWTVVPParameters{Audience: []string{requester}}, *vpSubmission) default: return nil, fmt.Errorf("presentation submission embed target <%s> is not implemented", et) } diff --git a/credential/exchange/submission_test.go b/credential/exchange/submission_test.go index 0f001b5a..d7dd3450 100644 --- a/credential/exchange/submission_test.go +++ b/credential/exchange/submission_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/cryptosuite/jws2020" "github.com/goccy/go-json" "github.com/oliveagle/jsonpath" @@ -64,7 +65,7 @@ func TestBuildPresentationSubmission(t *testing.T) { resolver, err := resolution.NewResolver([]resolution.Resolver{key.Resolver{}}...) assert.NoError(tt, err) - _, _, _, err = credential.VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(submissionBytes)) + _, _, _, err = integrity.VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(submissionBytes)) assert.Error(tt, err) assert.Contains(tt, err.Error(), "credential must have a proof") }) @@ -92,7 +93,7 @@ func TestBuildPresentationSubmission(t *testing.T) { signer, verifier := getJWKSignerVerifier(tt) testVC := getTestVerifiableCredential(signer.ID, signer.ID) - credJWT, err := credential.SignVerifiableCredentialJWT(*signer, testVC) + credJWT, err := integrity.SignVerifiableCredentialJWT(*signer, testVC) assert.NoError(tt, err) assert.NotEmpty(tt, credJWT) @@ -107,7 +108,7 @@ func TestBuildPresentationSubmission(t *testing.T) { resolver, err := resolution.NewResolver([]resolution.Resolver{key.Resolver{}}...) assert.NoError(tt, err) - _, _, vp, err := credential.VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(submissionBytes)) + _, _, vp, err := integrity.VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, string(submissionBytes)) assert.NoError(tt, err) assert.NoError(tt, vp.IsValid()) @@ -358,7 +359,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { assert.Equal(tt, "test-verifiable-credential", asVC.ID) assert.Equal(tt, "Block", asVC.CredentialSubject["company"]) - _, vcJWTToken, asVCJWT, err := credential.ParseVerifiableCredentialFromJWT(*(vp.VerifiableCredential[1].(*string))) + _, vcJWTToken, asVCJWT, err := integrity.ParseVerifiableCredentialFromJWT(*(vp.VerifiableCredential[1].(*string))) assert.NoError(tt, err) assert.NotEmpty(tt, vcJWTToken) assert.NotEmpty(tt, asVCJWT) diff --git a/credential/exchange/verification.go b/credential/exchange/verification.go index 27fa0f5b..31141111 100644 --- a/credential/exchange/verification.go +++ b/credential/exchange/verification.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/TBD54566975/ssi-sdk/credential/integrity" + "github.com/TBD54566975/ssi-sdk/credential/parsing" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did/resolution" "github.com/TBD54566975/ssi-sdk/schema" @@ -56,7 +58,7 @@ func VerifyPresentationSubmission(ctx context.Context, verifier any, resolver re return nil, 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)) + _, _, vp, err := integrity.VerifyVerifiablePresentationJWT(ctx, jwtVerifier, resolver, string(submission)) if err != nil { return nil, errors.Wrap(err, "verification of the presentation submission failed") } @@ -134,8 +136,8 @@ func VerifyPresentationSubmissionVP(def PresentationDefinition, vp credential.Ve submissionDescriptor.ID, submissionDescriptor.Path) } - // TODO(gabe) add in signature verification of claims here https://github.com/TBD54566975/ssi-sdk/issues/71 - _, _, cred, err := credential.ToCredential(claim) + // get the credential from the claim + _, _, cred, err := parsing.ToCredential(claim) if err != nil { return nil, errors.Wrapf(err, "getting claim as json: <%s>", claim) } @@ -150,7 +152,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 := credential.ToCredentialJSONMap(claim) + credJSON, err := parsing.ToCredentialJSONMap(claim) if err != nil { return nil, errors.Wrapf(err, "getting credential as json: %v", cred) } diff --git a/credential/exchange/verification_test.go b/credential/exchange/verification_test.go index 362fc6c6..f2415074 100644 --- a/credential/exchange/verification_test.go +++ b/credential/exchange/verification_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/cryptosuite/jws2020" "github.com/stretchr/testify/assert" @@ -138,7 +139,7 @@ func TestVerifyPresentationSubmission(t *testing.T) { signer, verifier := getJWKSignerVerifier(tt) testVC := getTestVerifiableCredential(signer.ID, signer.ID) - credJWT, err := credential.SignVerifiableCredentialJWT(*signer, testVC) + credJWT, err := integrity.SignVerifiableCredentialJWT(*signer, testVC) assert.NoError(tt, err) presentationClaim := PresentationClaim{ Token: util.StringPtr(string(credJWT)), @@ -186,7 +187,7 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { assert.NoError(tt, err) assert.NotEmpty(tt, submissionBytes) - _, _, verifiablePresentation, err := credential.ParseVerifiablePresentationFromJWT(string(submissionBytes)) + _, _, verifiablePresentation, err := integrity.ParseVerifiablePresentationFromJWT(string(submissionBytes)) assert.NoError(tt, err) _, err = VerifyPresentationSubmissionVP(def, *verifiablePresentation) @@ -523,7 +524,7 @@ func TestVerifyPresentationSubmissionVP(t *testing.T) { } signer, _ := getJWKSignerVerifier(tt) testVC := getTestVerifiableCredential("test-issuer", "test-subject") - vcData, err := credential.SignVerifiableCredentialJWT(*signer, testVC) + vcData, err := integrity.SignVerifiableCredentialJWT(*signer, testVC) assert.NoError(tt, err) b := NewPresentationSubmissionBuilder(def.ID) assert.NoError(tt, b.SetDescriptorMap([]SubmissionDescriptor{ diff --git a/credential/jws.go b/credential/integrity/jws.go similarity index 88% rename from credential/jws.go rename to credential/integrity/jws.go index e791a801..7661cb71 100644 --- a/credential/jws.go +++ b/credential/integrity/jws.go @@ -1,6 +1,7 @@ -package credential +package integrity import ( + "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/goccy/go-json" "github.com/lestrrat-go/jwx/v2/jwa" @@ -14,7 +15,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 jwx.Signer, cred VerifiableCredential) ([]byte, error) { +func SignVerifiableCredentialJWS(signer jwx.Signer, cred credential.VerifiableCredential) ([]byte, error) { payload, err := json.Marshal(cred) if err != nil { return nil, errors.Wrap(err, "marshalling credential") @@ -38,7 +39,7 @@ func SignVerifiableCredentialJWS(signer jwx.Signer, cred VerifiableCredential) ( // 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, *VerifiableCredential, error) { +func ParseVerifiableCredentialFromJWS(token string) (*jws.Message, *credential.VerifiableCredential, error) { parsed, err := jws.Parse([]byte(token)) if err != nil { return nil, nil, errors.Wrap(err, "parsing JWS") @@ -55,7 +56,7 @@ func ParseVerifiableCredentialFromJWS(token string) (*jws.Message, *VerifiableCr return parsed, cred, err } - var cred VerifiableCredential + var cred credential.VerifiableCredential if err = json.Unmarshal(parsed.Payload(), &cred); err != nil { return nil, nil, errors.Wrap(err, "reconstructing Verifiable Credential") } @@ -66,7 +67,7 @@ func ParseVerifiableCredentialFromJWS(token string) (*jws.Message, *VerifiableCr // 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 jwx.Verifier, token string) (*jws.Message, *VerifiableCredential, error) { +func VerifyVerifiableCredentialJWS(verifier jwx.Verifier, token string) (*jws.Message, *credential.VerifiableCredential, error) { if err := verifier.VerifyJWS(token); err != nil { return nil, nil, errors.Wrap(err, "verifying JWS") } diff --git a/credential/jws_test.go b/credential/integrity/jws_test.go similarity index 96% rename from credential/jws_test.go rename to credential/integrity/jws_test.go index fb3d1425..3d6b6a95 100644 --- a/credential/jws_test.go +++ b/credential/integrity/jws_test.go @@ -1,16 +1,17 @@ -package credential +package integrity import ( "context" "testing" + "github.com/TBD54566975/ssi-sdk/credential" "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/stretchr/testify/assert" ) func TestVerifiableCredentialJWS(t *testing.T) { - testCredential := VerifiableCredential{ + testCredential := credential.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", diff --git a/credential/jwt.go b/credential/integrity/jwt.go similarity index 92% rename from credential/jwt.go rename to credential/integrity/jwt.go index 27713f4d..87a71df3 100644 --- a/credential/jwt.go +++ b/credential/integrity/jwt.go @@ -1,10 +1,11 @@ -package credential +package integrity import ( "context" "fmt" "time" + "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did/resolution" @@ -24,7 +25,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 jwx.Signer, cred VerifiableCredential) ([]byte, error) { +func SignVerifiableCredentialJWT(signer jwx.Signer, cred credential.VerifiableCredential) ([]byte, error) { if cred.IsEmpty() { return nil, errors.New("credential cannot be empty") } @@ -98,9 +99,9 @@ func SignVerifiableCredentialJWT(signer jwx.Signer, cred VerifiableCredential) ( // VerifyVerifiableCredentialJWT verifies the signature validity on the token and parses // the token in a verifiable credential. -// TODO(gabe) modify this to add additional verification steps such as credential status, expiration, etc. +// TODO(gabe) modify this to add additional validation steps such as credential status, expiration, etc. // related to https://github.com/TBD54566975/ssi-service/issues/122 -func VerifyVerifiableCredentialJWT(verifier jwx.Verifier, token string) (jws.Headers, jwt.Token, *VerifiableCredential, error) { +func VerifyVerifiableCredentialJWT(verifier jwx.Verifier, token string) (jws.Headers, jwt.Token, *credential.VerifiableCredential, error) { if err := verifier.Verify(token); err != nil { return nil, nil, nil, errors.Wrap(err, "verifying JWT") } @@ -111,7 +112,7 @@ func VerifyVerifiableCredentialJWT(verifier jwx.Verifier, token string) (jws.Hea // 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, *VerifiableCredential, error) { +func ParseVerifiableCredentialFromJWT(token string) (jws.Headers, jwt.Token, *credential.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") @@ -133,7 +134,7 @@ func ParseVerifiableCredentialFromJWT(token string) (jws.Headers, jwt.Token, *Ve } // ParseVerifiableCredentialFromToken takes a JWT object and parses it into a VerifiableCredential -func ParseVerifiableCredentialFromToken(token jwt.Token) (*VerifiableCredential, error) { +func ParseVerifiableCredentialFromToken(token jwt.Token) (*credential.VerifiableCredential, error) { // parse remaining JWT properties and set in the credential vcClaim, ok := token.Get(VCJWTProperty) if !ok { @@ -143,7 +144,7 @@ func ParseVerifiableCredentialFromToken(token jwt.Token) (*VerifiableCredential, if err != nil { return nil, errors.Wrap(err, "marshalling credential claim") } - var cred VerifiableCredential + var cred credential.VerifiableCredential if err = json.Unmarshal(vcBytes, &cred); err != nil { return nil, errors.Wrap(err, "reconstructing Verifiable Credential") } @@ -179,7 +180,7 @@ func ParseVerifiableCredentialFromToken(token jwt.Token) (*VerifiableCredential, if cred.CredentialSubject == nil { cred.CredentialSubject = make(map[string]any) } - cred.CredentialSubject[VerifiableCredentialIDProperty] = subStr + cred.CredentialSubject[credential.VerifiableCredentialIDProperty] = subStr } return &cred, nil @@ -195,7 +196,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 jwx.Signer, parameters JWTVVPParameters, presentation VerifiablePresentation) ([]byte, error) { +func SignVerifiablePresentationJWT(signer jwx.Signer, parameters JWTVVPParameters, presentation credential.VerifiablePresentation) ([]byte, error) { if presentation.IsEmpty() { return nil, errors.New("presentation cannot be empty") } @@ -270,7 +271,7 @@ func SignVerifiablePresentationJWT(signer jwx.Signer, parameters JWTVVPParameter // 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 jwx.Verifier, r resolution.Resolver, token string) (jws.Headers, jwt.Token, *VerifiablePresentation, error) { +func VerifyVerifiablePresentationJWT(ctx context.Context, verifier jwx.Verifier, r resolution.Resolver, token string) (jws.Headers, jwt.Token, *credential.VerifiablePresentation, error) { if r == nil { return nil, nil, nil, errors.New("r cannot be empty") } @@ -306,7 +307,7 @@ func VerifyVerifiablePresentationJWT(ctx context.Context, verifier jwx.Verifier, 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 nil, nil, nil, errors.Errorf("credential %d failed signature validation", i) } } @@ -318,7 +319,7 @@ func VerifyVerifiablePresentationJWT(ctx context.Context, verifier jwx.Verifier, // 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, *VerifiablePresentation, error) { +func ParseVerifiablePresentationFromJWT(token string) (jws.Headers, jwt.Token, *credential.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") @@ -331,7 +332,7 @@ 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 VerifiablePresentation + var pres credential.VerifiablePresentation if err = json.Unmarshal(vpBytes, &pres); err != nil { return nil, nil, nil, errors.Wrap(err, "reconstructing Verifiable Presentation") } diff --git a/credential/jwt_test.go b/credential/integrity/jwt_test.go similarity index 95% rename from credential/jwt_test.go rename to credential/integrity/jwt_test.go index dd88bf80..44aa0fc5 100644 --- a/credential/jwt_test.go +++ b/credential/integrity/jwt_test.go @@ -1,10 +1,11 @@ -package credential +package integrity import ( "context" "testing" "time" + "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did/key" @@ -16,7 +17,7 @@ import ( ) func TestVerifiableCredentialJWT(t *testing.T) { - testCredential := VerifiableCredential{ + 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"}, @@ -90,7 +91,7 @@ func TestVerifiableCredentialJWT(t *testing.T) { func TestVerifiablePresentationJWT(t *testing.T) { t.Run("bad audience", func(tt *testing.T) { - testPresentation := VerifiablePresentation{ + testPresentation := credential.VerifiablePresentation{ Context: []string{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, Type: []string{"VerifiablePresentation"}, @@ -118,7 +119,7 @@ func TestVerifiablePresentationJWT(t *testing.T) { }) t.Run("no VCs", func(tt *testing.T) { - testPresentation := VerifiablePresentation{ + testPresentation := credential.VerifiablePresentation{ Context: []string{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, Type: []string{"VerifiablePresentation"}, @@ -175,7 +176,7 @@ func TestVerifiablePresentationJWT(t *testing.T) { subjectKID := expandedSubjectDID.VerificationMethod[0].ID assert.NotEmpty(tt, subjectKID) - testCredential := VerifiableCredential{ + testCredential := credential.VerifiableCredential{ ID: uuid.NewString(), Context: []any{"https://www.w3.org/2018/credentials/v1"}, Type: []string{"VerifiableCredential"}, @@ -192,7 +193,7 @@ func TestVerifiablePresentationJWT(t *testing.T) { signedVC, err := SignVerifiableCredentialJWT(*issuerSigner, testCredential) assert.NoError(t, err) - testPresentation := VerifiablePresentation{ + testPresentation := credential.VerifiablePresentation{ Context: []string{"https://www.w3.org/2018/credentials/v1"}, Type: []string{"VerifiablePresentation"}, Holder: subjectDID.String(), diff --git a/credential/signature.go b/credential/integrity/signature.go similarity index 70% rename from credential/signature.go rename to credential/integrity/signature.go index 0c78c514..4a7cdd77 100644 --- a/credential/signature.go +++ b/credential/integrity/signature.go @@ -1,4 +1,4 @@ -package credential +package integrity import ( "context" @@ -6,6 +6,7 @@ import ( "fmt" "reflect" + "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/did/resolution" @@ -23,27 +24,29 @@ func VerifyCredentialSignature(ctx context.Context, genericCred any, r resolutio return false, errors.New("resolution cannot be empty") } switch typedCred := genericCred.(type) { - case *VerifiableCredential, VerifiableCredential, map[string]any: - _, token, cred, err := ToCredential(typedCred) + case map[string]any: + typedCredBytes, err := json.Marshal(typedCred) if err != nil { - return false, errors.Wrap(err, "error converting credential from generic type") + return false, errors.Wrap(err, "marshalling credential map") } - if token != nil { - return false, errors.New("JWT credentials must include a signature to be verified") + var cred credential.VerifiableCredential + if err = json.Unmarshal(typedCredBytes, &cred); err != nil { + return false, errors.Wrap(err, "unmarshalling credential object") } if cred.IsEmpty() { - return false, errors.New("credential cannot be empty") + return false, errors.New("map is not a valid credential") } - if cred.GetProof() == nil { - return false, errors.New("credential must have a proof") - } - return false, errors.New("data integrity signature verification not yet implemented") + return VerifyCredentialSignature(ctx, cred, r) + case *credential.VerifiableCredential: + return VerifyDataIntegrityCredential(*typedCred, r) + case credential.VerifiableCredential: + return VerifyDataIntegrityCredential(typedCred, r) case []byte: // turn it into a string and try again return VerifyCredentialSignature(ctx, string(typedCred), r) case string: // could be a Data Integrity credential - var cred VerifiableCredential + var cred credential.VerifiableCredential if err := json.Unmarshal([]byte(typedCred), &cred); err == nil { return VerifyCredentialSignature(ctx, cred, r) } @@ -94,3 +97,16 @@ func VerifyJWTCredential(cred string, r resolution.Resolver) (bool, error) { } return true, nil } + +// VerifyDataIntegrityCredential verifies the signature of a Data Integrity credential +// TODO(gabe): https://github.com/TBD54566975/ssi-sdk/issues/196 +func VerifyDataIntegrityCredential(cred credential.VerifiableCredential, _ resolution.Resolver) (bool, error) { + 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("not implemented") +} diff --git a/credential/signature_test.go b/credential/integrity/signature_test.go similarity index 91% rename from credential/signature_test.go rename to credential/integrity/signature_test.go index 476114d4..484f4ddb 100644 --- a/credential/signature_test.go +++ b/credential/integrity/signature_test.go @@ -1,10 +1,11 @@ -package credential +package integrity import ( "context" "testing" "time" + "github.com/TBD54566975/ssi-sdk/credential" "github.com/goccy/go-json" "github.com/TBD54566975/ssi-sdk/crypto" @@ -46,18 +47,15 @@ func TestVerifyCredentialSignature(t *testing.T) { _, 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") + assert.Contains(tt, err.Error(), "map is not a valid credential") }) t.Run("data integrity map credential type missing proof", func(tt *testing.T) { resolver, err := resolution.NewResolver([]resolution.Resolver{key.Resolver{}}...) assert.NoError(tt, err) - credential := getTestCredential() - credMap, err := ToCredentialJSONMap(credential) - assert.NoError(tt, err) - - _, err = VerifyCredentialSignature(context.Background(), credMap, resolver) + cred := getTestCredential() + _, err = VerifyCredentialSignature(context.Background(), cred, resolver) assert.Error(tt, err) assert.Contains(tt, err.Error(), "credential must have a proof") }) @@ -66,13 +64,13 @@ func TestVerifyCredentialSignature(t *testing.T) { resolver, err := resolution.NewResolver([]resolution.Resolver{key.Resolver{}}...) assert.NoError(tt, err) - credential := getTestCredential() - _, err = VerifyCredentialSignature(context.Background(), credential, resolver) + cred := getTestCredential() + _, err = VerifyCredentialSignature(context.Background(), cred, 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) + _, err = VerifyCredentialSignature(context.Background(), &cred, resolver) assert.Error(tt, err) assert.Contains(tt, err.Error(), "credential must have a proof") }) @@ -229,7 +227,7 @@ func TestVerifyJWTCredential(t *testing.T) { } func getTestJWTCredential(t *testing.T, signer jwx.Signer) string { - cred := VerifiableCredential{ + cred := credential.VerifiableCredential{ ID: uuid.NewString(), Context: []any{"https://www.w3.org/2018/credentials/v1"}, Type: []string{"VerifiableCredential"}, @@ -247,3 +245,13 @@ func getTestJWTCredential(t *testing.T, signer jwx.Signer) string { require.NotEmpty(t, signed) return string(signed) } + +func getTestCredential() credential.VerifiableCredential { + return credential.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", + IssuanceDate: "2021-01-01T19:23:24Z", + CredentialSubject: map[string]any{}, + } +} diff --git a/credential/manifest/validation.go b/credential/manifest/validation.go index f618bc70..91222598 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/parsing" errresp "github.com/TBD54566975/ssi-sdk/error" "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" diff --git a/credential/manifest/validation_test.go b/credential/manifest/validation_test.go index 8920de8a..6cf2262d 100644 --- a/credential/manifest/validation_test.go +++ b/credential/manifest/validation_test.go @@ -5,6 +5,7 @@ import ( "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/credential/exchange" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/cryptosuite" @@ -413,7 +414,7 @@ func getValidTestCredManifestCredApplicationJWTCred(t *testing.T) (CredentialMan require.NoError(t, err) signer, err := jwx.NewJWXSigner("test-id", "test-kid", privKey) require.NoError(t, err) - jwt, err := credential.SignVerifiableCredentialJWT(*signer, vc) + jwt, err := integrity.SignVerifiableCredentialJWT(*signer, vc) require.NoError(t, err) require.NotEmpty(t, jwt) diff --git a/credential/util.go b/credential/parsing/parsing.go similarity index 82% rename from credential/util.go rename to credential/parsing/parsing.go index bf1db825..eb575533 100644 --- a/credential/util.go +++ b/credential/parsing/parsing.go @@ -1,9 +1,11 @@ -package credential +package parsing import ( "fmt" "reflect" + "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/goccy/go-json" "github.com/lestrrat-go/jwx/v2/jws" @@ -12,7 +14,7 @@ import ( ) // ToCredential turn a generic cred into its known object model -func ToCredential(genericCred any) (jws.Headers, jwt.Token, *VerifiableCredential, error) { +func ToCredential(genericCred any) (jws.Headers, jwt.Token, *credential.VerifiableCredential, error) { switch typedCred := genericCred.(type) { case []byte: // could be a JWT @@ -22,24 +24,24 @@ func ToCredential(genericCred any) (jws.Headers, jwt.Token, *VerifiableCredentia } // could also be a vc - var cred VerifiableCredential + var cred credential.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: + case *credential.VerifiableCredential: return nil, nil, typedCred, nil - case VerifiableCredential: + case credential.VerifiableCredential: return nil, nil, &typedCred, nil case string: // first try the case where the string is JSON of a VC object - var cred VerifiableCredential + var cred credential.VerifiableCredential if err := json.Unmarshal([]byte(typedCred), &cred); err == nil { return nil, nil, &cred, nil } // next try it as a JWT - return ParseVerifiableCredentialFromJWT(typedCred) + return integrity.ParseVerifiableCredentialFromJWT(typedCred) case map[string]any: // VC or JWTVC JSON credMapBytes, err := json.Marshal(typedCred) @@ -48,7 +50,7 @@ func ToCredential(genericCred any) (jws.Headers, jwt.Token, *VerifiableCredentia } // first try as a VC object - var cred VerifiableCredential + var cred credential.VerifiableCredential if err = json.Unmarshal(credMapBytes, &cred); err == nil && !cred.IsEmpty() { return nil, nil, &cred, nil } @@ -74,7 +76,7 @@ func ToCredentialJSONMap(genericCred any) (map[string]any, error) { } // could also be a vc - var cred VerifiableCredential + var cred credential.VerifiableCredential if err = json.Unmarshal(typedCred, &cred); err != nil { return nil, errors.Wrap(err, "unmarshalling credential object") } @@ -89,7 +91,7 @@ func ToCredentialJSONMap(genericCred any) (map[string]any, error) { } // next try it as a JWT - _, token, _, err := ParseVerifiableCredentialFromJWT(typedCred) + _, token, _, err := integrity.ParseVerifiableCredentialFromJWT(typedCred) if err != nil { return nil, errors.Wrap(err, "parsing credential from JWT") } @@ -102,7 +104,7 @@ func ToCredentialJSONMap(genericCred any) (map[string]any, error) { return nil, errors.Wrap(err, "unmarshalling credential JWT") } return credJSON, nil - case VerifiableCredential, *VerifiableCredential: + case credential.VerifiableCredential, *credential.VerifiableCredential: credJSONBytes, err := json.Marshal(typedCred) if err != nil { return nil, errors.Wrap(err, "marshalling credential object") @@ -117,7 +119,7 @@ func ToCredentialJSONMap(genericCred any) (map[string]any, error) { } // VCJWTJSONToVC converts a JSON representation of a VC JWT into a VerifiableCredential -func VCJWTJSONToVC(vcJWTJSON []byte) (jws.Headers, jwt.Token, *VerifiableCredential, error) { +func VCJWTJSONToVC(vcJWTJSON []byte) (jws.Headers, 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 { @@ -130,7 +132,7 @@ func VCJWTJSONToVC(vcJWTJSON []byte) (jws.Headers, jwt.Token, *VerifiableCredent return nil, nil, nil, errors.Wrap(err, "could not get JWT headers") } - cred, err := ParseVerifiableCredentialFromToken(token) + cred, err := integrity.ParseVerifiableCredentialFromToken(token) if err != nil { return nil, nil, nil, errors.Wrap(err, "parsing credential from token") } diff --git a/credential/util_test.go b/credential/parsing/parsing_test.go similarity index 94% rename from credential/util_test.go rename to credential/parsing/parsing_test.go index d17ebec0..6ba0adbb 100644 --- a/credential/util_test.go +++ b/credential/parsing/parsing_test.go @@ -1,8 +1,10 @@ -package credential +package parsing import ( "testing" + "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/cryptosuite" "github.com/TBD54566975/ssi-sdk/cryptosuite/jws2020" @@ -128,7 +130,7 @@ func TestCredentialsFromInterface(t *testing.T) { assert.NoError(tt, err) testCred := getTestCredential() - signed, err := SignVerifiableCredentialJWT(*signer, testCred) + signed, err := integrity.SignVerifiableCredentialJWT(*signer, testCred) assert.NoError(tt, err) assert.NotEmpty(tt, signed) @@ -149,8 +151,8 @@ func TestCredentialsFromInterface(t *testing.T) { }) } -func getTestCredential() VerifiableCredential { - return VerifiableCredential{ +func getTestCredential() credential.VerifiableCredential { + return credential.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/schema/access.go b/credential/schema/access.go index bd61cafb..bd744a73 100644 --- a/credential/schema/access.go +++ b/credential/schema/access.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/parsing" "github.com/goccy/go-json" "github.com/pkg/errors" ) @@ -60,7 +60,7 @@ func (ra *RemoteAccess) GetVCJSONSchema(ctx context.Context, t VCJSONSchemaType, if err = json.NewDecoder(resp.Body).Decode(&schemaCred); err != nil { return nil, errors.Wrap(err, "error decoding schema to generic response") } - _, _, cred, err := credential.ToCredential(schemaCred) + _, _, cred, err := parsing.ToCredential(schemaCred) if err != nil { return nil, errors.Wrap(err, "error decoding schema from credential") } diff --git a/credential/validation/validation.go b/credential/validation/validation.go new file mode 100644 index 00000000..0431f677 --- /dev/null +++ b/credential/validation/validation.go @@ -0,0 +1,73 @@ +package validation + +import ( + "fmt" + + "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/util" + "github.com/pkg/errors" +) + +type CredentialValidator struct { + validators []Validator +} + +type Validator struct { + ID string + ValidateFunc Validate +} + +type ( + // OptionKey uniquely represents an option to be used in a validator + OptionKey string +) + +// Option represents a single option that may be required for a validator +type Option struct { + ID OptionKey + Option any +} + +// GetValidationOption returns a validation option given an ID +func GetValidationOption(opts []Option, id OptionKey) (any, error) { + for _, opt := range opts { + if opt.ID == id { + return opt.Option, nil + } + } + return nil, errors.Errorf("option with id <%s> not found", id) +} + +type Validate func(cred credential.VerifiableCredential, opts ...Option) error + +// NewCredentialValidator creates a new credential validator which executes in the order of the validators provided +// The validators introspect the contents of the credential, and do not handle signature verification. +func NewCredentialValidator(validators []Validator) (*CredentialValidator, error) { + // dedupe + var deduplicatedValidators []Validator + validatorCheck := make(map[string]bool) + for _, validator := range validators { + if _, ok := validatorCheck[validator.ID]; !ok { + validatorCheck[validator.ID] = true + deduplicatedValidators = append(deduplicatedValidators, validator) + } + } + if len(deduplicatedValidators) == 0 { + return nil, errors.New("no validators provided") + } + return &CredentialValidator{validators: deduplicatedValidators}, nil +} + +// ValidateCredential validates a credential given a credential validator +func (cv *CredentialValidator) ValidateCredential(cred credential.VerifiableCredential, opts ...Option) error { + ae := util.NewAppendError() + for _, validator := range cv.validators { + if err := validator.ValidateFunc(cred, opts...); err != nil { + ae.AppendString(fmt.Sprintf("[validator: %s]: %s", validator.ID, err.Error())) + } + } + if !ae.IsEmpty() { + return fmt.Errorf("credential validation failed with <%d> error, %s", ae.NumErrors(), ae.Error().Error()) + } + return nil +} diff --git a/credential/verification/verification_test.go b/credential/validation/validation_test.go similarity index 56% rename from credential/verification/verification_test.go rename to credential/validation/validation_test.go index 8982a001..bdbb26e3 100644 --- a/credential/verification/verification_test.go +++ b/credential/validation/validation_test.go @@ -1,4 +1,4 @@ -package verification +package validation import ( "testing" @@ -8,98 +8,98 @@ import ( "github.com/stretchr/testify/assert" ) -func TestVerifier(t *testing.T) { - t.Run("Test Basic Verifier", func(tt *testing.T) { - // empty verifier - _, err := NewCredentialVerifier(nil) +func TestValidator(t *testing.T) { + t.Run("Test Basic Validator", func(tt *testing.T) { + // empty validator + _, err := NewCredentialValidator(nil) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "no verifiers provided") + assert.Contains(tt, err.Error(), "no validators provided") - // no op verifier - noop := Verifier{ - ID: "noop", - VerifyFunc: NoOpVerifier, + // no op validator + noop := Validator{ + ID: "noop", + ValidateFunc: NoOpValidator, } - verifier, err := NewCredentialVerifier([]Verifier{noop}) + validator, err := NewCredentialValidator([]Validator{noop}) assert.NoError(tt, err) - assert.NotEmpty(tt, verifier) + assert.NotEmpty(tt, validator) - // verify - err = verifier.VerifyCredential(credential.VerifiableCredential{}) + // validate + err = validator.ValidateCredential(credential.VerifiableCredential{}) assert.NoError(tt, err) sampleCredential := getSampleCredential() - err = verifier.VerifyCredential(sampleCredential) + err = validator.ValidateCredential(sampleCredential) assert.NoError(t, err) }) - t.Run("Expiry Verifier", func(tt *testing.T) { - // expiry verifier - expiry := Verifier{ - ID: "expiration date checking", - VerifyFunc: VerifyExpiry, + t.Run("Expiry Validator", func(tt *testing.T) { + // expiry validator + expiry := Validator{ + ID: "expiration date checking", + ValidateFunc: ValidateExpiry, } - verifier, err := NewCredentialVerifier([]Verifier{expiry}) + validator, err := NewCredentialValidator([]Validator{expiry}) assert.NoError(tt, err) - assert.NotEmpty(tt, verifier) + assert.NotEmpty(tt, validator) sampleCredential := getSampleCredential() - err = verifier.VerifyCredential(sampleCredential) + err = validator.ValidateCredential(sampleCredential) assert.Error(tt, err) assert.Contains(tt, err.Error(), "credential has expired as of 2021-01-01 00:00:00 +0000 UTC") }) - t.Run("Schema Verifier", func(tt *testing.T) { - // set up schema verifier - schema := Verifier{ - ID: "JSON Schema checking", - VerifyFunc: VerifyJSONSchema, + t.Run("Schema Validator", func(tt *testing.T) { + // set up schema validator + schema := Validator{ + ID: "JSON Schema checking", + ValidateFunc: ValidateJSONSchema, } - verifier, err := NewCredentialVerifier([]Verifier{schema}) + validator, err := NewCredentialValidator([]Validator{schema}) assert.NoError(t, err) - assert.NotEmpty(t, verifier) + assert.NotEmpty(t, validator) sampleCredential := getSampleCredential() - // verify cred with no schema, no schema passed in - err = verifier.VerifyCredential(sampleCredential) + // validate cred with no schema, no schema passed in + err = validator.ValidateCredential(sampleCredential) assert.NoError(t, err) - // verify cred with no schema, schema passed in + // validate cred with no schema, schema passed in badSchema := `{"bad":"schema"}` - err = verifier.VerifyCredential(sampleCredential, WithSchema(badSchema)) + err = validator.ValidateCredential(sampleCredential, WithSchema(badSchema)) assert.Error(tt, err) assert.Contains(tt, err.Error(), "credential does not have a credentialSchema property") - // verify cred with schema, no schema passed in + // validate cred with schema, no schema passed in sampleCredential.CredentialSchema = &credential.CredentialSchema{ ID: "did:example:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", Type: credschema.JSONSchema2023Type.String(), } - err = verifier.VerifyCredential(sampleCredential) + err = validator.ValidateCredential(sampleCredential) assert.Error(tt, err) assert.Contains(tt, err.Error(), "no schema provided") - // verify cred with schema, schema passed in, cred with bad data + // validate cred with schema, schema passed in, cred with bad data knownSchema := getVCJSONSchema() - err = verifier.VerifyCredential(sampleCredential, WithSchema(knownSchema)) + err = validator.ValidateCredential(sampleCredential, WithSchema(knownSchema)) assert.Error(tt, err) assert.Contains(tt, err.Error(), "missing properties: 'emailAddress'") - // verify cred with schema, schema passed in, cred with good data + // validate cred with schema, schema passed in, cred with good data sampleCredential.CredentialSubject = map[string]any{ "id": "test-vc-id", "emailAddress": "grandma@aol.com", } - err = verifier.VerifyCredential(sampleCredential, WithSchema(knownSchema)) + err = validator.ValidateCredential(sampleCredential, WithSchema(knownSchema)) assert.NoError(tt, err) }) } -func NoOpVerifier(_ credential.VerifiableCredential, _ ...Option) error { +func NoOpValidator(_ credential.VerifiableCredential, _ ...Option) error { return nil } diff --git a/credential/verification/verifiers.go b/credential/validation/validators.go similarity index 66% rename from credential/verification/verifiers.go rename to credential/validation/validators.go index a9598151..411f2b32 100644 --- a/credential/verification/verifiers.go +++ b/credential/validation/validators.go @@ -1,4 +1,4 @@ -package verification +package validation import ( "fmt" @@ -15,14 +15,14 @@ const ( SchemaOption OptionKey = "schema" ) -// VerifyValidCredential verifies a credential's object model depending on the struct tags used on VerifiableCredential -func VerifyValidCredential(cred credential.VerifiableCredential, _ ...Option) error { +// ValidateCredential verifies a credential's object model depending on the struct tags used on VerifiableCredential +func ValidateCredential(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 +// ValidateExpiry 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, _ ...Option) error { +func ValidateExpiry(cred credential.VerifiableCredential, _ ...Option) error { if cred.ExpirationDate == "" { return nil } @@ -36,7 +36,7 @@ func VerifyExpiry(cred credential.VerifiableCredential, _ ...Option) error { return nil } -// WithSchema provides a schema as a verification option +// WithSchema provides a schema as a validation option func WithSchema(schema string) Option { return Option{ ID: SchemaOption, @@ -44,18 +44,18 @@ func WithSchema(schema string) Option { } } -// VerifyJSONSchema verifies a credential's data against a Verifiable Credential JSON Schema: +// ValidateJSONSchema 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 ...Option) error { +func ValidateJSONSchema(cred credential.VerifiableCredential, opts ...Option) error { hasSchemaProperty := cred.CredentialSchema != nil - schema, err := GetVerificationOption(opts, SchemaOption) + schema, err := GetValidationOption(opts, SchemaOption) if err != nil { // if the cred does not have a schema property, we cannot perform this check if !hasSchemaProperty { return nil } - return errors.Wrap(err, "cannot verify the credential against a JSON schema, no schema provided") + return errors.Wrap(err, "cannot validate the credential against a JSON schema, no schema provided") } // if the cred does not have a schema property, we cannot perform this check if !hasSchemaProperty { @@ -83,19 +83,19 @@ func optionToCredentialSchema(maybeSchema any) (*credschema.JSONSchema, error) { return &credSchema, nil } -func GetKnownVerifiers() []Verifier { - return []Verifier{ +func GetKnownVerifiers() []Validator { + return []Validator{ { - ID: "Object Validation", - VerifyFunc: VerifyValidCredential, + ID: "Data Model Validation", + ValidateFunc: ValidateCredential, }, { - ID: "Expiry Check", - VerifyFunc: VerifyExpiry, + ID: "Expiry Check", + ValidateFunc: ValidateExpiry, }, { - ID: "VC JSON Schema", - VerifyFunc: VerifyJSONSchema, + ID: "VC JSON Schema", + ValidateFunc: ValidateJSONSchema, }, } } diff --git a/credential/verification/verification.go b/credential/verification/verification.go deleted file mode 100644 index f23e0c97..00000000 --- a/credential/verification/verification.go +++ /dev/null @@ -1,73 +0,0 @@ -package verification - -import ( - "fmt" - - "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/util" - "github.com/pkg/errors" -) - -type CredentialVerifier struct { - verifiers []Verifier -} - -type Verifier struct { - ID string - VerifyFunc Verify -} - -type ( - // OptionKey uniquely represents an option to be used in a verifier - OptionKey string -) - -// 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 []Option, id OptionKey) (any, error) { - for _, opt := range opts { - if opt.ID == id { - return opt.Option, nil - } - } - return nil, errors.Errorf("option with id <%s> not found", id) -} - -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 - verifierCheck := make(map[string]bool) - for _, verifier := range verifiers { - if _, ok := verifierCheck[verifier.ID]; !ok { - verifierCheck[verifier.ID] = true - deduplicatedVerifiers = append(deduplicatedVerifiers, verifier) - } - } - if len(deduplicatedVerifiers) == 0 { - return nil, errors.New("no verifiers provided") - } - return &CredentialVerifier{verifiers: deduplicatedVerifiers}, nil -} - -// VerifyCredential verifies a credential given a credential verifier -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 { - ae.AppendString(fmt.Sprintf("[validator: %s]: %s", verifier.ID, err.Error())) - } - } - if !ae.IsEmpty() { - return fmt.Errorf("credential verification failed with <%d> error, %s", ae.NumErrors(), ae.Error().Error()) - } - return nil -} diff --git a/did/peer/peer0.go b/did/peer/peer0.go index 0961d462..c2d13bc3 100644 --- a/did/peer/peer0.go +++ b/did/peer/peer0.go @@ -35,7 +35,7 @@ func (Method0) Generate(kt crypto.KeyType, publicKey gocrypto.PublicKey) (*DIDPe } // Resolve resolves a did:peer into a DID Document -// To do so, it decodes the key, constructs a verification method, and returns a DID Document .This allows Method0 +// To do so, it decodes the key, constructs a verification method, and returns a DID Document .This allows Method0 // to implement the DID Resolver interface and be used to expand the did into the DID Document. func (Method0) resolve(didDoc did.DID, _ resolution.Option) (*resolution.Result, error) { d, ok := didDoc.(DIDPeer) diff --git a/example/usecase/apartment_application/apartment_application.go b/example/usecase/apartment_application/apartment_application.go index 11fe7a09..34d88a19 100644 --- a/example/usecase/apartment_application/apartment_application.go +++ b/example/usecase/apartment_application/apartment_application.go @@ -17,6 +17,7 @@ import ( "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/credential/exchange" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did/key" @@ -88,7 +89,7 @@ func main() { example.HandleExampleError(err, "Failed to make verifiable credential") example.HandleExampleError(vc.IsValid(), "Verifiable credential is not valid") - signedVCBytes, err := credential.SignVerifiableCredentialJWT(*govtSigner, *vc) + signedVCBytes, err := integrity.SignVerifiableCredentialJWT(*govtSigner, *vc) example.HandleExampleError(err, "Failed to sign vc") diff --git a/example/usecase/employer_university_flow/main.go b/example/usecase/employer_university_flow/main.go index 342f46ac..65ab6e52 100644 --- a/example/usecase/employer_university_flow/main.go +++ b/example/usecase/employer_university_flow/main.go @@ -54,10 +54,10 @@ import ( "fmt" "os" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/goccy/go-json" "github.com/sirupsen/logrus" - "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/did/key" @@ -176,7 +176,7 @@ func main() { r, err := resolution.NewResolver([]resolution.Resolver{key.Resolver{}, peer.Resolver{}}...) example.HandleExampleError(err, "failed to create DID r") - _, _, vp, err := credential.VerifyVerifiablePresentationJWT(context.Background(), *verifier, r, string(submission)) + _, _, vp, err := integrity.VerifyVerifiablePresentationJWT(context.Background(), *verifier, r, string(submission)) example.HandleExampleError(err, "failed to verify jwt") dat, err = json.Marshal(vp) diff --git a/example/usecase/employer_university_flow/pkg/issuer.go b/example/usecase/employer_university_flow/pkg/issuer.go index 8904b9e7..fc68941f 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/credential/integrity" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/goccy/go-json" @@ -61,12 +62,12 @@ func BuildExampleUniversityVC(signer jwx.Signer, universityDID, recipientDID str logrus.Debug(string(dat)) // sign the credential as a JWT - signedCred, err := credential.SignVerifiableCredentialJWT(signer, knownCred) + signedCred, err := integrity.SignVerifiableCredentialJWT(signer, knownCred) if err != nil { return "", "", err } cred = string(signedCred) - _, credToken, _, err := credential.ParseVerifiableCredentialFromJWT(string(signedCred)) + _, credToken, _, err := integrity.ParseVerifiableCredentialFromJWT(string(signedCred)) if err != nil { return "", "", err } diff --git a/example/usecase/employer_university_flow/pkg/verifier.go b/example/usecase/employer_university_flow/pkg/verifier.go index 08e1eecc..2a4e11bd 100644 --- a/example/usecase/employer_university_flow/pkg/verifier.go +++ b/example/usecase/employer_university_flow/pkg/verifier.go @@ -3,7 +3,7 @@ package pkg import ( "context" - "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did/resolution" @@ -16,7 +16,7 @@ import ( // 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 jwx.Verifier, r resolution.Resolver, submissionBytes []byte) error { - _, _, vp, err := credential.VerifyVerifiablePresentationJWT(context.Background(), verifier, r, string(submissionBytes)) + _, _, vp, err := integrity.VerifyVerifiablePresentationJWT(context.Background(), verifier, r, string(submissionBytes)) if err != nil { return errors.Wrap(err, "failed to validate VP signature") } diff --git a/sd-jwt/sd_jwt.go b/sd-jwt/sd_jwt.go index 5c5cf569..9aeae050 100644 --- a/sd-jwt/sd_jwt.go +++ b/sd-jwt/sd_jwt.go @@ -384,7 +384,7 @@ func parseDisclosures(disclosuresData []string, hashAlg HashFunc) (map[string]*D ds = append(ds, d) } disclosureDigests := make(map[string]*Disclosure, len(ds)) - //For each Disclosure provided: + // For each Disclosure provided: for _, disclosure := range ds { // Calculate the digest over the base64url-encoded string as described in Section 5.1.1.2. disclosureDigests[disclosure.Digest(hashAlg)] = disclosure @@ -444,16 +444,16 @@ func VerifySDPresentation(presentation []byte, verificationOptions VerificationO // Validate the SD-JWT: // - //Ensure that a signing algorithm was used that was deemed secure for the application. Refer to [RFC8725], Sections 3.1 and 3.2 for details. The none algorithm MUST NOT be accepted. - //Validate the signature over the SD-JWT. - //Validate the Issuer of the SD-JWT and that the signing key belongs to this Issuer. - //Check that the SD-JWT is valid using nbf, iat, and exp claims, if provided in the SD-JWT, and not selectively disclosed. + // Ensure that a signing algorithm was used that was deemed secure for the application. Refer to [RFC8725], Sections 3.1 and 3.2 for details. The none algorithm MUST NOT be accepted. + // Validate the signature over the SD-JWT. + // Validate the Issuer of the SD-JWT and that the signing key belongs to this Issuer. + // Check that the SD-JWT is valid using nbf, iat, and exp claims, if provided in the SD-JWT, and not selectively disclosed. sdToken, err := jwt.Parse([]byte(sdParts[0]), jwt.WithKey(jwa.KeyAlgorithmFrom(verificationOptions.Alg), verificationOptions.IssuerKey), jwt.WithValidate(true)) if err != nil { return nil, errors.Wrap(err, "parsing jwt") } - //Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure. + // Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure. hashAlg, err := GetHashAlg(sdToken) if err != nil { return nil, err @@ -468,7 +468,7 @@ func VerifySDPresentation(presentation []byte, verificationOptions VerificationO // Process the Disclosures and _sd keys in the SD-JWT as follows: // - //Create a copy of the SD-JWT payload, if required for further processing. + // Create a copy of the SD-JWT payload, if required for further processing. tokenClaims, err := sdToken.AsMap(context.Background()) if err != nil { return nil, errors.Wrap(err, "gathering token map") @@ -488,13 +488,13 @@ func VerifySDPresentation(presentation []byte, verificationOptions VerificationO // Determine the public key for the Holder from the SD-JWT. holderKey := verificationOptions.ResolveHolderKey(sdToken) - //Ensure that a signing algorithm was used that was deemed secure for the application. Refer to [RFC8725], Sections 3.1 and 3.2 for details. The none algorithm MUST NOT be accepted. - //TODO(https://github.com/TBD54566975/ssi-sdk/issues/377): support holder binding properly as specified in RFC7800. Alg should be coming from CNF. + // Ensure that a signing algorithm was used that was deemed secure for the application. Refer to [RFC8725], Sections 3.1 and 3.2 for details. The none algorithm MUST NOT be accepted. + // TODO(https://github.com/TBD54566975/ssi-sdk/issues/377): support holder binding properly as specified in RFC7800. Alg should be coming from CNF. holderBindingAlg := jwa.ES256K - //Validate the signature over the Holder Binding JWT. - //Check that the Holder Binding JWT is valid using nbf, iat, and exp claims, if provided in the Holder Binding JWT. - //Determine that the Holder Binding JWT is bound to the current transaction and was created for this Verifier (replay protection). This is usually achieved by a nonce and aud field within the Holder Binding JWT. + // Validate the signature over the Holder Binding JWT. + // Check that the Holder Binding JWT is valid using nbf, iat, and exp claims, if provided in the Holder Binding JWT. + // Determine that the Holder Binding JWT is bound to the current transaction and was created for this Verifier (replay protection). This is usually achieved by a nonce and aud field within the Holder Binding JWT. holderBindingToken, err := jwt.Parse([]byte(holderBindingJWT), jwt.WithKey(holderBindingAlg, holderKey), jwt.WithValidate(true)) if err != nil { return nil, errors.Wrap(err, "parsing and validating holder binding jwt") @@ -526,7 +526,7 @@ func VerifySDPresentation(presentation []byte, verificationOptions VerificationO // found inside disclosuresByDigest. func processPayload(claims map[string]any, disclosuresByDigest map[string]*Disclosure, digestsFound map[string]struct{}) error { - //Find all _sd keys in the SD-JWT payload. For each such key perform the following steps (*): + // Find all _sd keys in the SD-JWT payload. For each such key perform the following steps (*): for _, claimValue := range claims { switch claimMap := claimValue.(type) { case map[string]any: @@ -611,7 +611,7 @@ func VerifyIssuance(issuance []byte, verificationOptions IssuanceVerificationOpt return errors.Wrap(err, "getting token claim map") } - //Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure. + // Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure. hashAlg, err := GetHashAlg(sdToken) if err != nil { return err