This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## v0.1.0-preview.15 — 2026-09-03
|
||||
|
||||
- Permit applications to opt into an exact non-default HTTPS WebAuthn origin
|
||||
port for `localhost` and reserved `.test` relying-party IDs. The configured
|
||||
origin remains exact, production origins remain portless by default, and
|
||||
malformed, default, non-canonical, zero, or out-of-range ports fail closed.
|
||||
- Record the Gamertan local-Caddy dogfood pressure that required this explicit
|
||||
development boundary without weakening cross-origin ceremony rejection.
|
||||
|
||||
## v0.1.0-preview.14 — 2026-09-03
|
||||
|
||||
- Reject header-only, truncated, and structurally invalid PDF uploads in the
|
||||
|
||||
@@ -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.13`. APIs may change before a stable
|
||||
> **Public preview:** `v0.1.0-preview.15`. 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.13
|
||||
go get gamertan.com/web@v0.1.0-preview.15
|
||||
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.13
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.15
|
||||
```
|
||||
|
||||
The version belongs to the `gamertan.com/web` module. See the
|
||||
|
||||
+22
-3
@@ -12,8 +12,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -39,6 +41,11 @@ type Config struct {
|
||||
RPID string
|
||||
RPDisplayName string
|
||||
Origin string
|
||||
// AllowDevelopmentPort permits an explicit non-default HTTPS port only
|
||||
// for localhost or a reserved .test relying-party ID. Production origins
|
||||
// remain portless, while local applications can terminate trusted HTTPS
|
||||
// without requiring a privileged listener.
|
||||
AllowDevelopmentPort bool
|
||||
EnrollmentLifetime time.Duration
|
||||
RegistrationTTL time.Duration
|
||||
LoginTTL time.Duration
|
||||
@@ -66,7 +73,7 @@ func New(repository Repository, authService *auth.Service, config Config) (*Serv
|
||||
if repository == nil || authService == nil {
|
||||
return nil, errors.New("authwebauthn: repository and auth service are required")
|
||||
}
|
||||
if err := validateOrigin(config.RPID, config.Origin); err != nil {
|
||||
if err := validateOrigin(config.RPID, config.Origin, config.AllowDevelopmentPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(config.RPDisplayName) == "" || len(config.RPDisplayName) > 80 {
|
||||
@@ -725,12 +732,24 @@ func passwordMigrationBinding(userID string) [32]byte {
|
||||
return BindingDigest([]byte("gamertan-web/password-to-passkey/v1\x00" + userID))
|
||||
}
|
||||
|
||||
func validateOrigin(rpID, rawOrigin string) error {
|
||||
func validateOrigin(rpID, rawOrigin string, allowDevelopmentPort bool) error {
|
||||
if strings.TrimSpace(rpID) == "" || strings.TrimSpace(rawOrigin) == "" {
|
||||
return errors.New("authwebauthn: relying-party ID and origin are required")
|
||||
}
|
||||
origin, err := url.Parse(rawOrigin)
|
||||
if err != nil || origin.Scheme != "https" || origin.Hostname() != rpID || origin.Port() != "" || origin.User != nil || origin.Path != "" || origin.RawQuery != "" || origin.Fragment != "" {
|
||||
if err != nil || origin.Scheme != "https" || origin.Hostname() != rpID || origin.User != nil || origin.Path != "" || origin.RawQuery != "" || origin.Fragment != "" {
|
||||
return errors.New("authwebauthn: origin must be the exact HTTPS relying-party origin")
|
||||
}
|
||||
port := origin.Port()
|
||||
if port == "" {
|
||||
if origin.Host != rpID {
|
||||
return errors.New("authwebauthn: origin must be the exact HTTPS relying-party origin")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
developmentRP := rpID == "localhost" || strings.HasSuffix(rpID, ".test")
|
||||
value, portErr := strconv.ParseUint(port, 10, 16)
|
||||
if !allowDevelopmentPort || !developmentRP || portErr != nil || value == 0 || value == 443 || strconv.FormatUint(value, 10) != port || origin.Host != net.JoinHostPort(rpID, port) {
|
||||
return errors.New("authwebauthn: origin must be the exact HTTPS relying-party origin")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -231,11 +231,25 @@ func TestConfigurationAndEntropyFailures(t *testing.T) {
|
||||
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "http://tend.gamertan.com"},
|
||||
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://other.gamertan.com"},
|
||||
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://tend.gamertan.com/path"},
|
||||
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:8443"},
|
||||
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://tend.gamertan.com:8443", AllowDevelopmentPort: true},
|
||||
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:443", AllowDevelopmentPort: true},
|
||||
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:08443", AllowDevelopmentPort: true},
|
||||
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:0", AllowDevelopmentPort: true},
|
||||
} {
|
||||
if _, err = authwebauthn.New(store, authService, config); err == nil {
|
||||
t.Fatalf("accepted config=%+v", config)
|
||||
}
|
||||
}
|
||||
for _, config := range []authwebauthn.Config{
|
||||
{RPID: "localhost", RPDisplayName: "Tend Local", Origin: "https://localhost:8443", AllowDevelopmentPort: true},
|
||||
{RPID: "tend.test", RPDisplayName: "Tend Local", Origin: "https://tend.test:8443", AllowDevelopmentPort: true},
|
||||
} {
|
||||
configured, configureErr := authwebauthn.New(store, authService, config)
|
||||
if configureErr != nil || configured == nil {
|
||||
t.Fatalf("development config=%+v service=%v err=%v", config, configured, configureErr)
|
||||
}
|
||||
}
|
||||
service, err := authwebauthn.New(store, authService, authwebauthn.Config{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://tend.gamertan.com", Random: failingReader{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -49,3 +49,8 @@ application concern belongs in the shared module.
|
||||
passkey and replacement code digests. `authrecovery.BeginPasskey` and
|
||||
`FinishPasskey` now provide that boundary without creating an authenticated
|
||||
session; Gamertan keeps the raw grant only in a short-lived HttpOnly cookie.
|
||||
- A portless-only WebAuthn origin rule made an unprivileged local HTTPS
|
||||
exercise impossible even though WebAuthn origins include ports. The passkey
|
||||
service now permits an explicit development port only when applications opt
|
||||
in and the RP ID is `localhost` or reserved `.test`; production origins keep
|
||||
the original portless default.
|
||||
|
||||
@@ -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.13
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.15
|
||||
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.13
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.15
|
||||
```
|
||||
|
||||
Only imported packages are compiled and linked. The packages nevertheless
|
||||
|
||||
@@ -9,6 +9,11 @@ authorization decisions, session cookie, HTML, and local recovery command.
|
||||
## Fixed security policy
|
||||
|
||||
- Use an exact HTTPS origin whose hostname equals the relying-party ID.
|
||||
- Keep production origins portless. For local development only,
|
||||
`AllowDevelopmentPort` permits one explicit non-default port when the RP ID
|
||||
is exactly `localhost` or beneath the reserved `.test` top-level domain. The
|
||||
configured origin, browser `Origin`, and WebAuthn verifier origin must still
|
||||
match exactly.
|
||||
- Reject cross-origin ceremonies.
|
||||
- Require discoverable credentials and user verification.
|
||||
- Request no attestation conveyance.
|
||||
|
||||
Reference in New Issue
Block a user