This commit is contained in:
@@ -2,6 +2,17 @@
|
||||
|
||||
# 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
|
||||
|
||||
- Extend the direct-owner transaction boundary to invitations. Creating or
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
## Why Web Foundations?
|
||||
@@ -57,14 +57,14 @@ owns—and, just as importantly, what remains application policy.
|
||||
Pin the preview in an application module:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web@v0.1.0-preview.20
|
||||
go get gamertan.com/web@v0.1.0-preview.21
|
||||
go mod verify
|
||||
```
|
||||
|
||||
An application may name the first package it intends to adopt:
|
||||
|
||||
```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
|
||||
|
||||
@@ -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
@@ -375,7 +375,7 @@ func (service *Service) finishRegistrationCeremony(ctx context.Context, ceremony
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
encoded, err := json.Marshal(verified)
|
||||
@@ -407,6 +407,26 @@ func (service *Service) finishRegistrationCeremony(ctx context.Context, ceremony
|
||||
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) {
|
||||
challenge, err := service.randomBytes(32)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,3 +85,11 @@ application concern belongs in the shared module.
|
||||
role into invitation mutations and rechecks a current active direct Owner
|
||||
after acquiring the SQLite write lock. The application still owns fresh
|
||||
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.
|
||||
|
||||
@@ -26,7 +26,7 @@ The packages are ordinary Go imports. Pin the current preview and verify its
|
||||
module checksum:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import "gamertan.com/web/requestmeta"
|
||||
and request the containing module at an exact version:
|
||||
|
||||
```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
|
||||
|
||||
@@ -19,6 +19,10 @@ authorization decisions, session cookie, HTML, and local recovery command.
|
||||
- Request no attestation conveyance.
|
||||
- Permit ES256 only until another algorithm has explicit interoperability and
|
||||
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,
|
||||
single-use ceremony tokens.
|
||||
- Treat clone warnings as audit signals rather than automatic lockout for
|
||||
|
||||
Reference in New Issue
Block a user