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

Commit

Permalink
feat: wallet Prove supports creating JWT presentations.
Browse files Browse the repository at this point in the history
Signed-off-by: Filip Burlacu <filip.burlacu@securekey.com>
  • Loading branch information
Filip Burlacu authored and fqutishat committed Aug 29, 2022
1 parent ece4258 commit 1baf93b
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 32 deletions.
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
22 changes: 19 additions & 3 deletions pkg/wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,25 @@ 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
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

0 comments on commit 1baf93b

Please sign in to comment.