diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs index d2b9b688e5d449..ad3ad72299f271 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs @@ -9,6 +9,7 @@ using System.Net; using System.Runtime.Serialization; using System.Runtime.Versioning; +using System.Security.Cryptography.Asn1; using System.Security.Cryptography.X509Certificates.Asn1; using System.Text; using Internal.Cryptography; @@ -24,9 +25,8 @@ public class X509Certificate2 : X509Certificate private volatile PublicKey? _lazyPublicKey; private volatile AsymmetricAlgorithm? _lazyPrivateKey; private volatile X509ExtensionCollection? _lazyExtensions; - private static readonly string[] s_EcPublicKeyPrivateKeyLabels = { PemLabels.EcPrivateKey, PemLabels.Pkcs8PrivateKey }; - private static readonly string[] s_RsaPublicKeyPrivateKeyLabels = { PemLabels.RsaPrivateKey, PemLabels.Pkcs8PrivateKey }; - private static readonly string[] s_DsaPublicKeyPrivateKeyLabels = { PemLabels.Pkcs8PrivateKey }; + private static readonly string[] s_RsaPublicKeyPrivateKeyLabels = [PemLabels.RsaPrivateKey, PemLabels.Pkcs8PrivateKey]; + private static readonly string[] s_DsaPublicKeyPrivateKeyLabels = [PemLabels.Pkcs8PrivateKey]; public override void Reset() { @@ -1378,18 +1378,7 @@ public static X509Certificate2 CreateFromPem(ReadOnlySpan certPem, ReadOnl s_DsaPublicKeyPrivateKeyLabels, static keyPem => CreateAndImport(keyPem, DSA.Create), certificate.CopyWithPrivateKey), - Oids.EcPublicKey when IsECDiffieHellman(certificate) => - ExtractKeyFromPem( - keyPem, - s_EcPublicKeyPrivateKeyLabels, - static keyPem => CreateAndImport(keyPem, ECDiffieHellman.Create), - certificate.CopyWithPrivateKey), - Oids.EcPublicKey when IsECDsa(certificate) => - ExtractKeyFromPem( - keyPem, - s_EcPublicKeyPrivateKeyLabels, - static keyPem => CreateAndImport(keyPem, ECDsa.Create), - certificate.CopyWithPrivateKey), + Oids.EcPublicKey => ExtractKeyFromECPem(certificate, keyPem), Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024 => ExtractKeyFromPem( keyPem, @@ -1477,18 +1466,7 @@ public static X509Certificate2 CreateFromEncryptedPem(ReadOnlySpan certPem password, static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, DSA.Create), certificate.CopyWithPrivateKey), - Oids.EcPublicKey when IsECDiffieHellman(certificate) => - ExtractKeyFromEncryptedPem( - keyPem, - password, - static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, ECDiffieHellman.Create), - certificate.CopyWithPrivateKey), - Oids.EcPublicKey when IsECDsa(certificate) => - ExtractKeyFromEncryptedPem( - keyPem, - password, - static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, ECDsa.Create), - certificate.CopyWithPrivateKey), + Oids.EcPublicKey => ExtractKeyFromEncryptedECPem(certificate, keyPem, password), Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024 => ExtractKeyFromEncryptedPem( keyPem, @@ -1929,17 +1907,7 @@ private static X509Certificate2 ExtractKeyFromPem( { if (label.SequenceEqual(eligibleLabel)) { - using (TAlg key = factory(contents[fields.Location])) - { - try - { - return import(key); - } - catch (ArgumentException ae) - { - throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae); - } - } + return ExtractKeyFromPem(contents[fields.Location], factory, import); } } } @@ -1947,6 +1915,24 @@ private static X509Certificate2 ExtractKeyFromPem( throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey); } + private static X509Certificate2 ExtractKeyFromPem( + ReadOnlySpan keyPem, + Func, TAlg> factory, + Func import) where TAlg : IDisposable + { + using (TAlg key = factory(keyPem)) + { + try + { + return import(key); + } + catch (ArgumentException ae) + { + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae); + } + } + } + private static X509Certificate2 ExtractKeyFromEncryptedPem( ReadOnlySpan keyPem, ReadOnlySpan password, @@ -2009,5 +1995,238 @@ private static bool HasECDiffieHellmanKeyUsage(X509Certificate2 certificate) // considered valid for all usages, so we can use it for ECDH. return true; } + + [UnsupportedOSPlatform("browser")] + private static X509Certificate2 ExtractKeyFromEncryptedECPem( + X509Certificate2 certificate, + ReadOnlySpan keyPem, + ReadOnlySpan password) + { + Debug.Assert(certificate.GetKeyAlgorithm() == Oids.EcPublicKey); + + foreach ((ReadOnlySpan contents, PemFields fields) in PemEnumerator.Utf16(keyPem)) + { + ReadOnlySpan label = contents[fields.Label]; + + if (!label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey)) + { + continue; + } + + byte[] base64Buffer = CryptoPool.Rent(fields.DecodedDataLength); + int base64ClearSize = CryptoPool.ClearAll; + ArraySegment? decryptedPkcs8 = null; + + try + { + bool result = Convert.TryFromBase64Chars(contents[fields.Base64Data], base64Buffer, out int base64Written); + + if (!result || base64Written != fields.DecodedDataLength) + { + Debug.Fail("Preallocated buffer and validated data decoding failed."); + break; + } + + base64ClearSize = base64Written; + Debug.Assert(!decryptedPkcs8.HasValue); + decryptedPkcs8 = KeyFormatHelper.DecryptPkcs8(password, base64Buffer.AsMemory(0, base64Written), out int bytesRead); + + if (bytesRead != base64Written) + { + break; + } + + X509Certificate2? loaded = ExtractKeyFromECPrivateKeyInfo(certificate, decryptedPkcs8.Value); + + if (loaded is null) + { + break; + } + + return loaded; + } + catch (CryptographicException ce) + { + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ce); + } + finally + { + CryptoPool.Return(base64Buffer, base64ClearSize); + + if (decryptedPkcs8.HasValue) + { + CryptoPool.Return(decryptedPkcs8.Value); + } + } + } + + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey); + } + + [UnsupportedOSPlatform("browser")] + private static X509Certificate2 ExtractKeyFromECPem(X509Certificate2 certificate, ReadOnlySpan keyPem) + { + Debug.Assert(certificate.GetKeyAlgorithm() == Oids.EcPublicKey); + + foreach ((ReadOnlySpan contents, PemFields fields) in PemEnumerator.Utf16(keyPem)) + { + ReadOnlySpan label = contents[fields.Label]; + + if (label.SequenceEqual(PemLabels.EcPrivateKey)) + { + // EC PRIVATE KEYs do not have a key usage, so usage is determined by the certificate. + + // If we can load it is EC-DH, we should prefer that over EC-DSA. ECC keys that are "both" prefer + // to be imported as EC-DH. Importing it as EC-DSA would restrict it to EC-DSA, even if the key + // and certificate are valid for EC-DH. Other platforms don't have such restrictions. + if (IsECDiffieHellman(certificate)) + { + return ExtractKeyFromPem( + keyPem, + static keyPem => CreateAndImport(keyPem, ECDiffieHellman.Create), + certificate.CopyWithPrivateKey); + } + + if (IsECDsa(certificate)) + { + return ExtractKeyFromPem( + keyPem, + static keyPem => CreateAndImport(keyPem, ECDsa.Create), + certificate.CopyWithPrivateKey); + } + + // If we got here, then the key is neither EC-DH or EC-DSA eligible, but we had a matching PEM + // label. Break out and throw. + break; + } + + if (!label.SequenceEqual(PemLabels.Pkcs8PrivateKey)) + { + continue; + } + + byte[] base64Buffer = CryptoPool.Rent(fields.DecodedDataLength); + int clearSize = CryptoPool.ClearAll; + + try + { + bool result = Convert.TryFromBase64Chars(contents[fields.Base64Data], base64Buffer, out int base64Written); + + if (!result || base64Written != fields.DecodedDataLength) + { + Debug.Fail("Preallocated buffer and validated data decoding failed."); + break; + } + + clearSize = base64Written; + X509Certificate2? loaded = ExtractKeyFromECPrivateKeyInfo(certificate, base64Buffer.AsMemory(0, base64Written)); + + if (loaded is null) + { + break; + } + + return loaded; + } + catch (CryptographicException ce) + { + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ce); + } + finally + { + CryptoPool.Return(base64Buffer, clearSize); + } + } + + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey); + } + + [UnsupportedOSPlatform("browser")] + private static X509Certificate2? ExtractKeyFromECPrivateKeyInfo( + X509Certificate2 certificate, + ReadOnlyMemory privateKeyInfo) + { + // We are not going to perform any validation on the PrivateKeyInfo here. We just need a hint + // what algorithm to use. The actual algorithm will do whatever validation on the key as needed. + PrivateKeyInfoAsn privateKeyInfoAsn = PrivateKeyInfoAsn.Decode(privateKeyInfo, AsnEncodingRules.BER); + + const X509KeyUsageFlags EcdsaKeyUsageFlags = + X509KeyUsageFlags.DigitalSignature | + X509KeyUsageFlags.KeyCertSign | + X509KeyUsageFlags.CrlSign; + + X509KeyUsageFlags? usages = GetKeyUsageFlags(in privateKeyInfoAsn); + + // If any of the following is true we should load it as EC-DH + // * There is no keyUsage extension. Loading it as EC-DH will allow loading it as EC-DSA, too. + // * It has any keyUsage that is not a "signing" usage. That at minimum means loading it as EC-DH. + // it may still yet have a keyUsage that allows signing as well, in which case it will work for EC-DSA + // too. + // The certificate must also have a key usage that permits EC-DH, either with "no" usage (in which case + // it will work for EC-DSA, too) or as EC-DH explicitly. + if ((usages is null || (usages & ~EcdsaKeyUsageFlags) != 0) && IsECDiffieHellman(certificate)) + { + using (ECDiffieHellman ecdh = ECDiffieHellman.Create()) + { + ecdh.ImportPkcs8PrivateKey(privateKeyInfo.Span, out int pkcs8Read); + + if (pkcs8Read != privateKeyInfo.Length) + { + Debug.Fail("Unexpected trailing data in PKCS#8 buffer."); + throw new CryptographicException(); + } + + return certificate.CopyWithPrivateKey(ecdh); + } + } + + // If we are here, then either the key or certificate has a key usage that requires loading it as EC-DSA. + if (IsECDsa(certificate)) + { + using (ECDsa ecdsa = ECDsa.Create()) + { + ecdsa.ImportPkcs8PrivateKey(privateKeyInfo.Span, out int pkcs8Read); + + if (pkcs8Read != privateKeyInfo.Length) + { + Debug.Fail("Unexpected trailing data in PKCS#8 buffer."); + throw new CryptographicException(); + } + + return certificate.CopyWithPrivateKey(ecdsa); + } + } + + // If we get here, the key and certificate do not agree on algorithm use (the key has digitalSignature but + // the certificate has keyAgreement, for example). It cannot be loaded. + return null; + } + + private static X509KeyUsageFlags? GetKeyUsageFlags(ref readonly PrivateKeyInfoAsn keyInfo) + { + if (keyInfo.Attributes is null) + { + return null; + } + + foreach (AttributeAsn attr in keyInfo.Attributes) + { + if (attr.AttrType != Oids.KeyUsage) + { + continue; + } + + if (attr.AttrValues is [ReadOnlyMemory attrValue]) + { + X509KeyUsageExtension.DecodeX509KeyUsageExtension(attrValue.Span, out X509KeyUsageFlags usages); + return usages; + } + + // If the attribute has no value or too many values, consider it malformed. + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey); + } + + return null; + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509KeyUsageExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509KeyUsageExtension.cs index 7d5e6060e5158a..ce2bd4bb75a9ef 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509KeyUsageExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509KeyUsageExtension.cs @@ -61,7 +61,7 @@ private static byte[] EncodeX509KeyUsageExtension(X509KeyUsageFlags keyUsages) } - private static void DecodeX509KeyUsageExtension(byte[] encoded, out X509KeyUsageFlags keyUsages) + internal static void DecodeX509KeyUsageExtension(ReadOnlySpan encoded, out X509KeyUsageFlags keyUsages) { KeyUsageFlagsAsn keyUsagesAsn; diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs index 7c247857a6d2dd..07278ed63f3477 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.SLHDsa.Tests; using System.Security.Cryptography.Tests; using Test.Cryptography; @@ -826,6 +827,228 @@ public static void CreateFromPem_PublicOnly_CryptographicException_CertIsPkcs7() X509Certificate2.CreateFromPem(certContents)); } + [Theory] + [InlineData(X509KeyUsageFlags.CrlSign)] + [InlineData(X509KeyUsageFlags.KeyCertSign)] + [InlineData(X509KeyUsageFlags.DigitalSignature)] + public static void CreateFromPem_CanImportECCAnyPublicKeyWithSigningKeyUsage(X509KeyUsageFlags flags) + { + const string PrivateKey = + """ + -----BEGIN PRIVATE KEY----- + MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQghew4zS1/h2J+PJLX + SY2U8qo0pBbNaFXm5f3GzsTCIxigCgYIKoZIzj0DAQehRANCAAT83cB14Y8zLLxo + bliw/JsBoy7oyKD0zVMgRbieDBZEn/5UpHv2Xv6W0dE3mEG6goF3s8GT+pf4JUT2 + EfthzGhn + -----END PRIVATE KEY----- + """; + + const string AnyKeyUsageCertificate = + """ + -----BEGIN CERTIFICATE----- + MIIBITCBx6ADAgECAgkA1dyp2OqNXw0wCgYIKoZIzj0EAwIwFjEUMBIGA1UEAxML + ZXhhbXBsZS5jb20wHhcNMjYwMTA2MTg1ODUxWhcNMjcwMTA2MTg1ODUxWjAWMRQw + EgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPzd + wHXhjzMsvGhuWLD8mwGjLujIoPTNUyBFuJ4MFkSf/lSke/Ze/pbR0TeYQbqCgXez + wZP6l/glRPYR+2HMaGcwCgYIKoZIzj0EAwIDSQAwRgIhAPxduNwHwIafwVcfegnp + ocZs707jXBeVg1oxCZz5HwMeAiEAoFbL7kOyha8n0g2kkVaXNa0lWD62FZ1Jl+m9 + bFYUxF4= + -----END CERTIFICATE----- + """; + + string privateKeyWithSigningKeyUsage = AddKeyUsageAttributeToPkcs8Key(PrivateKey, flags); + using X509Certificate2 cert = X509Certificate2.CreateFromPem(AnyKeyUsageCertificate, privateKeyWithSigningKeyUsage); + AssertKeysMatch(privateKeyWithSigningKeyUsage, cert.GetECDsaPrivateKey); + } + + [Theory] + [InlineData(X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyAgreement)] + [InlineData(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.KeyAgreement)] + [InlineData(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyAgreement)] + public static void CreateFromPem_CanImportECCAnyPublicKeyWithMixedKeyUsage(X509KeyUsageFlags flags) + { + const string PrivateKey = + """ + -----BEGIN PRIVATE KEY----- + MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQghew4zS1/h2J+PJLX + SY2U8qo0pBbNaFXm5f3GzsTCIxigCgYIKoZIzj0DAQehRANCAAT83cB14Y8zLLxo + bliw/JsBoy7oyKD0zVMgRbieDBZEn/5UpHv2Xv6W0dE3mEG6goF3s8GT+pf4JUT2 + EfthzGhn + -----END PRIVATE KEY----- + """; + + const string AnyKeyUsageCertificate = + """ + -----BEGIN CERTIFICATE----- + MIIBITCBx6ADAgECAgkA1dyp2OqNXw0wCgYIKoZIzj0EAwIwFjEUMBIGA1UEAxML + ZXhhbXBsZS5jb20wHhcNMjYwMTA2MTg1ODUxWhcNMjcwMTA2MTg1ODUxWjAWMRQw + EgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPzd + wHXhjzMsvGhuWLD8mwGjLujIoPTNUyBFuJ4MFkSf/lSke/Ze/pbR0TeYQbqCgXez + wZP6l/glRPYR+2HMaGcwCgYIKoZIzj0EAwIDSQAwRgIhAPxduNwHwIafwVcfegnp + ocZs707jXBeVg1oxCZz5HwMeAiEAoFbL7kOyha8n0g2kkVaXNa0lWD62FZ1Jl+m9 + bFYUxF4= + -----END CERTIFICATE----- + """; + + string privateKeyWithSigningKeyUsage = AddKeyUsageAttributeToPkcs8Key(PrivateKey, flags); + using X509Certificate2 cert = X509Certificate2.CreateFromPem(AnyKeyUsageCertificate, privateKeyWithSigningKeyUsage); + AssertKeysMatch(privateKeyWithSigningKeyUsage, cert.GetECDsaPrivateKey); + AssertKeysMatch(privateKeyWithSigningKeyUsage, cert.GetECDiffieHellmanPrivateKey); + } + + [Theory] + [InlineData(X509KeyUsageFlags.CrlSign)] + [InlineData(X509KeyUsageFlags.KeyCertSign)] + [InlineData(X509KeyUsageFlags.DigitalSignature)] + public static void CreateFromEncryptedPem_CanImportECCAnyPublicKeyWithSigningKeyUsage(X509KeyUsageFlags flags) + { + const string Password = "PLACEHOLDER"; + const string PrivateKey = + """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIHAMCMGCiqGSIb3DQEMAQMwFQQQ2yoyxTdfjrkU0Qyc3IYVywIBAQSBmPQJanYv + mAH35aWV39G4/yDdbSZHZbPsmoEq3waW+yB7a0LykybjfJlMhGYJks3gZN6N21NR + XpnByhtPBTXzrzjxnLv/DAwZIpNuYOOkTmRKDpVsjBsHUF3Gw2b5h0YU2I4cUl2p + BXh95HPB2tUNrDiHd3Zya6OnGG+fg7Ya35XIyWTJ1ODnhkVc2SVkXk7Lgku3I3gq + CJuz + -----END ENCRYPTED PRIVATE KEY----- + """; + + const string AnyKeyUsageCertificate = + """ + -----BEGIN CERTIFICATE----- + MIIBHzCBxqADAgECAghjN3R7a8h36TAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtl + eGFtcGxlLmNvbTAeFw0yNjAxMDcxODExMzFaFw0yNzAxMDcxODExMzFaMBYxFDAS + BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeybq + p51w8CAD3rKIi/cKx6JKTR9Z7dGzt53gPpCS6fpqDJMC4revxduxoZ60MhZWFESL + rq3coMOQVWjZAAz8rjAKBggqhkjOPQQDAgNIADBFAiBm07dRWT23lsfefred+Kzh + ZO9CxVEnV0nBQPkJH8GlrAIhAMnIN8RgUmGeXHNdq4yBoLlEaQcVzMquERBkZ0AG + dmo9 + -----END CERTIFICATE----- + """; + + string privateKeyWithSigningKeyUsage = AddKeyUsageAttributeToPkcs8Key(PrivateKey, flags, Password); + using X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPem( + AnyKeyUsageCertificate, + privateKeyWithSigningKeyUsage, + Password); + + AssertKeysMatch(privateKeyWithSigningKeyUsage, cert.GetECDsaPrivateKey, Password); + } + + [Theory] + [InlineData(X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyAgreement)] + [InlineData(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.KeyAgreement)] + [InlineData(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyAgreement)] + public static void CreateFromEncryptedPem_CanImportECCAnyPublicKeyWithMixedKeyUsage(X509KeyUsageFlags flags) + { + const string Password = "PLACEHOLDER"; + const string PrivateKey = + """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIHAMCMGCiqGSIb3DQEMAQMwFQQQ2yoyxTdfjrkU0Qyc3IYVywIBAQSBmPQJanYv + mAH35aWV39G4/yDdbSZHZbPsmoEq3waW+yB7a0LykybjfJlMhGYJks3gZN6N21NR + XpnByhtPBTXzrzjxnLv/DAwZIpNuYOOkTmRKDpVsjBsHUF3Gw2b5h0YU2I4cUl2p + BXh95HPB2tUNrDiHd3Zya6OnGG+fg7Ya35XIyWTJ1ODnhkVc2SVkXk7Lgku3I3gq + CJuz + -----END ENCRYPTED PRIVATE KEY----- + """; + + const string AnyKeyUsageCertificate = + """ + -----BEGIN CERTIFICATE----- + MIIBHzCBxqADAgECAghjN3R7a8h36TAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtl + eGFtcGxlLmNvbTAeFw0yNjAxMDcxODExMzFaFw0yNzAxMDcxODExMzFaMBYxFDAS + BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeybq + p51w8CAD3rKIi/cKx6JKTR9Z7dGzt53gPpCS6fpqDJMC4revxduxoZ60MhZWFESL + rq3coMOQVWjZAAz8rjAKBggqhkjOPQQDAgNIADBFAiBm07dRWT23lsfefred+Kzh + ZO9CxVEnV0nBQPkJH8GlrAIhAMnIN8RgUmGeXHNdq4yBoLlEaQcVzMquERBkZ0AG + dmo9 + -----END CERTIFICATE----- + """; + + string privateKeyWithSigningKeyUsage = AddKeyUsageAttributeToPkcs8Key(PrivateKey, flags, Password); + using X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPem( + AnyKeyUsageCertificate, + privateKeyWithSigningKeyUsage, + Password); + + AssertKeysMatch(privateKeyWithSigningKeyUsage, cert.GetECDsaPrivateKey, Password); + AssertKeysMatch(privateKeyWithSigningKeyUsage, cert.GetECDiffieHellmanPrivateKey, Password); + } + + [Fact] + public static void CreateFromPem_CanImportECCAnyPublicKeyWithKeyAgreementUsage() + { + const string PrivateKey = + """ + -----BEGIN PRIVATE KEY----- + MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQghew4zS1/h2J+PJLX + SY2U8qo0pBbNaFXm5f3GzsTCIxigCgYIKoZIzj0DAQehRANCAAT83cB14Y8zLLxo + bliw/JsBoy7oyKD0zVMgRbieDBZEn/5UpHv2Xv6W0dE3mEG6goF3s8GT+pf4JUT2 + EfthzGhn + -----END PRIVATE KEY----- + """; + + const string AnyKeyUsageCertificate = + """ + -----BEGIN CERTIFICATE----- + MIIBITCBx6ADAgECAgkA1dyp2OqNXw0wCgYIKoZIzj0EAwIwFjEUMBIGA1UEAxML + ZXhhbXBsZS5jb20wHhcNMjYwMTA2MTg1ODUxWhcNMjcwMTA2MTg1ODUxWjAWMRQw + EgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPzd + wHXhjzMsvGhuWLD8mwGjLujIoPTNUyBFuJ4MFkSf/lSke/Ze/pbR0TeYQbqCgXez + wZP6l/glRPYR+2HMaGcwCgYIKoZIzj0EAwIDSQAwRgIhAPxduNwHwIafwVcfegnp + ocZs707jXBeVg1oxCZz5HwMeAiEAoFbL7kOyha8n0g2kkVaXNa0lWD62FZ1Jl+m9 + bFYUxF4= + -----END CERTIFICATE----- + """; + + string privateKeyWithAgreementKeyUsage = AddKeyUsageAttributeToPkcs8Key(PrivateKey, X509KeyUsageFlags.KeyAgreement); + using X509Certificate2 cert = X509Certificate2.CreateFromPem(AnyKeyUsageCertificate, privateKeyWithAgreementKeyUsage); + AssertKeysMatch(privateKeyWithAgreementKeyUsage, cert.GetECDiffieHellmanPrivateKey); + } + + [Fact] + public static void CreateFromEncryptedPem_CanImportECCAnyPublicKeyWithKeyAgreementKeyUsage() + { + const string Password = "PLACEHOLDER"; + const string PrivateKey = + """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIHAMCMGCiqGSIb3DQEMAQMwFQQQ2yoyxTdfjrkU0Qyc3IYVywIBAQSBmPQJanYv + mAH35aWV39G4/yDdbSZHZbPsmoEq3waW+yB7a0LykybjfJlMhGYJks3gZN6N21NR + XpnByhtPBTXzrzjxnLv/DAwZIpNuYOOkTmRKDpVsjBsHUF3Gw2b5h0YU2I4cUl2p + BXh95HPB2tUNrDiHd3Zya6OnGG+fg7Ya35XIyWTJ1ODnhkVc2SVkXk7Lgku3I3gq + CJuz + -----END ENCRYPTED PRIVATE KEY----- + """; + + const string AnyKeyUsageCertificate = + """ + -----BEGIN CERTIFICATE----- + MIIBHzCBxqADAgECAghjN3R7a8h36TAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtl + eGFtcGxlLmNvbTAeFw0yNjAxMDcxODExMzFaFw0yNzAxMDcxODExMzFaMBYxFDAS + BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeybq + p51w8CAD3rKIi/cKx6JKTR9Z7dGzt53gPpCS6fpqDJMC4revxduxoZ60MhZWFESL + rq3coMOQVWjZAAz8rjAKBggqhkjOPQQDAgNIADBFAiBm07dRWT23lsfefred+Kzh + ZO9CxVEnV0nBQPkJH8GlrAIhAMnIN8RgUmGeXHNdq4yBoLlEaQcVzMquERBkZ0AG + dmo9 + -----END CERTIFICATE----- + """; + + string privateKeyWithAgreementKeyUsage = AddKeyUsageAttributeToPkcs8Key( + PrivateKey, + X509KeyUsageFlags.KeyAgreement, + Password); + + using X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPem( + AnyKeyUsageCertificate, + privateKeyWithAgreementKeyUsage, + Password); + + AssertKeysMatch(privateKeyWithAgreementKeyUsage, cert.GetECDiffieHellmanPrivateKey, Password); + } + private static void AssertKeysMatch(string keyPem, Func keyLoader, string password = null) where T : IDisposable { IDisposable key = keyLoader(); @@ -892,7 +1115,8 @@ private static void AssertKeysMatch(string keyPem, Func keyLoader, string Assert.True(dsaPem.VerifyData(data, dsaSignature, HashAlgorithmName.SHA1)); break; case (ECDiffieHellman ecdh, ECDiffieHellman ecdhPem): - ECCurve curve = ecdh.KeySize switch { + ECCurve curve = ecdh.KeySize switch + { 256 => ECCurve.NamedCurves.nistP256, 384 => ECCurve.NamedCurves.nistP384, 521 => ECCurve.NamedCurves.nistP521, @@ -928,5 +1152,33 @@ private static void AssertKeysMatch(string keyPem, Func keyLoader, string } } } + + private static string AddKeyUsageAttributeToPkcs8Key( + ReadOnlySpan keyPem, + X509KeyUsageFlags flags, + string password = null) + { + X509KeyUsageExtension ext = new(flags, false); + + PemFields fields = PemEncoding.Find(keyPem); + byte[] data = Convert.FromBase64String(keyPem[fields.Base64Data].ToString()); + + if (keyPem[fields.Label].SequenceEqual("PRIVATE KEY")) + { + Pkcs8PrivateKeyInfo info = Pkcs8PrivateKeyInfo.Decode(data, out _, skipCopy: true); + info.Attributes.Add(new AsnEncodedData(ext.Oid, ext.RawData)); + return PemEncoding.WriteString("PRIVATE KEY", info.Encode()); + } + + if (keyPem[fields.Label].SequenceEqual("ENCRYPTED PRIVATE KEY")) + { + Pkcs8PrivateKeyInfo info = Pkcs8PrivateKeyInfo.DecryptAndDecode(password, data, out _); + info.Attributes.Add(new AsnEncodedData(ext.Oid, ext.RawData)); + PbeParameters parameters = new(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 1); + return PemEncoding.WriteString("ENCRYPTED PRIVATE KEY", info.Encrypt(password, parameters)); + } + + throw new InvalidOperationException("PEM-encoded PKCS#8 does not contain an understood PEM label."); + } } }