Verify passkey algorithms from COSE keys
verify / verify (push) Successful in 3m38s

This commit is contained in:
2026-09-04 12:19:15 -04:00
parent 3fe1547a5b
commit b1710e08b8
8 changed files with 114 additions and 6 deletions
+11
View File
@@ -2,6 +2,17 @@
# Changelog # Changelog
## v0.1.0-preview.21 — 2026-09-04
- Derive the registered credential algorithm from the verified COSE public key
embedded in authenticator data instead of the optional browser
`publicKeyAlgorithm` convenience member.
- Preserve the ES256-only policy while accepting standards-compliant response
serializers that omit redundant response conveniences, including the
Bitwarden/Vaultwarden passkey flow exercised through Gamertan.
- Add regression coverage for an ES256 credential whose convenience algorithm
is absent, plus malformed and non-ES256 credential rejection.
## v0.1.0-preview.20 — 2026-09-04 ## v0.1.0-preview.20 — 2026-09-04
- Extend the direct-owner transaction boundary to invitations. Creating or - Extend the direct-owner transaction boundary to invitations. Creating or
+3 -3
View File
@@ -17,7 +17,7 @@ router, handlers, HTML, authorization decisions, cache behavior, and
deployment. Adopt one boundary at a time; Go compiles and links only the deployment. Adopt one boundary at a time; Go compiles and links only the
packages you import. packages you import.
> **Public preview:** `v0.1.0-preview.20`. APIs may change before a stable > **Public preview:** `v0.1.0-preview.21`. APIs may change before a stable
> release. Linux is the maintained release platform. > release. Linux is the maintained release platform.
## Why Web Foundations? ## Why Web Foundations?
@@ -57,14 +57,14 @@ owns—and, just as importantly, what remains application policy.
Pin the preview in an application module: Pin the preview in an application module:
```bash ```bash
go get gamertan.com/web@v0.1.0-preview.20 go get gamertan.com/web@v0.1.0-preview.21
go mod verify go mod verify
``` ```
An application may name the first package it intends to adopt: An application may name the first package it intends to adopt:
```bash ```bash
go get gamertan.com/web/requestmeta@v0.1.0-preview.20 go get gamertan.com/web/requestmeta@v0.1.0-preview.21
``` ```
The version belongs to the `gamertan.com/web` module. See the The version belongs to the `gamertan.com/web` module. See the
+65
View File
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MPL-2.0
package authwebauthn
import (
"crypto/ecdh"
"crypto/rand"
"errors"
"testing"
"gamertan.com/web/internal/webauthnvendored/protocol/webauthncbor"
"gamertan.com/web/internal/webauthnvendored/protocol/webauthncose"
wa "gamertan.com/web/internal/webauthnvendored/webauthn"
)
func TestEnforceCredentialAlgorithmUsesVerifiedCOSEKey(t *testing.T) {
privateKey, err := ecdh.P256().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
publicKey := privateKey.PublicKey().Bytes()
encoded, err := webauthncbor.Marshal(map[int64]any{
1: int64(webauthncose.EllipticKey),
3: int64(webauthncose.AlgES256),
-1: int64(webauthncose.P256),
-2: publicKey[1:33],
-3: publicKey[33:65],
})
if err != nil {
t.Fatal(err)
}
credential := &wa.Credential{
PublicKey: encoded,
// This value is absent when a standards-compliant client serializes the
// mandatory attestation object without optional response conveniences.
Attestation: wa.CredentialAttestation{PublicKeyAlgorithm: 0},
}
if err = enforceCredentialAlgorithm(credential); err != nil {
t.Fatalf("verified ES256 COSE key rejected when convenience value was absent: %v", err)
}
}
func TestEnforceCredentialAlgorithmRejectsOtherOrInvalidKeys(t *testing.T) {
rsaKey, err := webauthncbor.Marshal(map[int64]any{
1: int64(webauthncose.RSAKey),
3: int64(webauthncose.AlgRS256),
-1: []byte{0xff},
-2: []byte{0x01, 0x00, 0x01},
})
if err != nil {
t.Fatal(err)
}
for name, credential := range map[string]*wa.Credential{
"nil": nil,
"malformed": {PublicKey: []byte("not-cose")},
"rsa": {PublicKey: rsaKey},
} {
t.Run(name, func(t *testing.T) {
if err := enforceCredentialAlgorithm(credential); !errors.Is(err, ErrUnsupportedCredential) {
t.Fatalf("error=%v", err)
}
})
}
}
+21 -1
View File
@@ -375,7 +375,7 @@ func (service *Service) finishRegistrationCeremony(ctx context.Context, ceremony
if err != nil { if err != nil {
return Credential{}, fmt.Errorf("authwebauthn: verify registration: %w", err) return Credential{}, fmt.Errorf("authwebauthn: verify registration: %w", err)
} }
if verified.Attestation.PublicKeyAlgorithm != int64(webauthncose.AlgES256) { if err = enforceCredentialAlgorithm(verified); err != nil {
return Credential{}, ErrUnsupportedCredential return Credential{}, ErrUnsupportedCredential
} }
encoded, err := json.Marshal(verified) encoded, err := json.Marshal(verified)
@@ -407,6 +407,26 @@ func (service *Service) finishRegistrationCeremony(ctx context.Context, ceremony
return record, nil return record, nil
} }
// enforceCredentialAlgorithm derives the algorithm from the verified COSE key
// carried inside authenticator data. AuthenticatorAttestationResponse's
// publicKeyAlgorithm member is an optional browser convenience value: clients
// that serialize the mandatory attestation object directly may omit it, and it
// is not the cryptographically authoritative representation.
func enforceCredentialAlgorithm(credential *wa.Credential) error {
if credential == nil {
return ErrUnsupportedCredential
}
parsed, err := webauthncose.ParsePublicKey(credential.PublicKey)
if err != nil {
return ErrUnsupportedCredential
}
key, ok := parsed.(webauthncose.EC2PublicKeyData)
if !ok || key.Algorithm != int64(webauthncose.AlgES256) {
return ErrUnsupportedCredential
}
return nil
}
func (service *Service) BeginLogin(ctx context.Context) (BeginResult, error) { func (service *Service) BeginLogin(ctx context.Context) (BeginResult, error) {
challenge, err := service.randomBytes(32) challenge, err := service.randomBytes(32)
if err != nil { if err != nil {
+8
View File
@@ -85,3 +85,11 @@ application concern belongs in the shared module.
role into invitation mutations and rechecks a current active direct Owner role into invitation mutations and rechecks a current active direct Owner
after acquiring the SQLite write lock. The application still owns fresh after acquiring the SQLite write lock. The application still owns fresh
authentication, recipient delivery, and the one-time secret presentation. authentication, recipient delivery, and the one-time secret presentation.
- A real Bitwarden/Vaultwarden owner enrollment reached successful WebAuthn
verification but was rejected by a redundant algorithm check because the
application's direct response serializer omitted the optional browser
`publicKeyAlgorithm` convenience member. Preview 21 keeps ES256-only policy
enforcement but derives it from the verified COSE key embedded in
authenticator data. This makes the server independent of serializer-specific
convenience fields without weakening origin, challenge, user-verification,
or algorithm validation.
+1 -1
View File
@@ -26,7 +26,7 @@ The packages are ordinary Go imports. Pin the current preview and verify its
module checksum: module checksum:
```bash ```bash
go get gamertan.com/web/requestmeta@v0.1.0-preview.20 go get gamertan.com/web/requestmeta@v0.1.0-preview.21
go mod verify go mod verify
``` ```
+1 -1
View File
@@ -18,7 +18,7 @@ import "gamertan.com/web/requestmeta"
and request the containing module at an exact version: and request the containing module at an exact version:
```bash ```bash
go get gamertan.com/web/requestmeta@v0.1.0-preview.20 go get gamertan.com/web/requestmeta@v0.1.0-preview.21
``` ```
Only imported packages are compiled and linked. The packages nevertheless Only imported packages are compiled and linked. The packages nevertheless
+4
View File
@@ -19,6 +19,10 @@ authorization decisions, session cookie, HTML, and local recovery command.
- Request no attestation conveyance. - Request no attestation conveyance.
- Permit ES256 only until another algorithm has explicit interoperability and - Permit ES256 only until another algorithm has explicit interoperability and
security evidence. security evidence.
- Enforce that policy from the verified COSE public key embedded in
authenticator data. Do not rely on the optional browser
`publicKeyAlgorithm` convenience member: direct standards-compliant response
serializers may omit it even when the attested credential is ES256.
- Store random challenges and verifier session data only behind opaque, - Store random challenges and verifier session data only behind opaque,
single-use ceremony tokens. single-use ceremony tokens.
- Treat clone warnings as audit signals rather than automatic lockout for - Treat clone warnings as audit signals rather than automatic lockout for