You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何从AttestationObject中正确提取公钥?(Passkeys服务端)

Passkeys注册阶段服务端提取公钥及验证问题

研究Passkeys时,注册阶段无法从客户端发送的attestationData中正确提取公钥。Web端教程多依赖JavaScript的getPublicKey()方法,但Flutter的Passkeys库没有这个方法,只能在NodeJS服务端手动提取,尝试多种方法均未解决。


测试数据

const attestation = {
    "attestationData": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAILxBOipox2vwkvO0SAE7mme20vs2iVsGk8VPFu/6lQPxpQECAyYgASFYIIGsoPOPLUt8AB40ssEf95YNmqgO16rKvXydLpU+A3TSIlggLr7aHpKAoRMWN1lGVRiBsMS1kdB10QEf1pxryoDQZ8A=",
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoib202bjVIdFVjZ1Q4T0NiTjJUZXQ4OWJtU3BFaUhpNUY3aWlZOVhQZmFUSSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9",
    "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgayg848tS3wAHjSywR/3lg2aqA7Xqsq9fJ0ulT4DdNIuvtoekoChExY3WUZVGIGwxLWR0HXRAR/WnGvKgNBnwA=="
}

const assertion = {
    "signature": "MEYCIQCaO2mh+E8SEWUOGW1XLMPq3z/LofM67/vUr6ut/Z9apgIhALWuIhawe+nzWjd//Zd670IxrP9gksMW0o7Gh/FYHkcG",
    "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAA=="
}

Base64编解码方法

function decodeBase64(data) {
    return Uint8Array.from(atob(data).split(""), (x) => x.charCodeAt(0));
}

function encodeBase64(data) {
    return btoa(String.fromCharCode(...new Uint8Array(data)));
}

NodeJS提取公钥步骤

  1. 使用CBOR解码attestationData:
const ctapMakeCredResp        = cbor.decodeFirstSync(decodeBase64(attestation.attestationData));
const authData = ctapMakeCredResp.authData;
const rpidHash = authData.subarray(0, 32);
const flags = authData.subarray(32, 33);
const counter = authData.subarray(33, 37).readUint32BE(0);

const aaguid = authData.subarray(37, 53);
const credIdLength = authData.subarray(53, 55).readUInt16BE(0);
const credID = authData.subarray(55, 55 + credIdLength);
const COSEPublicKey = authData.subarray(55  + credIdLength);
  1. 解码COSEPublicKey得到CBOR Map:
const publicKeyMap = cbor.decodeFirstSync(COSEPublicKey);
const publicKeyData = {
    kty: publicKeyMap.get(1),
    alg: publicKeyMap.get(3),
    crv: publicKeyMap.get(-1),
    x: publicKeyMap.get(-2),
    y: publicKeyMap.get(-3),
};

提取及公钥生成问题

  • 调试发现:从前端Base64公钥提取的Y值与COSEPublicKey中的一致,但X值始终不同。
  • 尝试以下两种方式生成公钥时,均触发Error: error:0680009B:asn1 encoding routines::too long错误:
const publicKey = crypto.createPublicKey({
    key: Buffer.from([0x04, ...publicKeyData.x, ...publicKeyData.y]),
    format: 'der',
    type: 'spki',
});
const publicKey = await crypto.subtle.importKey(
    'spki',
    Buffer.from([0x04, publicKeyData.x, publicKeyData.y]),
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['verify']
);

签名验证问题

使用前端提供的Base64公钥导入后,签名验证结果为false,验证代码如下:

const signature = decodeBase64(authSignature);
const authDataDecoded = decodeBase64(authenticatorData);
const clientData = decodeBase64(clientDataJSON);

const rawSig = fromAsn1DERtoRSSignature(signature, 256);

const digest = concatBuffer(
    authDataDecoded,
    await crypto.subtle.digest('SHA-256', clientData)
);

const result = await crypto.subtle.verify(
    { name: 'ECDSA', hash: { name: 'SHA-256' } },
    publicKey,
    rawSig,
    digest
);

内容的提问来源于stack exchange,提问作者Leo Letto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 01:07:48