Skip to content

Commit ad38f95

Browse files
github-actions[bot]vcsjonesjeffhandley
authored
[release/10.0] Fix EC-DSA / EC-DH PEM key loading with PKCS#8 key load attributes (#123036)
Backport of #122997 to release/10.0 /cc @vcsjones ## Customer Impact - [x] Customer reported - [ ] Found internally Customer reported in #122925. Customers that use `X509Certificate2.CreateFromPem` may receive an error when importing an EC-DSA on Windows, preventing them from importing the key. This is because the key import mechanism only observed the key usages on the certificate. It did not observe the usages on the key itself. ## Regression - [x] Yes - [ ] No This regressed in .NET 10 from #115249. That pull request fixed a similar issue with EC-DH, but caused the EC-DSA scenario to regress. ## Testing New tests were added to cover all new scenarios. Existing tests were in place to ensure known scenarios continued to work. ## Risk Low. Between the previous certificate-based test variance and the new key-based test variance, both sides of the cert+key pairing are covered. --------- Co-authored-by: Kevin Jones <kevin@vcsjones.com> Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com>
1 parent 5638a68 commit ad38f95

File tree

3 files changed

+511
-40
lines changed

3 files changed

+511
-40
lines changed

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs

Lines changed: 257 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Net;
1010
using System.Runtime.Serialization;
1111
using System.Runtime.Versioning;
12+
using System.Security.Cryptography.Asn1;
1213
using System.Security.Cryptography.X509Certificates.Asn1;
1314
using System.Text;
1415
using Internal.Cryptography;
@@ -24,9 +25,8 @@ public class X509Certificate2 : X509Certificate
2425
private volatile PublicKey? _lazyPublicKey;
2526
private volatile AsymmetricAlgorithm? _lazyPrivateKey;
2627
private volatile X509ExtensionCollection? _lazyExtensions;
27-
private static readonly string[] s_EcPublicKeyPrivateKeyLabels = { PemLabels.EcPrivateKey, PemLabels.Pkcs8PrivateKey };
28-
private static readonly string[] s_RsaPublicKeyPrivateKeyLabels = { PemLabels.RsaPrivateKey, PemLabels.Pkcs8PrivateKey };
29-
private static readonly string[] s_DsaPublicKeyPrivateKeyLabels = { PemLabels.Pkcs8PrivateKey };
28+
private static readonly string[] s_RsaPublicKeyPrivateKeyLabels = [PemLabels.RsaPrivateKey, PemLabels.Pkcs8PrivateKey];
29+
private static readonly string[] s_DsaPublicKeyPrivateKeyLabels = [PemLabels.Pkcs8PrivateKey];
3030

3131
public override void Reset()
3232
{
@@ -1378,18 +1378,7 @@ public static X509Certificate2 CreateFromPem(ReadOnlySpan<char> certPem, ReadOnl
13781378
s_DsaPublicKeyPrivateKeyLabels,
13791379
static keyPem => CreateAndImport(keyPem, DSA.Create),
13801380
certificate.CopyWithPrivateKey),
1381-
Oids.EcPublicKey when IsECDiffieHellman(certificate) =>
1382-
ExtractKeyFromPem<ECDiffieHellman>(
1383-
keyPem,
1384-
s_EcPublicKeyPrivateKeyLabels,
1385-
static keyPem => CreateAndImport(keyPem, ECDiffieHellman.Create),
1386-
certificate.CopyWithPrivateKey),
1387-
Oids.EcPublicKey when IsECDsa(certificate) =>
1388-
ExtractKeyFromPem<ECDsa>(
1389-
keyPem,
1390-
s_EcPublicKeyPrivateKeyLabels,
1391-
static keyPem => CreateAndImport(keyPem, ECDsa.Create),
1392-
certificate.CopyWithPrivateKey),
1381+
Oids.EcPublicKey => ExtractKeyFromECPem(certificate, keyPem),
13931382
Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024 =>
13941383
ExtractKeyFromPem<MLKem>(
13951384
keyPem,
@@ -1477,18 +1466,7 @@ public static X509Certificate2 CreateFromEncryptedPem(ReadOnlySpan<char> certPem
14771466
password,
14781467
static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, DSA.Create),
14791468
certificate.CopyWithPrivateKey),
1480-
Oids.EcPublicKey when IsECDiffieHellman(certificate) =>
1481-
ExtractKeyFromEncryptedPem<ECDiffieHellman>(
1482-
keyPem,
1483-
password,
1484-
static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, ECDiffieHellman.Create),
1485-
certificate.CopyWithPrivateKey),
1486-
Oids.EcPublicKey when IsECDsa(certificate) =>
1487-
ExtractKeyFromEncryptedPem<ECDsa>(
1488-
keyPem,
1489-
password,
1490-
static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, ECDsa.Create),
1491-
certificate.CopyWithPrivateKey),
1469+
Oids.EcPublicKey => ExtractKeyFromEncryptedECPem(certificate, keyPem, password),
14921470
Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024 =>
14931471
ExtractKeyFromEncryptedPem<MLKem>(
14941472
keyPem,
@@ -1929,24 +1907,32 @@ private static X509Certificate2 ExtractKeyFromPem<TAlg>(
19291907
{
19301908
if (label.SequenceEqual(eligibleLabel))
19311909
{
1932-
using (TAlg key = factory(contents[fields.Location]))
1933-
{
1934-
try
1935-
{
1936-
return import(key);
1937-
}
1938-
catch (ArgumentException ae)
1939-
{
1940-
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae);
1941-
}
1942-
}
1910+
return ExtractKeyFromPem(contents[fields.Location], factory, import);
19431911
}
19441912
}
19451913
}
19461914

