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 importCryptoKey input to PRF extension #1945

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 85 additions & 7 deletions index.bs

This comment was marked as off-topic.

This comment was marked as off-topic.

Original file line number Diff line number Diff line change
Expand Up @@ -6754,6 +6754,14 @@ Note: this extension may be implemented for [=authenticators=] that do not use [
dictionary AuthenticationExtensionsPRFInputs {
AuthenticationExtensionsPRFValues eval;
record<USVString, AuthenticationExtensionsPRFValues> evalByCredential;
AuthenticationExtensionsPRFImportCryptoKeyParams importCryptoKey;
};

dictionary AuthenticationExtensionsPRFImportCryptoKeyParams {
required KeyFormat format;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nothing but Raw can work as a format, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, at least not without preprocessing by the client.

For example, I did one experiment where my application code wrapped the 32 raw bytes in a PKCS#8 header and fed it into importKey as a P-256 private key. It "works" (except when it doesn't because of the key domain mismatch, see other thread below), but it's quite an ugly hack.

required AlgorithmIdentifier algorithm;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to subset algorithms?

HMAC is useless, right? Because nobody else could have the HMAC key. (And there's HKDF if someone wants a PRF.)

RSA is out.

PBKDF2 doesn't make sense?

AES-* work.

ECDSA and ECDH? We could define things to work with P-256 if we wanted. Are asymmetric algorithms interesting? The ability to sign things as an identity without throwing UI? The ability to public-key decrypt? I guess this would imply bringing in CryptoKeyPair.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we want to subset algorithms?

Maybe not? See: #1946

HMAC is useless, right? Because nobody else could have the HMAC key. (And there's HKDF if someone wants a PRF.)

Mostly yes, unless you import it as extractable and use wrapKey to move it somewhere else (or exportKey of course, but that defeats the point of all this).

PBKDF2 doesn't make sense?

Agreed.

Are asymmetric algorithms interesting? The ability to sign things as an identity without throwing UI? The ability to public-key decrypt?

Signing is a bit awkward due to the lack of UI as you note, but asymmetric encryption certainly is interesting. For example, it would allow for encrypting to multiple PRF-backed keys for redundancy. This can be done today via key wrapping, but not with never-extractable keys.

ECDSA and ECDH? We could define things to work with P-256 if we wanted.

Right, we could. We can't naively use the PRF directly as a private key, because the P-256 key space is slightly smaller than the full 232-1 range, so we'd need to specify some way to either fail out or recalculate some input salt in case the key falls outside the valid range. I would prefer if we could do something like that generically in WebCrypto, for example extend HKDF with asymmetric keys as output keys, but I presume it would take far more effort to get something like that accepted and rolled out in implementations.

The more I've thought about this, though, the more I've come to think that non-extractability in WebCrypto perhaps isn't that impressive after all. A malicious actor that has the ability to access CryptoKey objects probably also has the ability to overwrite properties on window.crypto.subtle - and therefore also has the ability to intercept any WebCrypto invocation, set extractable to true and exfiltrate the key. Even without that, just access to a CryptoKey object is enough to use that key for signing and decryption operations - you don't really need to steal the keys if you can just steal what they were meant to protect.

I still think there is value in the ability to import PRF outputs directly to CryptoKey objects, as that still reduces an honest application's exposure to raw key material. But I'm not sure that non-extractability is really that important - and if it's not, we might as well hard-code this to output HKDF keys and let application developers go from there. (WebCrypto HKDF cannot derive asymmetric keys, but you can generate asymmetric keys and wrap them with HKDF-derived keys.)

On the other hand, I guess this line of reasoning also comes back around in favour of importing PRF outputs directly to asymmetric private keys - since the domain separation here actually can make truly non-extractable keys. It's still true that malicious code could abuse the resulting CryptoKey object, but a truly non-extractable ECDH key is a powerful thing to have access to. Again in the multi-recipient encryption example, you could use ECDH to derive symmetric encryption keys - those CryptoKeys would be extractable, but they would exist only alongside the cleartext anyway. And with ECDH you could rotate the encryption keys after every decryption, so that any leaked key won't be able to access any future versions of the contents.

So okay, do you think it would make sense to specify a procedure for the client to transform the PRF output into an asymmetric key? I'm thinking in that case we should probably base it on HKDF or the like, rather than restricting ourselves to key sizes around 32 bits only.

required boolean extractable;
required sequence<KeyUsage> keyUsages;
};

partial dictionary AuthenticationExtensionsClientInputs {
Expand All @@ -6767,17 +6775,56 @@ Note: this extension may be implemented for [=authenticators=] that do not use [

: <dfn>evalByCredential</dfn>
:: A record mapping [=base64url encoding|base64url encoded=] [=credential IDs=] to PRF inputs to evaluate for that credential. Only applicable during [=assertions=] when {{PublicKeyCredentialRequestOptions/allowCredentials}} is not empty.

: <dfn>importCryptoKey</dfn>
:: Arguments for the [=client=] to invoke {{SubtleCrypto/importKey}}, along with the PRF result as the `keyData` argument.
If present, the extension outputs will be {{Promise}}s resolving to {{CryptoKey}} values;
if not present, the extension outputs will be {{BufferSource}} values.

The client ensures domain separation between {{BufferSource}} and {{CryptoKey}} results,
so that an extension input requesting unextractable {{CryptoKey}} values cannot be "downgraded"
to requesting the same results as extractable {{BufferSource}} values.
For the same reason, the client also ensures domain separation between the [TRUE] and [FALSE] values
Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect the domain separation should cover all the parameters? Here's a silly example, but perhaps there are better ones:

A service uses this facility to get an opaque AES-GCM key and claims that, even if the Javascript is compromised, the compromise is limited to existing ciphertexts and ciphertexts that are encrypted in the future, after the compromise is eliminated, are safe again because the key couldn't be extracted.

But if they use a counter for a nonce, which is safe in general, an attacker could tweak the import parameters to get an opaque AES-ECB key and precompute Ek(x) for the x that will be used by future AES-GCM ciphertexts.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds fair. Note that WebCrypto doesn't support AES-ECB, but I suppose you could probably emulate it by feeding single blocks at a time into AES-CBC.

I suppose for that we would need something like a hash of a canonical representation of the arguments - one invariant in order of EcKeyImportParams members, for example, or maybe even invariant in "AES-GCM" vs. {name: "AES-GCM"}.

of <code>{{importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code>.

Note: The {{KeyFormat}}, {{AlgorithmIdentifier}} and {{KeyUsage}} types are defined in [[!WebCryptoApi]].
</div>

: Client extension processing ([=registration extension|registration=])
::
1. If {{AuthenticationExtensionsPRFInputs/evalByCredential}} is present, return a {{DOMException}} whose name is “{{NotSupportedError}}”.
1. Set `hmac-secret` to [TRUE] in the authenticator extensions input.
1. If {{AuthenticationExtensionsPRFInputs/eval}} is present and a future extension to [[FIDO-CTAP]] permits evaluation of the PRF at creation time, configure `hmac-secret` inputs accordingly:
* Let `salt1` be the value of <code>SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || {{AuthenticationExtensionsPRFInputs/eval}}.{{AuthenticationExtensionsPRFValues/first}})</code>.
* If <code>{{AuthenticationExtensionsPRFInputs/eval}}.{{AuthenticationExtensionsPRFValues/second}}</code> is present, let `salt2` be the value of <code>SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || {{AuthenticationExtensionsPRFInputs/eval}}.{{AuthenticationExtensionsPRFValues/second}})</code>.

* Let |prefix| be a byte string as follows:
* If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is not present, let |prefix| be <code>UTF8Encode("WebAuthn PRF")</code>.
* If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is present and <code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> is [TRUE], let |prefix| be <code>UTF8Encode("WebAuthn PRF:CryptoKey")</code>.
* If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is present and <code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> is [FALSE], let |prefix| be <code>UTF8Encode("WebAuthn PRF:CryptoKey:Extractable")</code>.
* Let `salt1` be the value of <code>SHA-256(|prefix| || 0x00 || {{AuthenticationExtensionsPRFInputs/eval}}.{{AuthenticationExtensionsPRFValues/first}})</code>.
* If <code>{{AuthenticationExtensionsPRFInputs/eval}}.{{AuthenticationExtensionsPRFValues/second}}</code> is present, let `salt2` be the value of <code>SHA-256(|prefix| || 0x00 || {{AuthenticationExtensionsPRFInputs/eval}}.{{AuthenticationExtensionsPRFValues/second}})</code>.
1. Set {{AuthenticationExtensionsPRFOutputs/enabled}} to the value of `hmac-secret` in the authenticator extensions output. If not present, set {{AuthenticationExtensionsPRFOutputs/enabled}} to [FALSE].
1. Set {{AuthenticationExtensionsPRFOutputs/results}} to the decrypted PRF result(s), if any.
1. If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is not present:
1. Set {{AuthenticationExtensionsPRFOutputs/results}} to the decrypted PRF result(s), if any.

1. If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is present:
1. Let |firstOutput| and |secondOutput| be {{BufferSource}} values containing the respective decrypted PRF result(s), if any.
1. Let |subtleCrypto| be an instance of {{SubtleCrypto}}.
1. If |firstOutput| is present, set
<code>{{AuthenticationExtensionsPRFOutputs/results}}.{{AuthenticationExtensionsPRFValues/first}}</code>
to the result of invoking <code>|subtleCrypto|.{{SubtleCrypto/importKey}}</code> with the arguments
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/format}}</code>,
|firstOutput|,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/algorithm}}</code>,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> and
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/keyUsages}}</code>.
1. If |secondOutput| is present, set
<code>{{AuthenticationExtensionsPRFOutputs/results}}.{{AuthenticationExtensionsPRFValues/second}}</code>
to the result of invoking <code>|subtleCrypto|.{{SubtleCrypto/importKey}}</code> with the arguments
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/format}}</code>,
|secondOutput|,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/algorithm}}</code>,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> and
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/keyUsages}}</code>.

