Skip to content

Commit

Permalink
check exp/nbf
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Nov 25, 2023
1 parent 3c533d9 commit 0958244
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 5 deletions.
19 changes: 19 additions & 0 deletions auth/api/iam/s2s_vptoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import (
// TODO: Might want to make this configurable at some point
const accessTokenValidity = 15 * time.Minute

// maxPresentationValidity defines the maximum validity of a presentation.
const maxPresentationValidity = 10 * time.Second

// handleS2SAccessTokenRequest handles the /token request with vp_token bearer grant type, intended for service-to-service exchanges.
// It performs cheap checks first (parameter presence and validity), then the more expensive ones (checking signatures and matching VCs to the presentation definition):
// 1. Check presence and format of parameters
Expand Down Expand Up @@ -94,13 +97,29 @@ func (s Wrapper) handleS2SAccessTokenRequest(issuer did.DID, params map[string]s
}

for _, presentation := range pexEnvelope.Presentations {
// Presenter should be credential holder
err = credential.VerifyPresenterIsCredentialSubject(presentation)
if err != nil {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: fmt.Sprintf("verifiable presentation is invalid: %s", err.Error()),
}
}
// Presentation should not be valid for too long
created := credential.PresentationIssuanceDate(presentation)
expires := credential.PresentationExpirationDate(presentation)
if created == nil || expires == nil {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "assertion parameter is invalid: missing creation or expiration date",
}
}
if expires.Sub(*created) > maxPresentationValidity {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: fmt.Sprintf("assertion parameter is invalid: presentation is valid for too long (max %s)", maxPresentationValidity),
}
}
}

// Validate the presentation submission:
Expand Down
51 changes: 50 additions & 1 deletion auth/api/iam/s2s_vptoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package iam

import (
"github.com/lestrrat-go/jwx/v2/jwt"
"net/http"
"testing"
"time"
Expand Down Expand Up @@ -178,9 +179,57 @@ func TestWrapper_handleS2SAccessTokenRequest(t *testing.T) {
assert.EqualError(t, err, "invalid_request - missing required parameters")
assert.Nil(t, resp)
})
t.Run("missing presentation expiry date", func(t *testing.T) {
ctx := newTestClient(t)
presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
require.NoError(t, token.Remove(jwt.ExpirationKey))
}, verifiableCredential)

params := map[string]string{
"assertion": url.QueryEscape(presentation.Raw()),
"presentation_submission": url.QueryEscape(string(submissionJSON)),
"scope": requestedScope,
}

_, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, params)

require.EqualError(t, err, "invalid_request - assertion parameter is invalid: missing creation or expiration date")
})
t.Run("missing presentation not before date", func(t *testing.T) {
ctx := newTestClient(t)
presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
require.NoError(t, token.Remove(jwt.NotBeforeKey))
}, verifiableCredential)

params := map[string]string{
"assertion": url.QueryEscape(presentation.Raw()),
"presentation_submission": url.QueryEscape(string(submissionJSON)),
"scope": requestedScope,
}

_, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, params)

require.EqualError(t, err, "invalid_request - assertion parameter is invalid: missing creation or expiration date")
})
t.Run("missing presentation valid for too long", func(t *testing.T) {
ctx := newTestClient(t)
presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
require.NoError(t, token.Set(jwt.ExpirationKey, time.Now().Add(time.Hour)))
}, verifiableCredential)

params := map[string]string{
"assertion": url.QueryEscape(presentation.Raw()),
"presentation_submission": url.QueryEscape(string(submissionJSON)),
"scope": requestedScope,
}

_, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, params)

