This commit is contained in:
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
# Changelog
|
# 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
|
## v0.1.0-preview.14 — 2026-09-03
|
||||||
|
|
||||||
- Reject header-only, truncated, and structurally invalid PDF uploads in the
|
- 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
|
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.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.
|
> 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.13
|
go get gamertan.com/web@v0.1.0-preview.15
|
||||||
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.13
|
go get gamertan.com/web/requestmeta@v0.1.0-preview.15
|
||||||
```
|
```
|
||||||
|
|
||||||
The version belongs to the `gamertan.com/web` module. See the
|
The version belongs to the `gamertan.com/web` module. See the
|
||||||
|
|||||||
+22
-3
@@ -12,8 +12,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,6 +41,11 @@ type Config struct {
|
|||||||
RPID string
|
RPID string
|
||||||
RPDisplayName string
|
RPDisplayName string
|
||||||
Origin 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
|
EnrollmentLifetime time.Duration
|
||||||
RegistrationTTL time.Duration
|
RegistrationTTL time.Duration
|
||||||
LoginTTL time.Duration
|
LoginTTL time.Duration
|
||||||
@@ -66,7 +73,7 @@ func New(repository Repository, authService *auth.Service, config Config) (*Serv
|
|||||||
if repository == nil || authService == nil {
|
if repository == nil || authService == nil {
|
||||||
return nil, errors.New("authwebauthn: repository and auth service are required")
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(config.RPDisplayName) == "" || len(config.RPDisplayName) > 80 {
|
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))
|
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) == "" {
|
if strings.TrimSpace(rpID) == "" || strings.TrimSpace(rawOrigin) == "" {
|
||||||
return errors.New("authwebauthn: relying-party ID and origin are required")
|
return errors.New("authwebauthn: relying-party ID and origin are required")
|
||||||
}
|
}
|
||||||
origin, err := url.Parse(rawOrigin)
|
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 errors.New("authwebauthn: origin must be the exact HTTPS relying-party origin")
|
||||||
}
|
}
|
||||||
return nil
|
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: "http://tend.gamertan.com"},
|
||||||
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://other.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: "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 {
|
if _, err = authwebauthn.New(store, authService, config); err == nil {
|
||||||
t.Fatalf("accepted config=%+v", config)
|
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{}})
|
service, err := authwebauthn.New(store, authService, authwebauthn.Config{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://tend.gamertan.com", Random: failingReader{}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -49,3 +49,8 @@ application concern belongs in the shared module.
|
|||||||
passkey and replacement code digests. `authrecovery.BeginPasskey` and
|
passkey and replacement code digests. `authrecovery.BeginPasskey` and
|
||||||
`FinishPasskey` now provide that boundary without creating an authenticated
|
`FinishPasskey` now provide that boundary without creating an authenticated
|
||||||
session; Gamertan keeps the raw grant only in a short-lived HttpOnly cookie.
|
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:
|
module checksum:
|
||||||
|
|
||||||
```bash
|
```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
|
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.13
|
go get gamertan.com/web/requestmeta@v0.1.0-preview.15
|
||||||
```
|
```
|
||||||
|
|
||||||
Only imported packages are compiled and linked. The packages nevertheless
|
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
|
## Fixed security policy
|
||||||
|
|
||||||
- Use an exact HTTPS origin whose hostname equals the relying-party ID.
|
- 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.
|
- Reject cross-origin ceremonies.
|
||||||
- Require discoverable credentials and user verification.
|
- Require discoverable credentials and user verification.
|
||||||
- Request no attestation conveyance.
|
- Request no attestation conveyance.
|
||||||
|
|||||||
Reference in New Issue
Block a user