Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

feat: wallet Prove supports creating JWT presentations. #3350

Merged
merged 1 commit into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions pkg/doc/verifiable/example_presentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,29 @@ func ExampleParsePresentation() {
panic(fmt.Errorf("failed to decode VP JWS: %w", err))
}

// Marshal the VP to JSON to verify the result of decoding.
// Marshal the VP to a JSON string, containing the JWS.
vpBytes, err := json.Marshal(vp)
if err != nil {
panic(fmt.Errorf("failed to marshal VP to JSON string: %w", err))
}

fmt.Println(string(vpBytes))

// When Presentation.JWT is set, the Presentation marshals to a JSON string of the original JWT.
// Clear it here, so we can marshal to JSON-LD to validate the contents.
vp.JWT = ""

// Marshal the VP to JSON to verify the result of decoding.
vpBytes, err = json.Marshal(vp)
if err != nil {
panic(fmt.Errorf("failed to marshal VP to JSON: %w", err))
}

fmt.Println(string(vpBytes))

// Output: {"@context":["https://www.w3.org/2018/credentials/v1","https://www.w3.org/2018/credentials/examples/v1"],"holder":"did:example:ebfeb1f712ebc6f1c276e12ec21","id":"urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5","type":["VerifiablePresentation","UniversityDegreeCredential"],"verifiableCredential":[{"@context":["https://www.w3.org/2018/credentials/v1","https://www.w3.org/2018/credentials/examples/v1"],"credentialSchema":[],"credentialSubject":{"degree":{"type":"BachelorDegree","university":"MIT"},"id":"did:example:ebfeb1f712ebc6f1c276e12ec21","name":"Jayden Doe","spouse":"did:example:c276e12ec21ebfeb1f712ebc6f1"},"expirationDate":"2020-01-01T19:23:24Z","id":"http://example.edu/credentials/1872","issuanceDate":"2010-01-01T19:23:24Z","issuer":{"id":"did:example:76e12ec712ebc6f1c221ebfeb1f","name":"Example University"},"referenceNumber":83294847,"type":["VerifiableCredential","UniversityDegreeCredential"]}]}
// Output:
// "eyJhbGciOiJFZERTQSIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLCJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiIsIlVuaXZlcnNpdHlEZWdyZWVDcmVkZW50aWFsIl0sInZlcmlmaWFibGVDcmVkZW50aWFsIjpbeyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImNyZWRlbnRpYWxTY2hlbWEiOltdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwidW5pdmVyc2l0eSI6Ik1JVCJ9LCJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsIm5hbWUiOiJKYXlkZW4gRG9lIiwic3BvdXNlIjoiZGlkOmV4YW1wbGU6YzI3NmUxMmVjMjFlYmZlYjFmNzEyZWJjNmYxIn0sImV4cGlyYXRpb25EYXRlIjoiMjAyMC0wMS0wMVQxOToyMzoyNFoiLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6ZXhhbXBsZTo3NmUxMmVjNzEyZWJjNmYxYzIyMWViZmViMWYiLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5In0sInJlZmVyZW5jZU51bWJlciI6OC4zMjk0ODQ3ZSswNywidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWVDcmVkZW50aWFsIl19XX19.RlO_1B-7qhQNwo2mmOFUWSa8A6hwaJrtq3q7yJDkKq4k6B-EJ-oyLNM6H_g2_nko2Yg9Im1CiROFm6nK12U_AQ"
// {"@context":["https://www.w3.org/2018/credentials/v1","https://www.w3.org/2018/credentials/examples/v1"],"holder":"did:example:ebfeb1f712ebc6f1c276e12ec21","id":"urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5","type":["VerifiablePresentation","UniversityDegreeCredential"],"verifiableCredential":[{"@context":["https://www.w3.org/2018/credentials/v1","https://www.w3.org/2018/credentials/examples/v1"],"credentialSchema":[],"credentialSubject":{"degree":{"type":"BachelorDegree","university":"MIT"},"id":"did:example:ebfeb1f712ebc6f1c276e12ec21","name":"Jayden Doe","spouse":"did:example:c276e12ec21ebfeb1f712ebc6f1"},"expirationDate":"2020-01-01T19:23:24Z","id":"http://example.edu/credentials/1872","issuanceDate":"2010-01-01T19:23:24Z","issuer":{"id":"did:example:76e12ec712ebc6f1c221ebfeb1f","name":"Example University"},"referenceNumber":83294847,"type":["VerifiableCredential","UniversityDegreeCredential"]}]}
}

