webauthn
webauthn copied to clipboard
Add "sign" extension
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 https://github.com/w3c/webauthn/pull/1945#issuecomment-1813211351.
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.
Have you looked at https://github.com/WebKit/explainers/tree/main/remote-cryptokeys? Is there some overlap?
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 differentkeyUsagesarguments. 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.
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.
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).
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.
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.
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.
- 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.
Hi @OR13 - is this a reply to my email to the COSE/JOSE mail lists?
Yes, but also to the part of the PR that questions how much details you need for pre hash.
We are now iterating on the extension API in this fork repo: https://github.com/yubicolabs/webauthn-sign-extension
We'll cherry-pick the relevant changes back into this PR when we're satisfied with the design.
I've updated the PR to match draft version 3 from the fork repo. This includes the following changes:
-
Version 3
- Published: 2025-05-19
- Client: Fixed CBOR map key in reference to authenticator data embedded in unsigned extension output.
- Editorial and formatting fixes.
-
Version 2
- Published: 2025-04-07
- Changed error code when
allowListis empty - Moved
att-objfrom authenticator data to unsigned extension outputs and client extension outputs - Changed
key-refs: [+ bstr]authenticator input to singlekey-ref: bstr - Reference [[I-D.cose-2p-algs]] instead of ARKG for definition of COSE_Key_Ref
- Deleted
generatedKey.keyHandleclient extension output - Added
algauthenticator output andgeneratedKey.algorithmclient output - Renamed
phDatainput totbs - Removed assumption of
tbsbeing pre-hashed by the RP; this may instead be signaled using distinct COSEAlgorithmIdentifier values in thegenerateKey.algorithmsinput. - Changed CBOR alias
tbs = 0(previouslyphData = 0) totbs = 6
For consistency with the rest of the spec and previous PRs that have already addressed this, the use of USVString should be changed to DOMString instead.