chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,418 @@
// Package webauthn 实现 telesrv passkey 登录所需的 WebAuthn/FIDO2 服务端原语:
// 生成注册/断言挑战选项、验证注册 attestation、验证登录 assertion。
//
// 它只处理字节与 JSON,不依赖 tg/domain/store——便于独立单测(配套
// internal/webauthn/webauthntest 的软件 authenticator)。MTProto 场景下,客户端把
// WebAuthn 各字段 base64url 解码后塞进 TL,故这里直接收原始字节(clientDataJSON、
// authenticatorData、signature、attestationObject、credentialID)。
//
// 支持算法:ES256(ECDSA P-256,平台 passkey 主流)与 EdDSA(Ed25519)。
package webauthn
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math/big"
"github.com/fxamacker/cbor/v2"
)
// 验证错误。调用方据此映射到 TL 错误码/拒绝登录。
var (
ErrClientDataInvalid = errors.New("webauthn: client data invalid")
ErrChallengeMismatch = errors.New("webauthn: challenge mismatch")
ErrOriginNotAllowed = errors.New("webauthn: origin not allowed")
ErrRPIDMismatch = errors.New("webauthn: rp id hash mismatch")
ErrUserNotPresent = errors.New("webauthn: user-present flag not set")
ErrAuthDataInvalid = errors.New("webauthn: authenticator data invalid")
ErrAttestationInvalid = errors.New("webauthn: attestation object invalid")
ErrPublicKeyUnsupported = errors.New("webauthn: unsupported public key algorithm")
ErrSignatureInvalid = errors.New("webauthn: signature invalid")
ErrCounterRegressed = errors.New("webauthn: sign counter regressed (possible cloned authenticator)")
)
// authenticatorData 标志位(WebAuthn §6.1)。
const (
flagUserPresent = 0x01
flagUserVerified = 0x04
flagAttestedCredData = 0x40
flagExtensionData = 0x80
)
// COSE algorithm identifiers。
const (
coseAlgES256 = -7
coseAlgEdDSA = -8
)
// COSE key type / curve。
const (
coseKtyOKP = 1
coseKtyEC2 = 2
coseCrvP256 = 1
coseCrvEd25519 = 6
)
// b64 是 WebAuthn 用的 base64url 无填充编码。
var b64 = base64.RawURLEncoding
// decodeB64URL 容忍有/无填充的 base64url。
func decodeB64URL(s string) ([]byte, error) {
if out, err := base64.RawURLEncoding.DecodeString(s); err == nil {
return out, nil
}
return base64.URLEncoding.DecodeString(s)
}
// RegistrationParams 是构造 publicKey creation options 所需的输入。
type RegistrationParams struct {
RPID string
RPName string
UserID []byte // 通常是 "dcId:userId" 的字节
UserName string
UserDisplay string
Challenge []byte
TimeoutMillis int
ExcludeIDs [][]byte // 已有凭据 id,避免同一 authenticator 重复注册
}
// BuildRegistrationOptions 返回 account.passkeyRegistrationOptions 的 DataJSON 内容
// (顶层含 publicKey 字段,与 DrKLO PasskeysController 读取约定一致)。
func BuildRegistrationOptions(p RegistrationParams) ([]byte, error) {
if p.RPID == "" || len(p.Challenge) == 0 || len(p.UserID) == 0 {
return nil, errors.New("webauthn: incomplete registration params")
}
timeout := p.TimeoutMillis
if timeout == 0 {
timeout = 120000
}
exclude := make([]map[string]any, 0, len(p.ExcludeIDs))
for _, id := range p.ExcludeIDs {
exclude = append(exclude, map[string]any{"type": "public-key", "id": b64.EncodeToString(id)})
}
pub := map[string]any{
"rp": map[string]any{"id": p.RPID, "name": orDefault(p.RPName, "Telegram")},
"user": map[string]any{"id": b64.EncodeToString(p.UserID), "name": p.UserName, "displayName": orDefault(p.UserDisplay, p.UserName)},
"challenge": b64.EncodeToString(p.Challenge),
"pubKeyCredParams": []map[string]any{
{"type": "public-key", "alg": coseAlgES256},
{"type": "public-key", "alg": coseAlgEdDSA},
},
"timeout": timeout,
"attestation": "none",
"authenticatorSelection": map[string]any{
"residentKey": "required",
"userVerification": "preferred",
},
"excludeCredentials": exclude,
}
return json.Marshal(map[string]any{"publicKey": pub})
}
// LoginParams 是构造 publicKey request options(断言挑战)的输入。
type LoginParams struct {
RPID string
Challenge []byte
TimeoutMillis int
}
// BuildLoginOptions 返回 auth.passkeyLoginOptions 的 DataJSON 内容。无 allowCredentials
// (discoverable/usernameless 登录,用户由 user_handle 反查)。
func BuildLoginOptions(p LoginParams) ([]byte, error) {
if p.RPID == "" || len(p.Challenge) == 0 {
return nil, errors.New("webauthn: incomplete login params")
}
timeout := p.TimeoutMillis
if timeout == 0 {
timeout = 120000
}
pub := map[string]any{
"challenge": b64.EncodeToString(p.Challenge),
"timeout": timeout,
"rpId": p.RPID,
"userVerification": "preferred",
"allowCredentials": []any{},
}
return json.Marshal(map[string]any{"publicKey": pub})
}
// clientData 是 clientDataJSON 的解析结构。
type clientData struct {
Type string `json:"type"`
Challenge string `json:"challenge"`
Origin string `json:"origin"`
}
// verifyClientData 校验 type/challenge/origin。allowedOrigins 为空表示不强校验 origin
// (dev:服务端通常不预知 Android apk-key-hash origin)。
func verifyClientData(clientDataJSON []byte, wantType string, challenge []byte, allowedOrigins []string) error {
var cd clientData
if err := json.Unmarshal(clientDataJSON, &cd); err != nil {
return ErrClientDataInvalid
}
if cd.Type != wantType {
return fmt.Errorf("%w: type=%q want %q", ErrClientDataInvalid, cd.Type, wantType)
}
got, err := decodeB64URL(cd.Challenge)
if err != nil {
return ErrClientDataInvalid
}
if subtle.ConstantTimeCompare(got, challenge) != 1 {
return ErrChallengeMismatch
}
if len(allowedOrigins) > 0 {
ok := false
for _, o := range allowedOrigins {
if o == cd.Origin {
ok = true
break
}
}
if !ok {
return fmt.Errorf("%w: %q", ErrOriginNotAllowed, cd.Origin)
}
}
return nil
}
// ChallengeFromClientData 提取 clientDataJSON 里的 challenge 原始字节,供调用方按挑战
// 反查/消费一次性挑战 store(此时尚未验证,仅做查表键)。
func ChallengeFromClientData(clientDataJSON []byte) ([]byte, error) {
var cd clientData
if err := json.Unmarshal(clientDataJSON, &cd); err != nil {
return nil, ErrClientDataInvalid
}
b, err := decodeB64URL(cd.Challenge)
if err != nil || len(b) == 0 {
return nil, ErrClientDataInvalid
}
return b, nil
}
// parsedAuthData 是 authenticatorData 的解析结果。
type parsedAuthData struct {
rpIDHash []byte
flags byte
signCount uint32
aaguid []byte
credID []byte
credPubKey []byte // COSE 公钥原始字节(仅注册时存在)
}
func parseAuthData(authData []byte) (parsedAuthData, error) {
if len(authData) < 37 {
return parsedAuthData{}, ErrAuthDataInvalid
}
out := parsedAuthData{
rpIDHash: authData[0:32],
flags: authData[32],
signCount: binary.BigEndian.Uint32(authData[33:37]),
}
rest := authData[37:]
if out.flags&flagAttestedCredData != 0 {
if len(rest) < 18 {
return parsedAuthData{}, ErrAuthDataInvalid
}
out.aaguid = rest[0:16]
credIDLen := int(binary.BigEndian.Uint16(rest[16:18]))
rest = rest[18:]
if len(rest) < credIDLen {
return parsedAuthData{}, ErrAuthDataInvalid
}
out.credID = rest[:credIDLen]
rest = rest[credIDLen:]
// 之后是 COSE 公钥(变长 CBOR);用 UnmarshalFirst 只吃一个 CBOR item。
var raw cbor.RawMessage
remaining, err := cbor.UnmarshalFirst(rest, &raw)
if err != nil {
return parsedAuthData{}, ErrAuthDataInvalid
}
out.credPubKey = rest[:len(rest)-len(remaining)]
}
return out, nil
}
func rpIDHashFor(rpID string) []byte {
h := sha256.Sum256([]byte(rpID))
return h[:]
}
// Credential 是注册验证产出的凭据材料,调用方据此持久化。
type Credential struct {
ID []byte // credential id 原始字节
COSEPublicKey []byte // COSE 公钥原始字节
SignCount uint32
AAGUID []byte
}
// VerifyRegistrationInput 是注册验证输入。
type VerifyRegistrationInput struct {
ClientDataJSON []byte
AttestationObject []byte
RPID string
Challenge []byte
AllowedOrigins []string
}
type attestationObject struct {
Fmt string `cbor:"fmt"`
AuthData []byte `cbor:"authData"`
AttStmt cbor.RawMessage `cbor:"attStmt"`
}
// VerifyRegistration 校验 attestation 并提取凭据公钥。attestation 格式仅要求能取出
// 公钥(消费级 passkey 一般是 "none",不校验 attestation statement 签名)。
func VerifyRegistration(in VerifyRegistrationInput) (*Credential, error) {
if err := verifyClientData(in.ClientDataJSON, "webauthn.create", in.Challenge, in.AllowedOrigins); err != nil {
return nil, err
}
var att attestationObject
if err := cbor.Unmarshal(in.AttestationObject, &att); err != nil {
return nil, ErrAttestationInvalid
}
ad, err := parseAuthData(att.AuthData)
if err != nil {
return nil, err
}
if subtle.ConstantTimeCompare(ad.rpIDHash, rpIDHashFor(in.RPID)) != 1 {
return nil, ErrRPIDMismatch
}
if ad.flags&flagUserPresent == 0 {
return nil, ErrUserNotPresent
}
if ad.flags&flagAttestedCredData == 0 || len(ad.credID) == 0 || len(ad.credPubKey) == 0 {
return nil, ErrAttestationInvalid
}
// 公钥能解析即认为有效(none attestation)。
if _, err := parseCOSEKey(ad.credPubKey); err != nil {
return nil, err
}
return &Credential{
ID: append([]byte(nil), ad.credID...),
COSEPublicKey: append([]byte(nil), ad.credPubKey...),
SignCount: ad.signCount,
AAGUID: append([]byte(nil), ad.aaguid...),
}, nil
}
// VerifyAssertionInput 是登录断言验证输入。
type VerifyAssertionInput struct {
COSEPublicKey []byte
ClientDataJSON []byte
AuthenticatorData []byte
Signature []byte
RPID string
Challenge []byte
AllowedOrigins []string
StoredSignCount uint32
}
// VerifyAssertion 校验登录断言,成功返回新的签名计数器(供持久化)。
func VerifyAssertion(in VerifyAssertionInput) (uint32, error) {
if err := verifyClientData(in.ClientDataJSON, "webauthn.get", in.Challenge, in.AllowedOrigins); err != nil {
return 0, err
}
ad, err := parseAuthData(in.AuthenticatorData)
if err != nil {
return 0, err
}
if subtle.ConstantTimeCompare(ad.rpIDHash, rpIDHashFor(in.RPID)) != 1 {
return 0, ErrRPIDMismatch
}
if ad.flags&flagUserPresent == 0 {
return 0, ErrUserNotPresent
}
pub, err := parseCOSEKey(in.COSEPublicKey)
if err != nil {
return 0, err
}
// 签名覆盖 authenticatorData || sha256(clientDataJSON)。
cdHash := sha256.Sum256(in.ClientDataJSON)
signed := make([]byte, 0, len(in.AuthenticatorData)+len(cdHash))
signed = append(signed, in.AuthenticatorData...)
signed = append(signed, cdHash[:]...)
if err := pub.verify(signed, in.Signature); err != nil {
return 0, err
}
// 计数器回退检测:仅当双方都非 0 时严格(passkey 常报 0)。
if ad.signCount != 0 && in.StoredSignCount != 0 && ad.signCount <= in.StoredSignCount {
return 0, ErrCounterRegressed
}
return ad.signCount, nil
}
// publicKey 抽象 ES256/EdDSA 验签。
type publicKey struct {
ec *ecdsa.PublicKey
ed ed25519.PublicKey
}
func (p publicKey) verify(signed, sig []byte) error {
switch {
case p.ec != nil:
digest := sha256.Sum256(signed)
if !ecdsa.VerifyASN1(p.ec, digest[:], sig) {
return ErrSignatureInvalid
}
return nil
case p.ed != nil:
if !ed25519.Verify(p.ed, signed, sig) {
return ErrSignatureInvalid
}
return nil
default:
return ErrPublicKeyUnsupported
}
}
type coseKey struct {
Kty int `cbor:"1,keyasint"`
Alg int `cbor:"3,keyasint"`
Crv int `cbor:"-1,keyasint"`
X []byte `cbor:"-2,keyasint"`
Y []byte `cbor:"-3,keyasint"`
}
func parseCOSEKey(raw []byte) (publicKey, error) {
var k coseKey
if err := cbor.Unmarshal(raw, &k); err != nil {
return publicKey{}, ErrPublicKeyUnsupported
}
switch k.Kty {
case coseKtyEC2:
if k.Alg != coseAlgES256 || k.Crv != coseCrvP256 || len(k.X) != 32 || len(k.Y) != 32 {
return publicKey{}, ErrPublicKeyUnsupported
}
pub := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: new(big.Int).SetBytes(k.X),
Y: new(big.Int).SetBytes(k.Y),
}
if !pub.Curve.IsOnCurve(pub.X, pub.Y) {
return publicKey{}, ErrPublicKeyUnsupported
}
return publicKey{ec: pub}, nil
case coseKtyOKP:
if k.Alg != coseAlgEdDSA || k.Crv != coseCrvEd25519 || len(k.X) != ed25519.PublicKeySize {
return publicKey{}, ErrPublicKeyUnsupported
}
return publicKey{ed: ed25519.PublicKey(append([]byte(nil), k.X...))}, nil
default:
return publicKey{}, ErrPublicKeyUnsupported
}
}
func orDefault(s, def string) string {
if s == "" {
return def
}
return s
}