func ExamplePresentation_JWTClaims() {
Expand Down
39 changes: 25 additions & 14 deletions pkg/doc/verifiable/presentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ type Presentation struct {
credentials []interface{}
Holder string
Proofs []Proof
JWT string
CustomFields CustomFields
}

Expand Down Expand Up @@ -224,6 +225,12 @@ func WithJWTCredentials(cs ...string) CreatePresentationOpt {

// MarshalJSON converts Verifiable Presentation to JSON bytes.
func (vp *Presentation) MarshalJSON() ([]byte, error) {
if vp.JWT != "" {
// If vc.JWT exists, marshal only the JWT, since all other values should be unchanged
// from when the JWT was parsed.
return []byte("\"" + vp.JWT + "\""), nil
}

raw, err := vp.raw()
if err != nil {
return nil, fmt.Errorf("JSON marshalling of verifiable presentation: %w", err)
Expand Down Expand Up @@ -295,6 +302,7 @@ func (vp *Presentation) raw() (*rawPresentation, error) {
Holder: vp.Holder,
Proof: proof,
CustomFields: vp.CustomFields,
JWT: vp.JWT,
}

if len(vp.credentials) > 0 {
Expand All @@ -312,6 +320,7 @@ type rawPresentation struct {
Credential interface{} `json:"verifiableCredential,omitempty"`
Holder string `json:"holder,omitempty"`
Proof json.RawMessage `json:"proof,omitempty"`
JWT string `json:"jwt,omitempty"`
// All unmapped fields are put here.
CustomFields `json:"-"`
}
Expand Down Expand Up @@ -399,7 +408,7 @@ func WithPresJSONLDDocumentLoader(documentLoader jsonld.DocumentLoader) Presenta
func ParsePresentation(vpData []byte, opts ...PresentationOpt) (*Presentation, error) {
vpOpts := getPresentationOpts(opts)

vpDataDecoded, vpRaw, err := decodeRawPresentation(vpData, vpOpts)
vpDataDecoded, vpRaw, vpJWT, err := decodeRawPresentation(vpData, vpOpts)
if err != nil {
return nil, err
}
Expand All @@ -418,6 +427,8 @@ func ParsePresentation(vpData []byte, opts ...PresentationOpt) (*Presentation, e
return nil, fmt.Errorf("verifiableCredential is required")
}

p.JWT = vpJWT

return p, nil
}

Expand Down Expand Up @@ -569,20 +580,20 @@ func validateVPJSONSchema(data []byte) error {
}

//nolint:gocyclo
func decodeRawPresentation(vpData []byte, vpOpts *presentationOpts) ([]byte, *rawPresentation, error) {
vpStr := string(vpData)
func decodeRawPresentation(vpData []byte, vpOpts *presentationOpts) ([]byte, *rawPresentation, string, error) {
vpStr := string(unQuote(vpData))

if jwt.IsJWS(vpStr) {
if vpOpts.publicKeyFetcher == nil {
return nil, nil, errors.New("public key fetcher is not defined")
if !vpOpts.disabledProofCheck && vpOpts.publicKeyFetcher == nil {
return nil, nil, "", errors.New("public key fetcher is not defined")
}

vcDataFromJwt, rawCred, err := decodeVPFromJWS(vpStr, !vpOpts.disabledProofCheck, vpOpts.publicKeyFetcher)
if err != nil {
return nil, nil, fmt.Errorf("decoding of Verifiable Presentation from JWS: %w", err)
return nil, nil, "", fmt.Errorf("decoding of Verifiable Presentation from JWS: %w", err)
}

return vcDataFromJwt, rawCred, nil
return vcDataFromJwt, rawCred, vpStr, nil
}

embeddedProofCheckOpts := &embeddedProofCheckOpts{
Expand All @@ -595,32 +606,32 @@ func decodeRawPresentation(vpData []byte, vpOpts *presentationOpts) ([]byte, *ra
if jwt.IsJWTUnsecured(vpStr) {
rawBytes, rawPres, err := decodeVPFromUnsecuredJWT(vpStr)
if err != nil {
return nil, nil, fmt.Errorf("decoding of Verifiable Presentation from unsecured JWT: %w", err)
return nil, nil, "", fmt.Errorf("decoding of Verifiable Presentation from unsecured JWT: %w", err)
}

if _, err := checkEmbeddedProof(rawBytes, embeddedProofCheckOpts); err != nil {
return nil, nil, err
return nil, nil, "", err
}

return rawBytes, rawPres, nil
return rawBytes, rawPres, "", nil
}

vpBytes, vpRaw, err := decodeVPFromJSON(vpData)
if err != nil {
return nil, nil, err
return nil, nil, "", err
}

_, err = checkEmbeddedProof(vpBytes, embeddedProofCheckOpts)
if err != nil {
return nil, nil, err
return nil, nil, "", err
}

// check that embedded proof is present, if not, it's not a verifiable presentation
if vpOpts.requireProof && vpRaw.Proof == nil {
return nil, nil, errors.New("embedded proof is missing")
return nil, nil, "", errors.New("embedded proof is missing")
}

return vpBytes, vpRaw, err
return vpBytes, vpRaw, "", err
}

func decodeVPFromJSON(vpData []byte) ([]byte, *rawPresentation, error) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/doc/verifiable/presentation_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func newJWTPresClaims(vp *Presentation, audience []string, minimizeVP bool) (*JW
return nil, err
}

rawVP.JWT = ""

presClaims := &JWTPresClaims{
Claims: jwtClaims,
Presentation: rawVP,
Expand Down
10 changes: 10 additions & 0 deletions pkg/doc/verifiable/presentation_jwt_proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func TestParsePresentationFromJWS(t *testing.T) {
vp, err := newTestPresentation(t, vpBytes)
require.NoError(t, err)

// Validate the JWT field, then clear it to validate against the original presentation.
require.Equal(t, string(jws), vpFromJWT.JWT)
vpFromJWT.JWT = ""

require.Equal(t, vp, vpFromJWT)
})

Expand All @@ -46,6 +50,9 @@ func TestParsePresentationFromJWS(t *testing.T) {
vp, err := newTestPresentation(t, vpBytes)
require.NoError(t, err)

require.Equal(t, string(jws), vpFromJWT.JWT)
vpFromJWT.JWT = ""

require.Equal(t, vp, vpFromJWT)
})

Expand Down Expand Up @@ -113,6 +120,9 @@ func TestParsePresentationFromJWS_EdDSA(t *testing.T) {
WithPresPublicKeyFetcher(SingleKey(signer.PublicKeyBytes(), kms.ED25519)))
require.NoError(t, err)

require.Equal(t, vpJWSStr, vpFromJWS.JWT)
vpFromJWS.JWT = ""

// unmarshalled presentation must be the same as original one
require.Equal(t, vp, vpFromJWS)
}
Expand Down
52 changes: 51 additions & 1 deletion pkg/wallet/contents.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,17 @@ func (cs *contentStore) checkDataModel(content []byte, opts *addContentOpts) err
}

func getContentID(content []byte) (string, error) {
jti, err := getJWTContentID(string(content))
if err != nil {
return "", err
}

if jti != "" {
return jti, nil
}

var cid contentID
if err := json.Unmarshal(content, &cid); err != nil {
if err = json.Unmarshal(content, &cid); err != nil {
return "", fmt.Errorf("failed to read content to be saved : %w", err)
}

Expand All @@ -467,6 +476,47 @@ func getContentID(content []byte) (string, error) {
return key, nil
}

type hasJTI struct {
JTI string `json:"jti"`
}

func getJWTContentID(jwtStr string) (string, error) {
parts := strings.Split(unQuote(jwtStr), ".")
if len(parts) != 3 { // nolint: gomnd
return "", nil // assume not a jwt
}

credBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return "", fmt.Errorf("decode base64 JWT data: %w", err)
}

cred := &hasJTI{}

err = json.Unmarshal(credBytes, cred)
if err != nil {
return "", fmt.Errorf("failed to unmarshal JWT data: %w", err)
}

if cred.JTI == "" {
return "", fmt.Errorf("JWT data has no ID")
}

return cred.JTI, nil
}

func unQuote(s string) string {
if len(s) <= 1 {
return s
}

if s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}

return s
}

// getContentKeyPrefix returns key prefix by wallet content type and storage key.
func getContentKeyPrefix(ct ContentType, key string) string {
return fmt.Sprintf("%s_%s", ct, key)
Expand Down
51 changes: 51 additions & 0 deletions pkg/wallet/contents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ const (
"kid":"z6MkiEh8RQL83nkPo8ehDeX7"
}
}`
sampleJWTCredContentValid = "eyJhbGciOiJFZERTQSIsImtpZCI6IiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzc5MDY2MDQsImlhdCI6M" +
"TI2MjM3MzgwNCwiaXNzIjoiZGlkOmV4YW1wbGU6NzZlMTJlYzcxMmViYzZmMWMyMjFlYmZlYjFmIiwianRpIjoiaHR0cDovL2V4YW1wbGU" +
"uZWR1L2NyZWRlbnRpYWxzLzE4NzIiLCJuYmYiOjEyNjIzNzM4MDQsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2Z" +
"TEyZWMyMSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3c" +
"udzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZGVncmVlIjp7InR5cGUiOiJCY" +
"WNoZWxvckRlZ3JlZSIsInVuaXZlcnNpdHkiOiJNSVQifSwiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjE" +
"iLCJuYW1lIjoiSmF5ZGVuIERvZSIsInNwb3VzZSI6ImRpZDpleGFtcGxlOmMyNzZlMTJlYzIxZWJmZWIxZjcxMmViYzZmMSJ9LCJpc3N1Z" +
"XIiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSJ9LCJyZWZlcmVuY2VOdW1iZXIiOjguMzI5NDg0N2UrMDcsInR5cGUiOlsiVmVyaWZ" +
"pYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdfX0.a5yKMPmDnEXvM-fG3BaOqfdkqdvU4s2rzeZuOzLmk" +
"TH1y9sJT-mgTe7map5E9x7abrNVpyYbaH7JaAb9Yhr1DQ"
)

func TestContentTypes(t *testing.T) {
Expand Down Expand Up @@ -361,6 +371,18 @@ func TestContentStores(t *testing.T) {
require.Empty(t, response)
})

t.Run("save JWTVC to store - success", func(t *testing.T) {
sp := getMockStorageProvider()

contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()})
require.NotEmpty(t, contentStore)

require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{}))

err := contentStore.Save(token, Credential, []byte(sampleJWTCredContentValid))
require.NoError(t, err)
})

t.Run("save key to store - success", func(t *testing.T) {
sp := getMockStorageProvider()
sampleUser := uuid.New().String()
Expand Down Expand Up @@ -452,6 +474,35 @@ func TestContentStores(t *testing.T) {
require.Contains(t, err.Error(), "JSON-LD doc has different structure after compaction")
})

t.Run("save JWTVC to store - failures", func(t *testing.T) {
sp := getMockStorageProvider()

contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()})
require.NotEmpty(t, contentStore)

require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{}))

// assumes bad data is not JWT, fails to parse as JSON
err := contentStore.Save(token, Credential, []byte("f"))
require.Error(t, err)
require.Contains(t, err.Error(), "failed to read content to be saved")

// fail to decode payload that isn't base64
err = contentStore.Save(token, Credential, []byte("!!!!.!!!!.!!!!"))
require.Error(t, err)
require.Contains(t, err.Error(), "decode base64 JWT data")

// YWJjZGVm is abcdef in base64, so this isn't valid JSON
err = contentStore.Save(token, Credential, []byte("YWJjZGVm.YWJjZGVm.YWJjZGVm"))
require.Error(t, err)
require.Contains(t, err.Error(), "failed to unmarshal JWT data")

// e30 is {} in base64, so jwt is empty
err = contentStore.Save(token, Credential, []byte("e30.e30.signature"))
require.Error(t, err)
require.Contains(t, err.Error(), "JWT data has no ID")
})

t.Run("save key to store - failure", func(t *testing.T) {
sp := getMockStorageProvider()
sampleUser := uuid.New().String()
Expand Down
23 changes: 20 additions & 3 deletions pkg/wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,26 @@ func (c *Wallet) Prove(authToken string, proofOptions *ProofOptions, credentials

presentation.Holder = proofOptions.Controller

err = c.addLinkedDataProof(authToken, presentation, proofOptions, purpose)
if err != nil {
return nil, fmt.Errorf("failed to prove credentials: %w", err)
switch proofOptions.ProofFormat {
case ExternalJWTProofFormat:
// TODO: look into passing audience identifier
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please create a task for this TODO in aries, so that we don't miss it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sudeshrshetty good idea, done: #3354

// https://github.com/hyperledger/aries-framework-go/issues/3354
claims, e := presentation.JWTClaims(nil, false)
if e != nil {
return nil, fmt.Errorf("failed to generate JWT claims for VP: %w", e)
}

jws, e := c.verifiableClaimsToJWT(authToken, claims, proofOptions)
if e != nil {
return nil, fmt.Errorf("failed to generate JWT VP: %w", e)
}

presentation.JWT = jws
default: // default case is EmbeddedLDProofFormat
err = c.addLinkedDataProof(authToken, presentation, proofOptions, purpose)
if err != nil {
return nil, fmt.Errorf("failed to prove credentials: %w", err)
}
}

return presentation, nil
Expand Down
Loading