19471915
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey);
19481916
}
19491917

1918+
private static X509Certificate2 ExtractKeyFromPem<TAlg>(
1919+
ReadOnlySpan<char> keyPem,
1920+
Func<ReadOnlySpan<char>, TAlg> factory,
1921+
Func<TAlg, X509Certificate2> import) where TAlg : IDisposable
1922+
{
1923+
using (TAlg key = factory(keyPem))
1924+
{
1925+
try
1926+
{
1927+
return import(key);
1928+
}
1929+
catch (ArgumentException ae)
1930+
{
1931+
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae);
1932+
}
1933+
}
1934+
}
1935+
19501936
private static X509Certificate2 ExtractKeyFromEncryptedPem<TAlg>(
19511937
ReadOnlySpan<char> keyPem,
19521938
ReadOnlySpan<char> password,
@@ -2009,5 +1995,238 @@ private static bool HasECDiffieHellmanKeyUsage(X509Certificate2 certificate)
20091995
// considered valid for all usages, so we can use it for ECDH.
20101996
return true;
20111997
}
1998+
1999+
[UnsupportedOSPlatform("browser")]
2000+
private static X509Certificate2 ExtractKeyFromEncryptedECPem(
2001+
X509Certificate2 certificate,
2002+
ReadOnlySpan<char> keyPem,
2003+
ReadOnlySpan<char> password)
2004+
{
2005+
Debug.Assert(certificate.GetKeyAlgorithm() == Oids.EcPublicKey);
2006+
2007+
foreach ((ReadOnlySpan<char> contents, PemFields fields) in PemEnumerator.Utf16(keyPem))
2008+
{
2009+
ReadOnlySpan<char> label = contents[fields.Label];
2010+
2011+
if (!label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey))
2012+
{
2013+
continue;
2014+
}
2015+
2016+
byte[] base64Buffer = CryptoPool.Rent(fields.DecodedDataLength);
2017+
int base64ClearSize = CryptoPool.ClearAll;
2018+
ArraySegment<byte>? decryptedPkcs8 = null;
2019+
2020+
try
2021+
{
2022+
bool result = Convert.TryFromBase64Chars(contents[fields.Base64Data], base64Buffer, out int base64Written);
2023+
2024+
if (!result || base64Written != fields.DecodedDataLength)
2025+
{
2026+
Debug.Fail("Preallocated buffer and validated data decoding failed.");
2027+
break;
2028+
}
2029+
2030+
base64ClearSize = base64Written;
2031+
Debug.Assert(!decryptedPkcs8.HasValue);
2032+
decryptedPkcs8 = KeyFormatHelper.DecryptPkcs8(password, base64Buffer.AsMemory(0, base64Written), out int bytesRead);
2033+
2034+
if (bytesRead != base64Written)
2035+
{
2036+
break;
2037+
}
2038+
2039+
X509Certificate2? loaded = ExtractKeyFromECPrivateKeyInfo(certificate, decryptedPkcs8.Value);
2040+
2041+
if (loaded is null)
2042+
{
2043+
break;
2044+
}
2045+
2046+
return loaded;
2047+
}
2048+
catch (CryptographicException ce)
2049+
{
2050+
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ce);
2051+
}
2052+
finally
2053+
{
2054+
CryptoPool.Return(base64Buffer, base64ClearSize);
2055+
2056+
if (decryptedPkcs8.HasValue)
2057+
{
2058+
CryptoPool.Return(decryptedPkcs8.Value);
2059+
}
2060+
}
2061+
}
2062+
2063+
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey);
2064+
}
2065+
2066+
[UnsupportedOSPlatform("browser")]
2067+
private static X509Certificate2 ExtractKeyFromECPem(X509Certificate2 certificate, ReadOnlySpan<char> keyPem)
2068+
{
2069+
Debug.Assert(certificate.GetKeyAlgorithm() == Oids.EcPublicKey);
2070+
2071+
foreach ((ReadOnlySpan<char> contents, PemFields fields) in PemEnumerator.Utf16(keyPem))
2072+
{
2073+
ReadOnlySpan<char> label = contents[fields.Label];
2074+
2075+
if (label.SequenceEqual(PemLabels.EcPrivateKey))
2076+
{
2077+
// EC PRIVATE KEYs do not have a key usage, so usage is determined by the certificate.
2078+
2079+
// If we can load it is EC-DH, we should prefer that over EC-DSA. ECC keys that are "both" prefer
2080+
// to be imported as EC-DH. Importing it as EC-DSA would restrict it to EC-DSA, even if the key
2081+
// and certificate are valid for EC-DH. Other platforms don't have such restrictions.
2082+
if (IsECDiffieHellman(certificate))
2083+
{
2084+
return ExtractKeyFromPem(
2085+
keyPem,
2086+
static keyPem => CreateAndImport(keyPem, ECDiffieHellman.Create),
2087+
certificate.CopyWithPrivateKey);
2088+
}
2089+
2090+
if (IsECDsa(certificate))
2091+
{
2092+
return ExtractKeyFromPem(
2093+
keyPem,
2094+
static keyPem => CreateAndImport(keyPem, ECDsa.Create),
2095+
certificate.CopyWithPrivateKey);
2096+
}
2097+
2098+
// If we got here, then the key is neither EC-DH or EC-DSA eligible, but we had a matching PEM
2099+
// label. Break out and throw.
2100+
break;
2101+
}
2102+
2103+
if (!label.SequenceEqual(PemLabels.Pkcs8PrivateKey))
2104+
{
2105+
continue;
2106+
}
2107+
2108+
byte[] base64Buffer = CryptoPool.Rent(fields.DecodedDataLength);
2109+
int clearSize = CryptoPool.ClearAll;
2110+
2111+
try
2112+
{
2113+
bool result = Convert.TryFromBase64Chars(contents[fields.Base64Data], base64Buffer, out int base64Written);
2114+
2115+
if (!result || base64Written != fields.DecodedDataLength)
2116+
{
2117+
Debug.Fail("Preallocated buffer and validated data decoding failed.");
2118+
break;
2119+
}
2120+
2121+
clearSize = base64Written;
2122+
X509Certificate2? loaded = ExtractKeyFromECPrivateKeyInfo(certificate, base64Buffer.AsMemory(0, base64Written));
2123+
2124+
if (loaded is null)
2125+
{
2126+
break;
2127+
}
2128+
2129+
return loaded;
2130+
}
2131+
catch (CryptographicException ce)
2132+
{
2133+
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ce);
2134+
}
2135+
finally
2136+
{
2137+
CryptoPool.Return(base64Buffer, clearSize);
2138+
}
2139+
}
2140+
2141+
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey);
2142+
}
2143+
2144+
[UnsupportedOSPlatform("browser")]
2145+
private static X509Certificate2? ExtractKeyFromECPrivateKeyInfo(
2146+
X509Certificate2 certificate,
2147+
ReadOnlyMemory<byte> privateKeyInfo)
2148+
{
2149+
// We are not going to perform any validation on the PrivateKeyInfo here. We just need a hint
2150+
// what algorithm to use. The actual algorithm will do whatever validation on the key as needed.
2151+
PrivateKeyInfoAsn privateKeyInfoAsn = PrivateKeyInfoAsn.Decode(privateKeyInfo, AsnEncodingRules.BER);
2152+
2153+
const X509KeyUsageFlags EcdsaKeyUsageFlags =
2154+
X509KeyUsageFlags.DigitalSignature |
2155+
X509KeyUsageFlags.KeyCertSign |
2156+
X509KeyUsageFlags.CrlSign;
2157+
2158+
X509KeyUsageFlags? usages = GetKeyUsageFlags(in privateKeyInfoAsn);
2159+
2160+
// If any of the following is true we should load it as EC-DH
2161+
// * There is no keyUsage extension. Loading it as EC-DH will allow loading it as EC-DSA, too.
2162+
// * It has any keyUsage that is not a "signing" usage. That at minimum means loading it as EC-DH.
2163+
// it may still yet have a keyUsage that allows signing as well, in which case it will work for EC-DSA
2164+
// too.
2165+
// The certificate must also have a key usage that permits EC-DH, either with "no" usage (in which case
2166+
// it will work for EC-DSA, too) or as EC-DH explicitly.
2167+
if ((usages is null || (usages & ~EcdsaKeyUsageFlags) != 0) && IsECDiffieHellman(certificate))
2168+
{
2169+
using (ECDiffieHellman ecdh = ECDiffieHellman.Create())
2170+
{
2171+
ecdh.ImportPkcs8PrivateKey(privateKeyInfo.Span, out int pkcs8Read);
2172+
2173+
if (pkcs8Read != privateKeyInfo.Length)
2174+
{
2175+
Debug.Fail("Unexpected trailing data in PKCS#8 buffer.");
2176+
throw new CryptographicException();
2177+
}
2178+
2179+
return certificate.CopyWithPrivateKey(ecdh);
2180+
}
2181+
}
2182+
2183+
// If we are here, then either the key or certificate has a key usage that requires loading it as EC-DSA.
2184+
if (IsECDsa(certificate))
2185+
{
2186+
using (ECDsa ecdsa = ECDsa.Create())
2187+
{
2188+
ecdsa.ImportPkcs8PrivateKey(privateKeyInfo.Span, out int pkcs8Read);
2189+
2190+
if (pkcs8Read != privateKeyInfo.Length)
2191+
{
2192+
Debug.Fail("Unexpected trailing data in PKCS#8 buffer.");
2193+
throw new CryptographicException();
2194+
}
2195+
2196+
return certificate.CopyWithPrivateKey(ecdsa);
2197+
}
2198+
}
2199+
2200+
// If we get here, the key and certificate do not agree on algorithm use (the key has digitalSignature but
2201+
// the certificate has keyAgreement, for example). It cannot be loaded.
2202+
return null;
2203+
}
2204+
2205+
private static X509KeyUsageFlags? GetKeyUsageFlags(ref readonly PrivateKeyInfoAsn keyInfo)
2206+
{
2207+
if (keyInfo.Attributes is null)
2208+
{
2209+
return null;
2210+
}
2211+
2212+
foreach (AttributeAsn attr in keyInfo.Attributes)
2213+
{
2214+
if (attr.AttrType != Oids.KeyUsage)
2215+
{
2216+
continue;
2217+
}
2218+
2219+
if (attr.AttrValues is [ReadOnlyMemory<byte> attrValue])
2220+
{
2221+
X509KeyUsageExtension.DecodeX509KeyUsageExtension(attrValue.Span, out X509KeyUsageFlags usages);
2222+
return usages;
2223+
}
2224+
2225+
// If the attribute has no value or too many values, consider it malformed.
2226+
throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey);
2227+
}
2228+
2229+
return null;
2230+
}
20122231
}
20132232
}

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509KeyUsageExtension.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private static byte[] EncodeX509KeyUsageExtension(X509KeyUsageFlags keyUsages)
6161
}
6262

6363

64-
private static void DecodeX509KeyUsageExtension(byte[] encoded, out X509KeyUsageFlags keyUsages)
64+
internal static void DecodeX509KeyUsageExtension(ReadOnlySpan<byte> encoded, out X509KeyUsageFlags keyUsages)
6565
{
6666
KeyUsageFlagsAsn keyUsagesAsn;
6767

0 commit comments

Comments
 (0)