View file

@ -0,0 +1,175 @@
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)
}
}

View file

@ -0,0 +1,135 @@
// Package webauthntest 提供一个纯 Go 软件 authenticator,模拟平台 passkey 设备
// 生成符合 WebAuthn 规范的 attestation(注册)与 assertion(登录)。
//
// 它与被测的 internal/webauthn 是「同一规范的两份独立实现」:authenticator 按规范
// 构造字节并用真私钥签名,服务端按规范验证。e2e/单测通过即说明双方对规范的理解一致。
// 仅供测试使用(被 _test.go 导入)。
package webauthntest
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"github.com/fxamacker/cbor/v2"
)
const (
flagUserPresent = 0x01
flagUserVerified = 0x04
flagAttestedCredData = 0x40
)
var b64 = base64.RawURLEncoding
// Authenticator 是一个 ES256(ECDSA P-256)软件 authenticator,持有一对密钥与一个
// 凭据 id,可重复用于注册与多次登录(签名计数器递增)。
type Authenticator struct {
priv *ecdsa.PrivateKey
credID []byte
aaguid []byte
signCount uint32
}
// New 生成一个新的软件 authenticator(随机 ES256 密钥 + 随机 credential id)。
func New() (*Authenticator, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
credID := make([]byte, 32)
if _, err := rand.Read(credID); err != nil {
return nil, err
}
aaguid := make([]byte, 16) // 全 0 aaguid(与多数 platform passkey 一致)
return &Authenticator{priv: priv, credID: credID, aaguid: aaguid}, nil
}
// CredentialID 返回 credential id 原始字节。
func (a *Authenticator) CredentialID() []byte { return append([]byte(nil), a.credID...) }
// CredentialIDB64 返回 base64url(无填充) 的 credential id(对应 TL 的 ID/RawID string)。
func (a *Authenticator) CredentialIDB64() string { return b64.EncodeToString(a.credID) }
type clientData struct {
Type string `json:"type"`
Challenge string `json:"challenge"`
Origin string `json:"origin"`
CrossOrigin bool `json:"crossOrigin"`
}
func (a *Authenticator) clientDataJSON(typ, origin string, challenge []byte) []byte {
out, _ := json.Marshal(clientData{Type: typ, Challenge: b64.EncodeToString(challenge), Origin: origin})
return out
}
func (a *Authenticator) cosePublicKey() []byte {
x := make([]byte, 32)
y := make([]byte, 32)
a.priv.PublicKey.X.FillBytes(x)
a.priv.PublicKey.Y.FillBytes(y)
key := struct {
Kty int `cbor:"1,keyasint"`
Alg int `cbor:"3,keyasint"`
Crv int `cbor:"-1,keyasint"`
X []byte `cbor:"-2,keyasint"`
Y []byte `cbor:"-3,keyasint"`
}{Kty: 2, Alg: -7, Crv: 1, X: x, Y: y}
out, _ := cbor.Marshal(key)
return out
}
func (a *Authenticator) authData(rpID string, flags byte, includeCred bool) []byte {
h := sha256.Sum256([]byte(rpID))
buf := make([]byte, 0, 128)
buf = append(buf, h[:]...)
buf = append(buf, flags)
var counter [4]byte
binary.BigEndian.PutUint32(counter[:], a.signCount)
buf = append(buf, counter[:]...)
if includeCred {
buf = append(buf, a.aaguid...)
var l [2]byte
binary.BigEndian.PutUint16(l[:], uint16(len(a.credID)))
buf = append(buf, l[:]...)
buf = append(buf, a.credID...)
buf = append(buf, a.cosePublicKey()...)
}
return buf
}
// Register 模拟 createCredential,返回 clientDataJSON 与 attestationObject(fmt=none)。
func (a *Authenticator) Register(rpID, origin string, challenge []byte) (clientDataJSON, attestationObject []byte, err error) {
cd := a.clientDataJSON("webauthn.create", origin, challenge)
ad := a.authData(rpID, flagUserPresent|flagUserVerified|flagAttestedCredData, true)
att := struct {
Fmt string `cbor:"fmt"`
AttStmt map[string]any `cbor:"attStmt"`
AuthData []byte `cbor:"authData"`
}{Fmt: "none", AttStmt: map[string]any{}, AuthData: ad}
obj, err := cbor.Marshal(att)
if err != nil {
return nil, nil, err
}
return cd, obj, nil
}
// Assert 模拟 getCredential,返回 clientDataJSON、authenticatorData 与 signature。
// 每次调用签名计数器 +1。
func (a *Authenticator) Assert(rpID, origin string, challenge []byte) (clientDataJSON, authenticatorData, signature []byte, err error) {
a.signCount++
cd := a.clientDataJSON("webauthn.get", origin, challenge)
ad := a.authData(rpID, flagUserPresent|flagUserVerified, false)
cdHash := sha256.Sum256(cd)
signed := append(append([]byte(nil), ad...), cdHash[:]...)
digest := sha256.Sum256(signed)
sig, err := ecdsa.SignASN1(rand.Reader, a.priv, digest[:])
if err != nil {
return nil, nil, nil, err
}
return cd, ad, sig, nil
}