是否可以使用 WebAuthn 在浏览器中对文档进行数字签名?

信息安全 电子签名 网络认证
2021-08-17 09:44:32

WebAuthn是一种相对较新的身份验证 API,它使用公钥加密而不是密码之类的东西。

我想知道是否可以将加密部分用于不同的目的,特别是在浏览器中创建文档的数字签名。

这个想法是找到一种方法让用户以服务器无法伪造或操纵的方式在浏览器中签署文档。因此,Web 应用程序本身绝不能知道用于签名的私钥,据我所知,WebAuthn 就是这种情况。

因此,如果不是 WebAuthn 期望的随机挑战,我会发送我希望用户签名的文档的内容,如果我正确理解 MDN 上的解释,我应该取回我的文档的加密签名哈希。

最后,我应该有一个数字签名,可用于证明该文档是由来自特定硬件令牌的特定私钥签名的。并且浏览器内的 Web 应用程序和服务器组件都没有看到私钥,也不能伪造这个签名(当然,应用程序可以通过在签名之前切换内容来破坏这一点,但不能稍后)。

我找不到为此目的使用 WebAuthn 的人的任何信息,而且我能找到的任何与浏览器中的数字签名相关的内容都与 Java 小程序之类的东西有关,而如今这些东西根本不再是一种选择。

我的想法总体上合理吗?还是我误解了 WebAuthn 的工作原理,根本不可能以这种方式使用它?这种方法是否有我遗漏的弱点或缺陷?

2个回答

这是可能的,但是您需要牢记一些注意事项。

Webauthn 基本上由Authenticator对 Web 应用程序提供的挑战进行签名(技术术语:Relying Party)工作。在标准流程中,挑战是由 Web 应用程序随机生成的,但它只是一个字节串,因此理论上,您可以在其中放置任何内容,例如文档的哈希值。

现在,警告:

  1. 随机挑战是为了防止重放攻击。当你用哈希替换它时,它就变得确定了。对于文档签名方案来说,这可能不是问题,但了解这些限制很重要。

  2. Authenticator 实际上签署的不是挑战,而是一对客户端数据验证器数据(前者包含挑战)。因此,在验证签名时,仅拥有文档是不够的。

    • 客户端数据不难重构——您只需要签名文档(计算其哈希值,从而计算挑战值)和Web 应用程序的来源https://docsign.example.com(例如)。
    • 另一方面,身份验证器数据是由身份验证器设备生成的,因此无法重新创建它。您唯一的选择是将其与文档的签名一起存储。

实际上,这意味着当您调用 时navigator.credentials.get(),它会产生一个PublicKeyCredential,除了其他内容之外,它的对象中将包含三个有趣的字段: 当然,您会想要保留签名,但为了在将来可以验证它,您需要保留身份验证数据并始终随身携带签名(甚至可以说您的签名)。如果您愿意,您也可以保留,但您不必保留,因为它很容易重构(假设验证者知道用于签名的服务的域)。responseclientDataJSONauthenticatorDatasignature signatureauthenticatorDataclientDataJSON

是的,这是可能的

Web Authentication API 的安全挑战可以是任何字节数组(至少 16 个字节),并且它将由客户端安全私钥签名。因此,它也可用于通过PublicKeyCredentialRequestOptions.challenge在对 的调用中将文档作为 a 传递来签署所有类型的文档或消息navigator.credentials.get()

PublicKeyCredential调用返回的包含一个属性response,该属性包含一个属性,该signature属性是您要查找的签名。

developer.mozilla.com 的示例代码稍作修改以适合您的用例:

fetch(/* get file from server */).then(response => response.arrayBuffer()).then(fileFromServer => {
    const options = {
        challenge: fileFromServer,
        rp: {
            name: "Example CORP",
            id  : "login.example.com"
        },
        user: {
            id: userId,
            name: "jdoe@example.com",
            displayName: "John Doe"
        },
        pubKeyCredParams: [
            {
                type: "public-key",
                alg: -7
            }
        ]
    }
    
    navigator.credentials.get({  publickey: options })
        .then(function (pubKeyCredential) {
            const signature = pubKeyCredential.response.signature

            fetch(/* Send signature to server */)
    }).catch(function (err) {
      // Deal with any error
    })
})