Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "sign" extension #2078

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft

Add "sign" extension #2078

wants to merge 6 commits into from

Conversation

emlun
Copy link
Member

@emlun emlun commented May 28, 2024

This extension allows for signing arbitrary data using a key associated with but different from a WebAuthn credential key pair. Motivating use cases of this include:

  • Enabling use of attested, hardware-bound signing keys for applications such as digital identity wallets and similar verifiable credentials (client-RP layer)
  • Using FIDO security keys (possibly unattended) for general-purpose digital signatures, with seamless interoperability with existing cryptographic protocols (client-authenticator layer)

By "signing arbitrary data" we mean a distinction from a WebAuthn assertion signature, which signs not over the challenge parameter provided by the RP or client, but over the concatenation of authenticator data and a hash of a JSON object embedding that challenge. In contrast, signatures returned from this extension are made over the given input unaltered. The signing key pair is distinct from its parent WebAuthn credential key pair, so this arbitrary input cannot be used to bypass the domain binding restrictions for WebAuthn credentials.

This addresses some of the same use cases as #1895 would, but goes a step further to enable truly hardware-bound keys. As discussed at some length in #1945, WebCrypto keys are never truly unextractable unless the client enforces domain separation before converting PRF outputs to CryptoKeys. Even then, those keys are not hardware-bound as they are exposed to the client process. This PR is what was meant by "pursuing [...] other ways" in #1945 (comment).

This extension does not cover encryption use cases as #1895 and #1945 would, but instead we intend to also propose an architecturally analogous kem (key encapsulation mechanism) extension to address those use cases.


Preview | Diff

@nsatragno
Copy link
Member

Have you looked at https://github.com/WebKit/explainers/tree/main/remote-cryptokeys? Is there some overlap?

@emlun
Copy link
Member Author

emlun commented Jun 13, 2024

Thanks, I was not aware of that. There is some overlap, and it should be fairly straightforward to make the CTAP layer of this "sign" extension compatible as a key store backend for remote CryptoKeys. However, remote CryptoKeys doesn't address all the concerns that informed the design of the "sign" extension:

  • Origin-bound keys. The "sign" extension inherits the origin binding from WebAuthn. Remote CryptoKeys recommends, but does not require, that keys be scoped to a particular web origin.
  • Fixed key capabilities. To prevent downgrade-style attacks, the "sign" extension fixes the capabilities of keys (specifically: whether the key requires UP/UV) at creation time. Remote CryptoKeys recommends, but does not require, some unspecified access control, and getRemoteKey() presumably allows accessing the same key with different keyUsages arguments. Perhaps individual key store implementations could forbid this, but that's a very weak promise for the API as a whole in that case. Especially without...
  • Attestation. This is the really big one - support for hardware attestation will be required if the API is to be used for things like (inter)national digital identity wallets. The "sign" extension supports this in much the same way as the top-level WebAuthn attestation, and the attestation signs over the fixed key capabilities described above. The Web Crypto API is not well equipped to convey hardware attestation information to the RP, and this is also missing from the current Remote CryptoKeys proposal.
  • Interoperability. The "sign" extension defines an explicit interop protocol (on top of CTAP) between client and authenticator, so there's a concrete path for authenticator vendors to implement these features and have them work in any browser that supports the extension. Remote CryptoKeys only vaguely suggests that "[the key store] may be a secure key store, password manager, USB or BlueTooth device, etc." but makes no attempt to define an interface for it, let alone require some minimal interop profile, so compatibility is certain to differ between browsers. This might even be one of the Non-Goals, depending on how you read that section?

I do agree that WebCrypto is in some ways a more appropriate home for these features. But on the other hand, one powerful benefit of doing this in WebAuthn instead, with algorithm identifiers etc. sent to and interpreted only by the authenticator, is that authenticators can introduce support for new algorithms without the client explicitly supporting it. For example, we want to be able to create signatures using a key derived by ARKG - in the "sign" extension this only needs a new alg value understood by both the RP and the authenticator, no change to the client is needed. Same goes if we want to, say, use different elliptic curves or introduce PQC algorithms. With WebCrypto, all these things would need standardizing new AlgorithmIdentifier values/subtypes and waiting for all relevant browsers to implement them.

@ve7jtb
Copy link
Contributor

ve7jtb commented Jun 13, 2024

What we are trying to do is create a standardized API for a WSCD https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework/blob/v1.4.0/docs/arf.md#42-reference-architecture

The EU has strict certification requirements for the storage of private keys. These are requirements met by only a handful of mobile phones.

This gives Fido/Passkeys an opening to provide the crypto functionality used by EUDI wallets.

Attestation is an important part of the value proposition to make this work for EUDI.

While exposing this via webcrypto would be interesting, the larger use is actually for native applications, which will have to use cloud HSM for the storage unless we come up with a local alternative. This extension would also ideally be provided by the platform authenticator and could be CC certified.

@marsouin
Copy link

Passkeys would be an ideal use case for the EUDI wallet, but it would be great to prevent the keys from being exposed to Javascript for instance. Also support for different schemes would be ideal, especially in order to provide ZK proofs (I'm thinking EdDSA with different curves).

@emlun
Copy link
Member Author

emlun commented Jul 1, 2024

it would be great to prevent the keys from being exposed to Javascript for instance

Indeed, this is precisely the goal of this extension.

Also support for different schemes would be ideal

We've designed the extension with some algorithm agility to hopefully support this, but this would of course still rely on standardization of algorithm IDs and data interchange formats.

@nadalin nadalin added this to the Futures (catch-all) milestone Jul 10, 2024
Copy link
Contributor

@selfissued selfissued left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I support this idea. Where are we on implementations?

@emlun emlun marked this pull request as draft September 11, 2024 15:28
@emlun
Copy link
Member Author

emlun commented Sep 11, 2024

Thank you @selfissued!

Where are we on implementations?

Only barely started, I'm afraid. We (Yubico) have some rough internal proof-of-concept prototypes (of all three of authenticator, client and RP), but nothing yet ready to share. No commitments from other WG participants at this point.

Note that as noted in the inline `ISSUE` annotations, what "pre-hashed" means is
currently underspecified and will need to be clarified.
@nadalin nadalin added the @Risk Items that are at risk for L3 label Sep 18, 2024
@OR13
Copy link

OR13 commented Sep 20, 2024

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:

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:

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.

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

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.

@emlun
Copy link
Member Author

emlun commented Sep 21, 2024

Hi @OR13 - is this a reply to my email to the COSE/JOSE mail lists?

@OR13
Copy link

OR13 commented Sep 21, 2024

Yes, but also to the part of the PR that questions how much details you need for pre hash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants