JS `WebAuthn` `Attestation` (`FIDO2`) `Format` 与 `Authenticator` `Registration`

大家好!欢迎来到今天的“WebAuthn Attestation Format 和 Authenticator Registration”专场脱口秀。我是今天的段子手,不对,是技术专家,我们来一起扒一扒 WebAuthn 里面的那些弯弯绕。

首先,我们得明确一点:WebAuthn 就像一个非常谨慎的保安,它要确保注册的用户(Authenticator)真的是他们声称的那个人,并且这个设备(Authenticator)是可信的。而 Attestation Format 就是保安用来验证用户身份的“身份证明”。

一、WebAuthn 基础概念回顾:Authenticator 的自我介绍

在 WebAuthn 的世界里,Authenticator 就像一个来面试的候选人。它要跟网站(Relying Party,RP)证明自己是靠谱的。这个证明的过程分为两个阶段:

  1. Registration (注册): Authenticator 第一次来,要告诉 RP “我是谁,我有什么能力,我怎么证明我是我”。
  2. Authentication (认证): Authenticator 已经注册过了,每次来都要证明 “我还是我,我没被掉包”。

我们今天主要关注 Registration 阶段,尤其是 Authenticator 如何把自己介绍给 RP,以及 RP 如何判断这个介绍是不是真的。

二、Attestation:Authenticator 的出生证明

Attestation 其实就是 Authenticator 提供的“出生证明”,证明它是由哪个厂家生产的,型号是什么,以及它是否被篡改过。这个“出生证明”是用密码学方法签名的,所以可以验证真伪。

1. Attestation Statement Format(证明声明格式)

这就是“出生证明”的具体格式。不同的 Authenticator 厂商可能使用不同的格式,WebAuthn 定义了一些常见的格式:

  • Packed: 最常见,也比较简单。Authenticator 使用自己的私钥对 Attestation Statement 进行签名。
  • TPM: 可信平台模块 (Trusted Platform Module) 使用,安全性很高,但是实现比较复杂。
  • Android Key Attestation: 安卓设备使用,通过 Google 的 Keymaster 服务进行验证。
  • None: Authenticator 声称自己不提供任何 Attestation 信息,RP 需要自己承担风险。

2. Attestation Statement(证明声明)

Attestation Statement 包含了 Authenticator 的信息和签名。它通常是一个 CBOR (Concise Binary Object Representation) 编码的数据结构。

Attestation Statement 的结构通常包含以下几个关键字段:

  • fmt: Attestation Format 的字符串标识符,例如 "packed", "tpm", "android-key" 等。
  • attStmt: 包含实际的 Attestation 信息,例如签名、证书链等。
  • authData: 认证数据,包含了 Authenticator 的信息,例如 AAGUID (Authenticator Attestation GUID)。

三、Authenticator Registration 流程:保安如何核实身份

Authenticator Registration 的流程大致如下:

  1. RP (网站) 生成 Challenge: RP 生成一个随机数,作为挑战值 (challenge)。
  2. RP 将 Challenge 发送给用户设备: 通过 WebAuthn API,将 challenge 发送给用户设备。
  3. Authenticator 生成密钥对: Authenticator 生成一个用于 WebAuthn 的密钥对。
  4. Authenticator 创建 Attestation Statement: Authenticator 使用自己的私钥对 Attestation Statement 进行签名,并包含必要的 Attestation 信息。
  5. Authenticator 将 Credential、Attestation Statement 和 Challenge 返回给 RP: 通过 WebAuthn API,将这些数据返回给 RP。
  6. RP 验证 Attestation Statement: RP 根据 fmt 字段选择相应的验证方法,验证 Attestation Statement 的签名和内容。
  7. RP 保存 Credential 信息: 如果 Attestation 验证通过,RP 将 Credential 信息(例如公钥、AAGUID 等)保存到数据库中。

四、代码示例:Packed Attestation Format 的验证

我们以最常见的 Packed Attestation Format 为例,展示如何验证 Attestation Statement。

// 假设我们已经从 WebAuthn API 获取了 registrationResponse

