c726a6a9e2
---ci---
project: orca
phase: 5
milestone: v0.12
status: execute
---/ci---
internal/webauthn/store.go: SQLite credential store (0600, public
keys only). Put/Get/List/Delete/UpdateSignCount.
internal/webauthn/connector.go: WebAuthn ceremony handler for the
bundled Dex. BeginRegistration/FinishRegistration/BeginLogin/FinishLogin
at /orca/webauthn/{register,login}. go-webauthn library for crypto.
RP ID = cluster Traefik domain (C-38). Public-key credentials only
(private key never leaves authenticator; R-021 invariant holds).
9 tests pass (4 store + 5 connector). go vet clean. Full build green.
103 lines
3.1 KiB
Go
103 lines
3.1 KiB
Go
package webauthn
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestConnectorBuild verifies a Connector can be built with a valid
|
|
// RP ID + origin (C-38).
|
|
func TestConnectorBuild(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, err := NewStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
defer store.Close()
|
|
c, err := NewConnector(store, "cluster.example.com", "https://cluster.example.com")
|
|
if err != nil {
|
|
t.Fatalf("NewConnector: %v", err)
|
|
}
|
|
if c.RPID() != "cluster.example.com" {
|
|
t.Errorf("RPID = %q, want cluster.example.com", c.RPID())
|
|
}
|
|
}
|
|
|
|
// TestConnectorHealthz verifies the /orca/webauthn/healthz endpoint
|
|
// responds with the RP ID.
|
|
func TestConnectorHealthz(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
c, _ := NewConnector(store, "test.cluster", "https://test.cluster")
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("GET", "/orca/webauthn/healthz", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("healthz status = %d, want 200", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "test.cluster") {
|
|
t.Errorf("healthz body should contain rp_id: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestConnectorBeginRegistrationNoUsername verifies the register
|
|
// endpoint rejects requests without a username.
|
|
func TestConnectorBeginRegistrationNoUsername(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
c, _ := NewConnector(store, "test.cluster", "https://test.cluster")
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("GET", "/orca/webauthn/register", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("register without username: %d, want 400", rec.Code)
|
|
}
|
|
}
|
|
|
|
// TestConnectorBeginLoginNotRegistered verifies login for an
|
|
// unregistered user returns 404.
|
|
func TestConnectorBeginLoginNotRegistered(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
c, _ := NewConnector(store, "test.cluster", "https://test.cluster")
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("GET", "/orca/webauthn/login?username=ghost", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("login unregistered: %d, want 404", rec.Code)
|
|
}
|
|
}
|
|
|
|
// TestStoreModeEnforced verifies the DB file is 0600 after creation.
|
|
func TestStoreModeEnforced(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, err := NewStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
defer store.Close()
|
|
// Trigger a write so the DB file is created on disk.
|
|
store.PutCredential(&Credential{
|
|
UserID: "u",
|
|
CredentialID: []byte("c"),
|
|
PublicKey: []byte("p"),
|
|
})
|
|
info, err := os.Stat(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("stat db: %v", err)
|
|
}
|
|
if info.Mode().Perm()&0o077 != 0 {
|
|
t.Errorf("db mode = %o, want 0600", info.Mode().Perm())
|
|
}
|
|
}
|