可靠地验证 JWS 证书链和域

Posted

技术标签:

【中文标题】可靠地验证 JWS 证书链和域【英文标题】:Reliably verify a JWS certificate chain and domain 【发布时间】:2020-06-11 13:14:15 【问题描述】:

我正在使用 Node.JS 编写后端代码来验证来自 Google 的 SafetyNet API 的 JWS。 我很惊讶没有找到现成的可用模块,因此我开始研究使用可用库对 JWS 的一些简单验证:

首先,Google 表示需要执行以下步骤:

    从 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?

使用嵌入密钥是否确实使用根 CA 验证嵌入证书链 x5c,以及针对证书的签名?还是我需要从 Google 明确获取公钥来单独验证证书?

然后,一个有点相关的问题涉及 Google 用于执行此验证的 API:有一个 API https://www.googleapis.com/androidcheck/v1/attestations/verify?key=... 可以执行此确切操作,但它似乎已从 Google 的文档中删除,只能在过时的文章中找到引用以及关于 SafetyNet 的 SO 答案,例如 this one,这似乎表明此 API 仅用于测试,并且在生产中您应该自己执行证书验证。有谁知道这个 API 是否适合生产使用?如果每个人都打算手动验证 JWS,我觉得 Google 不会提供更多文档和代码示例有点令人惊讶,因为这个过程很容易出错,并且错误可能会产生严重影响?到目前为止,我只找到了一些 Java 中的第 3 方示例,但没有找到来自 Google 的服务器端代码示例。

【问题讨论】:

您是否使用了“node-jose”或任何其他解决方案? 【参考方案1】:

这里有the steps,您需要按照 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')

所有对我有帮助的文章:

    步骤摘要 - Here 详细解释与实施 - Here 你应该记住的事情 - Here 来自 google 的核对清单以正确执行 - Here 深入了解流程 - Here

【讨论】:

以上是关于可靠地验证 JWS 证书链和域的主要内容,如果未能解决你的问题,请参考以下文章

无法可靠地验证 SKSpriteNode 的纹理

作用域链和域解析

证书链和TLS Pinning

使用 PHP 验证来自 Android SafetyNet 的 JWS 响应

可靠地告诉 SAML 请求是不是被压缩?

安全哈希算法SHA-1已完全不可靠,请尽量避免使用!