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

Commit

Permalink
chore: Support verification of 'vc' claims with holder binding (#3490)
Browse files Browse the repository at this point in the history
Support verification of 'vc' claims with holder binding

Closes #3489

Signed-off-by: Sandra Vrtikapa <[email protected]>

Signed-off-by: Sandra Vrtikapa <[email protected]>
sandrask authored Jan 23, 2023
1 parent 39b647b commit 5778522
Showing 6 changed files with 257 additions and 23 deletions.
27 changes: 23 additions & 4 deletions pkg/doc/sdjwt/common/common.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ const (

SDAlgorithmKey = "_sd_alg"
SDKey = "_sd"
CNFKey = "cnf"

disclosureParts = 3
saltIndex = 0
@@ -272,7 +273,7 @@ func GetSDAlg(claims map[string]interface{}) (string, error) {
obj, ok := claims[SDAlgorithmKey]
if !ok {
// if claims contain 'vc' claim it may be present in credential subject
obj, ok = getSDAlgFromCredentialSubject(claims)
obj, ok = GetKeyFromCredentialSubject(SDAlgorithmKey, claims)
if !ok {
return "", fmt.Errorf("%s must be present in SD-JWT", SDAlgorithmKey)
}
@@ -286,8 +287,8 @@ func GetSDAlg(claims map[string]interface{}) (string, error) {
return alg, nil
}

// getSDAlgFromCredentialSubject returns SD algorithm from VC credential subject.
func getSDAlgFromCredentialSubject(claims map[string]interface{}) (interface{}, bool) {
// GetKeyFromCredentialSubject returns key value from VC credential subject.
func GetKeyFromCredentialSubject(key string, claims map[string]interface{}) (interface{}, bool) {
vcObj, ok := claims["vc"]
if !ok {
return nil, false
@@ -308,14 +309,32 @@ func getSDAlgFromCredentialSubject(claims map[string]interface{}) (interface{},
return nil, false
}

obj, ok := cs[SDAlgorithmKey]
obj, ok := cs[key]
if !ok {
return nil, false
}

return obj, true
}

// GetCNF returns confirmation claim 'cnf'.
func GetCNF(claims map[string]interface{}) (map[string]interface{}, error) {
obj, ok := claims[CNFKey]
if !ok {
obj, ok = GetKeyFromCredentialSubject(CNFKey, claims)
if !ok {
return nil, fmt.Errorf("%s must be present in SD-JWT", CNFKey)
}
}

cnf, ok := obj.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%s must be an object", CNFKey)
}

return cnf, nil
}

// GetDisclosureDigests returns digests from claims map.
func GetDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) {
disclosuresObj, ok := claims[SDKey]
88 changes: 87 additions & 1 deletion pkg/doc/sdjwt/common/common_test.go
Original file line number Diff line number Diff line change
@@ -446,7 +446,7 @@ func TestGetDisclosedClaims(t *testing.T) {
})
}

func TestGetSDAlgFromVC(t *testing.T) {
func TestGetSDAlg(t *testing.T) {
r := require.New(t)

t.Run("success", func(t *testing.T) {
@@ -567,6 +567,55 @@ func TestGetSDAlgFromVC(t *testing.T) {
})
}

func TestGetCNF(t *testing.T) {
r := require.New(t)

t.Run("success - cnf is at the top level", func(t *testing.T) {
claims := make(map[string]interface{})
claims["cnf"] = map[string]interface{}{
"jwk": map[string]interface{}{
"kty": "RSA",
"e": "AQAB",
"n": "pm4bOHBg-oYhAyPWzR56AWX3rUIXp11",
},
}

cnf, err := GetCNF(claims)
r.NoError(err)
r.NotEmpty(cnf["jwk"])
})

t.Run("success - cnf is in VC credential subject", func(t *testing.T) {
var payload map[string]interface{}

err := json.Unmarshal([]byte(vcSample), &payload)
require.NoError(t, err)

cnf, err := GetCNF(payload)
r.NoError(err)
r.NotEmpty(cnf["jwk"])
})

t.Run("error - cnf not found (empty claims)", func(t *testing.T) {
cnf, err := GetCNF(make(map[string]interface{}))
r.Error(err)
r.Empty(cnf)

r.Contains(err.Error(), "cnf must be present in SD-JWT")
})

t.Run("error - cnf is not an object", func(t *testing.T) {
claims := make(map[string]interface{})
claims["cnf"] = "abc"

cnf, err := GetCNF(claims)
r.Error(err)
r.Empty(cnf)

r.Contains(err.Error(), "cnf must be an object")
})
}

type NoopSignatureVerifier struct {
}

@@ -595,3 +644,40 @@ type payload struct {

const specExample2bJWT = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIjBsa1NjS2ppSk1IZWRKZnE3c0pCN0hRM3FvbUdmckVMYm81Z1podktSV28iLCAiMWgyOWdnUWkxeG91LV9OalZ5eW9DaEsyYXN3VXRvMlVqQ2ZGLTFMYWhBOCIsICIzQ29MVUxtRHh4VXdfTGR5WUVUanVkdVh1RXBHdUJ5NHJYSG90dUQ0MFg0IiwgIkFJRHlveHgxaXB5NDUtR0ZwS2d2Yy1ISWJjVnJsTGxyWUxYbXgzZXYyZTQiLCAiT2x0aGZSb0ZkUy1KNlM4Mk9XbHJPNHBXaG9lUk1ySF9LR1BfaDZFYXBZUSIsICJyNGRicEdlZWlhMDJTeUdMNWdCZEhZMXc4SWhqVDN4eDA1UnNmeXlIVWs0Il0sICJhZGRyZXNzIjogeyJfc2QiOiBbIjZPS053bkdHS1dYQ0k5dWlqTkFzdjY0dTIyZUxTNHJNZExObGcxZnFKcDQiLCAiSEVWTWdELU5LSzVOdlhQYkFSb3JWZE9ESVRta1V5dU1wQ3NfbTdIWG5ZYyIsICJVcTAyblY3M0swYmRSSzIzcnphYm1uRGE0TzhZTlFadnQ5eDhMeWtva19ZIiwgIm94RlJpbG5vMjZVWWU3a3FNTTRiZHE4SXZOTXRJaTZGOHB0dC11aVBMYk0iXX0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTUxNjIzOTAyMiwgImV4cCI6IDE1MTYyNDcwMjIsICJfc2RfYWxnIjogInNoYS0yNTYifQ.M45AUExpi9THOTVIHfBmb2GL0WXJf4TeWB5QPmsxdBkj9pUcLOPR8YVafLIt8m_imYHTBYYcAyf7qSnquxMxGQ` // nolint:lll
const specExample2bDisclosures = `~WyJSdHczZUFFUE5wWjIwTkhZSzNNRWNnIiwgImZhbWlseV9uYW1lIiwgIlx1NWM3MVx1NzUzMCJd~WyJicjgxenVSc0NUcXJuWEp4MHVqMkRRIiwgImdpdmVuX25hbWUiLCAiXHU1OTJhXHU5MGNlIl0~WyI1Z2NXRmxWSEM1VVEwbktrallybDlnIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJJTms2bkx4WGFybDF4NmVabHdBOTV3IiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAbmlob24uY29tIl0~WyJNOVY2N3V0UC1hTF9lR1B0UU5hM0RRIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJzNFhNSmxXQ2Eza3hDWk4wSVVrbnlBIiwgImNvdW50cnkiLCAiSlAiXQ~` // nolint:lll

const vcSample = `
{
"iat": 1673987547,
"iss": "did:example:76e12ec712ebc6f1c221ebfeb1f",
"jti": "http://example.edu/credentials/1872",
"nbf": 1673987547,
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"credentialSubject": {
"_sd": [
"GJFkje8c1iayy1HQW__JEhuHTz8QGlkcMaxDTjT1wpQ",
"goPn0hokFnQBktqzXxgTK-4CCldmLjlRwUVCIltDyRg",
"FAiNODIxDMwGTljNYcVKkx7LBsr1pb-U6XuAfVFuOGY"
],
"_sd_alg": "sha-256",
"cnf": {
"jwk": {
"crv": "Ed25519",
"kty": "OKP",
"x": "7jtkxxk0Pb3E0O6JXJiN8HyIp2DpCiqaHCWfMXl9ZFo"
}
},
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21"
},
"first_name": "First name",
"id": "http://example.edu/credentials/1872",
"info": "Info",
"issuanceDate": "2023-01-17T22:32:27.468109817+02:00",
"issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f",
"last_name": "Last name",
"type": "VerifiableCredential"
}
}`
134 changes: 134 additions & 0 deletions pkg/doc/sdjwt/integration_test.go
Original file line number Diff line number Diff line change
@@ -18,8 +18,10 @@ import (
"github.com/go-jose/go-jose/v3/jwt"
"github.com/stretchr/testify/require"

afjose "github.com/hyperledger/aries-framework-go/pkg/doc/jose"
"github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport"
afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder"
"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer"
"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/verifier"
@@ -242,6 +244,116 @@ func TestSDJWTFlow(t *testing.T) {

printObject(t, "Verified Claims", verifiedClaims)
})

t.Run("success - create VC plus holder binding", func(t *testing.T) {
holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader)
r.NoError(err)

holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey)
require.NoError(t, err)

credentialSubject := map[string]interface{}{
"degree": map[string]interface{}{
"degree": "MIT",
"type": "BachelorDegree",
},
"name": "Jayden Doe",
"spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1",
}

// Note: if issuer can be empty; should I add it as an option then
// All reference apps have it as part of call
token, err := issuer.New("", credentialSubject, nil, &unsecuredJWTSigner{},
issuer.WithID("did:example:ebfeb1f712ebc6f1c276e12ec21"),
issuer.WithExpiry(nil),
issuer.WithNotBefore(nil),
issuer.WithIssuedAt(nil),
issuer.WithHolderPublicKey(holderPublicJWK))
r.NoError(err)

credSubjectCFI, err := token.Serialize(false)
r.NoError(err)

cfi := common.ParseCombinedFormatForIssuance(credSubjectCFI)

var selectiveCredentialSubject map[string]interface{}
err = token.DecodeClaims(&selectiveCredentialSubject)
r.NoError(err)

printObject(t, "Selective Credential Subject", selectiveCredentialSubject)

// create VC - we will use template here
var vc map[string]interface{}
err = json.Unmarshal([]byte(sampleVC), &vc)
r.NoError(err)

// update VC with 'selective' credential subject
vc["vc"].(map[string]interface{})["credentialSubject"] = selectiveCredentialSubject

// sign VC with 'selective' credential subject
signedJWT, err := afjwt.NewSigned(vc, nil, signer)
r.NoError(err)

sdJWT := &issuer.SelectiveDisclosureJWT{Disclosures: cfi.Disclosures, SignedJWT: signedJWT}

// create combined format for issuance for VC
vcCombinedFormatForIssuance, err := sdJWT.Serialize(false)
r.NoError(err)

fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", vcCombinedFormatForIssuance))

// Holder will parse combined format for issuance and hold on to that
// combined format for issuance and the claims that can be selected.
claims, err := holder.Parse(vcCombinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
r.NoError(err)

printObject(t, "Holder Claims", claims)

r.Equal(3, len(claims))

const testAudience = "https://test.com/verifier"
const testNonce = "nonce"

holderSigner := afjwt.NewEd25519Signer(holderPrivateKey)

selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name", "email", "street_address"}, claims)

// Holder will disclose only sub-set of claims to verifier.
combinedFormatForPresentation, err := holder.CreatePresentation(vcCombinedFormatForIssuance, selectedDisclosures,
holder.WithHolderBinding(&holder.BindingInfo{
Payload: holder.BindingPayload{
Nonce: testNonce,
Audience: testAudience,
IssuedAt: jwt.NewNumericDate(time.Now()),
},
Signer: holderSigner,
}))
r.NoError(err)

fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation))

// Verifier will validate combined format for presentation and create verified claims.
// In this case it will be VC since VC was passed in.
verifiedClaims, err := verifier.Parse(combinedFormatForPresentation,
verifier.WithSignatureVerifier(signatureVerifier))
r.NoError(err)

printObject(t, "Verified Claims", verifiedClaims)

r.Equal(len(vc), len(verifiedClaims))
})
}

type unsecuredJWTSigner struct{}

func (s unsecuredJWTSigner) Sign(_ []byte) ([]byte, error) {
return []byte(""), nil
}

func (s unsecuredJWTSigner) Headers() afjose.Headers {
return map[string]interface{}{
afjose.HeaderAlgorithm: afjwt.AlgorithmNone,
}
}

func createComplexClaims() map[string]interface{} {
@@ -308,3 +420,25 @@ func prettyPrint(msg []byte) (string, error) {

return prettyJSON.String(), nil
}

const sampleVC = `
{
"iat": 1673987547,
"iss": "did:example:76e12ec712ebc6f1c221ebfeb1f",
"jti": "http://example.edu/credentials/1872",
"nbf": 1673987547,
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"credentialSubject": {},
"first_name": "First name",
"id": "http://example.edu/credentials/1872",
"info": "Info",
"issuanceDate": "2023-01-17T22:32:27.468109817+02:00",
"issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f",
"last_name": "Last name",
"type": "VerifiableCredential"
}
}`
12 changes: 11 additions & 1 deletion pkg/doc/sdjwt/issuer/issuer.go
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ type Claims jwt.Claims
// newOpts holds options for creating new SD-JWT.
type newOpts struct {
Subject string
JTI string
ID string

Expiry *jwt.NumericDate
@@ -106,6 +107,13 @@ func WithSubject(subject string) NewOpt {
}
}

// WithJTI is an option for SD-JWT payload.
func WithJTI(jti string) NewOpt {
return func(opts *newOpts) {
opts.JTI = jti
}
}

// WithID is an option for SD-JWT payload.
func WithID(id string) NewOpt {
return func(opts *newOpts) {
@@ -194,6 +202,7 @@ func createPayload(issuer string, nOpts *newOpts) *payload {

payload := &payload{
Issuer: issuer,
JTI: nOpts.JTI,
ID: nOpts.ID,
Subject: nOpts.Subject,
IssuedAt: nOpts.IssuedAt,
@@ -357,7 +366,8 @@ func generateSalt() (string, error) {
type payload struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
ID string `json:"jti,omitempty"`
ID string `json:"id,omitempty"`
JTI string `json:"jti,omitempty"`

Expiry *jwt.NumericDate `json:"exp,omitempty"`
NotBefore *jwt.NumericDate `json:"nbf,omitempty"`
1 change: 1 addition & 0 deletions pkg/doc/sdjwt/issuer/issuer_test.go
Original file line number Diff line number Diff line change
@@ -154,6 +154,7 @@ func TestNew(t *testing.T) {
WithIssuedAt(jwt.NewNumericDate(issued)),
WithExpiry(jwt.NewNumericDate(expiry)),
WithNotBefore(jwt.NewNumericDate(notBefore)),
WithJTI("jti"),
WithID("id"),
WithSubject("subject"),
WithSaltFnc(generateSalt),
18 changes: 1 addition & 17 deletions pkg/doc/sdjwt/verifier/verifier.go
Original file line number Diff line number Diff line change
@@ -19,8 +19,6 @@ import (
"github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier"
)

const confirmationClaim = "cnf"

// jwtParseOpts holds options for the SD-JWT parsing.
type parseOpts struct {
detachedPayload []byte
@@ -218,7 +216,7 @@ func verifyHolderJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error {
}

func getSignatureVerifier(claims map[string]interface{}) (jose.SignatureVerifier, error) {
cnf, err := getCNF(claims)
cnf, err := common.GetCNF(claims)
if err != nil {
return nil, err
}
@@ -231,20 +229,6 @@ func getSignatureVerifier(claims map[string]interface{}) (jose.SignatureVerifier
return signatureVerifier, nil
}

func getCNF(claims map[string]interface{}) (map[string]interface{}, error) {
obj, ok := claims[confirmationClaim]
if !ok {
return nil, fmt.Errorf("%s must be present in SD-JWT", confirmationClaim)
}

cnf, ok := obj.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%s must be an object", confirmationClaim)
}

return cnf, nil
}

// getSignatureVerifierFromCNF will evolve over time as we support more cnf modes and algorithms.
func getSignatureVerifierFromCNF(cnf map[string]interface{}) (jose.SignatureVerifier, error) {
jwkObj, ok := cnf["jwk"]

0 comments on commit 5778522

Please sign in to comment.