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

Conversation

emlun
Copy link
Member

@emlun emlun commented Aug 23, 2023

Fixes #1895.

This draft allows as much flexibility in the importKey invocation as possible. It might be a better idea to hard-code this key import to use for example HKDF instead; I'll open a separate meta-PR for that.


Preview | Diff

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"}.

};

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.


dictionary AuthenticationExtensionsPRFImportCryptoKeyParams {
required KeyFormat format;
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.

@nadalin nadalin added the @Risk Items that are at risk for L3 label Sep 6, 2023
@agl
Copy link
Contributor

agl commented Sep 6, 2023

(Deriving EC private keys from a seed: https://boringssl.googlesource.com/boringssl/+/refs/heads/master/crypto/ec_extra/ec_derive.c#28)

@emlun
Copy link
Member Author

emlun commented Sep 6, 2023

Summary of discussion on 2023-09-06 WG call:

There needs to be a point to doing this, and the unique value proposition we can offer here is that the platform can guarantee that keys imported this way are truly unextractable. The most promising use case we imagine is asymmetric encryption: using PRF to derive a truly unextractable private key which can be used for encryption (i.e., RSA) or to derive symmetric encryption keys (i.e., ECDH). Even though any derived or wrapped keys will be extractable, the encryption use case mitigates that issue since the keys will be present only when the cleartext is also, and the unextractable asymmetric key allows for rotating the symmetric keys at frequent intervals to further mitigate the impact of a leaked symmetric key.

As such, the #1946 alternative of hard-coding this to import an HKDF key would be rather pointless, since any keys derived from that would be extractable without enabling the use cases enabled by an asymmetric key. We will instead move this design further toward having the client do more in its controlled environment to enable truly unexportable asymmetric keys.

@emlun emlun marked this pull request as draft September 8, 2023 14:37
@selfissued selfissued requested a review from ve7jtb September 12, 2023 13:52
@MasterKale
Copy link
Contributor

WG meeting @ TPAC 9/12: We've spent a lot of time trying to figure out what direction to go with this. However we've not yet identified sufficient interest in a discrete use case (e.g. provider-centric need for prf output) to say that this "swiss army knife" PR (vs the more limited #1946 that got closed) should be pursued any further.

Leaving this open for now, but unless a specific use case for prf can be identified that cannot be served by this extension in its current form then we should probably close it at the next meeting.

@emlun
Copy link
Member Author

emlun commented Sep 20, 2023

2023-09-20 WG call: @emlun to write an explainer of a concrete use case to inform the design and the go/no-go decision.

@emlun
Copy link
Member Author

emlun commented Oct 4, 2023

To summarize some of the discussion above, here's the rationale for which types of keys would be relevant to support:

  • HMAC: no, because noone else would have the key.
  • HKDF: no, because derived keys would still be extractable.
  • PBKDF2: not applicable.
  • AES: no, because encryption keys do not need to be protected from parties handling the cleartext (assuming each key is used for only one message).
  • RSA (encrypt): yes, ignoring key size limitations.
  • ECDH: yes, see below.
  • RSA (sign): maybe - less useful than RSA (encrypt).
  • ECDSA: maybe - less useful than ECDH.

As a concrete use case, I've recently been involved in a pilot project prototyping a web-based wallet app that uses WebAuthn PRF to derive encryption keys used to encrypt wallet contents. The current encryption architecture used in the pilot is described here. Aside from the self-critique already covered there, this app could benefit from having access to unextractable ECDH private keys.

It is possible with current PRF and WebCrypto capabilities to use RSA or ECDH for asymmetric encryption, so that one can have multiple PRF keys without having all of them present whenever encrypted content changes. But the drawback is that the asymmetric private keys are not hardware bound, and thus could be leaked if there's malicious script on the page. Leaking the asymmetric private keys is worse than leaking the encryption keys derived from them - whenever the encryption keys are in memory, the decrypted cleartext is too, but the encryption keys can be rotated frequently to protect future versions of the encrypted contents. Thus a leaked encryption key would give access only to one version of the encrypted data, which was already decrypted in memory at the time - but a leaked asymmetric private key would give access to all future versions as well.

A similar case could be made for RSA and ECDSA signing keys to sign arbitrary data, but this would not be useful for applications that need single-use signing keys for privacy. I plan to bring that use case up separately.

So, with this in mind, I think the most useful key types to support are ECDH and possibly RSA-OAEP. For that, I imagine the API could look something like this:

extensions: {
  prf: {
    eval: {
      first: new Uint8Array([1, 2, 3, 4]),
    },
    deriveCryptoKey:
      // EcKeyGenParams
      { name: "ECDH", namedCurve: "P-256" },

      // OR RsaHashedKeyGenParams
      { name: "RSA-OEAP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
  }
}

where the client would then use the PRF output as a seed for deriving the private key by some deterministic procedure, for example that described in NIST SP-800-56Ar3 section 5.6.1.2.1 ([ECC] Key Pair Generation Using Extra Random Bits) (which seems similar to the Montgomery reduction in BoringSSL linked above?).

It may be that it's not appropriate to derive RSA keys from only 32 bytes of entropy, in which case ECDH would be the only valid key type.

(I tried to add an example implementation of asymmetric encryption as a collapsed section, but the markdown didn't seem to render correctly. You can find the example code in the second-oldest entry in this comment's edit history.)

@emlun
Copy link
Member Author

emlun commented Oct 4, 2023

On the other hand one could argue that this still misses the mark - it's still the client handling the private keys, so they are still not hardware-bound. Perhaps we should instead do this with a new extension that enables actually hardware-bound private keys.

But again in favour of doing this in PRF: this would be compatible with existing authenticators that support the CTAP2 hmac-secret extension; a new extension would not be compatible with existing authenticators.

@Firstyear
Copy link
Contributor

On the other hand one could argue that this still misses the mark - it's still the client handling the private keys, so they are still not hardware-bound. Perhaps we should instead do this with a new extension that enables actually hardware-bound private keys.

Yes, but we don't have a TPM here. I think if someone wants hardware bound keys, they need to use a proper HSM. We aren't false advertising at all about what this will do.

Where this is useful is short lived keys. Where the key is used and destroyed quickly. For example, a password manager which quickly gets the secret and does the decrypt of the secret and exposes it in memory, then the secret is used and destroyed. Or the ability to sign an email, or something else.

I think this still has a use and I think it has a place. It's not missing a mark at all. We are allowing short-lived, derived keys that exist in memory, and there is a "chain" that to hold the key, you very likely have the hardware authenticator with you, but it's not a strict promise the key is bound to the authenticator.

fwadnjar

This comment was marked as off-topic.

@emlun
Copy link
Member Author

emlun commented Nov 15, 2023

We're pursuing these use cases in other ways which, unlike this, would enable actually hardware-bound keys that do not leave the authenticator.

@emlun
Copy link
Member Author

emlun commented May 30, 2024

PR #2078 is the "other ways" referred to in my previous comment.

@emlun emlun deleted the prf-cryptokey branch May 30, 2024 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposal/discussion: non-extractable CryptoKey output from the prf extension
6 participants