JHH*_*JHH 3 ssl node.js safetynet json-web-signature node-jose
我正在 Node.JS 中编写后端代码来验证来自 Google SafetyNet API 的 JWS。我很惊讶没有为此找到现成的模块,因此我开始使用可用的库来研究 JWS 的一些简单验证:
首先,谷歌表示需要执行以下步骤:
- 从 JWS 消息中提取 SSL 证书链。
- 验证 SSL 证书链并使用 SSL 主机名匹配来验证叶证书是否已颁发给主机名 attest.android.com。
- 使用证书来验证 JWS 消息的签名。
- 检查 JWS 消息的数据,确保其与原始请求中的数据匹配。特别是,请确保时间戳已经过验证,并且应用程序签名证书的随机数、包名称和哈希值与预期值匹配。
(来自https://developer.android.com/training/safetynet/attestation#verify-attestation-response)
我发现node-jose提供了一个简单的接口来验证JWS,并且它有一个允许嵌入密钥的选项。我试图准确了解此过程的作用以及它是否足以验证 JWS 的真实性?
const {JWS} = require('node-jose');
const result = await JWS.createVerify({allowEmbeddedKey: true}).verify(jws);
if (result.key.kid === 'attest.android.com') {
// Are we good to go or do we manually need to verify the certificate chain further?
}
Run Code Online (Sandbox Code Playgroud)
使用嵌入式密钥是否确实可以x5c使用根 CA 以及针对证书的签名来验证嵌入式证书链?或者我是否需要明确从 Google 获取公钥来单独验证证书?
然后,一个有点相关的问题涉及 Google 用于执行此验证的 API:有一个 APIhttps://www.googleapis.com/androidcheck/v1/attestations/verify?key=...可以执行此精确操作,但它似乎已从 Google 文档中删除,并且只能在过时的文章和有关 SafetyNet 的 SO 答案中找到引用,例如这似乎表明该 API 仅用于测试,在生产中您应该自己执行证书验证。有谁知道这个 API 是否适合生产使用?如果每个人都打算手动验证 JWS,我觉得 Google 不会提供更多文档和代码示例有点令人惊讶,因为这个过程很容易出错,而且错误可能会产生严重影响?到目前为止,我只找到了一些 Java 中的第三方示例,但没有来自 Google 的服务器端代码示例。
以下是您需要按照 Google 建议执行的步骤。
绝对可以随意浏览所有参考链接,以更好地了解该过程。请查看此处使用的每个库函数,了解它们在做什么以及这是否正是您希望它们执行的操作。我编写了伪代码来解释这些步骤。您可能需要在示例证明令牌上运行它们来测试它们并相应地更改一些内容。
如果能在一个地方查看 SafetyNet 的整个节点实现也是很好的。
// following steps should be performed
// 1. decode the JWS
// 2. the source of the first certificate in x5c array of jws header
// should be attest.google.com
// 3. to make sure if the JWS was not tampered with, validate the signature of JWS (how signature verification is done is explained in the reference links)
// with the certificate whose source we validated
// 4. if the signature was valid, we need to know if the certificate was valid by
// explicitly checking the certificate chain
// 5. Validate the payload by matching the package name, apkCertificateDigest
// and nonce value (apkCertificateDigest is base64 encoding of the hash of signing app's certificate)
// 6. and now you can trust the ctsProfileMatch and BasicIntegrity flags
// let's see some code in node, though this will not run as-is,
// it provides an outline on how to do it and which functions to consider when implementing
const pki = require('node-forge').pki;
const jws = require('jws');
const pem = require("pem");
const forge = require('node-forge');
const signedAttestation = "Your signed attestation here";
function deviceAttestationCheck(signedAttestation) {
// 1. decode the jws
const decodedJws = jws.decode(signedAttestation);
const payload = JSON.parse(decodedJws.payload);
// convert the certificate received in the x5c array into valid certificates by adding
// '-----BEGIN CERTIFICATE-----\n' and '-----END CERTIFICATE-----'
// at the start and end respectively for each certificate in the array
// and by adding '\n' at every 64 char
// you'll have to write your own function to do the simple string reformatting
// get the x5c certificate array
const x5cArray = decodedJws.header.x5c;
updatedX5cArray = doTheReformatting(x5cArray);
// 2. verify the source to be attest.google.com
certToVerify = updatedX5cArray[0];
const details = pem.readCertificateInfo(certToVerify);
// check if details.commanName === "attest.google.com"
const certs = updatedX5cArray.map((cert) => pki.certificateFromPem(cert));
// 3. Verify the signature with the certificate that we received
// the first element of the certificate(certs array) is the one that was issued to us, so we should use that to verify the signature
const isSignatureValid = jws.verify(signedAttestation, 'RS256', certs[0]);
// 4. to be sure if the certificate we used to verify the signature is the valid one, we should validate the certificate chain
const gsr2Reformatted = doTheReformatting(gsr2);
const rootCert = pki.certificateFromPem(gsr2Reformatted);
const caStore = pki.createCaStore([rootCert]);
// NOTE: this pki implementation does not check for certificate revocation list, which is something that you'll need to do separately
const isChainValid = pki.verifyCertificateChain(caStore, certs);
// 5. now we can validate the payload
// check the timestamps, to be within certain time say 1 hour
// check nonce value, to contain the data that you expect, refer links below
// check apkPackageName to be your app's package name
// check apkCertificateDigestSha256 to be from your app - quick tip -look at the function below on how to generate this
// finally you can trust the ctsProfileMatch - true/false depending on strict security need and basicIntegrity - true, minimum to check
}
// this function takes your signing certificate(should be of the form '----BEGIN CERT....data...---END CERT...') and converts into the SHA256 digest in hex, which looks like - 92:8H:N9:84:YT:94:8N.....
// we need to convert this hex digest to base64
// 1. 92:8H:N9:84:YT:94:8N.....
// 2. 928hn984yt948n - remove the colon and toLowerCase
// 3. encode it in base64
function certificateToSha256DigestHex(certPem) {
const cert = pki.certificateFromPem(certPem);
const der = forge.asn1.toDer(pki.certificateToAsn1(cert)).getBytes();
const m = forge.md.sha256.create();
m.start();
m.update(der);
const fingerprint = m.digest()
.toHex()
.match(/.{2}/g)
.join(':')
.toUpperCase();
return fingerprint
}
// 92:8H:N9:84:YT:94:8N => 928hn984yt948n
function stringToHex(sha256string) {
return sha256string.split(":").join('').toLowerCase();
}
// this is what google sends you in apkCertificateDigestSha256 array
// 928hn984yt948n => "OIHf9wjfjkjf9fj0a="
function hexToBase64(hexString) {
return Buffer.from(hexString, 'hex').toString('base64')
}
Run Code Online (Sandbox Code Playgroud)
所有对我有帮助的文章:
| 归档时间: |
|
| 查看次数: |
3320 次 |
| 最近记录: |