async function verifyPackedAttestation(registrationResponse) {
  const { attestationObject, clientDataJSON } = registrationResponse;

  // 1. 解析 attestationObject
  const attestation = CBOR.decode(attestationObject);
  const fmt = attestation.fmt;
  const attStmt = attestation.attStmt;
  const authData = attestation.authData;

  // 2. 验证 fmt 是否为 "packed"
  if (fmt !== "packed") {
    throw new Error("Attestation format is not 'packed'");
  }

  // 3. 验证 clientDataHash
  const clientDataHash = await crypto.subtle.digest('SHA-256', clientDataJSON);
  const parsedAuthData = parseAuthData(authData);  // 假设我们有这个函数来解析 authData
  if (!arraysEqual(parsedAuthData.rpIdHash, new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(window.location.origin))))) {
    throw new Error("rpIdHash mismatch!");
  }
  if (!arraysEqual(parsedAuthData.clientDataHash, new Uint8Array(clientDataHash))) {
    throw new Error("clientDataHash mismatch!");
  }

  // 4. 验证签名
  const signature = attStmt.sig;
  const alg = attStmt.alg;
  const pubArea = attStmt.pubArea;
  let certificate = attStmt.certInfo;

  let verificationResult = false;

  if (attStmt.x5c && attStmt.x5c.length > 0) {
      // 4.1 If x5c is present, validate the certificate chain
      certificate = attStmt.x5c[0];
      const isValidCertChain = await validateCertificateChain(attStmt.x5c); // 假设有验证证书链的函数

      if (!isValidCertChain) {
          throw new Error("Invalid certificate chain");
      }

      // 4.2 Verify signature using the certificate
      verificationResult = await verifySignature(certificate, signature, authData);
  } else {
      // 4.3 If x5c is absent, retrieve public key from pubArea and verify signature
      const publicKey = await extractPublicKeyFromPubArea(pubArea); // 假设有函数从pubArea提取公钥
      verificationResult = await verifySignature(publicKey, signature, authData);
  }

  if (!verificationResult) {
    throw new Error("Signature verification failed");
  }

  // 5. 返回验证结果
  return true;
}

// 辅助函数 (需要根据实际情况实现)
async function validateCertificateChain(certificateChain) {
  // 实现证书链验证逻辑
  // 例如:检查证书是否过期,是否被吊销等
  return true; // 简化示例,假设验证通过
}

async function verifySignature(key, signature, data) {
  // 实现签名验证逻辑
  const algorithm = {
      name: "ECDSA",
      hash: "SHA-256",
  };
  try {
      const isVerified = await crypto.subtle.verify(
          algorithm,
          key,
          signature,
          data
      );
      return isVerified;
  } catch (error) {
      console.error("Signature verification error:", error);
      return false;
  }
}

async function extractPublicKeyFromPubArea(pubArea) {
    // TODO: Implement logic to extract public key from pubArea
    // This depends on the structure of the pubArea.
    // For example, if it's an ASN.1 encoded structure, you'd need to parse it.
    // See the FIDO2 specification for details.
    return null;
}

function parseAuthData(authData) {
    const rpIdHash = authData.slice(0, 32);
    const flags = authData[32];
    const signCount = new DataView(authData.slice(33, 37).buffer).getUint32(0, false);
    let aaguid = null;
    let credentialIdLength = null;
    let credentialId = null;
    let credentialPublicKey = null;
    let offset = 37;

    const atFlag = flags & 0x40;
    const edFlag = flags & 0x80;

    if (atFlag) {
        aaguid = authData.slice(offset, offset + 16);
        offset += 16;

        credentialIdLength = new DataView(authData.slice(offset, offset + 2).buffer).getUint16(0, false);
        offset += 2;

        credentialId = authData.slice(offset, offset + credentialIdLength);
        offset += credentialIdLength;

        credentialPublicKey = authData.slice(offset);
    }

    return {
        rpIdHash: rpIdHash,
        flags: flags,
        signCount: signCount,
        aaguid: aaguid,
        credentialIdLength: credentialIdLength,
        credentialId: credentialId,
        credentialPublicKey: credentialPublicKey,
        clientDataHash: authData.slice(37+16+2+credentialIdLength,37+16+2+credentialIdLength+32) // Assuming clientDataHash is after credentialPublicKey
    };
}

function arraysEqual(a, b) {
    if (a === b) return true;
    if (a == null || b == null) return false;
    if (a.length !== b.length) return false;

    for (var i = 0; i < a.length; ++i) {
      if (a[i] !== b[i]) return false;
    }
    return true;
}

