owpengram-server/internal/webauthn/webauthn_test.go

175 lines
6.1 KiB
Go

package webauthn_test
import (
"bytes"
"crypto/rand"
"errors"
"testing"
"telesrv/internal/webauthn"
"telesrv/internal/webauthn/webauthntest"
)
const (
testRPID = "telesrv.test"
testOrigin = "android:apk-key-hash:Zm9vYmFy"
)
func randChallenge(t *testing.T) []byte {
t.Helper()
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
t.Fatalf("rand: %v", err)
}
return b
}
func registerAuth(t *testing.T) (*webauthntest.Authenticator, *webauthn.Credential) {
t.Helper()
auth, err := webauthntest.New()
if err != nil {
t.Fatalf("new authenticator: %v", err)
}
ch := randChallenge(t)
cd, att, err := auth.Register(testRPID, testOrigin, ch)
if err != nil {
t.Fatalf("register: %v", err)
}
cred, err := webauthn.VerifyRegistration(webauthn.VerifyRegistrationInput{
ClientDataJSON: cd, AttestationObject: att, RPID: testRPID, Challenge: ch,
})
if err != nil {
t.Fatalf("VerifyRegistration: %v", err)
}
if !bytes.Equal(cred.ID, auth.CredentialID()) {
t.Fatalf("credential id mismatch: got %x want %x", cred.ID, auth.CredentialID())
}
if len(cred.COSEPublicKey) == 0 {
t.Fatal("empty COSE public key")
}
return auth, cred
}
func TestRegisterThenAssert(t *testing.T) {
auth, cred := registerAuth(t)
ch := randChallenge(t)
cd, ad, sig, err := auth.Assert(testRPID, testOrigin, ch)
if err != nil {
t.Fatalf("assert: %v", err)
}
count, err := webauthn.VerifyAssertion(webauthn.VerifyAssertionInput{
COSEPublicKey: cred.COSEPublicKey, ClientDataJSON: cd, AuthenticatorData: ad, Signature: sig,
RPID: testRPID, Challenge: ch, StoredSignCount: cred.SignCount,
})
if err != nil {
t.Fatalf("VerifyAssertion: %v", err)
}
if count != 1 {
t.Fatalf("sign count = %d, want 1", count)
}
}
func TestAssertCounterRegressionRejected(t *testing.T) {
auth, cred := registerAuth(t)
ch := randChallenge(t)
cd, ad, sig, _ := auth.Assert(testRPID, testOrigin, ch) // signCount -> 1
if _, err := webauthn.VerifyAssertion(webauthn.VerifyAssertionInput{
COSEPublicKey: cred.COSEPublicKey, ClientDataJSON: cd, AuthenticatorData: ad, Signature: sig,
RPID: testRPID, Challenge: ch, StoredSignCount: 5, // 已存计数器更大 => 克隆嫌疑
}); !errors.Is(err, webauthn.ErrCounterRegressed) {
t.Fatalf("err = %v, want ErrCounterRegressed", err)
}
}
func TestAssertChallengeMismatch(t *testing.T) {
auth, cred := registerAuth(t)
ch := randChallenge(t)
cd, ad, sig, _ := auth.Assert(testRPID, testOrigin, ch)
if _, err := webauthn.VerifyAssertion(webauthn.VerifyAssertionInput{
COSEPublicKey: cred.COSEPublicKey, ClientDataJSON: cd, AuthenticatorData: ad, Signature: sig,
RPID: testRPID, Challenge: randChallenge(t), StoredSignCount: cred.SignCount,
}); !errors.Is(err, webauthn.ErrChallengeMismatch) {
t.Fatalf("err = %v, want ErrChallengeMismatch", err)
}
}
func TestAssertSignatureTamperRejected(t *testing.T) {
auth, cred := registerAuth(t)
ch := randChallenge(t)
cd, ad, sig, _ := auth.Assert(testRPID, testOrigin, ch)
sig[len(sig)-1] ^= 0xFF
if _, err := webauthn.VerifyAssertion(webauthn.VerifyAssertionInput{
COSEPublicKey: cred.COSEPublicKey, ClientDataJSON: cd, AuthenticatorData: ad, Signature: sig,
RPID: testRPID, Challenge: ch, StoredSignCount: cred.SignCount,
}); !errors.Is(err, webauthn.ErrSignatureInvalid) {
t.Fatalf("err = %v, want ErrSignatureInvalid", err)
}
}
func TestAssertWrongRPIDRejected(t *testing.T) {
auth, cred := registerAuth(t)
ch := randChallenge(t)
cd, ad, sig, _ := auth.Assert("other.rp", testOrigin, ch)
if _, err := webauthn.VerifyAssertion(webauthn.VerifyAssertionInput{
COSEPublicKey: cred.COSEPublicKey, ClientDataJSON: cd, AuthenticatorData: ad, Signature: sig,
RPID: testRPID, Challenge: ch, StoredSignCount: cred.SignCount,
}); !errors.Is(err, webauthn.ErrRPIDMismatch) {
t.Fatalf("err = %v, want ErrRPIDMismatch", err)
}
}
func TestRegistrationWrongRPIDRejected(t *testing.T) {
auth, _ := func() (*webauthntest.Authenticator, *webauthn.Credential) {
a, err := webauthntest.New()
if err != nil {
t.Fatalf("new: %v", err)
}
return a, nil
}()
ch := randChallenge(t)
cd, att, _ := auth.Register("attacker.rp", testOrigin, ch)
if _, err := webauthn.VerifyRegistration(webauthn.VerifyRegistrationInput{
ClientDataJSON: cd, AttestationObject: att, RPID: testRPID, Challenge: ch,
}); !errors.Is(err, webauthn.ErrRPIDMismatch) {
t.Fatalf("err = %v, want ErrRPIDMismatch", err)
}
}
func TestOriginEnforcement(t *testing.T) {
auth, cred := registerAuth(t)
ch := randChallenge(t)
cd, ad, sig, _ := auth.Assert(testRPID, testOrigin, ch)
// 允许列表匹配 => 通过。
if _, err := webauthn.VerifyAssertion(webauthn.VerifyAssertionInput{
COSEPublicKey: cred.COSEPublicKey, ClientDataJSON: cd, AuthenticatorData: ad, Signature: sig,
RPID: testRPID, Challenge: ch, AllowedOrigins: []string{testOrigin}, StoredSignCount: cred.SignCount,
}); err != nil {
t.Fatalf("matching origin should pass: %v", err)
}
// 允许列表不含该 origin => 拒绝。
if _, err := webauthn.VerifyAssertion(webauthn.VerifyAssertionInput{
COSEPublicKey: cred.COSEPublicKey, ClientDataJSON: cd, AuthenticatorData: ad, Signature: sig,
RPID: testRPID, Challenge: ch, AllowedOrigins: []string{"android:apk-key-hash:other"}, StoredSignCount: cred.SignCount,
}); !errors.Is(err, webauthn.ErrOriginNotAllowed) {
t.Fatalf("err = %v, want ErrOriginNotAllowed", err)
}
}
func TestBuildOptionsContainPublicKey(t *testing.T) {
reg, err := webauthn.BuildRegistrationOptions(webauthn.RegistrationParams{
RPID: testRPID, UserID: []byte("2:123"), UserName: "user", Challenge: randChallenge(t),
})
if err != nil {
t.Fatalf("BuildRegistrationOptions: %v", err)
}
if !bytes.Contains(reg, []byte(`"publicKey"`)) || !bytes.Contains(reg, []byte(`"challenge"`)) {
t.Fatalf("registration options missing publicKey/challenge: %s", reg)
}
login, err := webauthn.BuildLoginOptions(webauthn.LoginParams{RPID: testRPID, Challenge: randChallenge(t)})
if err != nil {
t.Fatalf("BuildLoginOptions: %v", err)
}
if !bytes.Contains(login, []byte(`"publicKey"`)) || !bytes.Contains(login, []byte(`"rpId"`)) {
t.Fatalf("login options missing publicKey/rpId: %s", login)
}
}