Skip to content

Commit

Permalink
implement 'all credential subjects must have the same ID'
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Dec 8, 2023
1 parent 8154e60 commit 72a7076
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 36 deletions.
37 changes: 20 additions & 17 deletions auth/api/iam/s2s_vptoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ func (r *Wrapper) handleS2SAccessTokenRequest(issuer did.DID, scope string, subm
}
}

var credentialSubjectID did.DID
for _, presentation := range pexEnvelope.Presentations {
if err := validateS2SPresentationMaxValidity(presentation); err != nil {
return nil, err
}
if err := validatePresentationSigner(presentation); err != nil {
if subjectDID, err := validatePresentationSigner(presentation, credentialSubjectID); err != nil {
return nil, err
} else {
credentialSubjectID = *subjectDID
}
if err := r.validatePresentationAudience(presentation, issuer); err != nil {
return nil, err
Expand Down Expand Up @@ -96,7 +99,7 @@ func (r *Wrapper) handleS2SAccessTokenRequest(issuer did.DID, scope string, subm
}

// All OK, allow access
response, err := r.createS2SAccessToken(issuer, time.Now(), pexEnvelope.Presentations, *submission, *definition, scope)
response, err := r.createS2SAccessToken(issuer, time.Now(), pexEnvelope.Presentations, *submission, *definition, scope, credentialSubjectID)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -143,25 +146,19 @@ func (r *Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessT
}

func (r *Wrapper) createS2SAccessToken(issuer did.DID, issueTime time.Time, presentations []vc.VerifiablePresentation,
submission pe.PresentationSubmission, definition PresentationDefinition, scope string) (*oauth.TokenResponse, error) {
// TODO: RFC021 isn't clear on this, so take credential subject from first VP for now.
// See https://github.com/nuts-foundation/nuts-specification/issues/269
clientDID, err := credential.PresentationSigner(presentations[0])
if err != nil {
return nil, fmt.Errorf("unable to extract client DID from presentation: %w", err)
}
submission pe.PresentationSubmission, definition PresentationDefinition, scope string, credentialSubjectDID did.DID) (*oauth.TokenResponse, error) {
accessToken := AccessToken{
Token: crypto.GenerateNonce(),
Issuer: issuer.String(),
ClientId: clientDID.String(),
ClientId: credentialSubjectDID.String(),
IssuedAt: issueTime,
Expiration: issueTime.Add(accessTokenValidity),
Scope: scope,
VPToken: presentations,
PresentationDefinition: &definition,
PresentationSubmission: &submission,
}
err = r.accessTokenStore().Put(accessToken.Token, accessToken)
err := r.accessTokenStore().Put(accessToken.Token, accessToken)
if err != nil {
return nil, fmt.Errorf("unable to store access token: %w", err)
}
Expand Down Expand Up @@ -217,21 +214,27 @@ func validateS2SPresentationMaxValidity(presentation vc.VerifiablePresentation)
}

// validatePresentationSigner checks if the presenter of the VP is the same as the subject of the VCs being presented.
func validatePresentationSigner(presentation vc.VerifiablePresentation) error {
ok, err := credential.PresenterIsCredentialSubject(presentation)
func validatePresentationSigner(presentation vc.VerifiablePresentation, expectedCredentialSubjectDID did.DID) (*did.DID, error) {
subjectDID, err := credential.PresenterIsCredentialSubject(presentation)
if err != nil {
return oauth.OAuth2Error{
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: err.Error(),
}
}
if !ok {
return oauth.OAuth2Error{
if subjectDID == nil {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "presentation signer is not credential subject",
}
}
return nil
if !expectedCredentialSubjectDID.Empty() && !subjectDID.Equals(expectedCredentialSubjectDID) {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "not all presentations have the same credential subject ID",
}
}
return subjectDID, nil
}

// validateS2SPresentationNonce checks if the nonce has been used before; 'nonce' claim for JWTs or LDProof's 'nonce' for JSON-LD.
Expand Down
18 changes: 16 additions & 2 deletions auth/api/iam/s2s_vptoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,17 @@ func TestWrapper_handleS2SAccessTokenRequest(t *testing.T) {
assert.EqualError(t, err, "invalid_request - assertion parameter is invalid: unable to parse PEX envelope as verifiable presentation: invalid JWT")
assert.Nil(t, resp)
})
t.Run("not all VPs have the same credential subject ID", func(t *testing.T) {
ctx := newTestClient(t)

secondSubjectID := did.MustParseDID("did:web:example.com:other")
secondPresentation := test.CreateJSONLDPresentation(t, secondSubjectID, proofVisitor, credential.JWTNutsOrganizationCredential(t, secondSubjectID))
assertionJSON, _ := json.Marshal([]VerifiablePresentation{presentation, secondPresentation})

resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, string(assertionJSON))
assert.EqualError(t, err, "invalid_request - not all presentations have the same credential subject ID")
assert.Nil(t, resp)
})
t.Run("nonce", func(t *testing.T) {
t.Run("replay attack (nonce is reused)", func(t *testing.T) {
ctx := newTestClient(t)
Expand Down Expand Up @@ -390,13 +401,15 @@ func TestWrapper_handleS2SAccessTokenRequest(t *testing.T) {
}

func TestWrapper_createAccessToken(t *testing.T) {
credentialSubjectID := did.MustParseDID("did:nuts:B8PUHs2AUHbFF1xLLK4eZjgErEcMXHxs68FteY7NDtCY")
verificationMethodID := ssi.MustParseURI(credentialSubjectID.String() + "#1")
credential, err := vc.ParseVerifiableCredential(jsonld.TestOrganizationCredential)
require.NoError(t, err)
presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
VerifiableCredential: []vc.VerifiableCredential{*credential},
Proof: []interface{}{
proof.LDProof{
VerificationMethod: ssi.MustParseURI("did:nuts:B8PUHs2AUHbFF1xLLK4eZjgErEcMXHxs68FteY7NDtCY#1"),
VerificationMethod: verificationMethodID,
},
},
})
Expand All @@ -409,7 +422,8 @@ func TestWrapper_createAccessToken(t *testing.T) {
t.Run("ok", func(t *testing.T) {
ctx := newTestClient(t)

accessToken, err := ctx.client.createS2SAccessToken(issuerDID, time.Now(), []VerifiablePresentation{test.ParsePresentation(t, presentation)}, submission, definition, "everything")
vps := []VerifiablePresentation{test.ParsePresentation(t, presentation)}
accessToken, err := ctx.client.createS2SAccessToken(issuerDID, time.Now(), vps, submission, definition, "everything", credentialSubjectID)

require.NoError(t, err)
assert.NotEmpty(t, accessToken.AccessToken)
Expand Down
4 changes: 3 additions & 1 deletion vcr/credential/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"encoding/json"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vcr/assets"
"github.com/stretchr/testify/require"
"testing"
Expand Down Expand Up @@ -75,12 +76,13 @@ func ValidNutsOrganizationCredential(t *testing.T) vc.VerifiableCredential {
return inputVC
}

func JWTNutsOrganizationCredential(t *testing.T) vc.VerifiableCredential {
func JWTNutsOrganizationCredential(t *testing.T, subjectID did.DID) vc.VerifiableCredential {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
token := jwt.New()
require.NoError(t, token.Set("vc", map[string]interface{}{
"credentialSubject": map[string]interface{}{
"id": subjectID,
"organization": map[string]interface{}{
"city": "IJbergen",
"name": "care",
Expand Down
12 changes: 6 additions & 6 deletions vcr/credential/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,20 @@ func ResolveSubjectDID(credentials ...vc.VerifiableCredential) (*did.DID, error)

// PresenterIsCredentialSubject checks if the presenter of the VP is the same as the subject of the VCs being presented.
// If the presentation signer or credential subject can't be resolved, it returns an error.
// If parsing succeeds and the signer DID is the same as the credential subject DID, it returns true.
func PresenterIsCredentialSubject(vp vc.VerifiablePresentation) (bool, error) {
// If parsing succeeds and the signer DID is the same as the credential subject DID, it returns the DID.
func PresenterIsCredentialSubject(vp vc.VerifiablePresentation) (*did.DID, error) {
signerDID, err := PresentationSigner(vp)
if err != nil {
return false, err
return nil, err
}
credentialSubjectID, err := ResolveSubjectDID(vp.VerifiableCredential...)
if err != nil {
return false, err
return nil, err
}
if !credentialSubjectID.Equals(*signerDID) {
return false, nil
return nil, nil
}
return true, nil
return signerDID, nil
}

// PresentationIssuanceDate returns the date at which the presentation was issued.
Expand Down
18 changes: 9 additions & 9 deletions vcr/credential/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestResolveSubjectDID(t *testing.T) {
}

func TestPresenterIsCredentialSubject(t *testing.T) {
subjectDID := ssi.MustParseURI("did:test:123")
subjectDID := did.MustParseDID("did:test:123")
keyID := ssi.MustParseURI("did:test:123#1")
t.Run("ok", func(t *testing.T) {
vp := test.ParsePresentation(t, vc.VerifiablePresentation{
Expand All @@ -85,13 +85,13 @@ func TestPresenterIsCredentialSubject(t *testing.T) {
})
is, err := PresenterIsCredentialSubject(vp)
assert.NoError(t, err)
assert.True(t, is)
assert.Equal(t, subjectDID, *is)
})
t.Run("no proof", func(t *testing.T) {
vp := test.ParsePresentation(t, vc.VerifiablePresentation{})
is, err := PresenterIsCredentialSubject(vp)
actual, err := PresenterIsCredentialSubject(vp)
assert.EqualError(t, err, "presentation should have exactly 1 proof, got 0")
assert.False(t, is)
assert.Nil(t, actual)
})
t.Run("no VC subject", func(t *testing.T) {
vp := test.ParsePresentation(t, vc.VerifiablePresentation{
Expand All @@ -107,7 +107,7 @@ func TestPresenterIsCredentialSubject(t *testing.T) {
})
is, err := PresenterIsCredentialSubject(vp)
assert.EqualError(t, err, "unable to get subject DID from VC: there must be at least 1 credentialSubject")
assert.False(t, is)
assert.Nil(t, is)
})
t.Run("no VC subject ID", func(t *testing.T) {
vp := test.ParsePresentation(t, vc.VerifiablePresentation{
Expand All @@ -125,7 +125,7 @@ func TestPresenterIsCredentialSubject(t *testing.T) {
})
is, err := PresenterIsCredentialSubject(vp)
assert.EqualError(t, err, "unable to get subject DID from VC: credential subjects have no ID")
assert.False(t, is)
assert.Nil(t, is)
})
t.Run("proof verification method does not equal VC subject ID", func(t *testing.T) {
vp := test.ParsePresentation(t, vc.VerifiablePresentation{
Expand All @@ -143,7 +143,7 @@ func TestPresenterIsCredentialSubject(t *testing.T) {
})
is, err := PresenterIsCredentialSubject(vp)
assert.NoError(t, err)
assert.False(t, is)
assert.Nil(t, is)
})
t.Run("proof type is unsupported", func(t *testing.T) {
vp := test.ParsePresentation(t, vc.VerifiablePresentation{
Expand All @@ -158,7 +158,7 @@ func TestPresenterIsCredentialSubject(t *testing.T) {
})
is, err := PresenterIsCredentialSubject(vp)
assert.EqualError(t, err, "invalid LD-proof for presentation: json: cannot unmarshal bool into Go value of type proof.LDProof")
assert.False(t, is)
assert.Nil(t, is)
})
t.Run("too many proofs", func(t *testing.T) {
vp := test.ParsePresentation(t, vc.VerifiablePresentation{
Expand All @@ -174,7 +174,7 @@ func TestPresenterIsCredentialSubject(t *testing.T) {
})
is, err := PresenterIsCredentialSubject(vp)
assert.EqualError(t, err, "presentation should have exactly 1 proof, got 2")
assert.False(t, is)
assert.Nil(t, is)
})
}

Expand Down
3 changes: 2 additions & 1 deletion vcr/pe/presentation_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"crypto/rand"
"embed"
"encoding/json"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"testing"

Expand Down Expand Up @@ -89,7 +90,7 @@ func definitions() testDefinitions {

func TestMatch(t *testing.T) {
jsonldVC := credential.ValidNutsOrganizationCredential(t)
jwtVC := credential.JWTNutsOrganizationCredential(t)
jwtVC := credential.JWTNutsOrganizationCredential(t, did.MustParseDID("did:web:example.com"))

t.Run("Basic", func(t *testing.T) {
t.Run("JSON-LD", func(t *testing.T) {
Expand Down

0 comments on commit 72a7076

Please sign in to comment.