From 406915e9d8ee6924ced98fdf4324fc4954da2abc Mon Sep 17 00:00:00 2001 From: Gerard Snaauw Date: Wed, 29 Nov 2023 10:25:22 +0100 Subject: [PATCH] make issuanceDate optional --- vc/vc.go | 20 +++++++++++++------ vc/vc_test.go | 53 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/vc/vc.go b/vc/vc.go index d070d31..e877b1c 100644 --- a/vc/vc.go +++ b/vc/vc.go @@ -88,7 +88,8 @@ func parseJWTCredential(raw string) (*VerifiableCredential, error) { } // parse nbf if _, ok := token.Get(jwt.NotBeforeKey); ok { - result.IssuanceDate = token.NotBefore() + nbf := token.NotBefore() + result.IssuanceDate = &nbf } // parse sub if token.Subject() != "" { @@ -139,7 +140,7 @@ type VerifiableCredential struct { // Issuer refers to the party that issued the credential Issuer ssi.URI `json:"issuer"` // IssuanceDate is a rfc3339 formatted datetime. Has alias ValidFrom - IssuanceDate time.Time `json:"issuanceDate"` + IssuanceDate *time.Time `json:"issuanceDate,omitempty"` // ValidFrom is a rfc3339 formatted datetime. It is optional, and is mutually exclusive with IssuanceDate (not enforced). // It's a forwards compatible alternative for IssuanceDate. // The jwt-vc 'nbf' field will unmarshal to IssuanceDate, which may not match with the JSON-LD definition of certain VCs. @@ -188,7 +189,7 @@ func (vc VerifiableCredential) JWT() jwt.Token { func (vc VerifiableCredential) ValidAt(t time.Time) bool { // IssuanceDate is a required field, but will default to the zero value when missing. (when ValidFrom != nil) // t > IssuanceDate - if !vc.IssuanceDate.IsZero() && t.Before(vc.IssuanceDate) { + if vc.IssuanceDate != nil && t.Before(*vc.IssuanceDate) { return false } // t > ValidFrom @@ -347,9 +348,8 @@ func CreateJWTVerifiableCredential(ctx context.Context, template VerifiableCrede jws.TypeKey: "JWT", } claims := map[string]interface{}{ - jwt.NotBeforeKey: template.IssuanceDate, - jwt.IssuerKey: template.Issuer.String(), - jwt.SubjectKey: subjectDID.String(), + jwt.IssuerKey: template.Issuer.String(), + jwt.SubjectKey: subjectDID.String(), "vc": map[string]interface{}{ "@context": template.Context, "type": template.Type, @@ -359,9 +359,17 @@ func CreateJWTVerifiableCredential(ctx context.Context, template VerifiableCrede if template.ID != nil { claims[jwt.JwtIDKey] = template.ID.String() } + if template.IssuanceDate != nil { + claims[jwt.NotBeforeKey] = *template.IssuanceDate + } if template.ExpirationDate != nil { claims[jwt.ExpirationKey] = *template.ExpirationDate } + if template.ValidFrom != nil || template.ValidUntil != nil { + // parseJWTCredential maps ValidFrom/ValidUntil to IssuanceDate/ExpirationDate, + // so a template using ValidFrom/ValidUntil would not match the final VC + return nil, errors.New("cannot use validFrom/validUntil to generate JWT-VCs") + } token, err := signer(ctx, claims, headers) if err != nil { return nil, fmt.Errorf("unable to sign JWT credential: %w", err) diff --git a/vc/vc_test.go b/vc/vc_test.go index 8f6fa46..f37f5d6 100644 --- a/vc/vc_test.go +++ b/vc/vc_test.go @@ -62,7 +62,7 @@ func TestVerifiableCredential_JSONMarshalling(t *testing.T) { input := VerifiableCredential{} marshalled, err := json.Marshal(input) require.NoError(t, err) - assert.Equal(t, "{\"@context\":null,\"credentialSubject\":null,\"issuanceDate\":\"0001-01-01T00:00:00Z\",\"issuer\":\"\",\"proof\":null,\"type\":null}", string(marshalled)) + assert.Equal(t, "{\"@context\":null,\"credentialSubject\":null,\"issuer\":\"\",\"proof\":null,\"type\":null}", string(marshalled)) }) }) t.Run("JWT", func(t *testing.T) { @@ -334,7 +334,7 @@ func TestCreateJWTVerifiableCredential(t *testing.T) { VerifiableCredentialTypeV1URI(), ssi.MustParseURI("https://example.com/custom"), }, - IssuanceDate: issuanceDate, + IssuanceDate: &issuanceDate, ExpirationDate: &expirationDate, CredentialSubject: []interface{}{ map[string]interface{}{ @@ -343,15 +343,22 @@ func TestCreateJWTVerifiableCredential(t *testing.T) { }, Issuer: issuerDID.URI(), } + captureFn := func(claims *map[string]any, headers *map[string]any) func(_ context.Context, c map[string]interface{}, h map[string]interface{}) (string, error) { + return func(_ context.Context, c map[string]interface{}, h map[string]interface{}) (string, error) { + if claims != nil { + *claims = c + } + if headers != nil { + *headers = h + } + return jwtCredential, nil + } + } ctx := context.Background() t.Run("all properties", func(t *testing.T) { var claims map[string]interface{} var headers map[string]interface{} - _, err := CreateJWTVerifiableCredential(ctx, template, func(_ context.Context, c map[string]interface{}, h map[string]interface{}) (string, error) { - claims = c - headers = h - return jwtCredential, nil - }) + _, err := CreateJWTVerifiableCredential(ctx, template, captureFn(&claims, &headers)) assert.NoError(t, err) assert.Equal(t, issuerDID.String(), claims[jwt.IssuerKey]) assert.Equal(t, subjectDID.String(), claims[jwt.SubjectKey]) @@ -366,18 +373,30 @@ func TestCreateJWTVerifiableCredential(t *testing.T) { assert.Equal(t, map[string]interface{}{"typ": "JWT"}, headers) }) t.Run("only mandatory properties", func(t *testing.T) { - minimumTemplate := template - minimumTemplate.ExpirationDate = nil - minimumTemplate.ID = nil + minimumTemplate := VerifiableCredential{CredentialSubject: template.CredentialSubject} var claims map[string]interface{} - _, err := CreateJWTVerifiableCredential(ctx, minimumTemplate, func(_ context.Context, c map[string]interface{}, _ map[string]interface{}) (string, error) { - claims = c - return jwtCredential, nil - }) + _, err := CreateJWTVerifiableCredential(ctx, minimumTemplate, captureFn(&claims, nil)) assert.NoError(t, err) + assert.Nil(t, claims[jwt.NotBeforeKey]) assert.Nil(t, claims[jwt.ExpirationKey]) assert.Nil(t, claims[jwt.JwtIDKey]) }) + t.Run("error - cannot use validFrom", func(t *testing.T) { + template := VerifiableCredential{ + CredentialSubject: template.CredentialSubject, + ValidFrom: &issuanceDate, + } + _, err := CreateJWTVerifiableCredential(ctx, template, captureFn(nil, nil)) + assert.EqualError(t, err, "cannot use validFrom/validUntil to generate JWT-VCs") + }) + t.Run("error - cannot use validUntil", func(t *testing.T) { + template := VerifiableCredential{ + CredentialSubject: template.CredentialSubject, + ValidUntil: &expirationDate, + } + _, err := CreateJWTVerifiableCredential(ctx, template, captureFn(nil, nil)) + assert.EqualError(t, err, "cannot use validFrom/validUntil to generate JWT-VCs") + }) } func TestVerifiableCredential_ValidAt(t *testing.T) { @@ -388,12 +407,12 @@ func TestVerifiableCredential_ValidAt(t *testing.T) { assert.True(t, VerifiableCredential{}.ValidAt(time.Now())) // valid on bounds - assert.True(t, VerifiableCredential{IssuanceDate: lll, ValidFrom: &lll}.ValidAt(lll)) + assert.True(t, VerifiableCredential{IssuanceDate: &lll, ValidFrom: &lll}.ValidAt(lll)) assert.True(t, VerifiableCredential{ExpirationDate: &lll, ValidUntil: &lll}.ValidAt(lll)) // invalid - assert.False(t, VerifiableCredential{IssuanceDate: hhh, ValidFrom: &lll}.ValidAt(lll)) - assert.False(t, VerifiableCredential{IssuanceDate: lll, ValidFrom: &hhh}.ValidAt(lll)) + assert.False(t, VerifiableCredential{IssuanceDate: &hhh, ValidFrom: &lll}.ValidAt(lll)) + assert.False(t, VerifiableCredential{IssuanceDate: &lll, ValidFrom: &hhh}.ValidAt(lll)) assert.False(t, VerifiableCredential{ExpirationDate: &hhh, ValidUntil: &lll}.ValidAt(hhh)) assert.False(t, VerifiableCredential{ExpirationDate: &lll, ValidUntil: &hhh}.ValidAt(hhh)) }