RAW private key import failure on WebKit browsers
Opened this issue · 2 comments
When performing a DER decode of a private key there seems to be an incompatibility with WebKit's WebCrypto implementation. We noticed that some key inputs would work in all browsers, but some would fail in Safari.
When importing a key from the RAW format, the key bytes are serialized into an EcPrivateKey data class and then further wrapped in the PKCS8 format via PrivateKeyInfo data class. This happens in the beforeEncoding method:
Now this works just fine in Blink based browsers such as Chrome, but does not work in WebKit based browsers such as Safari. This ticket appears to be at least part of the reason why: https://bugs.webkit.org/show_bug.cgi?id=233705
After some trial and error using an ASN1 decoder to diagnose the data I did find one specific shape that WebKit will accept, and that is to encode the EcPrivateKey (aka RFC5915?) payload as follows:
- include the public key bytes
- omit the
parameters
The path through beforeEncoding mentioned above does omit the parameters field, but does not allow for a way to include the public key so always fails.
This means we cannot use the RAW encoding path, but we can use certain DER encoded inputs provided the payload is constructed in a compatible way. Often the inputs we see are DER encoded data that has parameters in the EcPrivateKey payload and so it fails even though the public key is present.
As a workaround we have taken to using a re-encoding dance to pass the data through to the WebCrypto API in a compatible format by doing the following (curve designators are hard-coded for simplicity):
// Decodes a private key from DER encoded keypair data in a way that is compatible with WebCrypto on WebKit:
suspend fun decodePrivateKey(keyPairDerBytes: ByteArray): ECDH.PrivateKey {
val ecdh = CryptographyProvider.Default.get(ECDH)
val decoder = ecdh.privateKeyDecoder(EC.Curve.P256)
val decodedKeys = Der.decodeFromByteArray<EcPrivateKey>(keyPairDerBytes)
// Re-encode the key pair with parameters omitted:
val derWithoutParams = Der.encodeToByteArray(
EcPrivateKey(
version = 1,
privateKey = decodedKeys.privateKey,
publicKey = decodedKeys.publicKey,
),
)
// Wrap the encoded key as PKCS8:
val nestedBytes = Der.encodeToByteArray(
PrivateKeyInfo(
version = 0,
privateKeyAlgorithm = EcKeyAlgorithmIdentifier(EcParameters(ObjectIdentifier.secp256r1)),
privateKey = derWithoutParams,
),
)
// User DER format here will allow the encoded data to pass through to WebCrypto's `importKey` method directly:
return decoder.decodeFromByteArray(EC.PrivateKey.Format.DER, nestedBytes)
}While this workaround has been useful for us, but I am not sure what the real fix should be for consumers of this library so did not attempt a direct fix. A first step should be some unit tests to demonstrate the problem but the tests run in Chrome and so does not surface the problem. Perhaps a demo KMP app that exercises key encoding and can be loaded into Safari would be an alternative but I haven't had time to set that up yet.
Hey! Thanks for raising the issue. I was able to reproduce it locally!
include the public key bytes
Oh, that sounds a bit funny because CoreCrypto (which is used under the hood of apple provider) also doesn't support importing private keys without public keys. It's probably related :)
omit the parameters
That feels strange... But at least I understand where to start investigating. Thanks!
JIC, there are multiple implementations of private key import in WebKit, and it looks like one of them really doesn't support parameters inside of EcPrivateKey structure...
But, looks like by default gcrypt implementation is used?
I will see what is possible to do here; hopefully, there is an easy fix!
FYI, it's possible to run all browser tests in Safari in the project, just by adding useSafari() instead of or in addition to useChromeHeadless() here.
(and I see more than just failures in ECDSA :()
I will see if it's possible to enable those on CI...
JIC, there are multiple implementations of private key import in WebKit
Interesting. Perhaps that is a symptom of the API design with the format selection? It does appear to apply different restrictions depending on whether it is raw vs pkcs8 vs spki which I found a bit surprising.
FYI, it's possible to run all browser tests in Safari in the project, just by adding useSafari()
Thank you! I had no idea that Safari was available there. I am trying this out now to add some test coverage for the import issue.
BTW, it is fine locally, but may be tricky to get Safari tests working in CI due to the security dialog that pops up. There appear to be some workarounds we can try: https://youtrack.jetbrains.com/issue/KT-47392/KJS-Inconsistent-test-running-in-Safari