Re: [webauthn] Add "sign" extension (#2078)

I'm excited to see this proposal, its an API I have wanted for a long time.

I've implemented hacks around Digital Credential wallets, binding credentials to passkeys, by proxying information in and out of frames, and overloading the "challenge" to sign arbitrary data... Its all gross, this API would lead to a much better experience.

I've implemented hash and then sign for `ES256` in JOSE and COSE and using webcrypto and remote hsms.

We've been considering, how to communicate about the various different layers which are relevant for hash and then sign, you can see some background here: https://mailarchive.ietf.org/arch/msg/cose/JonuJfnRwpR7wlmZ40Vyt-uuwoY/

If we are talking only about the WebCrypto API side of this, here's some high level pseudocode, showing how the current APIs work for generating JOSE and COSE compliant signatures, using IANA registered algorithms:

Start by implementing a generic signer pattern, so your code can pair with web crypto or remote signers:

```js
const signer = (privateKey) => {
  return {
    sign: async (toBeSigned: UInt8Array): Promise<UInt8Array> => {
      // skip this step if you are passing a non exportable private key reference
      const signingKey = await window.crypto.subtle.importKey(
        "jwk",
        privateKey, // JWK, other formats
        {
          name: "ECDSA",
          namedCurve: "P-256",
        },
        true,
        ["sign"],
      )
      const signature = await window.crypto.subtle.sign(
        {
          name: "ECDSA",
          hash: { name: "SHA-256" },
        },
        signingKey,
        toBeSigned,
      );
      return signature;
    }
  }
}
```

This signer can then be passed to a JWS or COSE_Sign1.

Lets look at the remote kms API that pairs with this, the code will be different with Google, Microsoft or Amazon KMS interfaces but the general idea will be the same:

```js
export const signer = ({ name, client }: RequestRemoteSigner): { sign: (bytes: UInt8Array) => Promise<UInt8Array> } => {
  return {
    sign: async (bytes: ArrayBuffer) => {

      // on the client before calling the remote signer
      const digest = crypto.createHash("SHA-256")
      digest.update(Buffer.from(bytes))
      const digested = digest.digest()
   
      // calling the remote signer
      const [{ signature }] = await client.asymmetricSign({
        name: name, // identifier for the remote key to be used
        digest: {
          sha256: digested,
        },
      })
      // sometimes need to convert response signature from DER
      return Buffer.from(format.derToJose(Buffer.from(signature)), 'base64')
    },
  }
}
```

As I understand it you are proposing a remote signing API that would treat the device as basically a remote KMS, that can be called from the browser, but where some state from the browser flows to the device.

So you would have some provider setup:

```
const arbitrarySigner = new HardwareSigner({ ... })
```

And then the call to the remote signer would look like this:

```
 const [{ signature }] = await arbitrarySigner.asymmetricSign({
        // no pre-hashing
        value: bytes 
        // with client side pre hashing
        digest: {
          sha256: digested,
       },
})
```

Afterwards, you would construct the JWS or COSE_Sign1 from the result.
You could expose convenience APIs that combine these steps, to reduce implementation burden.

Depending on which crypto the hardware signer supports you would have to map parameters to algorithm names.

So if you are doing EdDSA with Ed25519 (no prehash)

You would use 1 : -8 in the header in COSE, but "alg: EdDSA" in the header in JOSE.

The algorithm identifiers in JOSE and COSE will either be compatible with the cryptographic capabilities of the device, or they won't.

Regarding Pre-Hashing, see Section 5.4 of https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf for some recent guidance on this subject, especially the bottom part about how to construct a pre hash signature with ML-DSA:

```
𝑀 ← BytesToBits(IntegerToBytes(1, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥 ∥ OID ∥ PH𝑀)
```

With ES256 there is no way to tell if the content was hashed on the client or server, and there is no domain separation required in the algorithm, or binding to the hash function used.

If you wanted to make a pre hash version of ES256 where there was domain separation, you would start by constructing the toBeSigned bytes like so:

```
algorithmIdentifier = "ES256 with SHA-384 PreHash" (or an OID or an entry in an IANA registry saying the same thing)
preHashedMessage = sha384(message)
toBeSignedWithPreHashing =  context + algorithmIdentifier + preHashedMessage
``` 

(this is just an example)

In COSE, ES256 means SHA-256, prehash (no domain separation), but ECDSA with P-256 / P-384 / P-521.

- https://datatracker.ietf.org/doc/html/rfc8152#section-8.1

In COSE, ES256 means SHA-256, prehash (no domain separation), but with ONLY P-256.

- https://datatracker.ietf.org/doc/html/rfc7518#section-3.4

If your goal is to support these algorithms, in COSE, without creating any new algorithm identifiers, you can expose the following interface instead of a fully specified algorithm identifier (which don't exist in COSE as of this post):

{ kty, crv, alg } -> [ 1, 1, -7 ]  / 0x83010126 -> ECDSA with P-256 and SHA-256, your API can map the fully specified parameters your hardware needs, to the parameterization that a COSE API needs.

In the long term, it would be better to fully specify the signing algorithm, so that a single identifier can be used to negotiate capabilities between the devices and web authn.








-- 
GitHub Notification of comment by OR13
Please view or discuss this issue at https://github.com/w3c/webauthn/pull/2078#issuecomment-2364535837 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Friday, 20 September 2024 20:04:06 UTC