diff --git a/vc/vc.go b/vc/vc.go index 529fbe4..a0bc2b0 100644 --- a/vc/vc.go +++ b/vc/vc.go @@ -4,8 +4,10 @@ import ( "encoding/json" "errors" "fmt" + "github.com/lestrrat-go/jwx/jwt" "github.com/nuts-foundation/go-did/did" "net/url" + "strings" "time" ssi "github.com/nuts-foundation/go-did" @@ -33,6 +35,94 @@ func VCContextV1URI() ssi.URI { } } +const ( + // JSONLDCredentialProofFormat is the format for JSON-LD based credentials. + JSONLDCredentialProofFormat string = "ldp_vc" + // JWTCredentialsProofFormat is the format for JWT based credentials. + JWTCredentialsProofFormat = "jwt_vc" +) + +var errCredentialSubjectWithoutID = errors.New("credential subjects have no ID") + +// ParseVerifiableCredential parses a Verifiable Credential from a string, which can be either in JSON-LD or JWT format. +// If the format is JWT, the parsed token can be retrieved using JWT(). +func ParseVerifiableCredential(raw string) (*VerifiableCredential, error) { + raw = strings.TrimSpace(raw) + if strings.HasPrefix(raw, "{") { + // Assume JSON-LD format + type Alias VerifiableCredential + normalizedVC, err := marshal.NormalizeDocument([]byte(raw), pluralContext, marshal.Plural(typeKey), marshal.Plural(credentialSubjectKey), marshal.Plural(proofKey)) + if err != nil { + return nil, err + } + alias := Alias{} + err = json.Unmarshal(normalizedVC, &alias) + if err != nil { + return nil, err + } + alias.format = JSONLDCredentialProofFormat + alias.raw = raw + result := VerifiableCredential(alias) + return &result, err + } else { + // Assume JWT format + token, err := jwt.Parse([]byte(raw)) + if err != nil { + return nil, err + } + var result VerifiableCredential + if innerVCInterf := token.PrivateClaims()["vc"]; innerVCInterf != nil { + innerVCJSON, _ := json.Marshal(innerVCInterf) + err = json.Unmarshal(innerVCJSON, &result) + if err != nil { + return nil, fmt.Errorf("invalid JWT 'vc' claim: %w", err) + } + } + // parse exp + exp := token.Expiration() + result.ExpirationDate = &exp + // parse iss + if iss, err := parseURIClaim(token, jwt.IssuerKey); err != nil { + return nil, err + } else if iss != nil { + result.Issuer = *iss + } + // parse nbf + result.IssuanceDate = token.NotBefore() + // parse sub + if token.Subject() != "" { + for _, credentialSubjectInterf := range result.CredentialSubject { + credentialSubject, isMap := credentialSubjectInterf.(map[string]interface{}) + if isMap { + credentialSubject["id"] = token.Subject() + } + } + } + var subject string + if subjectDID, err := result.SubjectDID(); err != nil { + // credentialSubject.id is optional + if !errors.Is(err, errCredentialSubjectWithoutID) { + return nil, fmt.Errorf("invalid JWT 'sub' claim: %w", err) + } + } else if subjectDID != nil { + subject = subjectDID.String() + } + if token.Subject() != subject { + return nil, errors.New("invalid JWT 'sub' claim: must equal credentialSubject.id") + } + // parse jti + if jti, err := parseURIClaim(token, jwt.JwtIDKey); err != nil { + return nil, err + } else if jti != nil { + result.ID = jti + } + result.format = JWTCredentialsProofFormat + result.raw = raw + result.token = token + return &result, nil + } +} + // VerifiableCredential represents a credential as defined by the Verifiable Credentials Data Model 1.0 specification (https://www.w3.org/TR/vc-data-model/). type VerifiableCredential struct { // Context defines the json-ld context to dereference the URIs @@ -53,6 +143,29 @@ type VerifiableCredential struct { CredentialSubject []interface{} `json:"credentialSubject"` // Proof contains the cryptographic proof(s). It must be extracted using the Proofs method or UnmarshalProofValue method for non-generic proof fields. Proof []interface{} `json:"proof"` + + format string + raw string + token jwt.Token +} + +// Format returns the format of the credential (e.g. jwt_vc or ldp_vc). +func (vc VerifiableCredential) Format() string { + return vc.format +} + +// Raw returns the source of the credential as it was parsed. +func (vc VerifiableCredential) Raw() string { + return vc.raw +} + +// JWT returns the JWT token if the credential was parsed from a JWT. +func (vc VerifiableCredential) JWT() jwt.Token { + if vc.token == nil { + return nil + } + token, _ := vc.token.Clone() + return token } // CredentialStatus defines the method on how to determine a credential is revoked. @@ -88,18 +201,19 @@ func (vc VerifiableCredential) MarshalJSON() ([]byte, error) { } func (vc *VerifiableCredential) UnmarshalJSON(b []byte) error { - type Alias VerifiableCredential - normalizedVC, err := marshal.NormalizeDocument(b, pluralContext, marshal.Plural(typeKey), marshal.Plural(credentialSubjectKey), marshal.Plural(proofKey)) - if err != nil { - return err + var str string + if len(b) > 0 && b[0] == '"' { + if err := json.Unmarshal(b, &str); err != nil { + return err + } + } else { + str = string(b) } - tmp := Alias{} - err = json.Unmarshal(normalizedVC, &tmp) - if err != nil { - return err + credential, err := ParseVerifiableCredential(str) + if err == nil { + *vc = *credential } - *vc = (VerifiableCredential)(tmp) - return nil + return err } // UnmarshalProofValue unmarshalls the proof to the given proof type. Always pass a slice as target since there could be multiple proofs. @@ -148,7 +262,7 @@ func (vc VerifiableCredential) SubjectDID() (*did.DID, error) { } } if subjectID.Empty() { - return nil, errors.New("unable to get subject DID from VC: credential subjects have no ID") + return nil, fmt.Errorf("unable to get subject DID from VC: %w", errCredentialSubjectWithoutID) } return &subjectID, nil } diff --git a/vc/vc_test.go b/vc/vc_test.go index 0cff488..ab5bac1 100644 --- a/vc/vc_test.go +++ b/vc/vc_test.go @@ -3,11 +3,84 @@ package vc import ( "encoding/json" ssi "github.com/nuts-foundation/go-did" + "github.com/stretchr/testify/require" + "strings" "testing" "github.com/stretchr/testify/assert" ) +// jwtCredential is taken from https://www.w3.org/TR/vc-data-model/#example-verifiable-credential-using-jwt-compact-serialization-non-normative +const jwtCredential = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOmFiZmUxM2Y3MTIxMjA0 +MzFjMjc2ZTEyZWNhYiNrZXlzLTEifQ.eyJzdWIiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxY +zI3NmUxMmVjMjEiLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsImlzc +yI6Imh0dHBzOi8vZXhhbXBsZS5jb20va2V5cy9mb28uandrIiwibmJmIjoxNTQxNDkzNzI0LCJpYXQiO +jE1NDE0OTM3MjQsImV4cCI6MTU3MzAyOTcyMywibm9uY2UiOiI2NjAhNjM0NUZTZXIiLCJ2YyI6eyJAY +29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd +3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZ +UNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjd +CI6eyJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IjxzcGFuIGxhbmc9J2ZyL +UNBJz5CYWNjYWxhdXLDqWF0IGVuIG11c2lxdWVzIG51bcOpcmlxdWVzPC9zcGFuPiJ9fX19.KLJo5GAy +BND3LDTn9H7FQokEsUEi8jKwXhGvoN3JtRa51xrNDgXDb0cq1UTYB-rK4Ft9YVmR1NI_ZOF8oGc_7wAp +8PHbF2HaWodQIoOBxxT-4WNqAxft7ET6lkH-4S6Ux3rSGAmczMohEEf8eCeN-jC8WekdPl6zKZQj0YPB +1rx6X0-xlFBs7cl6Wt8rfBP_tZ9YgVWrQmUWypSioc0MUyiphmyEbLZagTyPlUyflGlEdqrZAv6eSe6R +txJy6M1-lD7a5HTzanYTWBPAUHDZGyGKXdJw-W_x0IWChBzI8t3kpG253fg6V3tPgHeKXE94fz_QpYfg +--7kLsyBAfQGbg` + +func TestVerifiableCredential_UnmarshalJSON(t *testing.T) { + t.Run("JSON-LD", func(t *testing.T) { + input := VerifiableCredential{} + raw := `{ + "id":"did:example:123#vc-1", + "type":["VerifiableCredential", "custom"], + "credentialSubject": {"name": "test"} + }` + err := json.Unmarshal([]byte(raw), &input) + require.NoError(t, err) + assert.Equal(t, "did:example:123#vc-1", input.ID.String()) + assert.Equal(t, []ssi.URI{VerifiableCredentialTypeV1URI(), ssi.MustParseURI("custom")}, input.Type) + assert.Equal(t, []interface{}{map[string]interface{}{"name": "test"}}, input.CredentialSubject) + assert.Equal(t, JSONLDCredentialProofFormat, input.Format()) + assert.Equal(t, raw, input.Raw()) + assert.Nil(t, input.JWT()) + }) + t.Run("JWT", func(t *testing.T) { + input := VerifiableCredential{} + raw := strings.ReplaceAll(jwtCredential, "\n", "") + err := json.Unmarshal([]byte(`"`+raw+`"`), &input) + require.NoError(t, err) + assert.Equal(t, []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("UniversityDegreeCredential")}, input.Type) + assert.Len(t, input.CredentialSubject, 1) + assert.NotNil(t, input.CredentialSubject[0].(map[string]interface{})["degree"]) + assert.Equal(t, JWTCredentialsProofFormat, input.Format()) + assert.Equal(t, raw, input.Raw()) + assert.NotNil(t, input.JWT()) + }) +} + +func TestParseVerifiableCredential(t *testing.T) { + t.Run("JSON-LD", func(t *testing.T) { + input := VerifiableCredential{} + err := json.Unmarshal([]byte(`{ + "id":"did:example:123#vc-1", + "type":["VerifiableCredential", "custom"], + "credentialSubject": {"name": "test"} + }`), &input) + require.NoError(t, err) + assert.Equal(t, "did:example:123#vc-1", input.ID.String()) + assert.Equal(t, []ssi.URI{VerifiableCredentialTypeV1URI(), ssi.MustParseURI("custom")}, input.Type) + assert.Equal(t, []interface{}{map[string]interface{}{"name": "test"}}, input.CredentialSubject) + }) + t.Run("JWT", func(t *testing.T) { + input := VerifiableCredential{} + err := json.Unmarshal([]byte(`"`+strings.ReplaceAll(jwtCredential, "\n", "")+`"`), &input) + require.NoError(t, err) + assert.Equal(t, []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("UniversityDegreeCredential")}, input.Type) + assert.Len(t, input.CredentialSubject, 1) + assert.NotNil(t, input.CredentialSubject[0].(map[string]interface{})["degree"]) + }) +} + func TestVerifiableCredential_UnmarshalCredentialSubject(t *testing.T) { type exampleSubject struct { Name string diff --git a/vc/vp.go b/vc/vp.go index 0dad8e1..e62fbc2 100644 --- a/vc/vp.go +++ b/vc/vp.go @@ -2,6 +2,9 @@ package vc import ( "encoding/json" + "fmt" + "github.com/lestrrat-go/jwx/jwt" + "strings" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/internal/marshal" @@ -15,6 +18,13 @@ func VerifiablePresentationTypeV1URI() ssi.URI { return ssi.MustParseURI(VerifiablePresentationType) } +const ( + // JSONLDPresentationProofFormat is the format for JSON-LD based presentations. + JSONLDPresentationProofFormat string = "ldp_vp" + // JWTPresentationProofFormat is the format for JWT based presentations. + JWTPresentationProofFormat = "jwt_vp" +) + // VerifiablePresentation represents a presentation as defined by the Verifiable Credentials Data Model 1.0 specification (https://www.w3.org/TR/vc-data-model/). type VerifiablePresentation struct { // Context defines the json-ld context to dereference the URIs @@ -29,6 +39,77 @@ type VerifiablePresentation struct { VerifiableCredential []VerifiableCredential `json:"verifiableCredential,omitempty"` // Proof contains the cryptographic proof(s). It must be extracted using the Proofs method or UnmarshalProofValue method for non-generic proof fields. Proof []interface{} `json:"proof,omitempty"` + + format string + raw string + token jwt.Token +} + +// ParseVerifiablePresentation parses a Verifiable Presentation from a string, which can be either in JSON-LD or JWT format. +// If the format is JWT, the parsed token can be retrieved using JWT(). +func ParseVerifiablePresentation(raw string) (*VerifiablePresentation, error) { + if strings.HasPrefix(raw, "{") { + // Assume JSON-LD format + var result VerifiablePresentation + err := json.Unmarshal([]byte(raw), &result) + if err == nil { + result.format = JSONLDPresentationProofFormat + } + return &result, err + } else { + // Assume JWT format + token, err := jwt.Parse([]byte(raw)) + if err != nil { + return nil, err + } + var result VerifiablePresentation + if innerVPInterf := token.PrivateClaims()["vp"]; innerVPInterf != nil { + innerVPJSON, _ := json.Marshal(innerVPInterf) + err = json.Unmarshal(innerVPJSON, &result) + if err != nil { + return nil, fmt.Errorf("invalid JWT 'vp' claim: %w", err) + } + } + // parse jti + if jti, err := parseURIClaim(token, jwt.JwtIDKey); err != nil { + return nil, err + } else if jti != nil { + result.ID = jti + } + // parse iss + if iss, err := parseURIClaim(token, jwt.IssuerKey); err != nil { + return nil, err + } else if iss != nil { + result.Holder = iss + } + // the other claims don't have a designated field in VerifiablePresentation and can be accessed through JWT() + result.format = JWTPresentationProofFormat + result.raw = raw + result.token = token + return &result, nil + } +} + +func parseURIClaim(token jwt.Token, claim string) (*ssi.URI, error) { + if val, ok := token.Get(claim); ok { + if str, ok := val.(string); !ok { + return nil, fmt.Errorf("%s must be a string", claim) + } else { + return ssi.ParseURI(str) + } + } + return nil, nil +} + +// Format returns the format of the presentation (e.g. jwt_vp or ldp_vp). +func (vp VerifiablePresentation) Format() string { + return vp.format +} + +// JWT returns the JWT token if the presentation was parsed from a JWT. +func (vp VerifiablePresentation) JWT() jwt.Token { + token, _ := vp.token.Clone() + return token } // Proofs returns the basic proofs for this presentation. For specific proof contents, UnmarshalProofValue must be used. @@ -48,12 +129,19 @@ func (vp VerifiablePresentation) Proofs() ([]Proof, error) { } func (vp VerifiablePresentation) MarshalJSON() ([]byte, error) { - type alias VerifiablePresentation - tmp := alias(vp) - if data, err := json.Marshal(tmp); err != nil { - return nil, err - } else { - return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(verifiableCredentialKey), marshal.Unplural(proofKey)) + switch vp.format { + default: + fallthrough + case JSONLDPresentationProofFormat: + type alias VerifiablePresentation + tmp := alias(vp) + if data, err := json.Marshal(tmp); err != nil { + return nil, err + } else { + return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(verifiableCredentialKey), marshal.Unplural(proofKey)) + } + case JWTPresentationProofFormat: + return json.Marshal(vp.raw) } } @@ -69,6 +157,7 @@ func (vp *VerifiablePresentation) UnmarshalJSON(b []byte) error { return err } *vp = (VerifiablePresentation)(tmp) + vp.format = JSONLDPresentationProofFormat return nil } diff --git a/vc/vp_test.go b/vc/vp_test.go index 5616b54..fbbff1b 100644 --- a/vc/vp_test.go +++ b/vc/vp_test.go @@ -2,65 +2,101 @@ package vc import ( "encoding/json" - "testing" - ssi "github.com/nuts-foundation/go-did" + "github.com/stretchr/testify/require" + "testing" "github.com/stretchr/testify/assert" ) -func TestVerifiablePresentation_MarshalJSON(t *testing.T) { - t.Run("ok - single credential and proof", func(t *testing.T) { - input := VerifiablePresentation{ - VerifiableCredential: []VerifiableCredential{ - { - Type: []ssi.URI{VerifiableCredentialTypeV1URI()}, - }, - }, - Proof: []interface{}{ - JSONWebSignature2020Proof{ - Jws: "", - }, - }, - } - - bytes, err := json.Marshal(input) - - if !assert.NoError(t, err) { - return - } - assert.Contains(t, string(bytes), "\"proof\":{") - assert.Contains(t, string(bytes), "\"verifiableCredential\":{") - }) +// jwtPresentation is taken from https://www.w3.org/TR/vc-data-model/#example-verifiable-presentation-using-jwt-compact-serialization-non-normative +const jwtPresentation = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.e +yJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46d +XVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLCJhdWQiOiJkaWQ6ZXhhbXBsZ +To0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5M +zcyNCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiO +lsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vc +mcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50Y +XRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhb +CI6WyJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNkltUnBaRHBsZUdGd +GNHeGxPbUZpWm1VeE0yWTNNVEl4TWpBME16RmpNamMyWlRFeVpXTmhZaU5yWlhsekxURWlmUS5leUp6Z +FdJaU9pSmthV1E2WlhoaGJYQnNaVHBsWW1abFlqRm1OekV5WldKak5tWXhZekkzTm1VeE1tVmpNakVpT +ENKcWRHa2lPaUpvZEhSd09pOHZaWGhoYlhCc1pTNWxaSFV2WTNKbFpHVnVkR2xoYkhNdk16Y3pNaUlzS +W1semN5STZJbWgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjB2YTJWNWN5OW1iMjh1YW5kcklpd2libUptS +WpveE5UUXhORGt6TnpJMExDSnBZWFFpT2pFMU5ERTBPVE0zTWpRc0ltVjRjQ0k2TVRVM016QXlPVGN5T +Xl3aWJtOXVZMlVpT2lJMk5qQWhOak0wTlVaVFpYSWlMQ0oyWXlJNmV5SkFZMjl1ZEdWNGRDSTZXeUpvZ +EhSd2N6b3ZMM2QzZHk1M015NXZjbWN2TWpBeE9DOWpjbVZrWlc1MGFXRnNjeTkyTVNJc0ltaDBkSEJ6T +2k4dmQzZDNMbmN6TG05eVp5OHlNREU0TDJOeVpXUmxiblJwWVd4ekwyVjRZVzF3YkdWekwzWXhJbDBzS +W5SNWNHVWlPbHNpVm1WeWFXWnBZV0pzWlVOeVpXUmxiblJwWVd3aUxDSlZibWwyWlhKemFYUjVSR1ZuY +21WbFEzSmxaR1Z1ZEdsaGJDSmRMQ0pqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKa1pXZHlaV1VpT +25zaWRIbHdaU0k2SWtKaFkyaGxiRzl5UkdWbmNtVmxJaXdpYm1GdFpTSTZJanh6Y0dGdUlHeGhibWM5S +jJaeUxVTkJKejVDWVdOallXeGhkWExEcVdGMElHVnVJRzExYzJseGRXVnpJRzUxYmNPcGNtbHhkV1Z6U +EM5emNHRnVQaUo5ZlgxOS5LTEpvNUdBeUJORDNMRFRuOUg3RlFva0VzVUVpOGpLd1hoR3ZvTjNKdFJhN +TF4ck5EZ1hEYjBjcTFVVFlCLXJLNEZ0OVlWbVIxTklfWk9GOG9HY183d0FwOFBIYkYySGFXb2RRSW9PQ +nh4VC00V05xQXhmdDdFVDZsa0gtNFM2VXgzclNHQW1jek1vaEVFZjhlQ2VOLWpDOFdla2RQbDZ6S1pRa +jBZUEIxcng2WDAteGxGQnM3Y2w2V3Q4cmZCUF90WjlZZ1ZXclFtVVd5cFNpb2MwTVV5aXBobXlFYkxaY +WdUeVBsVXlmbEdsRWRxclpBdjZlU2U2UnR4Snk2TTEtbEQ3YTVIVHphbllUV0JQQVVIRFpHeUdLWGRKd +y1XX3gwSVdDaEJ6STh0M2twRzI1M2ZnNlYzdFBnSGVLWEU5NGZ6X1FwWWZnLS03a0xzeUJBZlFHYmciX +X19.ft_Eq4IniBrr7gtzRfrYj8Vy1aPXuFZU-6_ai0wvaKcsrzI4JkQEKTvbJwdvIeuGuTqy7ipO-EYi +7V4TvonPuTRdpB7ZHOlYlbZ4wA9WJ6mSVSqDACvYRiFvrOFmie8rgm6GacWatgO4m4NqiFKFko3r58Lu +eFfGw47NK9RcfOkVQeHCq4btaDqksDKeoTrNysF4YS89INa-prWomrLRAhnwLOo1Etp3E4ESAxg73CR2 +kA5AoMbf5KtFueWnMcSbQkMRdWcGC1VssC0tB0JffVjq7ZV6OTyV4kl1-UVgiPLXUTpupFfLRhf9QpqM +BjYgP62KvhIvW8BbkGUelYMetA` - t.Run("ok - multiple credential and proof", func(t *testing.T) { - input := VerifiablePresentation{ - VerifiableCredential: []VerifiableCredential{ - { - Type: []ssi.URI{VerifiableCredentialTypeV1URI()}, +func TestVerifiablePresentation_MarshalJSON(t *testing.T) { + t.Run("JSON-LD", func(t *testing.T) { + t.Run("ok - single credential and proof", func(t *testing.T) { + input := VerifiablePresentation{ + VerifiableCredential: []VerifiableCredential{ + { + Type: []ssi.URI{VerifiableCredentialTypeV1URI()}, + }, }, - { - Type: []ssi.URI{VerifiableCredentialTypeV1URI()}, + Proof: []interface{}{ + JSONWebSignature2020Proof{ + Jws: "", + }, }, - }, - Proof: []interface{}{ - JSONWebSignature2020Proof{ - Jws: "", + } + + bytes, err := json.Marshal(input) + + if !assert.NoError(t, err) { + return + } + assert.Contains(t, string(bytes), "\"proof\":{") + assert.Contains(t, string(bytes), "\"verifiableCredential\":{") + }) + t.Run("ok - multiple credential and proof", func(t *testing.T) { + input := VerifiablePresentation{ + VerifiableCredential: []VerifiableCredential{ + { + Type: []ssi.URI{VerifiableCredentialTypeV1URI()}, + }, + { + Type: []ssi.URI{VerifiableCredentialTypeV1URI()}, + }, }, - JSONWebSignature2020Proof{ - Jws: "", + Proof: []interface{}{ + JSONWebSignature2020Proof{ + Jws: "", + }, + JSONWebSignature2020Proof{ + Jws: "", + }, }, - }, - } + } - bytes, err := json.Marshal(input) + bytes, err := json.Marshal(input) - if !assert.NoError(t, err) { - return - } - assert.Contains(t, string(bytes), "\"proof\":[") - assert.Contains(t, string(bytes), "\"verifiableCredential\":[") + if !assert.NoError(t, err) { + return + } + assert.Contains(t, string(bytes), "\"proof\":[") + assert.Contains(t, string(bytes), "\"verifiableCredential\":[") + }) }) + } func TestVerifiablePresentation_UnmarshalProof(t *testing.T) { @@ -150,3 +186,34 @@ func TestVerifiablePresentation_ContainsContext(t *testing.T) { assert.False(t, input.ContainsContext(*u)) }) } + +func TestParseVerifiablePresentation(t *testing.T) { + t.Run("JSON-LD", func(t *testing.T) { + vp, err := ParseVerifiablePresentation(`{ + "id":"did:example:123#vp-1", + "@context":["https://www.w3.org/2018/credentials/v1"] + }`) + require.NoError(t, err) + require.NotNil(t, vp) + assert.Equal(t, JSONLDPresentationProofFormat, vp.Format()) + assert.Equal(t, "did:example:123#vp-1", vp.ID.String()) + assert.Equal(t, []ssi.URI{VCContextV1URI()}, vp.Context) + }) + t.Run("JWT", func(t *testing.T) { + vp, err := ParseVerifiablePresentation(jwtPresentation) + require.NoError(t, err) + require.NotNil(t, vp) + assert.Equal(t, JWTPresentationProofFormat, vp.Format()) + assert.Equal(t, "did:example:ebfeb1f712ebc6f1c276e12ec21", vp.Holder.String()) + assert.Equal(t, "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", vp.ID.String()) + assert.Equal(t, []string{"did:example:4a57546973436f6f6c4a4a57573"}, vp.JWT().Audience()) + assert.Len(t, vp.Type, 2) + assert.True(t, vp.IsType(ssi.MustParseURI("VerifiablePresentation"))) + assert.True(t, vp.IsType(ssi.MustParseURI("CredentialManagerPresentation"))) + // Assert contained JWT VerifiableCredential was unmarshalled + assert.Len(t, vp.VerifiableCredential, 1) + vc := vp.VerifiableCredential[0] + assert.Equal(t, JWTCredentialsProofFormat, vc.Format()) + assert.Equal(t, "http://example.edu/credentials/3732", vc.ID.String()) + }) +}