Note: If PRF results are obtained during [=registration=] then the [=[RP]=] MUST inspect the [=UV=] bit in the [=flags=] of the response in order to determine the correct value of {{PublicKeyCredentialRequestOptions/userVerification}} for future [=assertions=]. Otherwise results from [=assertions=] may be inconsistent with those from the [=registration=].

Expand All @@ -6790,10 +6837,36 @@ Note: If PRF results are obtained during [=registration=] then the [=[RP]=] MUST
1. If {{AuthenticationExtensionsPRFInputs/evalByCredential}} is present and [=map/exists|contains=] an [=map/entry=] whose [=map/key=] is the [=base64url encoding=] of the [=credential ID=] that will be returned, let |ev| be the [=map/value=] of that entry.
1. If |ev| is null and {{AuthenticationExtensionsPRFInputs/eval}} is present, then let |ev| be the value of {{AuthenticationExtensionsPRFInputs/eval}}.
1. If |ev| is not null:
1. Let `salt1` be the value of <code>SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || |ev|.{{AuthenticationExtensionsPRFValues/first}})</code>.
1. If <code>|ev|.{{AuthenticationExtensionsPRFValues/second}}</code> is present, let `salt2` be the value of <code>SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || |ev|.{{AuthenticationExtensionsPRFValues/second}})</code>.