require.EqualError(t, err, "invalid_request - assertion parameter is invalid: presentation is valid for too long (max 10s)")
})
t.Run("JWT VP", func(t *testing.T) {
ctx := newTestClient(t)
presentation := test.CreateJWTPresentation(t, *subjectDID, verifiableCredential)
presentation := test.CreateJWTPresentation(t, *subjectDID, nil, verifiableCredential)
ctx.verifier.EXPECT().VerifyVP(presentation, true, true, gomock.Any()).Return(presentation.VerifiableCredential, nil)

params := map[string]string{
Expand Down
57 changes: 57 additions & 0 deletions vcr/credential/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"errors"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"time"
)

// ResolveSubjectDID resolves the subject DID from the given credentials.
Expand Down Expand Up @@ -62,3 +64,58 @@ func VerifyPresenterIsCredentialSubject(vp vc.VerifiablePresentation) error {
}
return nil
}

// PresentationIssuanceDate returns the date at which the presentation was issued.
// For JSON-LD, it looks at the first LinkedData proof's 'created' property.
// For JWT, it looks at the 'iat' claim, or if that is not present, the 'nbf' claim.
// If it can't resolve the date, it returns nil.
func PresentationIssuanceDate(presentation vc.VerifiablePresentation) *time.Time {
var result time.Time
switch presentation.Format() {
case vc.JWTPresentationProofFormat:
jwt := presentation.JWT()
if result = jwt.IssuedAt(); result.IsZero() {
result = jwt.NotBefore()
}
case vc.JSONLDPresentationProofFormat:
var proofs []proof.LDProof
if err := presentation.UnmarshalProofValue(&proofs); err != nil {
return nil
}
if len(proofs) == 0 {
return nil
}
result = proofs[0].Created
}
if result.IsZero() {
return nil
}
return &result
}

// PresentationExpirationDate returns the date at which the presentation was issued.
// For JSON-LD, it looks at the first LinkedData proof's 'expires' property.
// For JWT, it looks at the 'exp' claim.
// If it can't resolve the date, it returns nil.
func PresentationExpirationDate(presentation vc.VerifiablePresentation) *time.Time {
var result time.Time
switch presentation.Format() {
case vc.JWTPresentationProofFormat:
result = presentation.JWT().Expiration()
case vc.JSONLDPresentationProofFormat:
var proofs []proof.LDProof
if err := presentation.UnmarshalProofValue(&proofs); err != nil {
return nil
}
if len(proofs) == 0 {
return nil
}
if proofs[0].Expires != nil {
result = *proofs[0].Expires
}
}
if result.IsZero() {
return nil
}
return &result
}
106 changes: 106 additions & 0 deletions vcr/credential/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
package credential

import (
"github.com/lestrrat-go/jwx/v2/jwt"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"github.com/nuts-foundation/nuts-node/vcr/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)

func TestResolveSubjectDID(t *testing.T) {
Expand Down Expand Up @@ -166,3 +170,105 @@ func TestVerifyPresenterIsCredentialSubject(t *testing.T) {
assert.EqualError(t, err, "presentation should have exactly 1 proof, got 2")
})
}

func TestPresentationIssuanceDate(t *testing.T) {
presenterDID := did.MustParseDID("did:test:123")
expected := time.Now().In(time.UTC).Truncate(time.Second)
t.Run("JWT iat", func(t *testing.T) {
presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
_ = token.Remove(jwt.NotBeforeKey)
require.NoError(t, token.Set(jwt.IssuedAtKey, expected))
})
actual := PresentationIssuanceDate(presentation)
assert.Equal(t, expected, *actual)
})
t.Run("JWT nbf", func(t *testing.T) {
presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
_ = token.Remove(jwt.IssuedAtKey)
require.NoError(t, token.Set(jwt.NotBeforeKey, expected))
})
actual := PresentationIssuanceDate(presentation)
assert.Equal(t, expected, *actual)
})
t.Run("JWT no iat or nbf", func(t *testing.T) {
presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
_ = token.Remove(jwt.IssuedAtKey)
_ = token.Remove(jwt.NotBeforeKey)
})
actual := PresentationIssuanceDate(presentation)
assert.Nil(t, actual)
})
t.Run("JSON-LD", func(t *testing.T) {
presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{
proof.LDProof{
ProofOptions: proof.ProofOptions{
Created: expected,
},
},
},
})
actual := PresentationIssuanceDate(presentation)
assert.Equal(t, expected, *actual)
})
t.Run("JSON-LD no proof", func(t *testing.T) {
presentation := test.ParsePresentation(t, vc.VerifiablePresentation{})
actual := PresentationIssuanceDate(presentation)
assert.Nil(t, actual)
})
t.Run("JSON-LD no created", func(t *testing.T) {
presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{
proof.LDProof{},
},
})
actual := PresentationIssuanceDate(presentation)
assert.Nil(t, actual)
})
}