代码解释:

  • verifyPackedAttestation(registrationResponse): 主函数,接收 WebAuthn API 返回的 registrationResponse。
  • CBOR.decode(attestationObject): 解析 CBOR 编码的 attestationObject。
  • fmt !== "packed": 检查 Attestation Format 是否为 "packed"。
  • validateCertificateChain(attStmt.x5c): 验证证书链的有效性。
  • verifySignature(certificate, signature, authData): 使用证书或公钥验证签名。

五、Attestation 的分类:保安的信任等级

Attestation 可以分为不同的类型,反映了 RP 对 Authenticator 的信任程度:

  • Attestation Conveyance Preference (证明传递偏好): RP 可以指定自己希望接收的 Attestation 类型。

    • none: RP 不要求 Attestation 信息。
    • indirect: RP 接收 Attestation 信息,但是不强制验证。
    • direct: RP 必须验证 Attestation 信息。
  • Attestation Trustworthiness (证明可信度): RP 可以根据 Authenticator 的 AAGUID (Authenticator Attestation GUID) 来判断其可信度。AAGUID 可以用来识别 Authenticator 的厂商和型号。

六、Authenticator Registration 的安全性考虑:保安的防盗技巧

在 Authenticator Registration 过程中,需要考虑以下安全性问题:

  • Challenge 的唯一性: Challenge 必须是随机的,并且只能使用一次,防止重放攻击。
  • RP ID 的验证: RP 必须验证收到的 Attestation Statement 中的 RP ID 是否与自己的域名一致,防止钓鱼攻击。
  • 证书链的验证: 如果 Attestation Statement 中包含证书链,RP 必须验证证书链的有效性,防止伪造证书。
  • AAGUID 的管理: RP 应该维护一个 AAGUID 列表,记录已知的可信 Authenticator 厂商和型号。
  • 用户验证要求: RP应该根据自身安全需求,选择合适的User Verification (UV)策略,比如需要用户输入PIN码或者指纹验证等。

七、总结:保安的职业素养

WebAuthn Attestation Format 和 Authenticator Registration 是 WebAuthn 安全性的重要组成部分。通过验证 Attestation Statement,RP 可以确保注册的 Authenticator 是可信的,并且没有被篡改。

总而言之,理解 Attestation Format 和 Authenticator Registration 的流程,就像了解保安如何核实身份一样,可以帮助我们更好地理解 WebAuthn 的安全性,并构建更安全的 Web 应用。

最后,来个小彩蛋:

WebAuthn 的世界远不止这些,还有各种各样的细节和坑等着你去踩。但是,只要你掌握了基本原理,就能在这个领域里游刃有余。希望今天的“脱口秀”能给你带来一些启发。

感谢大家的收听!我们下次再见!

表格总结:

概念 描述 作用
Authenticator 用于 WebAuthn 认证的设备,例如指纹识别器、安全密钥等。 提供用户身份验证功能。
Relying Party (RP) 网站或应用,依赖 Authenticator 进行用户身份验证。 验证用户身份,提供安全访问。
Attestation Authenticator 提供的“出生证明”,证明其是由哪个厂家生产的,型号是什么,以及它是否被篡改过。 验证 Authenticator 的真实性和可信度。
Attestation Format Attestation Statement 的具体格式,例如 Packed, TPM, Android Key 等。 定义 Attestation Statement 的结构和内容。
Attestation Statement 包含了 Authenticator 的信息和签名。 提供 Authenticator 的详细信息,用于验证其真实性。
AAGUID Authenticator Attestation GUID,用于识别 Authenticator 的厂商和型号。 帮助 RP 判断 Authenticator 的可信度。
Challenge RP 生成的随机数,用于防止重放攻击。 确保 Registration 和 Authentication 过程的安全性。
Packed 一种常见的 Attestation Format,Authenticator 使用自己的私钥对 Attestation Statement 进行签名。 简化 Attestation 流程,降低实现复杂度。
TPM 可信平台模块,一种硬件安全模块,提供更高的安全性。 提供更安全的 Attestation 机制。
Android Key 安卓设备使用的 Attestation 机制,通过 Google 的 Keymaster 服务进行验证。 方便安卓设备进行 Attestation。
None 不提供任何 Attestation 信息,RP 需要自己承担风险。 适用于对安全性要求不高的场景。

希望这个表格能帮助你更好地理解这些概念。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注