package webauthn import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "testing" ) func TestVerify_HappyPath(t *testing.T) { pub, priv, cred, store := setupCredential(t, "alice") _ = pub challenge := []byte("challenge-bytes-just-for-the-test") a := makeAssertion(t, priv, cred.ID, "trust.basicstack.de", "https://trust.basicstack.de", challenge) cfg := VerifyConfig{RPID: "trust.basicstack.de", AllowedOrigins: []string{"https://trust.basicstack.de"}} got, err := Verify(cfg, store, a, challenge) if err != nil { t.Fatalf("Verify: %v", err) } if got.ID != cred.ID { t.Errorf("credential id mismatch") } } func TestVerify_WrongChallenge(t *testing.T) { _, priv, cred, store := setupCredential(t, "alice") challenge := []byte("original-challenge-bytes-xxxxxxxx") a := makeAssertion(t, priv, cred.ID, "trust.basicstack.de", "https://trust.basicstack.de", challenge) cfg := VerifyConfig{RPID: "trust.basicstack.de", AllowedOrigins: []string{"https://trust.basicstack.de"}} _, err := Verify(cfg, store, a, []byte("different-challenge-bytes-yyyyyy")) if !errors.Is(err, ErrChallengeMismatch) { t.Fatalf("want ErrChallengeMismatch, got %v", err) } } func TestVerify_UnknownCredential(t *testing.T) { store := NewStore() cfg := VerifyConfig{RPID: "trust.basicstack.de", AllowedOrigins: []string{"https://trust.basicstack.de"}} _, err := Verify(cfg, store, Assertion{CredentialID: "nope"}, []byte("c")) if !errors.Is(err, ErrCredentialUnknown) { t.Fatalf("want ErrCredentialUnknown, got %v", err) } } func TestVerify_WrongOrigin(t *testing.T) { _, priv, cred, store := setupCredential(t, "alice") challenge := []byte("challenge-xxx-xxx-xxx-xxx") a := makeAssertion(t, priv, cred.ID, "trust.basicstack.de", "https://evil.example", challenge) cfg := VerifyConfig{RPID: "trust.basicstack.de", AllowedOrigins: []string{"https://trust.basicstack.de"}} _, err := Verify(cfg, store, a, challenge) if err == nil || !errors.Is(err, ErrAssertionInvalid) { t.Fatalf("want ErrAssertionInvalid, got %v", err) } } func TestVerify_WrongType(t *testing.T) { _, priv, cred, store := setupCredential(t, "alice") challenge := []byte("challenge-xxx-xxx-xxx-xxx") a := makeAssertionWithType(t, priv, cred.ID, "trust.basicstack.de", "https://trust.basicstack.de", challenge, "webauthn.create") cfg := VerifyConfig{RPID: "trust.basicstack.de", AllowedOrigins: []string{"https://trust.basicstack.de"}} _, err := Verify(cfg, store, a, challenge) if !errors.Is(err, ErrAssertionInvalid) { t.Fatalf("want ErrAssertionInvalid, got %v", err) } } func TestVerify_BadSignature(t *testing.T) { _, priv, cred, store := setupCredential(t, "alice") challenge := []byte("challenge-xxx-xxx-xxx-xxx") a := makeAssertion(t, priv, cred.ID, "trust.basicstack.de", "https://trust.basicstack.de", challenge) // Tamper with the signature. sig, _ := base64.RawURLEncoding.DecodeString(a.Signature) sig[0] ^= 0xff a.Signature = base64.RawURLEncoding.EncodeToString(sig) cfg := VerifyConfig{RPID: "trust.basicstack.de", AllowedOrigins: []string{"https://trust.basicstack.de"}} _, err := Verify(cfg, store, a, challenge) if !errors.Is(err, ErrAssertionInvalid) { t.Fatalf("want ErrAssertionInvalid, got %v", err) } } // --- helpers -------------------------------------------------------------- func setupCredential(t *testing.T, userSub string) (ed25519.PublicKey, ed25519.PrivateKey, Credential, *Store) { t.Helper() pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("gen key: %v", err) } idBytes := make([]byte, 16) _, _ = rand.Read(idBytes) cred := Credential{ ID: base64.RawURLEncoding.EncodeToString(idBytes), UserSub: userSub, PublicKey: pub, } store := NewStore() if err := store.AddCredential(cred); err != nil { t.Fatalf("AddCredential: %v", err) } return pub, priv, cred, store } // makeAssertion signs a mock WebAuthn assertion with the given private // key over the standard authenticator_data || sha256(clientDataJSON). func makeAssertion(t *testing.T, priv ed25519.PrivateKey, credID, rpID, origin string, challenge []byte) Assertion { return makeAssertionWithType(t, priv, credID, rpID, origin, challenge, "webauthn.get") } func makeAssertionWithType(t *testing.T, priv ed25519.PrivateKey, credID, rpID, origin string, challenge []byte, typ string) Assertion { t.Helper() cd := map[string]string{ "type": typ, "challenge": base64.RawURLEncoding.EncodeToString(challenge), "origin": origin, } cdBytes, _ := json.Marshal(cd) authData := MakeAuthenticatorData(rpID) cdHash := sha256.Sum256(cdBytes) signed := append(append([]byte(nil), authData...), cdHash[:]...) sig := ed25519.Sign(priv, signed) return Assertion{ CredentialID: credID, ClientDataJSON: base64.RawURLEncoding.EncodeToString(cdBytes), AuthenticatorData: base64.RawURLEncoding.EncodeToString(authData), Signature: base64.RawURLEncoding.EncodeToString(sig), } }