Skip to content

Commit

Permalink
Add validFrom and validUntil aliases for issuanceDate and expirationD…
Browse files Browse the repository at this point in the history
…ate (#97)

* add validFrom and validUntil aliases for issuanceDate and epirationDate

* add comments
  • Loading branch information
gerardsn authored Dec 1, 2023
1 parent 68fb219 commit c2e3cb8
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 21 deletions.
56 changes: 49 additions & 7 deletions vc/vc.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,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() != "" {
Expand Down Expand Up @@ -139,10 +140,18 @@ type VerifiableCredential struct {
Type []ssi.URI `json:"type"`
// Issuer refers to the party that issued the credential
Issuer ssi.URI `json:"issuer"`
// IssuanceDate is a rfc3339 formatted datetime.
IssuanceDate time.Time `json:"issuanceDate"`
// ExpirationDate is a rfc3339 formatted datetime. It is optional
// IssuanceDate is a rfc3339 formatted datetime. It is required, but may be replaced by alias ValidFrom
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 (vc data model v2) alternative for IssuanceDate.
// The jwt-vc 'nbf' field will unmarshal to IssuanceDate, which may not match with the JSON-LD definition of certain VCs.
ValidFrom *time.Time `json:"validFrom,omitempty"`
// ExpirationDate is a rfc3339 formatted datetime. Has alias ValidUntil. It is optional
ExpirationDate *time.Time `json:"expirationDate,omitempty"`
// ValidFrom is a rfc3339 formatted datetime. It is optional, and is mutually exclusive with ExpirationDate (not enforced).
// It's a forwards compatible (vc data model v2) alternative for ExpirationDate.
// The jwt-vc 'exp' field will unmarshal to ExpirationDate, which may not match with the JSON-LD definition of certain VCs.
ValidUntil *time.Time `json:"validUntil,omitempty"`
// CredentialStatus holds information on how the credential can be revoked. It must be extracted using the UnmarshalCredentialStatus method and a custom type.
CredentialStatus []any `json:"credentialStatus,omitempty"`
// CredentialSubject holds the actual data for the credential. It must be extracted using the UnmarshalCredentialSubject method and a custom type.
Expand Down Expand Up @@ -174,6 +183,32 @@ func (vc VerifiableCredential) JWT() jwt.Token {
return token
}

// ValidAt returns true if
// - t >= IssuanceDate and ValidFrom
// - t <= ExpirationDate and ValidUntil
// For any value that is missing, the evaluation defaults to true
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 != nil && t.Before(*vc.IssuanceDate) {
return false
}
// t > ValidFrom
if vc.ValidFrom != nil && t.Before(*vc.ValidFrom) {
return false
}
// t < ExpirationDate
if vc.ExpirationDate != nil && t.After(*vc.ExpirationDate) {
return false
}
// t < ValidUntil
if vc.ValidUntil != nil && t.After(*vc.ValidUntil) {
return false
}
// valid
return true
}

// CredentialStatus contains the required fields ID and Type, and the raw data for unmarshalling into a custom type.
type CredentialStatus struct {
ID ssi.URI `json:"id"`
Expand Down Expand Up @@ -358,9 +393,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,
Expand All @@ -370,9 +404,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)
Expand Down
65 changes: 51 additions & 14 deletions vc/vc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,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) {
Expand Down Expand Up @@ -401,7 +401,7 @@ func TestCreateJWTVerifiableCredential(t *testing.T) {
VerifiableCredentialTypeV1URI(),
ssi.MustParseURI("https://example.com/custom"),
},
IssuanceDate: issuanceDate,
IssuanceDate: &issuanceDate,
ExpirationDate: &expirationDate,
CredentialSubject: []interface{}{
map[string]interface{}{
Expand All @@ -410,15 +410,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])
Expand All @@ -433,16 +440,46 @@ 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) {
lll := time.Date(1999, 0, 0, 0, 0, 0, 0, time.UTC)
hhh := time.Date(2001, 0, 0, 0, 0, 0, 0, time.UTC)

// no validity period is always true; includes missing IssuanceDate(.IsZero() == true)
assert.True(t, VerifiableCredential{}.ValidAt(time.Now()))

// valid on bounds
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{ExpirationDate: &hhh, ValidUntil: &lll}.ValidAt(hhh))
assert.False(t, VerifiableCredential{ExpirationDate: &lll, ValidUntil: &hhh}.ValidAt(hhh))
}

0 comments on commit c2e3cb8

Please sign in to comment.