diff --git a/src/libraries/Common/src/Internal/Cryptography/PemEnumerator.cs b/src/libraries/Common/src/Internal/Cryptography/PemEnumerator.cs new file mode 100644 index 00000000000000..9622f7d712ebbd --- /dev/null +++ b/src/libraries/Common/src/Internal/Cryptography/PemEnumerator.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Security.Cryptography; + +namespace Internal.Cryptography +{ + internal readonly ref struct PemEnumerator + { + private readonly ReadOnlySpan _contents; + + public PemEnumerator(ReadOnlySpan contents) + { + _contents = contents; + } + + public Enumerator GetEnumerator() => new Enumerator(_contents); + + internal ref struct Enumerator + { + private ReadOnlySpan _contents; + private PemFields _pemFields; + + public Enumerator(ReadOnlySpan contents) + { + _contents = contents; + _pemFields = default; + } + + public PemFieldItem Current => new PemFieldItem(_contents, _pemFields); + + public bool MoveNext() + { + _contents = _contents[_pemFields.Location.End..]; + return PemEncoding.TryFind(_contents, out _pemFields); + } + + internal readonly ref struct PemFieldItem + { + private readonly ReadOnlySpan _contents; + private readonly PemFields _pemFields; + + public PemFieldItem(ReadOnlySpan contents, PemFields pemFields) + { + _contents = contents; + _pemFields = pemFields; + } + + public void Deconstruct(out ReadOnlySpan contents, out PemFields pemFields) + { + contents = _contents; + pemFields = _pemFields; + } + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs b/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs index 5adf4ab5d83d96..99dca98bca2d8a 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs @@ -12,5 +12,6 @@ internal static class PemLabels internal const string RsaPublicKey = "RSA PUBLIC KEY"; internal const string RsaPrivateKey = "RSA PRIVATE KEY"; internal const string EcPrivateKey = "EC PRIVATE KEY"; + internal const string X509Certificate = "CERTIFICATE"; } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/ref/System.Security.Cryptography.X509Certificates.cs b/src/libraries/System.Security.Cryptography.X509Certificates/ref/System.Security.Cryptography.X509Certificates.cs index e34a984d66bd36..2e175633880c1e 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/ref/System.Security.Cryptography.X509Certificates.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/ref/System.Security.Cryptography.X509Certificates.cs @@ -234,6 +234,10 @@ public X509Certificate2(string fileName, string? password, System.Security.Crypt public System.Security.Cryptography.X509Certificates.X500DistinguishedName SubjectName { get { throw null; } } public string Thumbprint { get { throw null; } } public int Version { get { throw null; } } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromEncryptedPem(System.ReadOnlySpan certPem, System.ReadOnlySpan keyPem, System.ReadOnlySpan password) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromEncryptedPemFile(string certPemFilePath, System.ReadOnlySpan password, string? keyPemFilePath = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPem(System.ReadOnlySpan certPem, System.ReadOnlySpan keyPem) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath, string? keyPemFilePath = null) { throw null; } public static System.Security.Cryptography.X509Certificates.X509ContentType GetCertContentType(byte[] rawData) { throw null; } public static System.Security.Cryptography.X509Certificates.X509ContentType GetCertContentType(string fileName) { throw null; } public string GetNameInfo(System.Security.Cryptography.X509Certificates.X509NameType nameType, bool forIssuer) { throw null; } @@ -269,6 +273,8 @@ public void Import(byte[] rawData) { } public void Import(byte[] rawData, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags) { } public void Import(string fileName) { } public void Import(string fileName, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags) { } + public void ImportFromPem(System.ReadOnlySpan certPem) { } + public void ImportFromPemFile(string certPemFilePath) { } public void Insert(int index, System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } public void Remove(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } public void RemoveRange(System.Security.Cryptography.X509Certificates.X509Certificate2Collection certificates) { } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx index bcbe019c37abc1..3c1833771980e9 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx @@ -292,6 +292,12 @@ The platform does not have a definition for an X509 certificate store named '{0}' with a StoreLocation of '{1}', and does not support creating it. + + The certificate contents do not contain a PEM with a CERTIFICATE label, or the content is malformed. + + + The key contents do not contain a PEM, the content is malformed, or the key does not match the certificate. + Enumeration has not started. Call MoveNext. diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj index 6507510a915f16..7ee5e30391cdf4 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj @@ -24,6 +24,10 @@ Link="Common\System\Security\Cryptography\KeyBlobHelpers.cs" /> + + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml @@ -679,6 +683,6 @@ - + diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs index 4239392ffa136a..ef2bdce23f1123 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs @@ -5,9 +5,12 @@ using Internal.Cryptography; using Internal.Cryptography.Pal; using System; +using System.Diagnostics; +using System.Formats.Asn1; using System.IO; using System.Runtime.Serialization; using System.Security; +using System.Security.Cryptography.X509Certificates.Asn1; using System.Text; namespace System.Security.Cryptography.X509Certificates @@ -22,6 +25,9 @@ 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 }; public override void Reset() { @@ -628,6 +634,351 @@ public bool Verify() } } + /// + /// Creates a new X509 certificate from the file contents of an RFC 7468 PEM-encoded + /// certificate and private key. + /// + /// The path for the PEM-encoded X509 certificate. + /// + /// If specified, the path for the PEM-encoded private key. + /// If unspecified, the file will be used to load + /// the private key. + /// + /// A new certificate with the private key. + /// + /// + /// The contents of the file path in do not contain + /// a PEM-encoded certificate, or it is malformed. + /// + /// -or- + /// + /// The contents of the file path in do not contain a + /// PEM-encoded private key, or it is malformed. + /// + /// -or- + /// + /// The contents of the file path in contains + /// a key that does not match the public key in the certificate. + /// + /// -or- + /// The certificate uses an unknown public key algorithm. + /// + /// + /// is . + /// + /// + /// + /// See for additional documentation about + /// exceptions that can be thrown. + /// + /// + /// The SubjectPublicKeyInfo from the certificate determines what PEM labels are accepted for the private key. + /// For RSA certificates, accepted private key PEM labels are "RSA PRIVATE KEY" and "PRIVATE KEY". + /// For ECDSA certificates, accepted private key PEM labels are "EC PRIVATE KEY" and "PRIVATE KEY". + /// For DSA certificates, the accepted private key PEM label is "PRIVATE KEY". + /// + /// PEM-encoded items that have a different label are ignored. + /// + /// Combined PEM-encoded certificates and keys do not require a specific order. For the certificate, the + /// the first certificate with a CERTIFICATE label is loaded. For the private key, the first private + /// key with an acceptable label is loaded. More advanced scenarios for loading certificates and + /// private keys can leverage to enumerate + /// PEM-encoded values and apply any custom loading behavior. + /// + /// + /// For password protected PEM-encoded keys, use to specify a password. + /// + /// + public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string? keyPemFilePath = default) + { + if (certPemFilePath is null) + throw new ArgumentNullException(nameof(certPemFilePath)); + + ReadOnlySpan certContents = File.ReadAllText(certPemFilePath); + ReadOnlySpan keyContents = keyPemFilePath is null ? certContents : File.ReadAllText(keyPemFilePath); + + return CreateFromPem(certContents, keyContents); + } + + /// + /// Creates a new X509 certificate from the file contents of an RFC 7468 PEM-encoded + /// certificate and password protected private key. + /// + /// The path for the PEM-encoded X509 certificate. + /// + /// If specified, the path for the password protected PEM-encoded private key. + /// If unspecified, the file will be used to load + /// the private key. + /// + /// The password for the encrypted PEM. + /// A new certificate with the private key. + /// + /// + /// The contents of the file path in do not contain + /// a PEM-encoded certificate, or it is malformed. + /// + /// -or- + /// + /// The contents of the file path in do not contain a + /// password protected PEM-encoded private key, or it is malformed. + /// + /// -or- + /// + /// The contents of the file path in contains + /// a key that does not match the public key in the certificate. + /// + /// -or- + /// The certificate uses an unknown public key algorithm. + /// -or- + /// The password specified for the private key is incorrect. + /// + /// + /// is . + /// + /// + /// + /// See for additional documentation about + /// exceptions that can be thrown. + /// + /// + /// Password protected PEM-encoded keys are always expected to have the PEM label "ENCRYPTED PRIVATE KEY". + /// + /// PEM-encoded items that have a different label are ignored. + /// + /// Combined PEM-encoded certificates and keys do not require a specific order. For the certificate, the + /// the first certificate with a CERTIFICATE label is loaded. For the private key, the first private + /// key with the label "ENCRYPTED PRIVATE KEY" is loaded. More advanced scenarios for loading certificates and + /// private keys can leverage to enumerate + /// PEM-encoded values and apply any custom loading behavior. + /// + /// + /// For PEM-encoded keys without a password, use . + /// + /// + public static X509Certificate2 CreateFromEncryptedPemFile(string certPemFilePath, ReadOnlySpan password, string? keyPemFilePath = default) + { + if (certPemFilePath is null) + throw new ArgumentNullException(nameof(certPemFilePath)); + + ReadOnlySpan certContents = File.ReadAllText(certPemFilePath); + ReadOnlySpan keyContents = keyPemFilePath is null ? certContents : File.ReadAllText(keyPemFilePath); + + return CreateFromEncryptedPem(certContents, keyContents, password); + } + + /// + /// Creates a new X509 certificate from the contents of an RFC 7468 PEM-encoded certificate and private key. + /// + /// The text of the PEM-encoded X509 certificate. + /// The text of the PEM-encoded private key. + /// A new certificate with the private key. + /// + /// The contents of do not contain a PEM-encoded certificate, or it is malformed. + /// -or- + /// The contents of do not contain a PEM-encoded private key, or it is malformed. + /// -or- + /// The contents of contains a key that does not match the public key in the certificate. + /// -or- + /// The certificate uses an unknown public key algorithm. + /// + /// + /// + /// The SubjectPublicKeyInfo from the certificate determines what PEM labels are accepted for the private key. + /// For RSA certificates, accepted private key PEM labels are "RSA PRIVATE KEY" and "PRIVATE KEY". + /// For ECDSA certificates, accepted private key PEM labels are "EC PRIVATE KEY" and "PRIVATE KEY". + /// For DSA certificates, the accepted private key PEM label is "PRIVATE KEY". + /// + /// PEM-encoded items that have a different label are ignored. + /// + /// If the PEM-encoded certificate and private key are in the same text, use the same + /// string for both and , such as: + /// + /// CreateFromPem(combinedCertAndKey, combinedCertAndKey); + /// + /// Combined PEM-encoded certificates and keys do not require a specific order. For the certificate, the + /// the first certificate with a CERTIFICATE label is loaded. For the private key, the first private + /// key with an acceptable label is loaded. More advanced scenarios for loading certificates and + /// private keys can leverage to enumerate + /// PEM-encoded values and apply any custom loading behavior. + /// + /// + /// For password protected PEM-encoded keys, use to specify a password. + /// + /// + public static X509Certificate2 CreateFromPem(ReadOnlySpan certPem, ReadOnlySpan keyPem) + { + using (X509Certificate2 certificate = ExtractCertificateFromPem(certPem)) + { + string keyAlgorithm = certificate.GetKeyAlgorithm(); + + return keyAlgorithm switch + { + Oids.Rsa => ExtractKeyFromPem(keyPem, s_RsaPublicKeyPrivateKeyLabels, RSA.Create, certificate.CopyWithPrivateKey), + Oids.Dsa => ExtractKeyFromPem(keyPem, s_DsaPublicKeyPrivateKeyLabels, DSA.Create, certificate.CopyWithPrivateKey), + Oids.EcPublicKey => ExtractKeyFromPem(keyPem, s_EcPublicKeyPrivateKeyLabels, ECDsa.Create, certificate.CopyWithPrivateKey), + _ => throw new CryptographicException(SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm)), + }; + } + } + + /// + /// Creates a new X509 certificate from the contents of an RFC 7468 PEM-encoded + /// certificate and password protected private key. + /// + /// The text of the PEM-encoded X509 certificate. + /// The text of the password protected PEM-encoded private key. + /// The password for the encrypted PEM. + /// A new certificate with the private key. + /// + /// The contents of do not contain a PEM-encoded certificate, or it is malformed. + /// -or- + /// + /// The contents of do not contain a password protected PEM-encoded private key, + /// or it is malformed. + /// + /// -or- + /// The contents of contains a key that does not match the public key in the certificate. + /// -or- + /// The certificate uses an unknown public key algorithm. + /// -or- + /// The password specified for the private key is incorrect. + /// + /// + /// + /// Password protected PEM-encoded keys are always expected to have the PEM label "ENCRYPTED PRIVATE KEY". + /// + /// PEM-encoded items that have a different label are ignored. + /// + /// If the PEM-encoded certificate and private key are in the same text, use the same + /// string for both and , such as: + /// + /// CreateFromEncryptedPem(combinedCertAndKey, combinedCertAndKey, theKeyPassword); + /// + /// Combined PEM-encoded certificates and keys do not require a specific order. For the certificate, the + /// the first certificate with a CERTIFICATE label is loaded. For the private key, the first private + /// key with the label "ENCRYPTED PRIVATE KEY" is loaded. More advanced scenarios for loading certificates and + /// private keys can leverage to enumerate + /// PEM-encoded values and apply any custom loading behavior. + /// + /// + /// For PEM-encoded keys without a password, use . + /// + /// + public static X509Certificate2 CreateFromEncryptedPem(ReadOnlySpan certPem, ReadOnlySpan keyPem, ReadOnlySpan password) + { + using (X509Certificate2 certificate = ExtractCertificateFromPem(certPem)) + { + string keyAlgorithm = certificate.GetKeyAlgorithm(); + + return keyAlgorithm switch + { + Oids.Rsa => ExtractKeyFromEncryptedPem(keyPem, password, RSA.Create, certificate.CopyWithPrivateKey), + Oids.Dsa => ExtractKeyFromEncryptedPem(keyPem, password, DSA.Create, certificate.CopyWithPrivateKey), + Oids.EcPublicKey => ExtractKeyFromEncryptedPem(keyPem, password, ECDsa.Create, certificate.CopyWithPrivateKey), + _ => throw new CryptographicException(SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm)), + }; + } + } + + private static X509Certificate2 ExtractCertificateFromPem(ReadOnlySpan certPem) + { + foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(certPem)) + { + ReadOnlySpan label = contents[fields.Label]; + + if (label.SequenceEqual(PemLabels.X509Certificate)) + { + // We verify below that every byte is written to. + byte[] certBytes = GC.AllocateUninitializedArray(fields.DecodedDataLength); + + if (!Convert.TryFromBase64Chars(contents[fields.Base64Data], certBytes, out int bytesWritten) + || bytesWritten != fields.DecodedDataLength) + { + Debug.Fail("The contents should have already been validated by the PEM reader."); + throw new CryptographicException(SR.Cryptography_X509_NoPemCertificate); + } + + try + { + // Check that the contents are actually an X509 DER encoded + // certificate, not something else that the constructor will + // will otherwise be able to figure out. + CertificateAsn.Decode(certBytes, AsnEncodingRules.DER); + } + catch (CryptographicException) + { + throw new CryptographicException(SR.Cryptography_X509_NoPemCertificate); + } + + return new X509Certificate2(certBytes); + } + } + + throw new CryptographicException(SR.Cryptography_X509_NoPemCertificate); + } + + private static X509Certificate2 ExtractKeyFromPem( + ReadOnlySpan keyPem, + string[] labels, + Func factory, + Func import) where TAlg : AsymmetricAlgorithm + { + foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(keyPem)) + { + ReadOnlySpan label = contents[fields.Label]; + + foreach (string eligibleLabel in labels) + { + if (label.SequenceEqual(eligibleLabel)) + { + TAlg key = factory(); + key.ImportFromPem(contents[fields.Location]); + + try + { + return import(key); + } + catch (ArgumentException ae) + { + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae); + } + } + } + } + + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey); + } + + private static X509Certificate2 ExtractKeyFromEncryptedPem( + ReadOnlySpan keyPem, + ReadOnlySpan password, + Func factory, + Func import) where TAlg : AsymmetricAlgorithm + { + foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(keyPem)) + { + ReadOnlySpan label = contents[fields.Label]; + + if (label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey)) + { + TAlg key = factory(); + key.ImportFromEncryptedPem(contents[fields.Location], password); + + try + { + return import(key); + } + catch (ArgumentException ae) + { + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae); + } + + } + } + + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey); + } + private static X509Extension? CreateCustomExtensionIfAny(Oid oid) => oid.Value switch { diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2Collection.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2Collection.cs index 101bfdd79d027e..ce00b573a7f86e 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2Collection.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/X509Certificate2Collection.cs @@ -2,8 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Internal.Cryptography; using Internal.Cryptography.Pal; using Microsoft.Win32.SafeHandles; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Security.Cryptography.X509Certificates.Asn1; namespace System.Security.Cryptography.X509Certificates { @@ -229,5 +233,106 @@ public void RemoveRange(X509Certificate2Collection certificates) throw; } } + + /// + /// Imports a collection of RFC 7468 PEM-encoded certificates. + /// + /// The path for the PEM-encoded X509 certificate collection. + /// + /// + /// See for additional documentation about + /// exceptions that can be thrown. + /// + /// + /// PEM-encoded items with a CERTIFICATE PEM label will be imported. PEM items + /// with other labels will be ignored. + /// + /// + /// More advanced scenarios for loading certificates and + /// can leverage to enumerate + /// PEM-encoded values and apply any custom loading behavior. + /// + /// + /// + /// The decoded contents of a PEM are invalid or corrupt and could not be imported. + /// + /// + /// is . + /// + public void ImportFromPemFile(string certPemFilePath) + { + if (certPemFilePath is null) + throw new ArgumentNullException(nameof(certPemFilePath)); + + ReadOnlySpan contents = System.IO.File.ReadAllText(certPemFilePath); + ImportFromPem(contents); + } + + /// + /// Imports a collection of RFC 7468 PEM-encoded certificates. + /// + /// The text of the PEM-encoded X509 certificate collection. + /// + /// + /// PEM-encoded items with a CERTIFICATE PEM label will be imported. PEM items + /// with other labels will be ignored. + /// + /// + /// More advanced scenarios for loading certificates and + /// can leverage to enumerate + /// PEM-encoded values and apply any custom loading behavior. + /// + /// + /// + /// The decoded contents of a PEM are invalid or corrupt and could not be imported. + /// + public void ImportFromPem(ReadOnlySpan certPem) + { + int added = 0; + + try + { + foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(certPem)) + { + ReadOnlySpan label = contents[fields.Label]; + + if (label.SequenceEqual(PemLabels.X509Certificate)) + { + // We verify below that every byte is written to. + byte[] certBytes = GC.AllocateUninitializedArray(fields.DecodedDataLength); + + if (!Convert.TryFromBase64Chars(contents[fields.Base64Data], certBytes, out int bytesWritten) + || bytesWritten != fields.DecodedDataLength) + { + Debug.Fail("The contents should have already been validated by the PEM reader."); + throw new CryptographicException(SR.Cryptography_X509_NoPemCertificate); + } + + try + { + // Check that the contents are actually an X509 DER encoded + // certificate, not something else that the constructor will + // will otherwise be able to figure out. + CertificateAsn.Decode(certBytes, AsnEncodingRules.DER); + } + catch (CryptographicException) + { + throw new CryptographicException(SR.Cryptography_X509_NoPemCertificate); + } + + Import(certBytes); + added++; + } + } + } + catch + { + for (int i = 0; i < added; i++) + { + RemoveAt(Count - 1); + } + throw; + } + } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/Cert.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/Cert.cs index c2c6572d4d8d53..b4c535bccf3ddb 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/Cert.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/Cert.cs @@ -56,6 +56,20 @@ public static ImportedCollection Import(string fileName, string password, X509Ke collection.Import(fileName, password, keyStorageFlags); return new ImportedCollection(collection); } + + public static ImportedCollection ImportFromPem(ReadOnlySpan certPem) + { + X509Certificate2Collection collection = new X509Certificate2Collection(); + collection.ImportFromPem(certPem); + return new ImportedCollection(collection); + } + + public static ImportedCollection ImportFromPemFile(string certPemFilePath) + { + X509Certificate2Collection collection = new X509Certificate2Collection(); + collection.ImportFromPemFile(certPemFilePath); + return new ImportedCollection(collection); + } } // diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs index d280c9b53c941d..ffb8241b0d31f0 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Text; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests @@ -1426,6 +1427,115 @@ public static void SerializedCertDisposeDoesNotRemoveKeyFile() } } + [Fact] + public static void ImportFromPem_SingleCertificate_Success() + { + using(ImportedCollection ic = Cert.ImportFromPem(TestData.ECDsaCertificate)) + { + Assert.Single(ic.Collection); + Assert.Equal("E844FA74BC8DCE46EF4F8605EA00008F161AB56F", ic.Collection[0].Thumbprint); + } + } + + [Fact] + public static void ImportFromPem_SingleCertificate_IgnoresUnrelatedPems_Success() + { + string pemAggregate = TestData.ECDsaPkcs8Key + TestData.ECDsaCertificate; + + using(ImportedCollection ic = Cert.ImportFromPem(pemAggregate)) + { + Assert.Single(ic.Collection); + Assert.Equal("E844FA74BC8DCE46EF4F8605EA00008F161AB56F", ic.Collection[0].Thumbprint); + } + } + + [Fact] + public static void ImportFromPem_MultiplePems_Success() + { + string pemAggregate = TestData.RsaCertificate + TestData.ECDsaCertificate; + + using(ImportedCollection ic = Cert.ImportFromPem(pemAggregate)) + { + Assert.Equal(2, ic.Collection.Count); + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", ic.Collection[0].Thumbprint); + Assert.Equal("E844FA74BC8DCE46EF4F8605EA00008F161AB56F", ic.Collection[1].Thumbprint); + } + } + + [Fact] + public static void ImportFromPemFile_MultiplePems_Success() + { + string pemAggregate = TestData.RsaCertificate + TestData.ECDsaCertificate; + + using (TempFileHolder aggregatePemFile = new TempFileHolder(pemAggregate)) + using(ImportedCollection ic = Cert.ImportFromPemFile(aggregatePemFile.FilePath)) + { + Assert.Equal(2, ic.Collection.Count); + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", ic.Collection[0].Thumbprint); + Assert.Equal("E844FA74BC8DCE46EF4F8605EA00008F161AB56F", ic.Collection[1].Thumbprint); + } + } + + [Fact] + public static void ImportFromPemFile_Null_Throws() + { + X509Certificate2Collection cc = new X509Certificate2Collection(); + + AssertExtensions.Throws("certPemFilePath", () => + cc.ImportFromPemFile(null)); + } + + [Fact] + public static void ImportFromPem_Exception_AllOrNothing() + { + using(ImportedCollection ic = Cert.ImportFromPem(TestData.DsaCertificate)) + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine(TestData.RsaCertificate); + builder.AppendLine(@" + -----BEGIN CERTIFICATE----- + MIII + -----END CERTIFICATE-----"); + builder.AppendLine(TestData.ECDsaCertificate); + + Assert.ThrowsAny(() => ic.Collection.ImportFromPem(builder.ToString())); + Assert.Single(ic.Collection); + Assert.Equal("35052C549E4E7805E4EA204C2BE7F4BC19B88EC8", ic.Collection[0].Thumbprint); + } + } + + [Fact] + public static void ImportFromPem_NonCertificateContent_Pkcs12_Fails() + { + X509Certificate2Collection cc = new X509Certificate2Collection(); + + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.RsaCertificate, TestData.RsaPkcs1Key)) + { + string content = Convert.ToBase64String(cert.Export(X509ContentType.Pkcs12)); + string certContents = $@" +-----BEGIN CERTIFICATE----- +{content} +-----END CERTIFICATE----- +"; + Assert.Throws(() => cc.ImportFromPem(certContents)); + } + } + + [Fact] + public static void ImportFromPem_NonCertificateContent_Pkcs7_Fails() + { + X509Certificate2Collection cc = new X509Certificate2Collection(); + + string content = Convert.ToBase64String(TestData.Pkcs7ChainDerBytes); + string certContents = $@" +-----BEGIN CERTIFICATE----- +{content} +-----END CERTIFICATE----- +"; + + Assert.Throws(() => cc.ImportFromPem(certContents)); + } + private static void TestExportSingleCert_SecureStringPassword(X509ContentType ct) { using (var pfxCer = new X509Certificate2(TestData.PfxData, TestData.CreatePfxDataPasswordSecureString(), Cert.EphemeralIfPossible)) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj index be18ed29c4ca65..c5cd7e39fe0082 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj @@ -34,9 +34,11 @@ + + content) + { + FilePath = Path.GetTempFileName(); + + using (StreamWriter writer = new StreamWriter(FilePath, append: false)) + { + writer.Write(content); + } + } + + public void Dispose() + { + try + { + File.Delete(FilePath); + } + catch + { + // Best effort + } + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs index ff9363f7c8bf35..506eb4ad4736a7 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs @@ -1780,5 +1780,303 @@ internal static DSAParameters GetDSA1024Params() "B2F79D3A99ED75F7B871E6EAF2680D96D574A5F4C13BACE3B4B44DE1").HexToByteArray(); return p; } + + public const string RsaCertificate = @" +-----BEGIN CERTIFICATE----- +MIIDNjCCAh4CCQCu2+oEr9yAxDANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzERMA8GA1UECAwIVmlyZ2luaWExEzARBgNVBAcMCkFsZXhhbmRyaWExEDAOBgNV +BAoMB0NvbnRvc28xFDASBgNVBAsMC0VuZ2luZWVyaW5nMB4XDTIwMDYwODE3MDk1 +M1oXDTIwMDcwODE3MDk1M1owXTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdp +bmlhMRMwEQYDVQQHDApBbGV4YW5kcmlhMRAwDgYDVQQKDAdDb250b3NvMRQwEgYD +VQQLDAtFbmdpbmVlcmluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AL/yBbH8MoxLuApvUhWMMV+5h+v15xY9or+TxyAkwwtP0mLKMWPeHZm2S4OT8JCn +fJmz7J+7J4Nf3VB0HYDN+41uEEwNX3Caps30Kt17dcJm3/sG9uBB7eiQdbj2HNSS +nzxYK7d2mKnWwZlHg0I7/ZR5TMM8eCdJpESsHRpF5dPGgdWFgUzbdi73Yyk5/PFM +lauHPM0d4TWHoWnY0yh7Y08PMY/4MH7HoMVt+mbpV2d3DRC8WS0jhU+mbCqce4St +BWTCmR+ObuHFdMSVx7o88MWmdteZXCX8N6ohPwAl9W02pOa5Vq37xwuUZ1VvT9G2 +ndi+VRRgN51HCmG1qB1LhtsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAIBtwgcLn +7LT+gfqqDsaZcMjpsHMu2TamR9inoRdxwoJKnG7dIe8tWL+nGxsN38DjjcZhb3y0 +Ilqi0e7LYqb0QyXAuS+Su2uJjypgvNd4stj+4Pl1EfU1rpzed4CA1O0pap5m0U++ +2YmWNNBmSZxcUi2ge09ZqrKm78a7Vtrpy8bNcixb+szrPSUFWh7WOXBCABusZ/OY +MnzZBXtQtQzDCtJb6IxevxAGod1XxInXQaB+nDnG4MD3v4MZQYgTI76+cAPxS6nx +pzJ/tfFzq7OAGBkrxcdmzqb1/caPWINKzwbDAEuNX2yeThP8eVrbRvGciy7LNzvR +EA0/67lEfPmRow== +-----END CERTIFICATE-----"; + + public const string ECDsaCertificate = @" +-----BEGIN CERTIFICATE----- +MIICDzCCAbWgAwIBAgIUeohSwWS4OT31fY5xvUxTEv07e8IwCgYIKoZIzj0EAwIw +XTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRMwEQYDVQQHDApBbGV4 +YW5kcmlhMRAwDgYDVQQKDAdDb250b3NvMRQwEgYDVQQLDAtEZXZlbG9wbWVudDAe +Fw0yMDA2MDgxOTE4NDVaFw0yMTA2MDgxOTE4NDVaMF0xCzAJBgNVBAYTAlVTMREw +DwYDVQQIDAhWaXJnaW5pYTETMBEGA1UEBwwKQWxleGFuZHJpYTEQMA4GA1UECgwH +Q29udG9zbzEUMBIGA1UECwwLRGV2ZWxvcG1lbnQwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAAQyB0wIKKfk5lmK4Z907qQnPsRRXh3TrU/VPMCTHxuBwoZqFBSE7gGm +JWLTnGwZ0MGMACP+N1HK4dU1S9VNoNw9o1MwUTAdBgNVHQ4EFgQUPzDLKQI9EfTa +rnMMO0/4p8rNwZgwHwYDVR0jBBgwFoAUPzDLKQI9EfTarnMMO0/4p8rNwZgwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiAwq18UD2DCufaMGTsz4JOQ +1vRqLI4hMLsIUQNyYgzbcgIhAJsB3qgv5WGyshlav98MPORcdCmYfkIvUCal0oPX +Wesb +-----END CERTIFICATE-----"; + + public const string ECDsaPkcs8Key = @" +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIg/wHlL4nNyuLFbK +00Ga7N5LbP/2YCgEXqBiB3el8syhRANCAAQyB0wIKKfk5lmK4Z907qQnPsRRXh3T +rU/VPMCTHxuBwoZqFBSE7gGmJWLTnGwZ0MGMACP+N1HK4dU1S9VNoNw9 +-----END PRIVATE KEY-----"; + + public const string ECDsaEncryptedPkcs8Key = @" +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHgMEsGCSqGSIb3DQEFDTA+MCkGCSqGSIb3DQEFDDAcBAh5RmzN7/AXZgICCAAw +DAYIKoZIhvcNAgkFADARBgUrDgMCBwQIQHgahqqSQKcEgZCvEBMgW8a7IXmT+weI +0mlM4AcTELDkE+SEKpYC5qVF4ZDyrw4OmnVLkSPiu0GUwgJIopazWOQfetMdgC5Q +n5pYHoRm9bek0s6TK9eoaTIA+M2T0MMNM0fWXWcYaT5B2/4Uv+mMEYgIRncFe1c1 +FEixDW6ObZIXVBbxl+zK1KwtCpdewXE4HRX/qpBrPhB8z2s= +-----END ENCRYPTED PRIVATE KEY-----"; + + public const string ECDsaECPrivateKey = @" +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICIP8B5S+JzcrixWytNBmuzeS2z/9mAoBF6gYgd3pfLMoAoGCCqGSM49 +AwEHoUQDQgAEMgdMCCin5OZZiuGfdO6kJz7EUV4d061P1TzAkx8bgcKGahQUhO4B +piVi05xsGdDBjAAj/jdRyuHVNUvVTaDcPQ== +-----END EC PRIVATE KEY-----"; + + public const string RsaPkcs1Key = @" +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAv/IFsfwyjEu4Cm9SFYwxX7mH6/XnFj2iv5PHICTDC0/SYsox +Y94dmbZLg5PwkKd8mbPsn7sng1/dUHQdgM37jW4QTA1fcJqmzfQq3Xt1wmbf+wb2 +4EHt6JB1uPYc1JKfPFgrt3aYqdbBmUeDQjv9lHlMwzx4J0mkRKwdGkXl08aB1YWB +TNt2LvdjKTn88UyVq4c8zR3hNYehadjTKHtjTw8xj/gwfsegxW36ZulXZ3cNELxZ +LSOFT6ZsKpx7hK0FZMKZH45u4cV0xJXHujzwxaZ215lcJfw3qiE/ACX1bTak5rlW +rfvHC5RnVW9P0bad2L5VFGA3nUcKYbWoHUuG2wIDAQABAoIBAEavE5XVr6+meqGt +GOdCdzQvGHS2W2D/VZ2DCAM4RnM1893ZY5LJStE+JlTP9/jtFJ9teKfhvc1NUiy8 +ddjnAcm1TF8VVZ4b9W1GizqAqn7qb3T7vZIb9UZ9XDy+tSM601Tfi0nGbLWuliCi +Cx4rBVjVyoTjEcQ2BD4du6HfN6FihzCu+DV6Dsm4Ar6orWRCYJ0v1doMYdGzOaoJ +SwcWCtAYkJ+krfYz37xlrFJfppMgYy67q040XBV7PuyjkGtt1GcNO98pw0puiCAs +VP+u+vnngNCjSqXlVSAZL1ISJylUxKkNRynABNBTfdGeRWCuhwc/M0vuEeX8A5Ce +NlDA1MECgYEA5iGjpQkyeXcsSP6zqE4LhA0L20YoCsnYnyout/Hn4C4AtUH3Yt2o +MvpRne7JxmGxmhSJkud+z6LfbaAKLU/s86wslOeclwlMkFz+CK75AFCDNecNgFCR +jENNBFIou329+/Rx7+fbGsgHfMwz2Cdv98hKCOmX3W3pZRCW+gVLMtECgYEA1YWG +q1v4M2QdB3f8hKiElicvlyNwVW7rHgbcsUiDPdGSLg/fREZeA3C4bnV+Mi8LlRDJ ++PoZyqML+xWhQDGWK+7r0iWuvFPsbCwk0BRTCYue0p7pCsUEjS0vVQPk0O1fLlOH +UdLtyyuYegsAtn5XSMN7+LeM0zyP+OWVfqWkEesCgYBSiYQYv+izedOPRpKG7Z7h +uJAlD89ytxwTUdy5qnBAjh9A4yzn75nQ1siI/Uiu9wDswyroXlC0BbVeqwSbZcwV +RQ4kRcF6xiIIsOGHmcHCpB27KmhEOiFJjiXEQ/dJ73pBMFXg9mY1/8H3t4FsqBBX +bSVoduc5yp7n2YBcoaNr0QKBgCPloPBqM94f9KluyKtc0X2U9PFJ6fbTAQA5Ux0S +/c2E0DiiPnzx/5hAeSFI64BwXFghTHNpSLDCnJ8H0eZC7+ZO8qKP50KOMW82NLIu +2I8ARCFQygkfelZpxE1crDlbzuaw8E0XUxcXKzlJZENKFk6LXuo/oZNZ2TKVFn8G +RgElAoGAMsD758wEk2bVTJFNISizHLHQ4yyCYgJSjmUqtP/GqK0qvo4Kz5TSm/2D +H7b+ao0a48b+i4EyuDTVT/CeJ3RoQzXvayc9WuipV/ewRBYRsEVzZwFO6D4aD+jI +a+7OpVqFLMtAySKTg0vPwcRF4OxQ/4YBE1e+ZGnx+vVTpkE3gjw= +-----END RSA PRIVATE KEY-----"; + + public const string RsaPkcs1PublicKey = @" + -----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAv/IFsfwyjEu4Cm9SFYwxX7mH6/XnFj2iv5PHICTDC0/SYsoxY94d +mbZLg5PwkKd8mbPsn7sng1/dUHQdgM37jW4QTA1fcJqmzfQq3Xt1wmbf+wb24EHt +6JB1uPYc1JKfPFgrt3aYqdbBmUeDQjv9lHlMwzx4J0mkRKwdGkXl08aB1YWBTNt2 +LvdjKTn88UyVq4c8zR3hNYehadjTKHtjTw8xj/gwfsegxW36ZulXZ3cNELxZLSOF +T6ZsKpx7hK0FZMKZH45u4cV0xJXHujzwxaZ215lcJfw3qiE/ACX1bTak5rlWrfvH +C5RnVW9P0bad2L5VFGA3nUcKYbWoHUuG2wIDAQAB +-----END RSA PUBLIC KEY-----"; + + public const string RsaPkcs8PublicKey = @" +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/IFsfwyjEu4Cm9SFYwx +X7mH6/XnFj2iv5PHICTDC0/SYsoxY94dmbZLg5PwkKd8mbPsn7sng1/dUHQdgM37 +jW4QTA1fcJqmzfQq3Xt1wmbf+wb24EHt6JB1uPYc1JKfPFgrt3aYqdbBmUeDQjv9 +lHlMwzx4J0mkRKwdGkXl08aB1YWBTNt2LvdjKTn88UyVq4c8zR3hNYehadjTKHtj +Tw8xj/gwfsegxW36ZulXZ3cNELxZLSOFT6ZsKpx7hK0FZMKZH45u4cV0xJXHujzw +xaZ215lcJfw3qiE/ACX1bTak5rlWrfvHC5RnVW9P0bad2L5VFGA3nUcKYbWoHUuG +2wIDAQAB +-----END PUBLIC KEY-----"; + + public const string RsaPkcs8Key = @" +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/8gWx/DKMS7gK +b1IVjDFfuYfr9ecWPaK/k8cgJMMLT9JiyjFj3h2ZtkuDk/CQp3yZs+yfuyeDX91Q +dB2AzfuNbhBMDV9wmqbN9Crde3XCZt/7BvbgQe3okHW49hzUkp88WCu3dpip1sGZ +R4NCO/2UeUzDPHgnSaRErB0aReXTxoHVhYFM23Yu92MpOfzxTJWrhzzNHeE1h6Fp +2NMoe2NPDzGP+DB+x6DFbfpm6Vdndw0QvFktI4VPpmwqnHuErQVkwpkfjm7hxXTE +lce6PPDFpnbXmVwl/DeqIT8AJfVtNqTmuVat+8cLlGdVb0/Rtp3YvlUUYDedRwph +tagdS4bbAgMBAAECggEARq8TldWvr6Z6oa0Y50J3NC8YdLZbYP9VnYMIAzhGczXz +3dljkslK0T4mVM/3+O0Un214p+G9zU1SLLx12OcBybVMXxVVnhv1bUaLOoCqfupv +dPu9khv1Rn1cPL61IzrTVN+LScZsta6WIKILHisFWNXKhOMRxDYEPh27od83oWKH +MK74NXoOybgCvqitZEJgnS/V2gxh0bM5qglLBxYK0BiQn6St9jPfvGWsUl+mkyBj +LrurTjRcFXs+7KOQa23UZw073ynDSm6IICxU/676+eeA0KNKpeVVIBkvUhInKVTE +qQ1HKcAE0FN90Z5FYK6HBz8zS+4R5fwDkJ42UMDUwQKBgQDmIaOlCTJ5dyxI/rOo +TguEDQvbRigKydifKi638efgLgC1Qfdi3agy+lGd7snGYbGaFImS537Pot9toAot +T+zzrCyU55yXCUyQXP4IrvkAUIM15w2AUJGMQ00EUii7fb379HHv59sayAd8zDPY +J2/3yEoI6ZfdbellEJb6BUsy0QKBgQDVhYarW/gzZB0Hd/yEqISWJy+XI3BVbuse +BtyxSIM90ZIuD99ERl4DcLhudX4yLwuVEMn4+hnKowv7FaFAMZYr7uvSJa68U+xs +LCTQFFMJi57SnukKxQSNLS9VA+TQ7V8uU4dR0u3LK5h6CwC2fldIw3v4t4zTPI/4 +5ZV+paQR6wKBgFKJhBi/6LN5049GkobtnuG4kCUPz3K3HBNR3LmqcECOH0DjLOfv +mdDWyIj9SK73AOzDKuheULQFtV6rBJtlzBVFDiRFwXrGIgiw4YeZwcKkHbsqaEQ6 +IUmOJcRD90nvekEwVeD2ZjX/wfe3gWyoEFdtJWh25znKnufZgFyho2vRAoGAI+Wg +8Goz3h/0qW7Iq1zRfZT08Unp9tMBADlTHRL9zYTQOKI+fPH/mEB5IUjrgHBcWCFM +c2lIsMKcnwfR5kLv5k7yoo/nQo4xbzY0si7YjwBEIVDKCR96VmnETVysOVvO5rDw +TRdTFxcrOUlkQ0oWTote6j+hk1nZMpUWfwZGASUCgYAywPvnzASTZtVMkU0hKLMc +sdDjLIJiAlKOZSq0/8aorSq+jgrPlNKb/YMftv5qjRrjxv6LgTK4NNVP8J4ndGhD +Ne9rJz1a6KlX97BEFhGwRXNnAU7oPhoP6Mhr7s6lWoUsy0DJIpODS8/BxEXg7FD/ +hgETV75kafH69VOmQTeCPA== +-----END PRIVATE KEY-----"; + + // password is `test`. + public const string RsaEncryptedPkcs8Key = @" +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFGTBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQIozMG90LwJXECAggA +MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECK51Ahgtvr/LBIIEyO38HrNbUgAI95h6 +JolbqSkwpPeDfQsxmRlWsqtlin8K63U5gCZS/I1nkkkC13xXHB6WOQ0DrYIGv5pk +ceKiGYNki5UMxw7voXBw2F4pih07iBiATW0uKQnlNxdxbaxH4AEVyxaYTmfI6een +cPyVpgjk8BqZWIrzrcCNq0MfSkI7X1u2VRpni79xETMkdBDeNA5KqwwmeDs8EpRR +laWj2u/s+fpYkR0G1HgrJswMX38Ybp5GBu9i+JJUleDY/ByFRwaEmaMyArdhcI5V +Fp5Bl4vFFsrB0gXf8tApKN3Bmhou2gpO5VbRp3j1iHU27JCa8yBr9KOyrZ8UZrIz +bBcwjHTDOUOp1hwRVu1RUij+b33Bf4IPSobesGovIUgIe0iV+luOZGgitITb5Q4Q +Rup+q4v/M6E2JLry2zDz/FHOmtk83Og+lp9kczHjpGbj7OrGqOyT8YGqpLbmgD6q +vsuus8nCTEpAWG0rmI3RMsOR/nHdQkhgRb8U74vMIFhKfupUVAhLRvB9reKx44p0 +k5TUS+YYxh2LdZLquUeYcjMKFbRjSnzO/6N5vo8/Y/AV646bTSzrySmNaOUYArvl +A4l/GMZOFjODfjj0/p2AGE4q5kC9cKmPS99TfqD+l4IBncw9TD/tzi+c/+R+WSy9 +evT2cwQE26byo7703B7mn5Y1e0068gitPUhx5mudEloVN8Hg7N+opZpMWLrCAMEx +6kyXbWSdnrSLqv/LTSKXq2WoBk1tJ+/vfsOYdvXHU7oSje8ZoCj70OHsk3f9q8eH +b6R/+hvOJBg4UTtljXJ2BpF8yq34GPLuINYtBuMh/wo7iKQzz9AxdlfSn/EaR8Bz +MnFJlQo76Ie0LSuiHMqMunF9z6TweHMQI37IocBqpOMDRo+jz+Q0AQTe/H8PhQk0 +tGN68tPCpgkuY4apjWcNv/6dxm9/ZpR0ZvbA2DXSnxK4p1BQUEeUX1bTrb5DKL6a +KqRPokfoUbdyikGc80+TOk9SJ95if1+2XwT8gN+NLSagCRnJPWdZaOQqInYcaaZ+ +fXWPymGWEUjfIa9L71YMTOaVJ73oooC0mGFBKxMlGNamhRSqXUnX8OMJ5Zzflajw +HkgsooaGaL0CjHn7h1zFdctHyJTjRLamKe4f0drBcREz9U3sU5DQ31+hScqtJp+C +k/MZgiRRJudfi2tjNFDa28iP9U0wygPZ0RvD9IXBpYZH7KroYMtT2ysK0q16EJc3 +UqhwwEpCj6rPcVktEh/sYmeS6HL19VL03hwdxkKdmQ/7PjI8/vCy96AeIxgpBEJm +ziWCZffAXkbYwSNL2YIHronuvyt4sfjCctnoQUIBoCOQIwPF6yfNKbpL3ihPg55I +EsaecMRl/FDCbew/NqXY1ujA/ntpSMCcE4SkKKIGmzinlINw3BdfLU8bVzlPknCY +3W5ZH9Z5FV4SsEXmrRWFcAKb5kd1hvEl7XtOWpRvOtDbk9WrCko/xY2fgDXyYm25 +fl2Y9C69W24gloqGX1EOOCv1sOnUBaOoS0QnGaTPoo7aos+TNah6vB3qjylwxFFE +YTdGR9lY5wKHcJeFATIIcQyL+cziHNJQCwM839Q3uc0Sj6JvVm0sCHd/VUgzhfzX +16v5sI3D+15h+GTFjA== +-----END ENCRYPTED PRIVATE KEY-----"; + + public const string Ed25519Certificate = @" +-----BEGIN CERTIFICATE----- +MIICFzCCAcmgAwIBAgIUXX/A8D+9ewGf98c9FjYwAAhRIsswBQYDK2VwMIGAMQsw +CQYDVQQGEwJVUzEcMBoGA1UECAwTRGlzdGljdCBvZiBDb2x1bWJpYTETMBEGA1UE +BwwKV2FzaGluZ3RvbjEUMBIGA1UECgwLRGV2ZWxvcG1lbnQxFDASBgNVBAsMC0Rl +dmVsb3BtZW50MRIwEAYDVQQDDAlUZXN0IENlcnQwHhcNMjAwNDI4MTUwMjMwWhcN +MjEwNDI4MTUwMjMwWjCBgDELMAkGA1UEBhMCVVMxHDAaBgNVBAgME0Rpc3RpY3Qg +b2YgQ29sdW1iaWExEzARBgNVBAcMCldhc2hpbmd0b24xFDASBgNVBAoMC0RldmVs +b3BtZW50MRQwEgYDVQQLDAtEZXZlbG9wbWVudDESMBAGA1UEAwwJVGVzdCBDZXJ0 +MCowBQYDK2VwAyEAJ0WQ+SKMG8q3g9alCNzq0t3vjAeN295rq6IdaFj0NmSjUzBR +MB0GA1UdDgQWBBTUdW0AAtdSTqb1Oc6J2Pfvsk4bpjAfBgNVHSMEGDAWgBTUdW0A +AtdSTqb1Oc6J2Pfvsk4bpjAPBgNVHRMBAf8EBTADAQH/MAUGAytlcANBAOjstGTq +0fmzySS+DjSNfvkB9IXmaGd3XGIM8SWTKCnFi7L+mXhDBgd9wLO3H+EulBiuKtIo +mf0l9zwCygC+aA8= +-----END CERTIFICATE-----"; + + + public const string OtherRsaPkcs1Key = @" +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA05yPUFo642M7gUYBYZgp9Itk/yPcrLcisUImPvQAsrrCeQDR +kKxo1rNApfEZAK5UffEL7kbYFRY7v0mhwWY4K2g17FJ5Bd7W54Sj5ziDzk5LKvvN +dxjZIIDi69z+AZYKcPWu4D6Znbi8isDeWv8mUTeFcA8Ine4YuvsAJ6b6IHGuz0Kr +qCZeE6319wjA6Bh14UziIPxOm1LLJ8Vlwq7dtngk034kUyliO9kWJg7aNatthn5U +q3nSUUCarhjfMFu0GhBy0TFRfsQmKNLOBMtrtx058aVbzeR3XO6pYhVsdF1cwhoP +SXPCYpgc9ZslH9u9RjQoP/0+vRuT3jBfHFKtvQIDAQABAoIBADufE93+3jKtBdoB +gGgf+Eo3cChW0Vk0bCjnS2FXXE7/QcXYDjOl8A/2B1P53yKK+7FUVhk3irA+SG03 +8MRN6auJPBAumHyn8YLfo1KFYNWix5j/wz84GA0JY2YzKLoHrT9waWozGRMQNscd +WkjnNMOTUhxlj/b3W65rA/soF7dF7Vohb4Ab251apJi/k/IBkv6gVUUH6adhPSxG +piEY4AZ0Vpngf14nI5gesStFLBhJqBtHbMeicdQbHJzg9OjNpQzNGCQzCx7EIteM +LJ/DZ+r6S9qm/m5qvgxALc0SMTi58RgIf0JwWpTrUuEEOfO0qgjKZoi8As8yFR27 +cU0n+MECgYEA7m5U81M+d/nV116Ln1+s9t0NbaR0Vzk68WsHok54lSUTItcsuCxo +CpCp7B0ARi8KzG3W9Tv+NPS64JZ7iYj/ST6w02IjgpbldpJXmPl4wV57uUOPsTq8 +hReeGEcXGN5wYTCo6jiWNmiLR4CNScmFjxcjwcXSGwac5+Sn/MttT5ECgYEA4zRR +JPaDqluVhGhu+uK5Jhb1tqeTlruSxorRuN2GAApaf354+SdfQffD73zqCpXDdtbq +uT7/Qu6tniR1bEFpLKSGeH/ayLYNfxTy9m9aC6ccxTmEhbDSntY/HHo50CtWJOWz +F5O4R6aeE5rqWfHiB+Ya9dDhdVPor5wUyPUOfW0CgYBMGw2kqaKb+zRFzZj1oz17 +gu3BXKgCG3N0Efza0v9sY+wqx9Iva4U+MhT7F/q5bFSfEkR1/NNUpfVssLv4F7Gc ++JTKtF2vVmkiIu4xFxhzaKxHY4hfQudf+DzvdOmrd30ZmMWiFbPk5BPpG+B9eATY +usMgG/vHwqGc54CzkV9v8QKBgHlveP9ckrr3AE8o0khd7b+h/eqGXqft0WE0ySsZ +m4lh/0StgFMK7CsCFkNmbGED8tkNvZ8NQLmxgDJKIkieHWyy8vxsua8VPtlxhPqa +QXKA+yuetmoOPESRFmJOIaBVyVEnRDWRyqjhMRQhdKhmU/0My9QetKJVGsThk2pl +MD1xAoGBANrkobUHP4sUIGenNfWld1As4tmKXRDTv2HHVKOCvIfF8XY5jMQBf96W +Tx7VNbqFa2IEKZW07Q3TmLgD4cplE+XxIKGRGIie3PECiQdO+EieEtPe+nFLDUrb +w4qGwpDllP58gBySGj3BQS8PG8d9fuOlCSfOQIW10dE8U9H0wwlE +-----END RSA PRIVATE KEY-----"; + + public const string OtherRsaPkcs8EncryptedKey = @" +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFGTBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQICs3NqD8hVe0CAggA +MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECBLUM63bM9vSBIIEyAk8lJcgxlkclPR0 +9BlUXGQA/ll6zCQa1DDiXdrLxTKuuedwOv0YF3X/iiZlhpey0E8zjKo13HQt7P+w +rpYVvA+qHH+4k0oc/ZC4ZD/ml4AIKCpQGs0AX8gQXJ9z8heFbCGAIYe2O8sx+Teh +V9YNbhzXiZ521+pPpMgzlrfeDFVimjCEw9ESNuHl1nJaHLzeC+S6TL5uXNJCy3BF +XwNYH53GfSIWwWcxiDKdHu0uI0pTUgCUKRgm4qWxqQvbO+dd+pBvqa/nrd5Yk8Sa +vSUmsSHBHiMnYoEs2guD3BD6YIzgzGQ3dR9LWCJNCh7BavnYoGFPCre79RiA+Wdy +8Sxd+irf12JESZdQPsUSAiMBHtZ3gpsvO1bA+2xwSqE0EcltHdrDyUoLzUAx+2yU +0uvJxRlchYsNLAkDcezOWA1W4uisNBQCXxklSLIVRk7zKvAtZ5vLm0fvybCYoHyH +xPTPs3k16xBF5lsquGF/XHcxfKjB2s8oMDRU/7ZnOSjCOyZV/Ey07ZkjhU5GbyxI +ninu5h7q2K79GWOm3B4LaEzl3IYFCFifJqod1UyI08fv3l2x5GdGlEL9Hdvl1nAW +Ty4JnhvvJR4VdGGOczJNDv5/ZDirN4wpW5Q5QUnbedIZjiTugxHC81Q1JxMXfFPs +iDLCVXAU4oZwfkQYJYbwvR/5Zu973zFRIjsp2cJgdN8XsS5mFUD69cnGnZaHEMJy +o+Mki2VMF6QiimVS4RezJFQcdglrY4Uy16J4indJFGIjJez78M1+Wdr0iDk5akOr +pln+Dj5yim3IG5+hfY5Q/jkbuPMSUkfsLhGAHrxKCLcJSj6+FYV4/Mk7lpC7WPND +xkVTYae0XDOAKGzXZ2fvS7ORk5ZJDn0RKoHEv31QXpIP4fyzM54x0tknnxlnI++J +erLOgEnl0r9F0uM4Lw/JqYRhfjrroMUgGxDWSHwGtHkanhjQNAKTY4zLp830HZY/ +yii7+Tt7q29/P4R0cHUcnqdz77re/zYZK7ru63UNZwnE6hybG4c9vneCVOT6bmxG +Acxu2L6nVpUhxhhDQ/K1d0JgFNRg15rQjA/LN5MTfIDimPZAdGhDPey0NLLl2BVG +jc9JRT7KopOh2eypWcXecNCHqoDAKcHdAi71OIZRKEQS8BGVk/LY55iwdgKqkqLF ++iuWtEfgDD9aekOBKLdTQqn8PvvHc3+QOFIoDzQ7iJLh7Uca6MUqK0uwBieDnK7B +8b9NLs2Uz1Rr4UqK+r+06QUQ/kOldgeLguouOt9fz+ugZNxfvDGXSbkzMXQbsSJ4 +EFs7DHD/76GXsgN6p95LeCdR1jGgFV/HlD+pdNUW5A+P3hs02UqvkWzeC4ZOBnLX +/oLjja5XWuxuYkyk0yE4UP0CzJMnrVwzKeqesrWKSP42miwSxPzSY4AY8pZnYY4Y +iOGN5vueX9hkfRmsrrud6rujPuS49ho+UkJuygmR9ElGktvzw1NbouhoaVsFyrRV +cEL42Y0WZVuUrNcm3X8ng5HkT2L5D9Rys3WV0GaaxximLzGzuhIsfG3cCiLq+ZT5 +XOmtog9dZs8lAIM58M/a/7q7dP0+FGxQ41/JslOjbWY5zypViy3aZssrkVF+7xZ/ +oWZazn8MBIuVRT4TcA== +-----END ENCRYPTED PRIVATE KEY-----"; + + public const string DsaCertificate = @" +-----BEGIN CERTIFICATE----- +MIIDWTCCAxWgAwIBAgIUFRQGA90GHC74cNK/hNzQDi7XJFYwCwYJYIZIAWUDBAMC +MF0xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhWaXJnaW5pYTETMBEGA1UEBwwKQWxl +eGFuZHJpYTEQMA4GA1UECgwHQ29udG9zbzEUMBIGA1UECwwLRGV2ZWxvcG1lbnQw +HhcNMjAwNjE5MTkyODIwWhcNMjAwNzE5MTkyODIwWjBdMQswCQYDVQQGEwJVUzER +MA8GA1UECAwIVmlyZ2luaWExEzARBgNVBAcMCkFsZXhhbmRyaWExEDAOBgNVBAoM +B0NvbnRvc28xFDASBgNVBAsMC0RldmVsb3BtZW50MIIBtjCCASsGByqGSM44BAEw +ggEeAoGBAJyiyioeXx1O98gRCMEjlPKMpr79KrcDkoroghtuXO1U6Cx34pBRjOQm +QLDPqSOriEo5VuG6SJc/ppfZx9TrSrzqB26hKTUmiaOKmwpfIfzpi72wgsZeMOtU +7JQ+FThfGyS8VxGh6G0h7xw26B/9ALxRw25zO1cy9ZJs0EY3hsHzAhUA/4dpclsc +k8K+SkWBTcPfU+x7wTUCgYB4LP6UvrvIiiFPxhk7AEGMMr0MhcJ3hhsgKWukUqIY +sJKBM5MpKCnej5BHvnLXdKodIxygcKR4dJX7BRv69L+2RJk+UrYL1qBco5HpUslu +mA0e3gNdwRLoOoGD14dn1LD1LdESsyMgwfHHJ0RRkYwacgCVXsvHv/eAkA8qq136 +dwOBhAACgYAHltgzkK3zD8yGdcGY0YgvN5l3lna1voLmcK+XtmehjMVy7OSSFICN +KybLBOvO8paydhCb1J0klkLPAoAjgP2cEd+KueeRyJpx+jD1MsjIEXIn5jtjXdUH +d0JJmHWAyHdNzmhXrXC7JLnj4ri7xMAV3GZGDpAnYvvL0LiXzFyomqNTMFEwHQYD +VR0OBBYEFF1l4ZrF3ND05CjGd//ev0dJLCB7MB8GA1UdIwQYMBaAFF1l4ZrF3ND0 +5CjGd//ev0dJLCB7MA8GA1UdEwEB/wQFMAMBAf8wCwYJYIZIAWUDBAMCAzEAMC4C +FQD6plYf60MDCvMjf1yQ8SBaFX3YYwIVAKqRQklh2b0Qhv+US222hb8xySJV +-----END CERTIFICATE-----"; + + public const string DsaPkcs8Key = @" +-----BEGIN PRIVATE KEY----- +MIIBSwIBADCCASsGByqGSM44BAEwggEeAoGBAJyiyioeXx1O98gRCMEjlPKMpr79 +KrcDkoroghtuXO1U6Cx34pBRjOQmQLDPqSOriEo5VuG6SJc/ppfZx9TrSrzqB26h +KTUmiaOKmwpfIfzpi72wgsZeMOtU7JQ+FThfGyS8VxGh6G0h7xw26B/9ALxRw25z +O1cy9ZJs0EY3hsHzAhUA/4dpclsck8K+SkWBTcPfU+x7wTUCgYB4LP6UvrvIiiFP +xhk7AEGMMr0MhcJ3hhsgKWukUqIYsJKBM5MpKCnej5BHvnLXdKodIxygcKR4dJX7 +BRv69L+2RJk+UrYL1qBco5HpUslumA0e3gNdwRLoOoGD14dn1LD1LdESsyMgwfHH +J0RRkYwacgCVXsvHv/eAkA8qq136dwQXAhUA216Tqp4OvdUBNv8QLv8Z5QPopGQ= +-----END PRIVATE KEY-----"; + + public const string DsaEncryptedPkcs8Key = @" +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBoTBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQI+PhdT1Kk/SkCAggA +MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECGV1ZmaiQtz2BIIBUA/6pNqTkXpkOLlI +22Lh0cm5+/foDRh3qTrAOSHHHV0Dz1xYvYMa9MFzONatLf55Rpb2ZPji3hXwUQfn +gOJeTBRTaMNz5LaKJiOIWj0qDckhgKt9cmgiBzVTvXO4pERp1uz5zcvaUOKj2TSv +ljxishj76MYQftIGMMkJQKf4OsHubCopuKUbzTPgJt0FuF4eT37+tiEMgbYrmA6p +REPE0vT1aY+LYdJLV/Dax/l4lMvYmQYOWs9TCLPlI5RZQxxte6zbcA13ESg/qLE3 +4Mx8xgXrPvCxp3h8KBKNMaJR1xzpr7UQOpkI9qja++3cJAl6O/0mdeqZct0V9Z8P +a3+wyUWo58z5sOPNdJHIMV6qw6m3w+IQoCJC7EbV0+Pyo5eSU5zbgm7YWZ9Yx6l8 +g1mCP4Q6Tqe6LjKfBsZAmYWSfKqoTKRjC3ocJMt53tIDpB5jFw== +-----END ENCRYPTED PRIVATE KEY-----"; } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509Certificate2PemTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509Certificate2PemTests.cs new file mode 100644 index 00000000000000..34803447ff4049 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509Certificate2PemTests.cs @@ -0,0 +1,440 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public static class X509Certificate2PemTests + { + [Fact] + public static void CreateFromPem_CryptographicException_NoCertificate() + { + Assert.Throws(() => + X509Certificate2.CreateFromPem(default, default)); + } + + [Fact] + public static void CreateFromPem_CryptographicException_MalformedCertificate() + { + const string CertContents = @" +-----BEGIN CERTIFICATE----- +MII +-----END CERTIFICATE----- +"; + Assert.Throws(() => + X509Certificate2.CreateFromPem(CertContents, default)); + } + + [Fact] + public static void CreateFromPem_CryptographicException_InvalidKeyAlgorithm() + { + CryptographicException ce = Assert.Throws(() => + X509Certificate2.CreateFromPem(TestData.Ed25519Certificate, default)); + + Assert.Contains("'1.3.101.112'", ce.Message); + } + + [Fact] + public static void CreateFromPem_CryptographicException_NoKey() + { + Assert.Throws(() => + X509Certificate2.CreateFromPem(TestData.RsaCertificate, default)); + } + + [Fact] + public static void CreateFromPem_CryptographicException_MalformedKey() + { + const string CertContents = @" +-----BEGIN RSA PRIVATE KEY----- +MII +-----END RSA PRIVATE KEY----- +"; + Assert.Throws(() => + X509Certificate2.CreateFromPem(TestData.RsaCertificate, CertContents)); + } + + [Fact] + public static void CreateFromPem_CryptographicException_CertIsPfx() + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.RsaCertificate, TestData.RsaPkcs1Key)) + { + string content = Convert.ToBase64String(cert.Export(X509ContentType.Pkcs12)); + string certContents = $@" +-----BEGIN CERTIFICATE----- +{content} +-----END CERTIFICATE----- +"; + Assert.Throws(() => + X509Certificate2.CreateFromPem(certContents, TestData.RsaPkcs1Key)); + } + } + + [Fact] + public static void CreateFromPem_CryptographicException_CertIsPkcs7() + { + string content = Convert.ToBase64String(TestData.Pkcs7ChainDerBytes); + string certContents = $@" +-----BEGIN CERTIFICATE----- +{content} +-----END CERTIFICATE----- +"; + Assert.Throws(() => + X509Certificate2.CreateFromPem(certContents, TestData.RsaPkcs1Key)); + } + + [Fact] + public static void CreateFromPem_Rsa_Pkcs1_Success() + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.RsaCertificate, TestData.RsaPkcs1Key)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_Pkcs8_Success() + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.RsaCertificate, TestData.RsaPkcs8Key)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs8Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_Aggregate_Pkcs8_Success() + { + string pemAggregate = TestData.RsaCertificate + TestData.RsaPkcs8Key; + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(pemAggregate, pemAggregate)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs8Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_Aggregate_Pkcs1_Success() + { + string pemAggregate = TestData.RsaCertificate + TestData.RsaPkcs1Key; + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(pemAggregate, pemAggregate)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_LoadsFirstCertificate_Success() + { + string certAggregate = TestData.RsaCertificate + TestData.ECDsaCertificate; + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(certAggregate, TestData.RsaPkcs1Key)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_IgnoresNonMatchingAlgorithmKeys_Success() + { + string keyAggregate = TestData.ECDsaECPrivateKey + TestData.RsaPkcs1Key; + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.RsaCertificate, keyAggregate)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_IgnoresPkcs1PublicKey_Success() + { + string keyAggregate = TestData.RsaPkcs1PublicKey + TestData.RsaPkcs1Key; + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.RsaCertificate, keyAggregate)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_IgnoresPkcs8PublicKey_Success() + { + string keyAggregate = TestData.RsaPkcs8PublicKey + TestData.RsaPkcs1Key; + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.RsaCertificate, keyAggregate)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Rsa_KeyMismatch_Fail() + { + CryptographicException ce = AssertExtensions.Throws(() => + X509Certificate2.CreateFromPem(TestData.RsaCertificate, TestData.OtherRsaPkcs1Key)); + + Assert.IsType(ce.InnerException); + } + + [Fact] + public static void CreateFromEncryptedPem_Rsa_Success() + { + X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPem( + TestData.RsaCertificate, + TestData.RsaEncryptedPkcs8Key, + "test"); + + using (cert) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaEncryptedPkcs8Key, cert.GetRSAPrivateKey, "test"); + } + } + + [Fact] + public static void CreateFromEncryptedPem_Rsa_KeyMismatch_Fail() + { + CryptographicException ce = AssertExtensions.Throws(() => + X509Certificate2.CreateFromEncryptedPem(TestData.RsaCertificate, TestData.OtherRsaPkcs8EncryptedKey, "test")); + + Assert.IsType(ce.InnerException); + } + + [Fact] + public static void CreateFromEncryptedPem_Rsa_InvalidPassword_Fail() + { + CryptographicException ce = Assert.Throws(() => + X509Certificate2.CreateFromEncryptedPem(TestData.RsaCertificate, TestData.RsaEncryptedPkcs8Key, "florp")); + + Assert.Contains("password may be incorrect", ce.Message); + } + + [Fact] + public static void CreateFromEncryptedPem_Rsa_IgnoresUnencryptedPem_Fail() + { + Assert.Throws(() => + X509Certificate2.CreateFromEncryptedPem(TestData.RsaCertificate, TestData.RsaPkcs8Key, "test")); + } + + [Fact] + public static void CreateFromPem_ECDsa_ECPrivateKey_Success() + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.ECDsaCertificate, TestData.ECDsaECPrivateKey)) + { + Assert.Equal("E844FA74BC8DCE46EF4F8605EA00008F161AB56F", cert.Thumbprint); + AssertKeysMatch(TestData.ECDsaECPrivateKey, cert.GetECDsaPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_ECDsa_Pkcs8_Success() + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.ECDsaCertificate, TestData.ECDsaPkcs8Key)) + { + Assert.Equal("E844FA74BC8DCE46EF4F8605EA00008F161AB56F", cert.Thumbprint); + AssertKeysMatch(TestData.ECDsaPkcs8Key, cert.GetECDsaPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_ECDsa_EncryptedPkcs8_Success() + { + X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPem( + TestData.ECDsaCertificate, + TestData.ECDsaEncryptedPkcs8Key, + "test"); + + using (cert) + { + Assert.Equal("E844FA74BC8DCE46EF4F8605EA00008F161AB56F", cert.Thumbprint); + AssertKeysMatch(TestData.ECDsaEncryptedPkcs8Key, cert.GetECDsaPrivateKey, "test"); + } + } + + [Fact] + public static void CreateFromPem_Dsa_Pkcs8_Success() + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(TestData.DsaCertificate, TestData.DsaPkcs8Key)) + { + Assert.Equal("35052C549E4E7805E4EA204C2BE7F4BC19B88EC8", cert.Thumbprint); + AssertKeysMatch(TestData.DsaPkcs8Key, cert.GetDSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPem_Dsa_EncryptedPkcs8_Success() + { + X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPem( + TestData.DsaCertificate, + TestData.DsaEncryptedPkcs8Key, + "test"); + + using (cert) + { + Assert.Equal("35052C549E4E7805E4EA204C2BE7F4BC19B88EC8", cert.Thumbprint); + AssertKeysMatch(TestData.DsaEncryptedPkcs8Key, cert.GetDSAPrivateKey, "test"); + } + } + + [Fact] + public static void CreateFromPemFile_NoKeyFile_Rsa_Success() + { + string pemAggregate = TestData.RsaCertificate + TestData.RsaPkcs1Key; + + using (TempFileHolder certAndKey = new TempFileHolder(pemAggregate)) + using (X509Certificate2 cert = X509Certificate2.CreateFromPemFile(certAndKey.FilePath)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPemFile_SameFile_Rsa_Success() + { + using (TempFileHolder aggregatePem = new TempFileHolder(TestData.RsaCertificate + TestData.RsaPkcs1Key)) + using (X509Certificate2 cert = X509Certificate2.CreateFromPemFile(aggregatePem.FilePath, aggregatePem.FilePath)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPemFile_WithKeyFile_Rsa_Success() + { + using (TempFileHolder certPem = new TempFileHolder(TestData.RsaCertificate)) + using (TempFileHolder keyPem = new TempFileHolder(TestData.RsaPkcs1Key)) + using (X509Certificate2 cert = X509Certificate2.CreateFromPemFile(certPem.FilePath, keyPem.FilePath)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPemFile_PrefersKeyFromKeyFile_Success() + { + using (TempFileHolder certPem = new TempFileHolder(TestData.RsaCertificate + TestData.OtherRsaPkcs1Key)) + using (TempFileHolder keyPem = new TempFileHolder(TestData.RsaPkcs1Key)) + using (X509Certificate2 cert = X509Certificate2.CreateFromPemFile(certPem.FilePath, keyPem.FilePath)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaPkcs1Key, cert.GetRSAPrivateKey); + } + } + + [Fact] + public static void CreateFromPemFile_Null_Throws() + { + AssertExtensions.Throws("certPemFilePath", () => + X509Certificate2.CreateFromPemFile(null)); + } + + [Fact] + public static void CreateFromEncryptedPemFile_NoKeyFile_Rsa_Success() + { + string pemAggregate = TestData.RsaCertificate + TestData.RsaEncryptedPkcs8Key; + + using (TempFileHolder certAndKey = new TempFileHolder(pemAggregate)) + using (X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPemFile(certAndKey.FilePath, "test")) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaEncryptedPkcs8Key, cert.GetRSAPrivateKey, "test"); + } + } + + [Fact] + public static void CreateFromEncryptedPemFile_SameFile_Rsa_Success() + { + using (TempFileHolder aggregatePem = new TempFileHolder(TestData.RsaCertificate + TestData.RsaEncryptedPkcs8Key)) + using (X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPemFile(aggregatePem.FilePath, "test", aggregatePem.FilePath)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaEncryptedPkcs8Key, cert.GetRSAPrivateKey, "test"); + } + } + + [Fact] + public static void CreateFromEncryptedPemFile_WithKeyFile_Rsa_Success() + { + using (TempFileHolder certPem = new TempFileHolder(TestData.RsaCertificate)) + using (TempFileHolder keyPem = new TempFileHolder(TestData.RsaEncryptedPkcs8Key)) + using (X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPemFile(certPem.FilePath, "test", keyPem.FilePath)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaEncryptedPkcs8Key, cert.GetRSAPrivateKey, "test"); + } + } + + [Fact] + public static void CreateFromEncryptedPemFile_PrefersKeyFromKeyFile_Success() + { + using (TempFileHolder certPem = new TempFileHolder(TestData.RsaCertificate + TestData.OtherRsaPkcs1Key)) + using (TempFileHolder keyPem = new TempFileHolder(TestData.RsaEncryptedPkcs8Key)) + using (X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPemFile(certPem.FilePath, "test", keyPem.FilePath)) + { + Assert.Equal("A33348E44A047A121F44E810E888899781E1FF19", cert.Thumbprint); + AssertKeysMatch(TestData.RsaEncryptedPkcs8Key, cert.GetRSAPrivateKey, "test"); + } + } + + [Fact] + public static void CreateFromEncryptedPemFile_Null_Throws() + { + AssertExtensions.Throws("certPemFilePath", () => + X509Certificate2.CreateFromEncryptedPemFile(null, default)); + } + + private static void AssertKeysMatch(string keyPem, Func keyLoader, string password = null) where T : AsymmetricAlgorithm + { + AsymmetricAlgorithm key = keyLoader(); + Assert.NotNull(key); + AsymmetricAlgorithm alg = key switch + { + RSA => RSA.Create(), + DSA => DSA.Create(), + ECDsa => ECDsa.Create(), + _ => null + }; + + using (key) + using (alg) + { + if (password is null) + { + alg.ImportFromPem(keyPem); + } + else + { + alg.ImportFromEncryptedPem(keyPem, password); + } + + byte[] data = alg.ExportPkcs8PrivateKey(); + + switch ((alg, key)) + { + case (RSA rsa, RSA rsaPem): + byte[] rsaSignature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + Assert.True(rsaPem.VerifyData(data, rsaSignature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); + break; + case (ECDsa ecdsa, ECDsa ecdsaPem): + byte[] ecdsaSignature = ecdsa.SignData(data, HashAlgorithmName.SHA256); + Assert.True(ecdsaPem.VerifyData(data, ecdsaSignature, HashAlgorithmName.SHA256)); + break; + case (DSA dsa, DSA dsaPem): + byte[] dsaSignature = dsa.SignData(data, HashAlgorithmName.SHA1); + Assert.True(dsaPem.VerifyData(data, dsaSignature, HashAlgorithmName.SHA1)); + break; + default: + throw new CryptographicException("Unknown key algorithm"); + } + } + } + } +}