package webauthn import ( "bytes" "encoding/base64" "encoding/json" "errors" "testing" ) // The salt is the identity of every wrapped key file ever written. If it // changes, every deployment's blob becomes unopenable, so it is pinned here. func TestPRFSaltIsStable(t *testing.T) { salt := PRFSalt() if len(salt) != 32 { t.Fatalf("salt is %d bytes, want 32", len(salt)) } if got := base64.RawURLEncoding.EncodeToString(salt); got != base64.RawURLEncoding.EncodeToString(PRFSalt()) { t.Fatal("PRFSalt is not deterministic") } // Mutating the returned slice must not affect the next caller. salt[0] ^= 0xff if bytes.Equal(salt, PRFSalt()) { t.Fatal("PRFSalt returned shared backing state") } } func TestDecodePRFResult(t *testing.T) { raw := make([]byte, 32) for i := range raw { raw[i] = byte(i + 1) } for _, enc := range []string{ base64.RawURLEncoding.EncodeToString(raw), base64.URLEncoding.EncodeToString(raw), } { got, err := DecodePRFResult(enc) if err != nil { t.Fatalf("DecodePRFResult(%q): %v", enc, err) } if !bytes.Equal(got, raw) { t.Errorf("decoded %x, want %x", got, raw) } } } // No PRF must be a distinguishable, named failure — never a silent fallback to // some other secret. func TestDecodePRFResultNoPRF(t *testing.T) { if _, err := DecodePRFResult(""); !errors.Is(err, ErrNoPRF) { t.Fatalf("err = %v, want ErrNoPRF", err) } } func TestDecodePRFResultRejectsUnusable(t *testing.T) { zeros := base64.RawURLEncoding.EncodeToString(make([]byte, 32)) short := base64.RawURLEncoding.EncodeToString(make([]byte, 16)) long := base64.RawURLEncoding.EncodeToString(make([]byte, 64)) for name, in := range map[string]string{ "not base64": "!!!!", "all zero": zeros, "too short": short, "too long": long, } { t.Run(name, func(t *testing.T) { if _, err := DecodePRFResult(in); err == nil { t.Fatalf("accepted a %s PRF result", name) } }) } } // Both option builders must ask for PRF, or the browser never produces a // secret and cold-start unlock silently never works. func TestOptionsRequestPRF(t *testing.T) { rp := NewRP(Config{Origin: "http://localhost:8080", RPID: "localhost", RPName: "maven"}) create, _, err := rp.CreationOptions([]byte("u"), "u") if err != nil { t.Fatalf("CreationOptions: %v", err) } if _, ok := extPRF(t, create)["prf"]; !ok { t.Error("creation options do not request the prf extension") } assert, _, err := rp.AssertionOptions() if err != nil { t.Fatalf("AssertionOptions: %v", err) } prf, ok := extPRF(t, assert)["prf"].(map[string]any) if !ok { t.Fatal("assertion options do not request the prf extension") } eval, _ := prf["eval"].(map[string]any) first, _ := eval["first"].(string) if first != base64.RawURLEncoding.EncodeToString(PRFSalt()) { t.Errorf("prf.eval.first = %q, want the fixed salt", first) } } func extPRF(t *testing.T, opts any) map[string]any { t.Helper() b, err := json.Marshal(opts) if err != nil { t.Fatalf("marshal options: %v", err) } var m struct { Extensions map[string]any `json:"extensions"` } if err := json.Unmarshal(b, &m); err != nil { t.Fatalf("unmarshal options: %v", err) } return m.Extensions }