func TestPresentationExpirationDate(t *testing.T) {
presenterDID := did.MustParseDID("did:test:123")
expected := time.Now().In(time.UTC).Truncate(time.Second)
t.Run("JWT", func(t *testing.T) {
presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
require.NoError(t, token.Set(jwt.ExpirationKey, expected))
})
actual := PresentationExpirationDate(presentation)
assert.Equal(t, expected, *actual)
})
t.Run("JWT no exp", func(t *testing.T) {
presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
_ = token.Remove(jwt.ExpirationKey)
})
actual := PresentationExpirationDate(presentation)
assert.Nil(t, actual)
})
t.Run("JSON-LD", func(t *testing.T) {
presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{
proof.LDProof{
ProofOptions: proof.ProofOptions{
Expires: &expected,
},
},
},
})
actual := PresentationExpirationDate(presentation)
assert.Equal(t, expected, *actual)
})
t.Run("JSON-LD no proof", func(t *testing.T) {
presentation := test.ParsePresentation(t, vc.VerifiablePresentation{})
actual := PresentationExpirationDate(presentation)
assert.Nil(t, actual)
})
t.Run("JSON-LD no expires", func(t *testing.T) {
presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{
proof.LDProof{},
},
})
actual := PresentationExpirationDate(presentation)
assert.Nil(t, actual)
})
}
23 changes: 19 additions & 4 deletions vcr/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ import (
)

// CreateJWTPresentation creates a JWT presentation with the given subject DID and credentials.
func CreateJWTPresentation(t *testing.T, subjectDID did.DID, credentials ...vc.VerifiableCredential) vc.VerifiablePresentation {
func CreateJWTPresentation(t *testing.T, subjectDID did.DID, tokenVisitor func(token jwt.Token), credentials ...vc.VerifiableCredential) vc.VerifiablePresentation {
headers := jws.NewHeaders()
require.NoError(t, headers.Set(jws.TypeKey, "JWT"))
claims := map[string]interface{}{
jwt.IssuerKey: subjectDID.String(),
jwt.SubjectKey: subjectDID.String(),
jwt.JwtIDKey: subjectDID.String() + "#" + uuid.NewString(),
jwt.NotBeforeKey: time.Now().Unix(),
jwt.ExpirationKey: time.Now().Add(time.Hour).Unix(),
jwt.ExpirationKey: time.Now().Add(5 * time.Second).Unix(),
"vp": vc.VerifiablePresentation{
Type: []ssi.URI{vc.VerifiablePresentationTypeV1URI()},
VerifiableCredential: credentials,
Expand All @@ -54,6 +54,9 @@ func CreateJWTPresentation(t *testing.T, subjectDID did.DID, credentials ...vc.V
for k, v := range claims {
require.NoError(t, unsignedToken.Set(k, v))
}
if tokenVisitor != nil {
tokenVisitor(unsignedToken)
}
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
token, _ := jwt.Sign(unsignedToken, jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(headers)))
result, err := vc.ParseVerifiablePresentation(string(token))
Expand All @@ -65,16 +68,28 @@ func CreateJWTPresentation(t *testing.T, subjectDID did.DID, credentials ...vc.V
// The presentation is not actually signed.
func CreateJSONLDPresentation(t *testing.T, subjectDID did.DID, verifiableCredential ...vc.VerifiableCredential) vc.VerifiablePresentation {
id := ssi.MustParseURI(subjectDID.String() + "#" + uuid.NewString())
data, _ := vc.VerifiablePresentation{
exp := time.Now().Add(5 * time.Second)
vp := vc.VerifiablePresentation{
ID: &id,
VerifiableCredential: verifiableCredential,
Proof: []interface{}{
proof.LDProof{
Type: ssi.JsonWebSignature2020,
VerificationMethod: ssi.MustParseURI(subjectDID.String() + "#1"),
ProofOptions: proof.ProofOptions{
Created: time.Now(),
Expires: &exp,
},
},
},
}.MarshalJSON()
}
return ParsePresentation(t, vp)
}

// ParsePresentation marshals the given presentation and parses it again, to make sure the format property is set correctly.
func ParsePresentation(t *testing.T, presentation vc.VerifiablePresentation) vc.VerifiablePresentation {
data, err := presentation.MarshalJSON()
require.NoError(t, err)
result, err := vc.ParseVerifiablePresentation(string(data))
require.NoError(t, err)
return *result
Expand Down

0 comments on commit 0958244

Please sign in to comment.