This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user