Skip to content

Commit

Permalink
feat: SD-JWT: Add NewFromVC to Issuer
Browse files Browse the repository at this point in the history
Pass VC into NewFromVC and the issuer will create selective disclosures for everything in vc->credential subject and add the fields from options.

Closes hyperledger-archives#3491

Signed-off-by: Sandra Vrtikapa <[email protected]>
  • Loading branch information
sandrask committed Jan 24, 2023
1 parent 5778522 commit 1984987
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 42 deletions.
22 changes: 16 additions & 6 deletions pkg/doc/sdjwt/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,32 +289,42 @@ func GetSDAlg(claims map[string]interface{}) (string, error) {

// GetKeyFromCredentialSubject returns key value from VC credential subject.
func GetKeyFromCredentialSubject(key string, claims map[string]interface{}) (interface{}, bool) {
vcObj, ok := claims["vc"]
csObj, ok := GetCredentialSubject(claims)
if !ok {
return nil, false
}

vc, ok := vcObj.(map[string]interface{})
cs, ok := csObj.(map[string]interface{})
if !ok {
return nil, false
}

csObj, ok := vc["credentialSubject"]
obj, ok := cs[key]
if !ok {
return nil, false
}

cs, ok := csObj.(map[string]interface{})
return obj, true
}

// GetCredentialSubject returns credential subject from vc.
func GetCredentialSubject(claims map[string]interface{}) (interface{}, bool) {
vcObj, ok := claims["vc"]
if !ok {
return nil, false
}

obj, ok := cs[key]
vc, ok := vcObj.(map[string]interface{})
if !ok {
return nil, false
}

return obj, true
csObj, ok := vc["credentialSubject"]
if !ok {
return nil, false
}

return csObj, true
}

// GetCNF returns confirmation claim 'cnf'.
Expand Down
119 changes: 110 additions & 9 deletions pkg/doc/sdjwt/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (

const (
testIssuer = "https://example.com/issuer"

year = 365 * 24 * 60 * time.Minute
)

func TestSDJWTFlow(t *testing.T) {
Expand All @@ -47,9 +49,17 @@ func TestSDJWTFlow(t *testing.T) {
"last_name": "Smith",
}

now := time.Now()

var timeOpts []issuer.NewOpt
timeOpts = append(timeOpts,
issuer.WithNotBefore(jwt.NewNumericDate(now)),
issuer.WithIssuedAt(jwt.NewNumericDate(now)),
issuer.WithExpiry(jwt.NewNumericDate(now.Add(year))))

t.Run("success - simple claims (flat option)", func(t *testing.T) {
// Issuer will issue SD-JWT for specified claims.
token, err := issuer.New(testIssuer, claims, nil, signer)
token, err := issuer.New(testIssuer, claims, nil, signer, timeOpts...)
r.NoError(err)

var simpleClaimsFlatOption map[string]interface{}
Expand Down Expand Up @@ -101,6 +111,9 @@ func TestSDJWTFlow(t *testing.T) {

// Issuer will issue SD-JWT for specified claims and holder public key.
token, err := issuer.New(testIssuer, claims, nil, signer,
issuer.WithNotBefore(jwt.NewNumericDate(now)),
issuer.WithIssuedAt(jwt.NewNumericDate(now)),
issuer.WithExpiry(jwt.NewNumericDate(now.Add(year))),
issuer.WithHolderPublicKey(holderPublicJWK))
r.NoError(err)

Expand Down Expand Up @@ -148,7 +161,7 @@ func TestSDJWTFlow(t *testing.T) {

printObject(t, "Verified Claims", verifiedClaims)

// expected claims cnf, iss, exp, iat, nbf, given_name; last_name was not disclosed
// expected claims cnf, iss, given_name, iat, nbf, exp; last_name was not disclosed
r.Equal(6, len(verifiedClaims))
})

Expand Down Expand Up @@ -193,8 +206,8 @@ func TestSDJWTFlow(t *testing.T) {
verifier.WithSignatureVerifier(signatureVerifier))
r.NoError(err)

// expected claims iss, exp, iat, nbf, given_name, email, street_address
r.Equal(7, len(verifiedClaims))
// expected claims iss, given_name, email, street_address; time options not provided
r.Equal(4, len(verifiedClaims))

printObject(t, "Verified Claims", verifiedClaims)
})
Expand Down Expand Up @@ -239,8 +252,8 @@ func TestSDJWTFlow(t *testing.T) {
verifier.WithSignatureVerifier(signatureVerifier))
r.NoError(err)

// expected claims iss, exp, iat, nbf, given_name, email, street_address
r.Equal(7, len(verifiedClaims))
// expected claims iss, given_name, email, street_address; time options not provided
r.Equal(4, len(verifiedClaims))

printObject(t, "Verified Claims", verifiedClaims)
})
Expand All @@ -265,9 +278,6 @@ func TestSDJWTFlow(t *testing.T) {
// 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)

Expand Down Expand Up @@ -342,6 +352,68 @@ func TestSDJWTFlow(t *testing.T) {

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

t.Run("success - NewFromVC API", func(t *testing.T) {
holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader)
r.NoError(err)

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

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

token, err := issuer.NewFromVC(vc, nil, signer,
issuer.WithID("did:example:ebfeb1f712ebc6f1c276e12ec21"),
issuer.WithHolderPublicKey(holderPublicJWK),
issuer.WithStructuredClaims(true))
r.NoError(err)

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

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

claims, err := holder.Parse(vcCombinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
r.NoError(err)

printObject(t, "Holder Claims", claims)

r.Equal(4, len(claims))

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

holderSigner := afjwt.NewEd25519Signer(holderPrivateKey)

selectedDisclosures := getDisclosuresFromClaimNames([]string{"degree", "name", "spouse"}, 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{}
Expand Down Expand Up @@ -442,3 +514,32 @@ const sampleVC = `
"type": "VerifiableCredential"
}
}`

const sampleVCFull = `
{
"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": {
"degree": {
"degree": "MIT",
"type": "BachelorDegree"
},
"name": "Jayden Doe",
"spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1"
},
"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"
}
}`
61 changes: 51 additions & 10 deletions pkg/doc/sdjwt/issuer/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ const (

decoyMinElements = 1
decoyMaxElements = 4

year = 365 * 24 * 60 * time.Minute
)

var mr = mathrand.New(mathrand.NewSource(time.Now().Unix())) // nolint:gochecknoglobals
Expand Down Expand Up @@ -152,18 +150,10 @@ func WithStructuredClaims(flag bool) NewOpt {
// New creates new signed Selective Disclosure JWT based on input claims.
func New(issuer string, claims interface{}, headers jose.Headers,
signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) {
now := time.Now()

nOpts := &newOpts{
jsonMarshal: json.Marshal,
getSalt: generateSalt,
HashAlg: defaultHash,

// TODO: Discuss with Troy about defaults
// TODO: We may not need defaulted values here at all
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(year)),
}

for _, opt := range opts {
Expand Down Expand Up @@ -193,6 +183,45 @@ func New(issuer string, claims interface{}, headers jose.Headers,
return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil
}

// NewFromVC creates new signed Selective Disclosure JWT based on vc.
func NewFromVC(vc map[string]interface{}, headers jose.Headers,
signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) {
csObj, ok := common.GetCredentialSubject(vc)
if !ok {
return nil, fmt.Errorf("credential subject not found")
}

cs, ok := csObj.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("credential subject must be an object")
}

token, err := New("", cs, nil, &unsecuredJWTSigner{}, opts...)
if err != nil {
return nil, err
}

var selectiveCredentialSubject map[string]interface{}

err = token.DecodeClaims(&selectiveCredentialSubject)
if err != nil {
return nil, err
}

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

// sign VC with 'selective' credential subject
signedJWT, err := afgjwt.NewSigned(vc, nil, signer)
if err != nil {
return nil, err
}

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

return sdJWT, nil
}

func createPayload(issuer string, nOpts *newOpts) *payload {
var cnf map[string]interface{}
if nOpts.HolderPublicKey != nil {
Expand Down Expand Up @@ -377,3 +406,15 @@ type payload struct {

SDAlg string `json:"_sd_alg,omitempty"`
}

type unsecuredJWTSigner struct{}

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

func (s unsecuredJWTSigner) Headers() jose.Headers {
return map[string]interface{}{
jose.HeaderAlgorithm: afgjwt.AlgorithmNone,
}
}
Loading

0 comments on commit 1984987

Please sign in to comment.