1. Let |prefix| be a byte string as follows:
- If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is not present, let |prefix| be <code>UTF8Encode("WebAuthn PRF")</code>.
- If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is present and <code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> is [TRUE], let |prefix| be <code>UTF8Encode("WebAuthn PRF:CryptoKey")</code>.
- If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is present and <code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> is [FALSE], let |prefix| be <code>UTF8Encode("WebAuthn PRF:CryptoKey:Extractable")</code>.
1. Let `salt1` be the value of <code>SHA-256(|prefix| || 0x00 || |ev|.{{AuthenticationExtensionsPRFValues/first}})</code>.
1. If <code>|ev|.{{AuthenticationExtensionsPRFValues/second}}</code> is present, let `salt2` be the value of <code>SHA-256(|prefix| || 0x00 || |ev|.{{AuthenticationExtensionsPRFValues/second}})</code>.
1. Send an `hmac-secret` extension to the [=authenticator=] using the values of `salt1` and, if set, `salt2` as the parameters of the same name in that process.
1. Decrypt the extension result and set {{AuthenticationExtensionsPRFOutputs/results}} to the PRF result(s), if any.
1. If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is not present:
1. Set {{AuthenticationExtensionsPRFOutputs/results}} to the decrypted PRF result(s), if any.

1. If {{AuthenticationExtensionsPRFInputs/importCryptoKey}} is present:
1. Let |firstOutput| and |secondOutput| be {{BufferSource}} values containing the respective decrypted PRF result(s), if any.
1. Let |subtleCrypto| be an instance of {{SubtleCrypto}}.
1. If |firstOutput| is present, set
<code>{{AuthenticationExtensionsPRFOutputs/results}}.{{AuthenticationExtensionsPRFValues/first}}</code>
to the result of invoking <code>|subtleCrypto|.{{SubtleCrypto/importKey}}</code> with the arguments
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/format}}</code>,
|firstOutput|,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/algorithm}}</code>,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> and
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/keyUsages}}</code>.
1. If |secondOutput| is present, set
<code>{{AuthenticationExtensionsPRFOutputs/results}}.{{AuthenticationExtensionsPRFValues/second}}</code>
to the result of invoking <code>|subtleCrypto|.{{SubtleCrypto/importKey}}</code> with the arguments
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/format}}</code>,
|secondOutput|,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/algorithm}}</code>,
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/extractable}}</code> and
<code>{{AuthenticationExtensionsPRFInputs/importCryptoKey}}.{{AuthenticationExtensionsPRFImportCryptoKeyParams/keyUsages}}</code>.

: Authenticator extension input / processing / output
:: [=prf|This extension=] uses the [[FIDO-CTAP]] `hmac-secret` extension when communicating with the authenticator. It thus does not specify any direct authenticator interaction for [=[RPS]=].
Expand All @@ -6802,7 +6875,12 @@ Note: If PRF results are obtained during [=registration=] then the [=[RP]=] MUST
:: <xmp class="idl">
dictionary AuthenticationExtensionsPRFOutputs {
boolean enabled;
AuthenticationExtensionsPRFValues results;
(AuthenticationExtensionsPRFValues or AuthenticationExtensionsPRFCryptoKeyValues) results;
};

dictionary AuthenticationExtensionsPRFCryptoKeyValues {
required Promise<CryptoKey> first;
Promise<CryptoKey> second;
};

partial dictionary AuthenticationExtensionsClientOutputs {
Expand Down
Loading