2

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

首先,谷歌说这些是所需的步骤:

  1. 从 JWS 消息中提取 SSL 证书链。
  2. 验证 SSL 证书链并使用 SSL 主机名匹配来验证叶证书是否已颁发给主机名 attest.android.com。
  3. 使用证书来验证 JWS 消息的签名。
  4. 检查 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:有一个 APIhttps://www.googleapis.com/androidcheck/v1/attestations/verify?key=...可以执行此确切操作,但它似乎已从 Google 的文档中删除,并且只能在过时的文章和关于 SafetyNet 的 SO 答案中找到参考因为这个似乎表明这个 API 仅用于测试,在生产中你应该自己执行证书验证。有谁知道这个 API 是否适合生产使用?如果每个人都打算手动验证 JWS,我觉得 Google 不会提供更多文档和代码示例有点令人惊讶,因为这个过程很容易出错,并且错误可能会产生严重影响?到目前为止,我只在 Java 中找到了一些 3rd 方示例,但没有从 Google 找到服务器端代码示例。

4

1 回答 1

3

Here are the steps that you would need to perform as recommended by Google.

Definitely feel free to go through all the reference links to understand the process a bit better. Do look into each library functions used here to know what they are doing and if that is exactly what you want them to do. I've written pseudocode to explain the steps. You might have to run them on a sample attestation token to test them out and change a few things accordingly.

It would also be good to look at the whole node implementation of SafetyNet in one place.

// 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')
}

All the articles that helped me:

  1. Summary for the steps - Here
  2. explanation in depth with implementation - Here
  3. Things you should keep in mind - Here
  4. checklist from google to do it correctly - Here
  5. Deep Dive into the process - Here
于 2020-05-22T06:27:47.733 回答