如何正确从 attestationObject 中提取公钥?

问题描述 投票:0回答:1

我正在研究万能钥匙,但找不到在证明(注册)步骤期间从客户端发送的证明数据中正确提取公钥的方法。我发现的大多数教程都提到使用带有 javascript 的 Web api 并调用

getPublicKey()
方法,但我使用的是 flutter 并且我用于密钥的库没有这样的方法,所以我必须手动提取公钥我的服务器,这是我尝试过的:

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 部分,我需要重新创建公钥:

  • 使用 cbor 解码证明:
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);
  • 解码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 编码,当我从原始公钥中提取 X 和 Y 值时,Y 结果与 COSEPublicKey 相同,但 X 始终不同。

从现在开始,我不确定我是否正确处理签名验证,我尝试通过以下方法创建公钥:

const publicKey = crypto.createPublicKey({
    key: Buffer.from([0x04, ...publicKeyData.x, ...publicKeyData.y]),
    format: 'der',
    type: 'spki',
});

上述方法会导致错误:

Error: error:0680009B:asn1 encoding routines::too long

也尝试过这样的操作,这会导致同样的错误:

const publicKey = await crypto.subtle.importKey(
    'spki',
    Buffer.from([0x04, publicKeyData.x, publicKeyData.y]),
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['verify']
);

为了验证:

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
);

我还尝试使用原始的 b64 编码公钥重新创建公钥,如下所示:

const publicKey = await crypto.subtle.importKey(
    'spki',
    base64Decode(attestation.publicKey),
    { name: 'ECDSA, namedCurve: 'P-256' },
    true,
    ['verify']
);

但是

verify
的结果是
false
,所以我的代码肯定有问题。

fromAsn1DERtoRSSignature
的源代码可以在这里找到。

对于我发现的大多数教程,总是使用 JavaScript 并在给定的浏览器中运行,但现在也是为了更好地了解该主题,我想学习如何处理此类用例。

我尝试了很多在网上看到的解决方案,也使用了代码助手,但没有一个对我有帮助,它们要么给出错误,要么导致错误断言。

node.js ecdsa webauthn passkey
1个回答
0
投票

首先,我没有实现整个代码并检查验证,而只关注实际问题,即如何提取/导入公钥:

如果您发布的测试数据确定了

publicKeyData.x
publicKeyData.y
,则原始密钥的结果为(十六进制编码):

publicKeyData.x 81ACA0F38F2D4B7C001E34B2C11FF7960D9AA80ED7AACABD7C9D2E953E0374D2
publicKeyData.y 2EBEDA1E9280A11316375946551881B0C4B591D075D1011FD69C6BCA80D067C0

事实证明,这个密钥与

attestation.publicKey
相同,后者只是格式和编码不同,更准确地说,它是X.509/SPKI格式(Base64编码)的ASN.1/DER编码密钥。这可以使用 ASN.1/DER 解析器轻松验证,例如这里

即除了

publicKeyData.x
publicKeyData.y
,您还可以使用
attestation.publicKey

可以使用 WebCrypto 直接导入此格式和编码的密钥。因此,您可以简单地 Base64 解码

attestation.publicKey
并使用
importKey()
导入它,这会生成
CryptoKey()
:

(async () => {

const attestation = {
    "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgayg848tS3wAHjSywR/3lg2aqA7Xqsq9fJ0ulT4DdNIuvtoekoChExY3WUZVGIGwxLWR0HXRAR/WnGvKgNBnwA=="
}

const publicKey = await crypto.subtle.importKey(
    'spki',
    decodeBase64(attestation.publicKey),
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['verify']
);
console.log(publicKey)

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

})();


注意:原则上,也可以导入原始公钥(即

publicKeyData.x
publicKeyData.y
)。但由于 WebCrypto 不支持原始密钥,因此必须将其转换为 WebCrypto 支持的格式。最接近原始密钥的是 JWK 格式的密钥,WebCrypto 支持该格式。然而,这比上面的变体稍微耗时一些。 在这里(在答案的编辑部分)您可以找到一个示例(使用 JWK 导入原始私钥)。


编辑: 由于根据您的评论,公钥在实际数据中不以 X.509/SPKI 格式提供,而仅以原始密钥提供,因此可以导入为 JWK(如上一节中所示),如下面的例子:

(async () => {

var x = hex2ab('81ACA0F38F2D4B7C001E34B2C11FF7960D9AA80ED7AACABD7C9D2E953E0374D2')
var y = hex2ab('2EBEDA1E9280A11316375946551881B0C4B591D075D1011FD69C6BCA80D067C0')

let publicKeyJwk = {
    crv: "P-256",
    kty: "EC",
    x: ab2b64url(x),
    y: ab2b64url(y)
}

const publicKey = await crypto.subtle.importKey(
    'jwk',
    publicKeyJwk,
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['verify']
)
console.log(publicKey)

// Test
const publicKeyDer = await crypto.subtle.exportKey('spki', publicKey)
console.log(ab2b64(publicKeyDer))

// Helper -----------
function hex2ab(hex){
    return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}

function ab2b64(arrayBuffer) {
    return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

function ab2b64url(arrayBuffer) {
    return ab2b64(arrayBuffer).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}

})();

作为测试,导入的密钥以 X.509/SPKI 格式导出,ASN.1/DER 编码,然后 Base64 编码。结果等于

attestation.publicKey
,这证明了两个密钥的等价性。

© www.soinside.com 2019 - 2024. All rights reserved.