diff --git a/resource/nonce.go b/resource/nonce.go index 347f3c94..3fad9d78 100644 --- a/resource/nonce.go +++ b/resource/nonce.go @@ -27,6 +27,24 @@ func generateDPoPNonce(random io.Reader) (string, error) { return base64.RawURLEncoding.EncodeToString(buf), nil } +// IssueDPoPNonce issues a fresh DPoP nonce this Verifier will accept, for +// a resource server to hand out ahead of a request rather than in answer +// to one: an OpenID4VCI 1.0 §7.2 Nonce Response's DPoP-Nonce header, +// say, which the Wallet then uses at the Credential Endpoint this +// Verifier guards. It returns "" and no error when Dependencies.Nonces is +// nil: the Verifier doesn't use DPoP nonces, so there is none to give. +// +// Each call stores a nonce until Config.Limits.DPoPNonceLifetime has +// passed. Called from an endpoint anyone may call (§7.1's Nonce Endpoint +// is), it lets anyone add records, as that endpoint's own c_nonce does: +// rate-limit it the same way. +func (v *Verifier) IssueDPoPNonce(ctx context.Context) (string, error) { + if v.deps.Nonces == nil { + return "", nil + } + return v.issueDPoPNonce(ctx, v.deps.Clock.Now()) +} + // issueDPoPNonce generates and persists a fresh nonce, valid from now // for v.cfg.Limits.DPoPNonceLifetime. Only ever called once v.deps.Nonces // is known to be non-nil. diff --git a/resource/nonce_test.go b/resource/nonce_test.go index 5df9e334..266f8cab 100644 --- a/resource/nonce_test.go +++ b/resource/nonce_test.go @@ -270,3 +270,25 @@ func TestInsufficientScopeCarriesNextNonce(t *testing.T) { t.Fatalf("status = %d, want 403", rec.Code) } } + +// TestIssueDPoPNonce: a nonce IssueDPoPNonce hands out ahead of a +// request (as an OpenID4VCI Nonce Response does) is accepted by Verify, +// once; a Verifier without Dependencies.Nonces has none to give. +func TestIssueDPoPNonce(t *testing.T) { + ctx := context.Background() + first := newNonceFixture(t, "", nil) + issued, err := first.verifier.IssueDPoPNonce(ctx) + if err != nil || issued == "" { + t.Fatalf("IssueDPoPNonce = %q, %v; want a nonce", issued, err) + } + if _, err := newNonceFixture(t, issued, first.nonces).verify(t); err != nil { + t.Fatalf("Verify(issued nonce) = %v, want accepted", err) + } + if _, err := newNonceFixture(t, issued, first.nonces).verify(t); err == nil { + t.Fatal("Verify(issued nonce again) = nil error, want it refused as spent") + } + + if nonce, err := newFixture(t).verifier.IssueDPoPNonce(ctx); nonce != "" || err != nil { + t.Errorf("IssueDPoPNonce without Nonces = %q, %v; want none and no error", nonce, err) + } +}