diff --git a/auth/services/oauth/relying_party.go b/auth/services/oauth/relying_party.go index 1aef0640ca..308f5da302 100644 --- a/auth/services/oauth/relying_party.go +++ b/auth/services/oauth/relying_party.go @@ -182,12 +182,14 @@ func (s *relyingParty) RequestRFC021AccessToken(ctx context.Context, requester d expires := time.Now().Add(time.Second * 5) // todo: support multiple wallets domain := verifier.String() + nonce := nutsCrypto.GenerateNonce() vp, err := s.wallet.BuildPresentation(ctx, signInstructions[0].VerifiableCredentials, holder.PresentationOptions{ Format: format, ProofOptions: proof.ProofOptions{ Created: time.Now(), Expires: &expires, Domain: &domain, + Nonce: &nonce, }, }, &requester, false) if err != nil { diff --git a/vcr/holder/wallet.go b/vcr/holder/wallet.go index 8e34f7848a..38b8761a4e 100644 --- a/vcr/holder/wallet.go +++ b/vcr/holder/wallet.go @@ -119,13 +119,15 @@ func (h wallet) buildJWTPresentation(ctx context.Context, subjectDID did.DID, cr jwt.IssuerKey: subjectDID.String(), jwt.SubjectKey: subjectDID.String(), jwt.JwtIDKey: id.String(), - "nonce": crypto.GenerateNonce(), "vp": vc.VerifiablePresentation{ Context: append([]ssi.URI{VerifiableCredentialLDContextV1}, options.AdditionalContexts...), Type: append([]ssi.URI{VerifiablePresentationLDType}, options.AdditionalTypes...), VerifiableCredential: credentials, }, } + if options.ProofOptions.Nonce != nil { + claims["nonce"] = *options.ProofOptions.Nonce + } if options.ProofOptions.Domain != nil { claims[jwt.AudienceKey] = *options.ProofOptions.Domain } @@ -172,8 +174,6 @@ func (h wallet) buildJSONLDPresentation(ctx context.Context, subjectDID did.DID, } ldProof := proof.NewLDProof(options.ProofOptions) - nonce := crypto.GenerateNonce() - ldProof.Nonce = &nonce signingResult, err := ldProof. Sign(ctx, document, signature.JSONWebSignature2020{ContextLoader: h.jsonldManager.DocumentLoader(), Signer: h.keyStore}, key) if err != nil { diff --git a/vcr/holder/wallet_test.go b/vcr/holder/wallet_test.go index 317bab6e8f..10c0521370 100644 --- a/vcr/holder/wallet_test.go +++ b/vcr/holder/wallet_test.go @@ -91,18 +91,20 @@ func TestWallet_BuildPresentation(t *testing.T) { assert.Equal(t, JSONLDPresentationFormat, result.Format()) ldProof, err := credential.ParseLDProof(*result) require.NoError(t, err) - assert.NotEmpty(t, ldProof.Nonce) + assert.Empty(t, ldProof.Nonce) }) t.Run("ok - custom options", func(t *testing.T) { ctrl := gomock.NewController(t) specialType := ssi.MustParseURI("SpecialPresentation") domain := "https://example.com" + nonce := "the-nonce" options := PresentationOptions{ AdditionalContexts: []ssi.URI{credential.NutsV1ContextURI}, AdditionalTypes: []ssi.URI{specialType}, ProofOptions: proof.ProofOptions{ ProofPurpose: "authentication", Domain: &domain, + Nonce: &nonce, }, Format: JSONLDPresentationFormat, } @@ -118,10 +120,12 @@ func TestWallet_BuildPresentation(t *testing.T) { require.NotNil(t, result) assert.True(t, result.IsType(specialType)) assert.True(t, result.ContainsContext(credential.NutsV1ContextURI)) - proofs, _ := result.Proofs() + var proofs []proof.LDProof + require.NoError(t, result.UnmarshalProofValue(&proofs)) require.Len(t, proofs, 1) assert.Equal(t, "authentication", proofs[0].ProofPurpose) assert.Equal(t, "https://example.com", *proofs[0].Domain) + assert.Equal(t, nonce, *proofs[0].Nonce) assert.Equal(t, JSONLDPresentationFormat, result.Format()) }) t.Run("ok - multiple VCs", func(t *testing.T) { @@ -159,7 +163,7 @@ func TestWallet_BuildPresentation(t *testing.T) { assert.Equal(t, JWTPresentationFormat, result.Format()) assert.NotNil(t, result.JWT()) nonce, _ := result.JWT().Get("nonce") - assert.NotEmpty(t, nonce) + assert.Empty(t, nonce) }) t.Run("ok - multiple VCs", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -180,12 +184,14 @@ func TestWallet_BuildPresentation(t *testing.T) { t.Run("optional proof options", func(t *testing.T) { exp := time.Now().Local().Truncate(time.Second) domain := "https://example.com" + nonce := "the-nonce" options := PresentationOptions{ Format: JWTPresentationFormat, ProofOptions: proof.ProofOptions{ Expires: &exp, Created: exp.Add(-1 * time.Hour), Domain: &domain, + Nonce: &nonce, }, } @@ -205,6 +211,8 @@ func TestWallet_BuildPresentation(t *testing.T) { assert.Equal(t, *options.ProofOptions.Expires, result.JWT().Expiration().Local()) assert.Equal(t, options.ProofOptions.Created, result.JWT().NotBefore().Local()) assert.Equal(t, []string{domain}, result.JWT().Audience()) + actualNonce, _ := result.JWT().Get("nonce") + assert.Equal(t, nonce, actualNonce) }) }) t.Run("validation", func(t *testing.T) { diff --git a/vcr/signature/proof/jsonld.go b/vcr/signature/proof/jsonld.go index 124ff075fd..ff5b71fd2b 100644 --- a/vcr/signature/proof/jsonld.go +++ b/vcr/signature/proof/jsonld.go @@ -62,6 +62,8 @@ type ProofOptions struct { // ProofPurpose contains a specific intent for the proof, the reason why an entity created it. // Acts as a safeguard to prevent the proof from being misused for a purpose other than the one it was intended for. ProofPurpose string `json:"proofPurpose"` + // Nonce contains a value that is used to prevent replay attacks + Nonce *string `json:"nonce,omitempty"` } // ValidAt checks if the proof is valid at a certain given time. @@ -81,7 +83,6 @@ func (o ProofOptions) ValidAt(at time.Time, maxSkew time.Duration) bool { // LDProof contains the fields of the Proof data model: https://w3c-ccg.github.io/data-integrity-spec/#proofs type LDProof struct { ProofOptions - Nonce *string `json:"nonce,omitempty"` // Type contains the signature type. Its is determined from the key type. Type ssi.ProofType `json:"type"` // VerificationMethod is the key identifier for the public/private key pair used to sign this proof diff --git a/vcr/test/test.go b/vcr/test/test.go index 53eab109e4..219dac18d2 100644 --- a/vcr/test/test.go +++ b/vcr/test/test.go @@ -81,8 +81,8 @@ func CreateJSONLDPresentation(t *testing.T, subjectDID did.DID, visitor func(pre ProofOptions: proof.ProofOptions{ Created: time.Now(), Expires: &exp, + Nonce: &nonce, }, - Nonce: &nonce, }, }, }