From b5f340a5604970e3400d964e4bd4a480a24ac4a7 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 20 May 2022 11:58:51 -0700 Subject: [PATCH 01/43] First stab at a CRL builder --- .../src/System/Security/Cryptography/Oids.cs | 1 + .../X509Certificates/CertificateAuthority.cs | 44 ++ .../ref/System.Security.Cryptography.cs | 21 + .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 1 + .../CertificateRevocationListBuilder.cs | 543 ++++++++++++++++++ 6 files changed, 613 insertions(+) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs index 8b530cd8e8814..5ec8a18acb0cf 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs @@ -106,6 +106,7 @@ internal static partial class Oids internal const string SubjectAltName = "2.5.29.17"; internal const string IssuerAltName = "2.5.29.18"; internal const string BasicConstraints2 = "2.5.29.19"; + internal const string CrlNumber = "2.5.29.20"; internal const string CrlDistributionPoints = "2.5.29.31"; internal const string CertPolicies = "2.5.29.32"; internal const string AnyCertPolicy = "2.5.29.32.0"; diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index 27cbec01d7719..f821dbad4127c 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Formats.Asn1; using System.Linq; +using System.Security.Cryptography.X509Certificates.Tests.CertificateCreation; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests.Common @@ -297,6 +298,49 @@ internal byte[] GetCertData() } internal byte[] GetCrl() + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + builder.RSASignaturePadding = RSASignaturePadding.Pkcs1; + + if (_revocationList is not null) + { + foreach ((byte[] serial, DateTimeOffset when) in _revocationList) + { + builder.AddEntry(serial, when); + } + } + + byte[] crl; + + DateTimeOffset thisUpdate; + DateTimeOffset nextUpdate; + + if (RevocationExpiration.HasValue) + { + nextUpdate = RevocationExpiration.GetValueOrDefault(); + thisUpdate = _cert.NotBefore; + } + else + { + thisUpdate = DateTimeOffset.UtcNow; + nextUpdate = thisUpdate.AddSeconds(2); + } + + using (RSA key = _cert.GetRSAPrivateKey()) + { + crl = builder.Build( + CorruptRevocationIssuerName ? s_nonParticipatingName : _cert.SubjectName, + X509SignatureGenerator.CreateForRSA(key, builder.RSASignaturePadding), + _crlNumber, + nextUpdate, + thisUpdate, + _akidExtension ??= CreateAkidExtension()); + } + + return crl; + } + + internal byte[] OldGetCrl() { byte[] crl = _crl; DateTimeOffset now = DateTimeOffset.UtcNow; diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 46fdda605813a..4dc14f9d72401 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2439,6 +2439,27 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public byte[] CreateSigningRequest() { throw null; } public byte[] CreateSigningRequest(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } } + public sealed partial class CertificateRevocationListBuilder + { + public CertificateRevocationListBuilder() { } + public System.Security.Cryptography.HashAlgorithmName? HashAlgorithm { get { throw null; } set { } } + public System.Security.Cryptography.RSASignaturePadding? RSASignaturePadding { get { throw null; } set { } } + public void AddEntry(byte[] serialNumber) { } + public void AddEntry(byte[] serialNumber, System.DateTimeOffset revocationTime) { } + public void AddEntry(System.ReadOnlySpan serialNumber) { } + public void AddEntry(System.ReadOnlySpan serialNumber, System.DateTimeOffset revocationTime) { } + public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } + public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.DateTimeOffset revocationTime) { } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.X509Certificates.X509Extension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.X509Certificates.X509Extension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate) { throw null; } + public void ExpireEntries(System.DateTimeOffset oldestRevocationTimeToKeep) { } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(byte[] currentCrl, out int currentCrlNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(System.ReadOnlySpan currentCrl, out int currentCrlNumber, out int bytesConsumed) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(System.ReadOnlySpan currentCrl, out int currentCrlNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(string currentCrl, out int currentCrlNumber) { throw null; } + } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] public static partial class DSACertificateExtensions diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 859e9a167cf02..6338d6a0dfd8a 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -219,6 +219,9 @@ The provided notBefore value is later than the notAfter value. + + The provided thisUpdate value is later than the nextUpdate value. + An X509Extension with OID '{0}' has already been specified. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 613ed62301353..89b1ba4c49eb7 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -416,6 +416,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs new file mode 100644 index 0000000000000..d475871fd10c9 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -0,0 +1,543 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Asn1; + +namespace System.Security.Cryptography.X509Certificates +{ + /// + /// + public sealed class CertificateRevocationListBuilder + { + private struct RevokedCertificate + { + internal byte[] Serial; + internal DateTimeOffset RevocationTime; + internal byte[]? Extensions; + + internal RevokedCertificate(ref AsnValueReader reader, int version) + { + AsnValueReader revokedCertificate = reader.ReadSequence(); + Serial = revokedCertificate.ReadIntegerBytes().ToArray(); + RevocationTime = ReadX509Time(ref revokedCertificate); + Extensions = null; + + if (version > 0 && revokedCertificate.HasData) + { + AsnValueReader crlExtensionsExplicit = + revokedCertificate.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); + + if (!crlExtensionsExplicit.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + Extensions = crlExtensionsExplicit.ReadEncodedValue().ToArray(); + crlExtensionsExplicit.ThrowIfNotEmpty(); + } + + revokedCertificate.ThrowIfNotEmpty(); + } + } + + private List _revoked; + private AsnWriter? _writer; + + public HashAlgorithmName? HashAlgorithm { get; set; } + public RSASignaturePadding? RSASignaturePadding { get; set; } + + public CertificateRevocationListBuilder() + { + _revoked = new(); + } + + private CertificateRevocationListBuilder(List revoked) + { + Debug.Assert(revoked != null); + _revoked = revoked; + } + + public static CertificateRevocationListBuilder Load(byte[] currentCrl, out int currentCrlNumber) + { + ArgumentNullException.ThrowIfNull(currentCrl); + + CertificateRevocationListBuilder ret = Load( + new ReadOnlySpan(currentCrl), + out int crlNumber, + out int bytesConsumed); + + if (bytesConsumed != currentCrl.Length) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + currentCrlNumber = crlNumber; + return ret; + } + + public static CertificateRevocationListBuilder Load( + ReadOnlySpan currentCrl, + out int currentCrlNumber, + out int bytesConsumed) + { + List list = new(); + int crlNumber = 0; + int payloadLength; + + try + { + AsnValueReader reader = new AsnValueReader(currentCrl, AsnEncodingRules.DER); + payloadLength = reader.PeekEncodedValue().Length; + + AsnValueReader certificateList = reader.ReadSequence(); + AsnValueReader tbsCertList = certificateList.ReadSequence(); + AlgorithmIdentifierAsn.Decode(ref certificateList, ReadOnlyMemory.Empty, out _); + + if (!certificateList.TryReadPrimitiveBitString(out _, out _)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + certificateList.ThrowIfNotEmpty(); + + int version = 0; + + if (tbsCertList.PeekTag().HasSameClassAndValue(Asn1Tag.Integer)) + { + // https://datatracker.ietf.org/doc/html/rfc5280#section-5.1 says the only + // version values are v1 (0) and v2 (1). + if (!tbsCertList.TryReadInt32(out version) || version != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + AlgorithmIdentifierAsn.Decode(ref tbsCertList, ReadOnlyMemory.Empty, out _); + // X500DN + tbsCertList.ReadSequence(); + + // thisUpdate + ReadX509Time(ref tbsCertList); + + // nextUpdate + ReadX509TimeOpt(ref tbsCertList); + + AsnValueReader revokedCertificates = tbsCertList.ReadSequence(); + + if (version > 0 && tbsCertList.HasData) + { + AsnValueReader crlExtensionsExplicit = tbsCertList.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); + AsnValueReader crlExtensions = crlExtensionsExplicit.ReadSequence(); + crlExtensionsExplicit.ThrowIfNotEmpty(); + + while (crlExtensions.HasData) + { + AsnValueReader extension = crlExtensions.ReadSequence(); + string extnId = extension.ReadObjectIdentifier(); + + if (extension.PeekTag().HasSameClassAndValue(Asn1Tag.Boolean)) + { + extension.ReadBoolean(); + } + + if (!extension.TryReadPrimitiveOctetString(out ReadOnlySpan extnValue)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + switch (extnId) + { + case Oids.CrlNumber: + { + AsnValueReader crlNumberReader = new AsnValueReader( + extnValue, + AsnEncodingRules.DER); + + // The CRL Number is out of range for this method. + if (!crlNumberReader.TryReadInt32(out crlNumber)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + crlNumberReader.ThrowIfNotEmpty(); + + break; + } + + case Oids.AuthorityInformationAccess: + { + AsnValueReader aiaReader = new AsnValueReader(extnValue, AsnEncodingRules.DER); + aiaReader.ReadSequence(); + aiaReader.ThrowIfNotEmpty(); + break; + } + } + } + } + + tbsCertList.ThrowIfNotEmpty(); + + while (revokedCertificates.HasData) + { + RevokedCertificate revokedCertificate = new RevokedCertificate(ref revokedCertificates, version); + list.Add(revokedCertificate); + } + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + + bytesConsumed = payloadLength; + currentCrlNumber = crlNumber; + return new CertificateRevocationListBuilder(list); + } + + public static CertificateRevocationListBuilder LoadPem(string currentCrl, out int currentCrlNumber) + { + ArgumentNullException.ThrowIfNull(currentCrl); + + return LoadPem(currentCrl.AsSpan(), out currentCrlNumber); + } + + public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan currentCrl, out int currentCrlNumber) + { + ReadOnlySpan source = currentCrl; + + while (PemEncoding.TryFind(source, out PemFields fields)) + { + if (source[fields.Label].SequenceEqual("CRL")) + { + byte[] rented = ArrayPool.Shared.Rent(fields.DecodedDataLength); + + if (!Convert.TryFromBase64Chars(source[fields.Base64Data], rented, out int bytesWritten)) + { + Debug.Fail("Base64Decode failed, but PemEncoding said it was legal"); + throw new UnreachableException(); + } + + CertificateRevocationListBuilder ret = Load( + rented.AsSpan(0, bytesWritten), + out currentCrlNumber, + out int bytesConsumed); + + Debug.Assert(bytesConsumed == bytesWritten); + ArrayPool.Shared.Return(rented); + return ret; + } + + source = source.Slice(fields.Location.End.GetOffset(source.Length)); + } + + throw new CryptographicException("No PEM-encoded CRL was found"); + } + + public void AddEntry(X509Certificate2 certificate) + { + AddEntry(certificate, DateTimeOffset.UtcNow); + } + + public void AddEntry(X509Certificate2 certificate, DateTimeOffset revocationTime) + { + ArgumentNullException.ThrowIfNull(certificate); + + byte[] serial = certificate.GetSerialNumber(); + Array.Reverse(serial); + + AddEntry(new ReadOnlySpan(serial), revocationTime); + } + + public void AddEntry(byte[] serialNumber) + { + AddEntry(serialNumber, DateTimeOffset.UtcNow); + } + + public void AddEntry(byte[] serialNumber, DateTimeOffset revocationTime) + { + ArgumentNullException.ThrowIfNull(serialNumber); + + AddEntry(new ReadOnlySpan(serialNumber), revocationTime); + } + + public void AddEntry(ReadOnlySpan serialNumber) + { + AddEntry(serialNumber, DateTimeOffset.UtcNow); + } + + public void AddEntry(ReadOnlySpan serialNumber, DateTimeOffset revocationTime) + { + _revoked.Add( + new RevokedCertificate + { + Serial = serialNumber.ToArray(), + RevocationTime = revocationTime.ToUniversalTime(), + }); + } + + public void ExpireEntries(DateTimeOffset oldestRevocationTimeToKeep) + { + _revoked.RemoveAll(rc => rc.RevocationTime < oldestRevocationTimeToKeep); + } + + public byte[] Build(X509Certificate2 issuerCertificate, int crlNumber, DateTimeOffset nextUpdate) + { + return Build(issuerCertificate, crlNumber, nextUpdate, DateTimeOffset.UtcNow); + } + + public byte[] Build( + X509Certificate2 issuerCertificate, + int crlNumber, + DateTimeOffset nextUpdate, + DateTimeOffset thisUpdate) + { + ArgumentNullException.ThrowIfNull(issuerCertificate); + + if (!issuerCertificate.HasPrivateKey) + throw new ArgumentException( + SR.Cryptography_CertReq_IssuerRequiresPrivateKey, + nameof(issuerCertificate)); + if (crlNumber < 0) + throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); + if (nextUpdate <= thisUpdate) + throw new ArgumentException(SR.Cryptography_CertReq_DatesReversed_CRL); + + // Check the Basic Constraints and Key Usage extensions to help identify inappropriate certificates. + // Note that this is not a security check. The system library backing X509Chain will use these same criteria + // to determine if a chain is valid; and a user can easily call the X509SignatureGenerator overload to + // bypass this validation. We're simply helping them at signing time understand that they've + // chosen the wrong cert. + var basicConstraints = (X509BasicConstraintsExtension?)issuerCertificate.Extensions[Oids.BasicConstraints2]; + var keyUsage = (X509KeyUsageExtension?)issuerCertificate.Extensions[Oids.KeyUsage]; + var akid = issuerCertificate.Extensions["Oids.Autho"]; + + if (basicConstraints == null) + throw new ArgumentException( + SR.Cryptography_CertReq_BasicConstraintsRequired, + nameof(issuerCertificate)); + if (!basicConstraints.CertificateAuthority) + throw new ArgumentException( + SR.Cryptography_CertReq_IssuerBasicConstraintsInvalid, + nameof(issuerCertificate)); + if (keyUsage != null && (keyUsage.KeyUsages & X509KeyUsageFlags.CrlSign) == 0) + throw new ArgumentException(SR.Cryptography_CertReq_IssuerKeyUsageInvalid, nameof(issuerCertificate)); + if (akid is null) + throw new ArgumentException("AKID needed", nameof(issuerCertificate)); + + AsymmetricAlgorithm? key = null; + string keyAlgorithm = issuerCertificate.GetKeyAlgorithm(); + X509SignatureGenerator generator; + + try + { + switch (keyAlgorithm) + { + case Oids.Rsa: + if (RSASignaturePadding is null) + { + throw new InvalidOperationException( + "The issuer certificate uses an RSA key, but no RSASignaturePadding value was provided."); + } + + RSA? rsa = issuerCertificate.GetRSAPrivateKey(); + key = rsa; + generator = X509SignatureGenerator.CreateForRSA(rsa!, RSASignaturePadding); + break; + case Oids.EcPublicKey: + ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey(); + key = ecdsa; + generator = X509SignatureGenerator.CreateForECDsa(ecdsa!); + break; + default: + throw new ArgumentException( + SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm), + nameof(issuerCertificate)); + } + + return Build(issuerCertificate.SubjectName, generator, crlNumber, nextUpdate, thisUpdate, akid); + } + finally + { + key?.Dispose(); + } + } + + public byte[] Build( + X500DistinguishedName issuerName, + X509SignatureGenerator generator, + int crlNumber, + DateTimeOffset nextUpdate, + X509Extension akid) + { + return Build(issuerName, generator, crlNumber, nextUpdate, DateTimeOffset.UtcNow, akid); + } + + public byte[] Build( + X500DistinguishedName issuerName, + X509SignatureGenerator generator, + int crlNumber, + DateTimeOffset nextUpdate, + DateTimeOffset thisUpdate, + X509Extension akid) + { + ArgumentNullException.ThrowIfNull(issuerName); + ArgumentNullException.ThrowIfNull(generator); + + if (crlNumber < 0) + throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); + if (nextUpdate <= thisUpdate) + throw new ArgumentException(SR.Cryptography_CertReq_DatesReversed_CRL); + + ArgumentNullException.ThrowIfNull(akid); + + HashAlgorithmName hashAlgorithm = HashAlgorithm.GetValueOrDefault(); + + if (string.IsNullOrEmpty(hashAlgorithm.Name)) + { + throw new InvalidOperationException( + "The hash algorithm to use during signing must be specified via the HashAlgorithm property."); + } + + byte[] signatureAlgId = generator.GetSignatureAlgorithmIdentifier(hashAlgorithm); + AsnWriter writer = (_writer ??= new AsnWriter(AsnEncodingRules.DER)); + + // TBSCertList + using (writer.PushSequence()) + { + // version v2(1) + writer.WriteInteger(1); + + // signature (AlgorithmIdentifier) + writer.WriteEncodedValue(signatureAlgId); + + // issuer + writer.WriteEncodedValue(issuerName.RawData); + + // thisUpdate + WriteX509Time(writer, thisUpdate); + + // nextUpdate + WriteX509Time(writer, nextUpdate); + + // revokedCertificates (don't write down if empty) + if (_revoked.Count > 0) + { + // SEQUENCE OF + using (writer.PushSequence()) + { + foreach (RevokedCertificate revoked in _revoked) + { + // Anonymous CRL Entry type + using (writer.PushSequence()) + { + writer.WriteInteger(revoked.Serial); + WriteX509Time(writer, revoked.RevocationTime); + + if (revoked.Extensions is not null) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) + { + writer.WriteEncodedValue(revoked.Extensions); + } + } + } + } + } + } + + // extensions [0] EXPLICIT Extensions + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) + { + // Extensions (SEQUENCE OF) + using (writer.PushSequence()) + { + // Authority Key Identifier Extension + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(akid.Oid!.Value!); + + if (akid.Critical) + { + writer.WriteBoolean(true); + } + + writer.WriteOctetString(akid.RawData); + } + + // CRL Number Extension + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier("2.5.29.20"); + + using (writer.PushOctetString()) + { + writer.WriteInteger(crlNumber); + } + } + } + } + } + + byte[] tbsCertList = writer.Encode(); + writer.Reset(); + + byte[] signature = generator.SignData(tbsCertList, hashAlgorithm); + + // CertificateList + using (writer.PushSequence()) + { + writer.WriteEncodedValue(tbsCertList); + writer.WriteEncodedValue(signatureAlgId); + writer.WriteBitString(signature); + } + + byte[] crl = writer.Encode(); + writer.Reset(); + return crl; + } + + private static DateTimeOffset ReadX509Time(ref AsnValueReader reader) + { + if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.UtcTime)) + { + return reader.ReadUtcTime(); + } + + return reader.ReadGeneralizedTime(); + } + + private static DateTimeOffset? ReadX509TimeOpt(ref AsnValueReader reader) + { + if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.UtcTime)) + { + return reader.ReadUtcTime(); + } + + if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.GeneralizedTime)) + { + return reader.ReadGeneralizedTime(); + } + + return null; + } + + private static void WriteX509Time(AsnWriter writer, DateTimeOffset time) + { + DateTimeOffset timeUtc = time.ToUniversalTime(); + int year = timeUtc.Year; + + if (year >= 1950 && year < 2050) + { + writer.WriteUtcTime(timeUtc); + } + else + { + writer.WriteGeneralizedTime(time, omitFractionalSeconds: true); + } + } + } +} From f56f1976eb53038dfd1de8063dd442c24850583a Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 26 May 2022 16:53:14 -0700 Subject: [PATCH 02/43] Use the new CertificateRevocationListBuilder in the test CA system --- .../X509Certificates/CertificateAuthority.cs | 165 ++---------------- 1 file changed, 12 insertions(+), 153 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index f821dbad4127c..0b71829820039 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -299,8 +299,18 @@ internal byte[] GetCertData() internal byte[] GetCrl() { + byte[] crl = _crl; + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset newExpiry = now.AddSeconds(2); + + if (crl != null && now < _crlExpiry) + { + return crl; + } + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); builder.RSASignaturePadding = RSASignaturePadding.Pkcs1; + builder.HashAlgorithm = HashAlgorithmName.SHA256; if (_revocationList is not null) { @@ -310,8 +320,6 @@ internal byte[] GetCrl() } } - byte[] crl; - DateTimeOffset thisUpdate; DateTimeOffset nextUpdate; @@ -337,159 +345,10 @@ internal byte[] GetCrl() _akidExtension ??= CreateAkidExtension()); } - return crl; - } - - internal byte[] OldGetCrl() - { - byte[] crl = _crl; - DateTimeOffset now = DateTimeOffset.UtcNow; - - if (crl != null && now < _crlExpiry) - { - return crl; - } - - DateTimeOffset newExpiry = now.AddSeconds(2); - - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - using (writer.PushSequence()) - { - writer.WriteObjectIdentifier("1.2.840.113549.1.1.11"); - writer.WriteNull(); - } - - byte[] signatureAlgId = writer.Encode(); - writer.Reset(); - - // TBSCertList - using (writer.PushSequence()) - { - // version v2(1) - writer.WriteInteger(1); - - // signature (AlgorithmIdentifier) - writer.WriteEncodedValue(signatureAlgId); - - // issuer - if (CorruptRevocationIssuerName) - { - writer.WriteEncodedValue(s_nonParticipatingName.RawData); - } - else - { - writer.WriteEncodedValue(_cert.SubjectName.RawData); - } - - if (RevocationExpiration.HasValue) - { - // thisUpdate - writer.WriteUtcTime(_cert.NotBefore); - - // nextUpdate - if (!OmitNextUpdateInCrl) - { - writer.WriteUtcTime(RevocationExpiration.Value); - } - } - else - { - // thisUpdate - writer.WriteUtcTime(now); - - // nextUpdate - if (!OmitNextUpdateInCrl) - { - writer.WriteUtcTime(newExpiry); - } - } - - // revokedCertificates (don't write down if empty) - if (_revocationList?.Count > 0) - { - // SEQUENCE OF - using (writer.PushSequence()) - { - foreach ((byte[] serial, DateTimeOffset when) in _revocationList) - { - // Anonymous CRL Entry type - using (writer.PushSequence()) - { - writer.WriteInteger(serial); - writer.WriteUtcTime(when); - } - } - } - } - - // extensions [0] EXPLICIT Extensions - using (writer.PushSequence(s_context0)) - { - // Extensions (SEQUENCE OF) - using (writer.PushSequence()) - { - if (_akidExtension == null) - { - _akidExtension = CreateAkidExtension(); - } - - // Authority Key Identifier Extension - using (writer.PushSequence()) - { - writer.WriteObjectIdentifier(_akidExtension.Oid.Value); - - if (_akidExtension.Critical) - { - writer.WriteBoolean(true); - } - - writer.WriteOctetString(_akidExtension.RawData); - } - - // CRL Number Extension - using (writer.PushSequence()) - { - writer.WriteObjectIdentifier("2.5.29.20"); - - using (writer.PushOctetString()) - { - writer.WriteInteger(_crlNumber); - } - } - } - } - } - - byte[] tbsCertList = writer.Encode(); - writer.Reset(); - - byte[] signature; - - using (RSA key = _cert.GetRSAPrivateKey()) - { - signature = - key.SignData(tbsCertList, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - if (CorruptRevocationSignature) - { - signature[5] ^= 0xFF; - } - } - - // CertificateList - using (writer.PushSequence()) - { - writer.WriteEncodedValue(tbsCertList); - writer.WriteEncodedValue(signatureAlgId); - writer.WriteBitString(signature); - } - - _crl = writer.Encode(); - + _crl = crl; _crlExpiry = newExpiry; _crlNumber++; - return _crl; + return crl; } internal void DesignateOcspResponder(X509Certificate2 responder) From 486ea9be203b4f8bf733f19e8c6440181ee0ecb7 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Wed, 1 Jun 2022 16:39:08 -0700 Subject: [PATCH 03/43] Start adding CrlBuilder tests --- .../CertificateCreation/CrlBuilderTests.cs | 174 ++++++++++++++++++ ...Cryptography.X509Certificates.Tests.csproj | 1 + .../src/Resources/Strings.resx | 9 +- .../CertificateRevocationListBuilder.cs | 17 +- 4 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs new file mode 100644 index 0000000000000..80c64d9b11588 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -0,0 +1,174 @@ +// Licensed to the.NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests.CertificateCreation +{ + public static class CrlBuilderTests + { + [Fact] + public static void AddEntryArgumentValidation() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + Assert.Throws("serialNumber", () => builder.AddEntry((byte[])null)); + Assert.Throws("serialNumber", () => builder.AddEntry((byte[])null, now)); + Assert.Throws("certificate", () => builder.AddEntry((X509Certificate2)null)); + Assert.Throws("certificate", () => builder.AddEntry((X509Certificate2)null, now)); + Assert.Throws("serialNumber", () => builder.AddEntry(Array.Empty())); + Assert.Throws("serialNumber", () => builder.AddEntry(Array.Empty(), now)); + Assert.Throws("serialNumber", () => builder.AddEntry(ReadOnlySpan.Empty)); + Assert.Throws("serialNumber", () => builder.AddEntry(ReadOnlySpan.Empty, now)); + } + + [Fact] + public static void BuildWithIssuerCertArgumentValidation() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-5); + DateTimeOffset notAfter = now.AddMinutes(5); + DateTimeOffset thisUpdate = now; + DateTimeOffset nextUpdate = now.AddMinutes(1); + + const string ParamName = "issuerCertificate"; + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + Assert.Throws(ParamName, () => builder.Build(null, 0, now)); + + using (ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP384)) + { + CertificateRequest certReq = new CertificateRequest("CN=Bad CA", key, HashAlgorithmName.SHA384); + + using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + { + ArgumentException ex; + + using (X509Certificate2 pubOnly = new X509Certificate2(cert.RawDataMemory.Span)) + { + ex = Assert.Throws(ParamName, () => builder.Build(pubOnly, 0, nextUpdate)); + Assert.Contains("private key", ex.Message); + + ex = Assert.Throws(ParamName, () => builder.Build(pubOnly, 0, nextUpdate, thisUpdate)); + Assert.Contains("private key", ex.Message); + } + + ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate)); + Assert.Contains("Basic Constraints", ex.Message); + Assert.DoesNotContain("appropriate", ex.Message); + + ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate, thisUpdate)); + Assert.Contains("Basic Constraints", ex.Message); + Assert.DoesNotContain("appropriate", ex.Message); + } + + certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); + + using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + { + ArgumentException ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate)); + Assert.Contains("Basic Constraints", ex.Message); + Assert.Contains("appropriate", ex.Message); + + ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate, thisUpdate)); + Assert.Contains("Basic Constraints", ex.Message); + Assert.Contains("appropriate", ex.Message); + } + + certReq.CertificateExtensions.Clear(); + certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign, true)); + + using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + { + ArgumentException ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate)); + Assert.Contains("Key Usage", ex.Message); + Assert.Contains("CrlSign", ex.Message); + Assert.DoesNotContain("KeyCertSign", ex.Message); + + ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate, thisUpdate)); + Assert.Contains("Key Usage", ex.Message); + Assert.Contains("CrlSign", ex.Message); + Assert.DoesNotContain("KeyCertSign", ex.Message); + } + + certReq.CertificateExtensions.RemoveAt(1); + certReq.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true)); + + // The certificate is acceptable now, move on to other arguments. + using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + { + Assert.Throws( + "crlNumber", + () => builder.Build(cert, -1, nextUpdate)); + + Assert.Throws( + "crlNumber", + () => builder.Build(cert, -1, nextUpdate, thisUpdate)); + + ArgumentException ex = Assert.Throws(() => builder.Build(cert, 0, now.AddYears(-10))); + Assert.Null(ex.ParamName); + Assert.Contains("thisUpdate", ex.Message); + Assert.Contains("nextUpdate", ex.Message); + + ex = Assert.Throws(() => builder.Build(cert, 0, thisUpdate, nextUpdate)); + Assert.Null(ex.ParamName); + Assert.Contains("thisUpdate", ex.Message); + Assert.Contains("nextUpdate", ex.Message); + } + } + } + + [Fact] + public static void BuildWithGeneratorArgumentValidation() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset thisUpdate = now; + DateTimeOffset nextUpdate = now.AddMinutes(1); + + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + Assert.Throws( + "issuerName", + () => builder.Build((X500DistinguishedName)null, default, 0, nextUpdate, default)); + Assert.Throws( + "issuerName", + () => builder.Build((X500DistinguishedName)null, default, 0, nextUpdate, thisUpdate, default)); + + X500DistinguishedName issuerName = new X500DistinguishedName("CN=Bad CA"); + + Assert.Throws( + "generator", + () => builder.Build(issuerName, default, 0, nextUpdate, default)); + Assert.Throws( + "generator", + () => builder.Build(issuerName, default, 0, nextUpdate, thisUpdate, default)); + + using (ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP384)) + { + X509SignatureGenerator generator = X509SignatureGenerator.CreateForECDsa(key); + + Assert.Throws( + "crlNumber", + () => builder.Build(issuerName, generator, -1, nextUpdate, default)); + Assert.Throws( + "crlNumber", + () => builder.Build(issuerName, generator, -1, nextUpdate, thisUpdate, default)); + + ArgumentException ex = Assert.Throws( + () => builder.Build(issuerName, generator, 0, now.AddYears(-10), default)); + Assert.Null(ex.ParamName); + Assert.Contains("thisUpdate", ex.Message); + Assert.Contains("nextUpdate", ex.Message); + + ex = Assert.Throws( + () => builder.Build(issuerName, generator, 0, thisUpdate, nextUpdate, default)); + Assert.Null(ex.ParamName); + Assert.Contains("thisUpdate", ex.Message); + Assert.Contains("nextUpdate", ex.Message); + } + } + } +} 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 bda7909c02c85..a1a9302d8016f 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 @@ -66,6 +66,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 6338d6a0dfd8a..f4e5ee26187e9 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -219,9 +219,6 @@ The provided notBefore value is later than the notAfter value. - - The provided thisUpdate value is later than the nextUpdate value. - An X509Extension with OID '{0}' has already been specified. @@ -258,6 +255,12 @@ Encoded OID length is too large (greater than 0x7f bytes). + + The provided thisUpdate value is later than the nextUpdate value. + + + The issuer certificate's Key Usage extension is present but does not contain the CrlSign flag. + FlushFinalBlock() method was called twice on a CryptoStream. It can only be called once. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index d475871fd10c9..0a5b1120bf828 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -271,6 +271,9 @@ public void AddEntry(ReadOnlySpan serialNumber) public void AddEntry(ReadOnlySpan serialNumber, DateTimeOffset revocationTime) { + if (serialNumber.IsEmpty) + throw new ArgumentException(SR.Arg_EmptyOrNullArray, nameof(serialNumber)); + _revoked.Add( new RevokedCertificate { @@ -304,7 +307,7 @@ public byte[] Build( if (crlNumber < 0) throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); if (nextUpdate <= thisUpdate) - throw new ArgumentException(SR.Cryptography_CertReq_DatesReversed_CRL); + throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); // Check the Basic Constraints and Key Usage extensions to help identify inappropriate certificates. // Note that this is not a security check. The system library backing X509Chain will use these same criteria @@ -313,7 +316,7 @@ public byte[] Build( // chosen the wrong cert. var basicConstraints = (X509BasicConstraintsExtension?)issuerCertificate.Extensions[Oids.BasicConstraints2]; var keyUsage = (X509KeyUsageExtension?)issuerCertificate.Extensions[Oids.KeyUsage]; - var akid = issuerCertificate.Extensions["Oids.Autho"]; + //var akid = issuerCertificate.Extensions["Oids.Autho"]; if (basicConstraints == null) throw new ArgumentException( @@ -324,9 +327,9 @@ public byte[] Build( SR.Cryptography_CertReq_IssuerBasicConstraintsInvalid, nameof(issuerCertificate)); if (keyUsage != null && (keyUsage.KeyUsages & X509KeyUsageFlags.CrlSign) == 0) - throw new ArgumentException(SR.Cryptography_CertReq_IssuerKeyUsageInvalid, nameof(issuerCertificate)); - if (akid is null) - throw new ArgumentException("AKID needed", nameof(issuerCertificate)); + throw new ArgumentException(SR.Cryptography_CRLBuilder_IssuerKeyUsageInvalid, nameof(issuerCertificate)); + //if (akid is null) + // throw new ArgumentException("AKID needed", nameof(issuerCertificate)); AsymmetricAlgorithm? key = null; string keyAlgorithm = issuerCertificate.GetKeyAlgorithm(); @@ -358,7 +361,7 @@ public byte[] Build( nameof(issuerCertificate)); } - return Build(issuerCertificate.SubjectName, generator, crlNumber, nextUpdate, thisUpdate, akid); + return Build(issuerCertificate.SubjectName, generator, crlNumber, nextUpdate, thisUpdate, null!); } finally { @@ -390,7 +393,7 @@ public byte[] Build( if (crlNumber < 0) throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); if (nextUpdate <= thisUpdate) - throw new ArgumentException(SR.Cryptography_CertReq_DatesReversed_CRL); + throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); ArgumentNullException.ThrowIfNull(akid); From f3de733b1cacda2b387e6260efd9f3d5c04aa464 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 2 Jun 2022 10:26:44 -0700 Subject: [PATCH 04/43] Add AKID --- .../src/System/Security/Cryptography/Oids.cs | 1 + .../X509Certificates/CertificateAuthority.cs | 53 +-- .../AuthorityKeyIdentifierTests.cs | 24 ++ .../ExtensionsTests/ComprehensiveTests.cs | 6 +- ...Cryptography.X509Certificates.Tests.csproj | 1 + .../ref/System.Security.Cryptography.cs | 25 +- .../src/System.Security.Cryptography.csproj | 1 + .../Security/Cryptography/CryptoConfig.cs | 1 + .../CertificateRevocationListBuilder.cs | 10 +- .../X509AuthorityKeyIdentifierExtension.cs | 377 ++++++++++++++++++ .../X509Certificates/X509Certificate.cs | 15 + .../X509Certificates/X509Certificate2.cs | 1 + .../X509SubjectKeyIdentifierExtension.cs | 33 +- 13 files changed, 488 insertions(+), 60 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs index 5ec8a18acb0cf..913270624a9e7 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs @@ -111,6 +111,7 @@ internal static partial class Oids internal const string CertPolicies = "2.5.29.32"; internal const string AnyCertPolicy = "2.5.29.32.0"; internal const string CertPolicyMappings = "2.5.29.33"; + internal const string AuthorityKeyIdentifier = "2.5.29.35"; internal const string CertPolicyConstraints = "2.5.29.36"; internal const string EnhancedKeyUsage = "2.5.29.37"; internal const string InhibitAnyPolicyExtension = "2.5.29.54"; diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index 0b71829820039..424ccf7942365 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Formats.Asn1; using System.Linq; -using System.Security.Cryptography.X509Certificates.Tests.CertificateCreation; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests.Common @@ -81,7 +80,7 @@ internal sealed class CertificateAuthority : IDisposable private byte[] _certData; private X509Extension _cdpExtension; private X509Extension _aiaExtension; - private X509Extension _akidExtension; + private X509AuthorityKeyIdentifierExtension _akidExtension; private List<(byte[], DateTimeOffset)> _revocationList; private byte[] _crl; @@ -343,6 +342,11 @@ internal byte[] GetCrl() nextUpdate, thisUpdate, _akidExtension ??= CreateAkidExtension()); + + if (CorruptRevocationSignature) + { + crl[^1] ^= 0xFF; + } } _crl = crl; @@ -637,54 +641,17 @@ private static X509Extension CreateCdpExtension(string cdp) return new X509Extension("2.5.29.31", writer.Encode(), false); } - private X509Extension CreateAkidExtension() + private X509AuthorityKeyIdentifierExtension CreateAkidExtension() { X509SubjectKeyIdentifierExtension skid = _cert.Extensions.OfType().SingleOrDefault(); - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - // AuthorityKeyIdentifier - using (writer.PushSequence()) + if (skid is null) { - if (skid == null) - { - // authorityCertIssuer [1] GeneralNames (SEQUENCE OF) - using (writer.PushSequence(s_context1)) - { - // directoryName [4] Name - byte[] dn = _cert.SubjectName.RawData; - - if (s_context4.Encode(dn) != 1) - { - throw new InvalidOperationException(); - } - - writer.WriteEncodedValue(dn); - } - - // authorityCertSerialNumber [2] CertificateSerialNumber (INTEGER) - byte[] serial = _cert.GetSerialNumber(); - Array.Reverse(serial); - writer.WriteInteger(serial, s_context2); - } - else - { - // keyIdentifier [0] KeyIdentifier (OCTET STRING) - AsnReader reader = new AsnReader(skid.RawData, AsnEncodingRules.BER); - ReadOnlyMemory contents; - - if (!reader.TryReadPrimitiveOctetString(out contents)) - { - throw new InvalidOperationException(); - } - - reader.ThrowIfNotEmpty(); - writer.WriteOctetString(contents.Span, s_context0); - } + return X509AuthorityKeyIdentifierExtension.CreateFromCertificate(_cert, false, true); } - return new X509Extension("2.5.29.35", writer.Encode(), false); + return X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(skid); } private enum OcspResponseStatus diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs new file mode 100644 index 0000000000000..bbb664dd9d985 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests.ExtensionsTests +{ + public static class AuthorityKeyIdentifierTests + { + [Fact] + public static void DefaultConstructor() + { + X509AuthorityKeyIdentifierExtension e = new X509AuthorityKeyIdentifierExtension(); + string oidValue = e.Oid.Value; + Assert.Equal("2.5.29.35", oidValue); + + Assert.Empty(e.RawData); + Assert.False(e.KeyIdentifier.HasValue, "e.KeyIdentifier.HasValue"); + Assert.Null(e.SimpleIssuer); + Assert.False(e.RawIssuer.HasValue, "e.RawIssuer.HasValue"); + Assert.False(e.SerialNumber.HasValue, "e.SerialNumber.HasValue"); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs index f5eec64f27994..e091904c6cf05 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs @@ -103,7 +103,11 @@ public static void ReadExtensions() byte[] expected = "30168014cb11e8cad2b4165801c9372e331616b94c9a0a1f".HexToByteArray(); Assert.Equal(expected, akid.RawData); - Assert.IsType(akid); + X509AuthorityKeyIdentifierExtension rich = Assert.IsType(akid); + Assert.Null(rich.SimpleIssuer); + Assert.False(rich.SerialNumber.HasValue); + Assert.True(rich.KeyIdentifier.HasValue); + Assert.Equal("CB11E8CAD2B4165801C9372E331616B94C9A0A1F", rich.KeyIdentifier.GetValueOrDefault().ByteArrayToHex()); } { 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 a1a9302d8016f..66bc41f2cbce0 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 @@ -35,6 +35,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 4dc14f9d72401..bf777280a78de 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2450,8 +2450,8 @@ public void AddEntry(System.ReadOnlySpan serialNumber) { } public void AddEntry(System.ReadOnlySpan serialNumber, System.DateTimeOffset revocationTime) { } public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.DateTimeOffset revocationTime) { } - public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.X509Certificates.X509Extension akid) { throw null; } - public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.X509Certificates.X509Extension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate) { throw null; } public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate) { throw null; } public void ExpireEntries(System.DateTimeOffset oldestRevocationTimeToKeep) { } @@ -2591,6 +2591,25 @@ public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEnc public System.Collections.Generic.IEnumerable EnumerateUris(System.Security.Cryptography.Oid accessMethodOid) { throw null; } public System.Collections.Generic.IEnumerable EnumerateUris(string accessMethodOid) { throw null; } } + public sealed partial class X509AuthorityKeyIdentifierExtension : System.Security.Cryptography.X509Certificates.X509Extension + { + public X509AuthorityKeyIdentifierExtension() { } + public X509AuthorityKeyIdentifierExtension(byte[] rawData, bool critical = false) { } + public X509AuthorityKeyIdentifierExtension(System.ReadOnlySpan rawData, bool critical = false) { } + public System.ReadOnlyMemory? KeyIdentifier { get { throw null; } } + public System.ReadOnlyMemory? RawIssuer { get { throw null; } } + public System.ReadOnlyMemory? SerialNumber { get { throw null; } } + public System.Security.Cryptography.X509Certificates.X500DistinguishedName? SimpleIssuer { get { throw null; } } + public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEncodedData) { } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension Create(byte[] keyIdentifier, System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, byte[] serialNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension Create(System.ReadOnlySpan keyIdentifier, System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.ReadOnlySpan serialNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension CreateFromCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, bool includeKeyIdentifier, bool includeIssuerAndSerial) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension CreateFromIssuerNameAndSerialNumber(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, byte[] serialNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension CreateFromIssuerNameAndSerialNumber(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.ReadOnlySpan serialNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier(byte[] subjectKeyIdentifier) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier(System.ReadOnlySpan subjectKeyIdentifier) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier(System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension subjectKeyIdentifier) { throw null; } + } public sealed partial class X509BasicConstraintsExtension : System.Security.Cryptography.X509Certificates.X509Extension { public X509BasicConstraintsExtension() { } @@ -2637,6 +2656,7 @@ public X509Certificate(string fileName, string? password) { } public X509Certificate(string fileName, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags) { } public System.IntPtr Handle { get { throw null; } } public string Issuer { get { throw null; } } + public System.ReadOnlyMemory SerialNumberBytes { get { throw null; } } public string Subject { get { throw null; } } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public static System.Security.Cryptography.X509Certificates.X509Certificate CreateFromCertFile(string filename) { throw null; } @@ -3152,6 +3172,7 @@ public X509SubjectKeyIdentifierExtension(System.Security.Cryptography.X509Certif public X509SubjectKeyIdentifierExtension(System.Security.Cryptography.X509Certificates.PublicKey key, System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierHashAlgorithm algorithm, bool critical) { } public X509SubjectKeyIdentifierExtension(string subjectKeyIdentifier, bool critical) { } public string? SubjectKeyIdentifier { get { throw null; } } + public System.ReadOnlyMemory SubjectKeyIdentifierBytes { get { throw null; } } public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEncodedData) { } } public enum X509SubjectKeyIdentifierHashAlgorithm diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 89b1ba4c49eb7..9627607a9f0a9 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -447,6 +447,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs index 8bcf3796e0b9a..4deea48ebbdc2 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs @@ -258,6 +258,7 @@ private static Dictionary DefaultNameHT ht.Add("2.5.29.15", typeof(X509Certificates.X509KeyUsageExtension)); ht.Add("2.5.29.37", typeof(X509Certificates.X509EnhancedKeyUsageExtension)); ht.Add(Oids.AuthorityInformationAccess, typeof(X509Certificates.X509AuthorityInformationAccessExtension)); + ht.Add(Oids.AuthorityKeyIdentifier, typeof(X509Certificates.X509AuthorityKeyIdentifierExtension)); // X509Chain class can be overridden to use a different chain engine. ht.Add("X509Chain", typeof(X509Certificates.X509Chain)); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 0a5b1120bf828..766f686ea96dc 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -245,11 +245,7 @@ public void AddEntry(X509Certificate2 certificate) public void AddEntry(X509Certificate2 certificate, DateTimeOffset revocationTime) { ArgumentNullException.ThrowIfNull(certificate); - - byte[] serial = certificate.GetSerialNumber(); - Array.Reverse(serial); - - AddEntry(new ReadOnlySpan(serial), revocationTime); + AddEntry(certificate.SerialNumberBytes.Span, revocationTime); } public void AddEntry(byte[] serialNumber) @@ -374,7 +370,7 @@ public byte[] Build( X509SignatureGenerator generator, int crlNumber, DateTimeOffset nextUpdate, - X509Extension akid) + X509AuthorityKeyIdentifierExtension akid) { return Build(issuerName, generator, crlNumber, nextUpdate, DateTimeOffset.UtcNow, akid); } @@ -385,7 +381,7 @@ public byte[] Build( int crlNumber, DateTimeOffset nextUpdate, DateTimeOffset thisUpdate, - X509Extension akid) + X509AuthorityKeyIdentifierExtension akid) { ArgumentNullException.ThrowIfNull(issuerName); ArgumentNullException.ThrowIfNull(generator); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs new file mode 100644 index 0000000000000..a96906a3915c3 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs @@ -0,0 +1,377 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Runtime.InteropServices.ComTypes; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.X509Certificates.Asn1; + +namespace System.Security.Cryptography.X509Certificates +{ + /// + /// Represents the Authority Key Identifier X.509 Extension (2.5.29.35). + /// + public sealed class X509AuthorityKeyIdentifierExtension : X509Extension + { + private bool _decoded; + private X500DistinguishedName? _simpleIssuer; + private ReadOnlyMemory? _keyIdentifier; + private ReadOnlyMemory? _rawIssuer; + private ReadOnlyMemory? _serialNumber; + + /// + /// Initializes a new instance of the + /// class. + /// + public X509AuthorityKeyIdentifierExtension() + : base(Oids.AuthorityKeyIdentifier) + { + _decoded = true; + } + + /// + /// Initializes a new instance of the + /// class from an encoded representation of the extension and an optional critical marker. + /// + /// + /// The encoded data used to create the extension. + /// + /// + /// if the extension is critical; + /// otherwise, . + /// + /// + /// is . + /// + /// + /// did not decode as an Authority Key Identifier extension. + /// + public X509AuthorityKeyIdentifierExtension(byte[] rawData, bool critical = false) + : base(Oids.AuthorityKeyIdentifier, rawData, critical) + { + Decode(RawData); + } + + /// + /// Initializes a new instance of the + /// class from an encoded representation of the extension and an optional critical marker. + /// + /// + /// The encoded data used to create the extension. + /// + /// + /// if the extension is critical; + /// otherwise, . + /// + /// + /// did not decode as an Authority Key Identifier extension. + /// + public X509AuthorityKeyIdentifierExtension(ReadOnlySpan rawData, bool critical = false) + : base(Oids.AuthorityKeyIdentifier, rawData, critical) + { + Decode(RawData); + } + + /// + public override void CopyFrom(AsnEncodedData asnEncodedData) + { + base.CopyFrom(asnEncodedData); + _decoded = false; + } + + public ReadOnlyMemory? KeyIdentifier + { + get + { + if (!_decoded) + { + Decode(RawData); + } + + return _keyIdentifier; + } + } + + public X500DistinguishedName? SimpleIssuer + { + get + { + if (!_decoded) + { + Decode(RawData); + } + + return _simpleIssuer; + } + } + + public ReadOnlyMemory? RawIssuer + { + get + { + if (!_decoded) + { + Decode(RawData); + } + + return _rawIssuer; + } + } + + public ReadOnlyMemory? SerialNumber + { + get + { + if (!_decoded) + { + Decode(RawData); + } + + return _serialNumber; + } + } + + public static X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier( + X509SubjectKeyIdentifierExtension subjectKeyIdentifier) + { + ArgumentNullException.ThrowIfNull(subjectKeyIdentifier); + + return CreateFromSubjectKeyIdentifier(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span); + } + + public static X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier( + byte[] subjectKeyIdentifier) + { + ArgumentNullException.ThrowIfNull(subjectKeyIdentifier); + + return CreateFromSubjectKeyIdentifier(new ReadOnlySpan(subjectKeyIdentifier)); + } + + public static X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier( + ReadOnlySpan subjectKeyIdentifier) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + using (writer.PushSequence()) + { + writer.WriteOctetString(subjectKeyIdentifier, new Asn1Tag(TagClass.ContextSpecific, 0)); + } + + // Most KeyIdentifier values are computed from SHA-1 (20 bytes), which produces a 24-byte + // value for this extension. + // Let's go ahead and be really generous before moving to redundant array allocation. + Span stackSpan = stackalloc byte[64]; + ReadOnlySpan encoded = stackSpan; + + if (writer.TryEncode(stackSpan, out int written)) + { + encoded = stackSpan.Slice(0, written); + } + else + { + encoded = writer.Encode(); + } + + return new X509AuthorityKeyIdentifierExtension(encoded); + } + + public static X509AuthorityKeyIdentifierExtension CreateFromIssuerNameAndSerialNumber( + X500DistinguishedName issuerName, + byte[] serialNumber) + { + ArgumentNullException.ThrowIfNull(issuerName); + ArgumentNullException.ThrowIfNull(serialNumber); + + return CreateFromIssuerNameAndSerialNumber(issuerName, new ReadOnlySpan(serialNumber)); + } + + public static X509AuthorityKeyIdentifierExtension CreateFromIssuerNameAndSerialNumber( + X500DistinguishedName issuerName, + ReadOnlySpan serialNumber) + { + ArgumentNullException.ThrowIfNull(issuerName); + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + using (writer.PushSequence()) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1))) + { + writer.WriteEncodedValue(issuerName.RawData); + } + + writer.WriteIntegerUnsigned(serialNumber, new Asn1Tag(TagClass.ContextSpecific, 2)); + } + + return new X509AuthorityKeyIdentifierExtension(writer.Encode()); + } + + public static X509AuthorityKeyIdentifierExtension Create( + byte[] keyIdentifier, + X500DistinguishedName issuerName, + byte[] serialNumber) + { + ArgumentNullException.ThrowIfNull(keyIdentifier); + ArgumentNullException.ThrowIfNull(issuerName); + ArgumentNullException.ThrowIfNull(serialNumber); + + return Create( + new ReadOnlySpan(keyIdentifier), + issuerName, + new ReadOnlySpan(serialNumber)); + } + + public static X509AuthorityKeyIdentifierExtension Create( + ReadOnlySpan keyIdentifier, + X500DistinguishedName issuerName, + ReadOnlySpan serialNumber) + { + ArgumentNullException.ThrowIfNull(issuerName); + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + using (writer.PushSequence()) + { + writer.WriteOctetString(keyIdentifier, new Asn1Tag(TagClass.ContextSpecific, 0)); + + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1))) + { + writer.WriteEncodedValue(issuerName.RawData); + } + + writer.WriteIntegerUnsigned(serialNumber, new Asn1Tag(TagClass.ContextSpecific, 2)); + } + + return new X509AuthorityKeyIdentifierExtension(writer.Encode()); + } + + public static X509AuthorityKeyIdentifierExtension CreateFromCertificate( + X509Certificate2 certificate, + bool includeKeyIdentifier, + bool includeIssuerAndSerial) + { + ArgumentNullException.ThrowIfNull(certificate); + + X509SubjectKeyIdentifierExtension? skid = null; + + if (includeKeyIdentifier) + { + skid = (X509SubjectKeyIdentifierExtension?)certificate.Extensions[Oids.SubjectKeyIdentifier]; + + if (skid is null) + { + throw new CryptographicException("Provided certificate does not have a subject key identifier"); + } + + if (includeIssuerAndSerial) + { + return Create( + skid.SubjectKeyIdentifierBytes.Span, + certificate.IssuerName, + certificate.SerialNumberBytes.Span); + } + + return CreateFromSubjectKeyIdentifier(skid.SubjectKeyIdentifierBytes.Span); + } + else if (includeIssuerAndSerial) + { + return CreateFromIssuerNameAndSerialNumber( + certificate.IssuerName, + certificate.SerialNumberBytes.Span); + } + + Span emptyExtension = stackalloc byte[] { 0x30, 0x00 }; + return new X509AuthorityKeyIdentifierExtension(emptyExtension); + } + + private void Decode(ReadOnlySpan rawData) + { + _keyIdentifier = null; + _simpleIssuer = null; + _rawIssuer = null; + _serialNumber = null; + + // https://datatracker.ietf.org/doc/html/rfc3280#section-4.2.1.1 + // AuthorityKeyIdentifier ::= SEQUENCE { + // keyIdentifier[0] KeyIdentifier OPTIONAL, + // authorityCertIssuer[1] GeneralNames OPTIONAL, + // authorityCertSerialNumber[2] CertificateSerialNumber OPTIONAL } + // + // KeyIdentifier::= OCTET STRING + + try + { + AsnValueReader reader = new AsnValueReader(rawData, AsnEncodingRules.DER); + AsnValueReader aki = reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + + Asn1Tag nextTag = default; + + if (aki.HasData) + { + nextTag = aki.PeekTag(); + } + + if (nextTag.HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 0))) + { + _keyIdentifier = aki.ReadOctetString(nextTag); + + if (aki.HasData) + { + nextTag = aki.PeekTag(); + } + } + + if (nextTag.HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 1))) + { + byte[] rawIssuer = aki.ReadOctetString(nextTag); + _rawIssuer = rawIssuer; + + AsnValueReader generalNames = new AsnValueReader(rawIssuer, AsnEncodingRules.DER); + bool foundIssuer = false; + + // Walk all of the entities to make sure they decode legally, so no early abort. + while (generalNames.HasData) + { + GeneralNameAsn.Decode(ref generalNames, rawIssuer, out GeneralNameAsn decoded); + + if (!foundIssuer && decoded.DirectoryName.HasValue) + { + // Even if the X500DN fails to load, don't interpret a second one. + // That makes the API only ever return "the first directoryName" + foundIssuer = true; + + try + { + _simpleIssuer = new X500DistinguishedName(decoded.DirectoryName.GetValueOrDefault().Span); + } + catch (CryptographicException) + { + } + } + } + + if (aki.HasData) + { + nextTag = aki.PeekTag(); + } + } + + if (nextTag.HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 2))) + { + _serialNumber = aki.ReadIntegerBytes(nextTag).ToArray(); + } + + aki.ThrowIfNotEmpty(); + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + + _decoded = true; + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs index bc36fbc7788f9..4c039ef00ef66 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs @@ -460,6 +460,21 @@ public virtual byte[] GetSerialNumber() return serialNumber; } + /// + /// Gets a value whose contents represent the big-endian representation of the + /// certificate's serial number. + /// + /// The big-endian representation of the certificate's serial number. + public ReadOnlyMemory SerialNumberBytes + { + get + { + ThrowIfInvalid(); + + return GetRawSerialNumber(); + } + } + public virtual string GetSerialNumberString() { ThrowIfInvalid(); 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 9546c53bdacc6..42a5cf94048cb 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 @@ -1240,6 +1240,7 @@ private static X509Certificate2 ExtractKeyFromEncryptedPem( Oids.EnhancedKeyUsage => new X509EnhancedKeyUsageExtension(), Oids.SubjectKeyIdentifier => new X509SubjectKeyIdentifierExtension(), Oids.AuthorityInformationAccess => new X509AuthorityInformationAccessExtension(), + Oids.AuthorityKeyIdentifier => new X509AuthorityKeyIdentifierExtension(), _ => null, }; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs index fd893ce6d2291..1edcbbad26125 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs @@ -7,10 +7,14 @@ namespace System.Security.Cryptography.X509Certificates { public sealed class X509SubjectKeyIdentifierExtension : X509Extension { + private byte[]? _subjectKeyIdentifier; + private string? _subjectKeyIdentifierString; + private bool _decoded; + public X509SubjectKeyIdentifierExtension() : base(Oids.SubjectKeyIdentifierOid) { - _subjectKeyIdentifier = null; + _subjectKeyIdentifierString = null; _decoded = true; } @@ -50,11 +54,22 @@ public string? SubjectKeyIdentifier { if (!_decoded) { - byte[] subjectKeyIdentifierValue; - X509Pal.Instance.DecodeX509SubjectKeyIdentifierExtension(RawData, out subjectKeyIdentifierValue); - _subjectKeyIdentifier = subjectKeyIdentifierValue.ToHexStringUpper(); - _decoded = true; + Decode(RawData); } + + return _subjectKeyIdentifierString; + } + } + + public ReadOnlyMemory SubjectKeyIdentifierBytes + { + get + { + if (!_decoded) + { + Decode(RawData); + } + return _subjectKeyIdentifier; } } @@ -120,7 +135,11 @@ private static byte[] GenerateSubjectKeyIdentifierFromPublicKey(PublicKey key, X } } - private string? _subjectKeyIdentifier; - private bool _decoded; + private void Decode(byte[] rawData) + { + X509Pal.Instance.DecodeX509SubjectKeyIdentifierExtension(rawData, out _subjectKeyIdentifier); + _subjectKeyIdentifierString = _subjectKeyIdentifier.ToHexStringUpper(); + _decoded = true; + } } } From 215437b4487450f775413a03f4d1543d86d729b5 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 2 Jun 2022 18:01:33 -0700 Subject: [PATCH 05/43] Add SAN extension --- .../ExtensionsTests/ComprehensiveTests.cs | 7 +- .../SubjectAlternativeNameTests.cs | 111 +++++++++++ ...Cryptography.X509Certificates.Tests.csproj | 1 + .../ref/System.Security.Cryptography.cs | 10 + .../src/System.Security.Cryptography.csproj | 1 + .../Security/Cryptography/CryptoConfig.cs | 1 + .../X509Certificates/X509Certificate2.cs | 1 + .../X509SubjectAlternativeNameExtension.cs | 187 ++++++++++++++++++ 8 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs index e091904c6cf05..0d4e500349118 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/ComprehensiveTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using System.Net; using Test.Cryptography; using Xunit; @@ -75,7 +76,11 @@ public static void ReadExtensions() Assert.Equal(expected, sans.RawData); - Assert.IsType(sans); + // This SAN only contains an alternate DirectoryName entry, so both the DNSNames and + // IPAddresses enumerations being empty is correct. + X509SubjectAlternativeNameExtension rich = Assert.IsType(sans); + Assert.Equal(Enumerable.Empty(), rich.EnumerateDnsNames()); + Assert.Equal(Enumerable.Empty(), rich.EnumerateIPAddresses()); } { diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs new file mode 100644 index 0000000000000..1dd6cc11124ff --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests.ExtensionsTests +{ + public static class SubjectAlternativeNameTests + { + [Fact] + public static void DefaultConstructor() + { + // TODO: Write behavior tests. + } + + [Fact] + public static void EnumerateDnsNames() + { + SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); + builder.AddDnsName("foo"); + builder.AddIpAddress(IPAddress.Loopback); + builder.AddUserPrincipalName("user@some.domain"); + builder.AddIpAddress(IPAddress.IPv6Loopback); + builder.AddDnsName("*.foo"); + X509Extension built = builder.Build(true); + + X509SubjectAlternativeNameExtension ext = new(); + ext.CopyFrom(built); + + Assert.Equal(new[] { "foo", "*.foo" }, ext.EnumerateDnsNames()); + } + + [Fact] + public static void EnumerateIPAddresses() + { + SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); + builder.AddDnsName("foo"); + builder.AddIpAddress(IPAddress.Loopback); + builder.AddUserPrincipalName("user@some.domain"); + builder.AddIpAddress(IPAddress.IPv6Loopback); + builder.AddDnsName("*.foo"); + X509Extension built = builder.Build(true); + + X509SubjectAlternativeNameExtension ext = new(); + ext.CopyFrom(built); + + Assert.Equal(new[] { IPAddress.Loopback, IPAddress.IPv6Loopback }, ext.EnumerateIPAddresses()); + } + + [Fact] + public static void MatchesIpAddress() + { + SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); + builder.AddDnsName("foo"); + builder.AddIpAddress(IPAddress.Loopback); + builder.AddUserPrincipalName("user@some.domain"); + builder.AddIpAddress(IPAddress.IPv6Loopback); + builder.AddDnsName("*.foo"); + X509Extension built = builder.Build(true); + + X509SubjectAlternativeNameExtension ext = new(); + ext.CopyFrom(built); + + Assert.False(ext.MatchesHostname(IPAddress.Broadcast.ToString()), "Matches IPAddress.Broadcast"); + Assert.False(ext.MatchesHostname(IPAddress.IPv6Any.ToString()), "Matches IPAddress.IPv6Any"); + Assert.False(ext.MatchesHostname(IPAddress.Any.ToString()), "Matches IPAddress.Any"); + Assert.False(ext.MatchesHostname(IPAddress.None.ToString()), "Matches IPAddress.None"); + Assert.True(ext.MatchesHostname(IPAddress.IPv6Loopback.ToString()), "Matches IPAddress.IPv6Loopback"); + Assert.True(ext.MatchesHostname(IPAddress.Loopback.ToString()), "Matches IPAddress.Loopback"); + } + + [Fact] + public static void MatchesDnsName() + { + SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); + builder.AddDnsName("foo"); + builder.AddIpAddress(IPAddress.Loopback); + builder.AddUserPrincipalName("user@some.domain"); + builder.AddIpAddress(IPAddress.IPv6Loopback); + builder.AddDnsName("*.foo"); + X509Extension built = builder.Build(true); + + X509SubjectAlternativeNameExtension ext = new(); + ext.CopyFrom(built); + + static void AssertMatches(X509SubjectAlternativeNameExtension ext, string target, bool expected) + { + if (expected) + { + Assert.True(ext.MatchesHostname(target), $"Matches '{target}'"); + } + else + { + Assert.False(ext.MatchesHostname(target), $"Matches '{target}'"); + } + } + + AssertMatches(ext, "foo", true); + AssertMatches(ext, "fOo", true); + AssertMatches(ext, "fOo.", true); + AssertMatches(ext, ".fOo.", false); + AssertMatches(ext, "BAR.fOo.", true); + AssertMatches(ext, "BAR.foo", true); + AssertMatches(ext, "baz.BAR.foo", false); + AssertMatches(ext, "baz.BAR.foo.", false); + AssertMatches(ext, "example.com", false); + } + } +} 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 66bc41f2cbce0..efb493e37bced 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 @@ -40,6 +40,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index bf777280a78de..2fe5723ebef70 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3162,6 +3162,16 @@ public void Open(System.Security.Cryptography.X509Certificates.OpenFlags flags) public void Remove(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } public void RemoveRange(System.Security.Cryptography.X509Certificates.X509Certificate2Collection certificates) { } } + public partial class X509SubjectAlternativeNameExtension : System.Security.Cryptography.X509Certificates.X509Extension + { + public X509SubjectAlternativeNameExtension() { } + public X509SubjectAlternativeNameExtension(byte[] rawData, bool critical = false) { } + public X509SubjectAlternativeNameExtension(System.ReadOnlySpan rawData, bool critical = false) { } + public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEncodedData) { } + public System.Collections.Generic.IEnumerable EnumerateDnsNames() { throw null; } + public System.Collections.Generic.IEnumerable EnumerateIPAddresses() { throw null; } + public bool MatchesHostname(string hostname) { throw null; } + } public sealed partial class X509SubjectKeyIdentifierExtension : System.Security.Cryptography.X509Certificates.X509Extension { public X509SubjectKeyIdentifierExtension() { } diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 9627607a9f0a9..67bd95e88d6a6 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -479,6 +479,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs index 4deea48ebbdc2..99a423d6e9535 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoConfig.cs @@ -259,6 +259,7 @@ private static Dictionary DefaultNameHT ht.Add("2.5.29.37", typeof(X509Certificates.X509EnhancedKeyUsageExtension)); ht.Add(Oids.AuthorityInformationAccess, typeof(X509Certificates.X509AuthorityInformationAccessExtension)); ht.Add(Oids.AuthorityKeyIdentifier, typeof(X509Certificates.X509AuthorityKeyIdentifierExtension)); + ht.Add(Oids.SubjectAltName, typeof(X509Certificates.X509SubjectAlternativeNameExtension)); // X509Chain class can be overridden to use a different chain engine. ht.Add("X509Chain", typeof(X509Certificates.X509Chain)); 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 42a5cf94048cb..0143355f8ba79 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 @@ -1241,6 +1241,7 @@ private static X509Certificate2 ExtractKeyFromEncryptedPem( Oids.SubjectKeyIdentifier => new X509SubjectKeyIdentifierExtension(), Oids.AuthorityInformationAccess => new X509AuthorityInformationAccessExtension(), Oids.AuthorityKeyIdentifier => new X509AuthorityKeyIdentifierExtension(), + Oids.SubjectAltName => new X509SubjectAlternativeNameExtension(), _ => null, }; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs new file mode 100644 index 0000000000000..98fb355deef02 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Formats.Asn1; +using System.Net; +using System.Security.Cryptography.Asn1; + +namespace System.Security.Cryptography.X509Certificates +{ + public class X509SubjectAlternativeNameExtension : X509Extension + { + private List? _decoded; + + public X509SubjectAlternativeNameExtension() : base(Oids.SubjectAltName) + { + } + + public X509SubjectAlternativeNameExtension(byte[] rawData, bool critical = false) + : base(Oids.SubjectAltName, rawData, critical) + { + _decoded = Decode(RawData); + } + + public X509SubjectAlternativeNameExtension(ReadOnlySpan rawData, bool critical = false) + : base(Oids.SubjectAltName, rawData, critical) + { + _decoded = Decode(RawData); + } + + public override void CopyFrom(AsnEncodedData asnEncodedData) + { + base.CopyFrom(asnEncodedData); + _decoded = null; + } + + public bool MatchesHostname(string hostname) + { + ArgumentNullException.ThrowIfNull(hostname); + + if (hostname.Length == 0) + { + return false; + } + + if (IPAddress.TryParse(hostname, out IPAddress? ipAddress)) + { + // Big enough for IPv6 + Span encodedAddr = stackalloc byte[16]; + + if (!ipAddress.TryWriteBytes(encodedAddr, out int written)) + { + return false; + } + + ReadOnlySpan match = encodedAddr.Slice(0, written); + + List decoded = (_decoded ??= Decode(RawData)); + + foreach (GeneralNameAsn item in decoded) + { + if (item.IPAddress.HasValue) + { + if (item.IPAddress.GetValueOrDefault().Span.SequenceEqual(match)) + { + return true; + } + } + } + } + else + { + ReadOnlySpan match = hostname; + + if (hostname.EndsWith('.')) + { + match = match.Slice(0, match.Length - 1); + + if (match.IsEmpty) + { + return false; + } + } + + ReadOnlySpan afterFirstDot = default; + int firstDot = match.IndexOf('.'); + + if (firstDot == 0) + { + return false; + } + + if (firstDot > 0) + { + afterFirstDot = match.Slice(firstDot + 1); + } + + foreach (string embedded in EnumerateDnsNames()) + { + if (embedded.Length == 0) + { + continue; + } + + ReadOnlySpan embeddedSpan = embedded; + + if (embedded.EndsWith('.')) + { + embeddedSpan = embeddedSpan.Slice(0, embeddedSpan.Length - 1); + } + + if (embeddedSpan.StartsWith("*.") && embeddedSpan.Length > 2) + { + if (embeddedSpan.Slice(2).Equals(afterFirstDot, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + else if (embeddedSpan.Equals(match, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + public IEnumerable EnumerateDnsNames() + { + List decoded = (_decoded ??= Decode(RawData)); + + return EnumerateDnsNames(decoded); + } + + private static IEnumerable EnumerateDnsNames(List decoded) + { + foreach (GeneralNameAsn item in decoded) + { + if (item.DnsName is not null) + { + yield return item.DnsName; + } + } + } + + public IEnumerable EnumerateIPAddresses() + { + List decoded = (_decoded ??= Decode(RawData)); + + return EnumerateIPAddresses(decoded); + } + + private static IEnumerable EnumerateIPAddresses(List decoded) + { + foreach (GeneralNameAsn item in decoded) + { + if (item.IPAddress.HasValue) + { + ReadOnlySpan value = item.IPAddress.GetValueOrDefault().Span; + + if (value.Length is 4 or 16) + { + yield return new IPAddress(value); + } + } + } + } + + private static List Decode(ReadOnlySpan rawData) + { + AsnValueReader outer = new AsnValueReader(rawData, AsnEncodingRules.DER); + AsnValueReader sequence = outer.ReadSequence(); + outer.ThrowIfNotEmpty(); + + List decoded = new List(); + + while (sequence.HasData) + { + GeneralNameAsn.Decode(ref sequence, default, out GeneralNameAsn item); + decoded.Add(item); + } + + return decoded; + } + } +} From 59b267a81a05d9c2b14f089359cc8c3f670f0bbd Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 2 Jun 2022 18:01:46 -0700 Subject: [PATCH 06/43] Use less memory in SANBuilder --- .../SubjectAlternativeNameBuilder.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs index 0d34c6d472a40..25d463df011e4 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs @@ -16,6 +16,7 @@ public sealed class SubjectAlternativeNameBuilder // Because GeneralNames is a SEQUENCE, just make a rolling list, it doesn't need to be re-sorted. private readonly List _encodedNames = new List(); + private readonly AsnWriter _writer = new AsnWriter(AsnEncodingRules.DER); public void AddEmailAddress(string emailAddress) { @@ -52,9 +53,10 @@ public void AddUserPrincipalName(string upn) if (string.IsNullOrEmpty(upn)) throw new ArgumentOutOfRangeException(nameof(upn), SR.Arg_EmptyOrNullString); - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - writer.WriteCharacterString(UniversalTagNumber.UTF8String, upn); - byte[] otherNameValue = writer.Encode(); + _writer.Reset(); + _writer.WriteCharacterString(UniversalTagNumber.UTF8String, upn); + byte[] otherNameValue = _writer.Encode(); + _writer.Reset(); OtherNameAsn otherName = new OtherNameAsn { @@ -67,20 +69,20 @@ public void AddUserPrincipalName(string upn) public X509Extension Build(bool critical = false) { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + _writer.Reset(); - using (writer.PushSequence()) + using (_writer.PushSequence()) { foreach (byte[] encodedName in _encodedNames) { - writer.WriteEncodedValue(encodedName); + _writer.WriteEncodedValue(encodedName); } } - return new X509Extension( - Oids.SubjectAltName, - writer.Encode(), - critical); + byte[] encoded = _writer.Encode(); + _writer.Reset(); + + return new X509Extension(Oids.SubjectAltName, encoded, critical); } private void AddGeneralName(GeneralNameAsn generalName) @@ -88,9 +90,10 @@ private void AddGeneralName(GeneralNameAsn generalName) try { // Verify that the general name can be serialized and store it. - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - generalName.Encode(writer); - _encodedNames.Add(writer.Encode()); + _writer.Reset(); + generalName.Encode(_writer); + _encodedNames.Add(_writer.Encode()); + _writer.Reset(); } catch (EncoderFallbackException) { From 13221652435f3dce8cb6c705f2f45cda0bb83cb2 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 3 Jun 2022 07:23:04 -0700 Subject: [PATCH 07/43] CDP Builder --- .../X509Certificates/CertificateAuthority.cs | 28 +------- .../CertificateCreation/CrlBuilderTests.cs | 15 ++++ .../ref/System.Security.Cryptography.cs | 1 + .../src/Resources/Strings.resx | 6 ++ .../src/System.Security.Cryptography.csproj | 1 + ...icateRevocationListBuilder.CdpExtension.cs | 69 +++++++++++++++++++ .../CertificateRevocationListBuilder.cs | 5 +- 7 files changed, 94 insertions(+), 31 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index 424ccf7942365..d6ab6817338b3 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -612,33 +612,7 @@ private static X509Extension CreateAiaExtension(string certLocation, string ocsp private static X509Extension CreateCdpExtension(string cdp) { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - // SEQUENCE OF - using (writer.PushSequence()) - { - // DistributionPoint - using (writer.PushSequence()) - { - // Because DistributionPointName is a CHOICE type this tag is explicit. - // (ITU-T REC X.680-201508 C.3.2.2(g)(3rd bullet)) - // distributionPoint [0] DistributionPointName - using (writer.PushSequence(s_context0)) - { - // [0] DistributionPointName (GeneralNames (SEQUENCE OF)) - using (writer.PushSequence(s_context0)) - { - // GeneralName ([6] IA5String) - writer.WriteCharacterString( - UniversalTagNumber.IA5String, - cdp, - new Asn1Tag(TagClass.ContextSpecific, 6)); - } - } - } - } - - return new X509Extension("2.5.29.31", writer.Encode(), false); + return CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(new[] { cdp }); } private X509AuthorityKeyIdentifierExtension CreateAkidExtension() diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index 80c64d9b11588..c66c9a87cb902 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -1,6 +1,7 @@ // Licensed to the.NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Test.Cryptography; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests.CertificateCreation @@ -170,5 +171,19 @@ public static void BuildWithGeneratorArgumentValidation() Assert.Contains("nextUpdate", ex.Message); } } + + [Fact] + public static void BuildSimpleCdp() + { + X509Extension ext = CertificateRevocationListBuilder.BuildCrlDistributionPointExtension( + new[] { "http://crl.microsoft.com/pki/crl/products/MicCodSigPCA_08-31-2010.crl" }); + + byte[] expected = ( + "304d304ba049a0478645687474703a2f2f63726c2e6d6963726f736f" + + "66742e636f6d2f706b692f63726c2f70726f64756374732f4d696343" + + "6f645369675043415f30382d33312d323031302e63726c").HexToByteArray(); + + Assert.Equal(expected, ext.RawData); + } } } diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 2fe5723ebef70..b4b3f26bcfb47 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2454,6 +2454,7 @@ public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certifica public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate) { throw null; } public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Extension BuildCrlDistributionPointExtension(System.Collections.Generic.IEnumerable uris, bool critical = false) { throw null; } public void ExpireEntries(System.DateTimeOffset oldestRevocationTimeToKeep) { } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(byte[] currentCrl, out int currentCrlNumber) { throw null; } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(System.ReadOnlySpan currentCrl, out int currentCrlNumber, out int bytesConsumed) { throw null; } diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index f4e5ee26187e9..4a48215d74260 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -609,6 +609,12 @@ One of the ocspUris or caIssuersUris enumerables contains a null value. + + At least one of the ocspUris or caIssuersUris values must be non-empty. + + + One of the provided CRL Distribution Point URIs is a null value. + Certificate '{0}' is corrupted. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 67bd95e88d6a6..c88c3b663af7c 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -417,6 +417,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs new file mode 100644 index 0000000000000..d66989f5477f4 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Formats.Asn1; + +namespace System.Security.Cryptography.X509Certificates +{ + public sealed partial class CertificateRevocationListBuilder + { + public static X509Extension BuildCrlDistributionPointExtension(IEnumerable uris, bool critical = false) + { + // CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint + // + // DistributionPoint::= SEQUENCE { + // distributionPoint[0] DistributionPointName OPTIONAL, + // reasons[1] ReasonFlags OPTIONAL, + // cRLIssuer[2] GeneralNames OPTIONAL } + + // DistributionPointName::= CHOICE { + // fullName[0] GeneralNames, + // nameRelativeToCRLIssuer[1] RelativeDistinguishedName } + + AsnWriter? writer = null; + + foreach (string uri in uris) + { + if (uri is null) + { + throw new ArgumentException(SR.Cryptography_X509_CDP_NullValue, nameof(uris)); + } + + if (writer is null) + { + writer = new AsnWriter(AsnEncodingRules.DER); + // CRLDistributionPoints + writer.PushSequence(); + } + + // DistributionPoint + using (writer.PushSequence()) + { + // DistributionPoint/DistributionPointName EXPLICIT [0] + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) + { + // DistributionPointName/GeneralName + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) + { + // GeneralName/Uri + writer.WriteCharacterString( + UniversalTagNumber.IA5String, + uri, + new Asn1Tag(TagClass.ContextSpecific, 6)); + } + } + } + } + + if (writer is null) + { + throw new ArgumentException(SR.Cryptography_X509_CDP_MustNotBuildEmpty, nameof(uris)); + } + + // CRLDistributionPoints + writer.PopSequence(); + return new X509Extension(Oids.CrlDistributionPoints, writer.Encode(), critical); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 766f686ea96dc..e9cc4bb86bd46 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -5,14 +5,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Formats.Asn1; -using System.Runtime.InteropServices; using System.Security.Cryptography.Asn1; namespace System.Security.Cryptography.X509Certificates { - /// - /// - public sealed class CertificateRevocationListBuilder + public sealed partial class CertificateRevocationListBuilder { private struct RevokedCertificate { From e8cd63e03208b13e97a87ccb795442dd841ae542 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 3 Jun 2022 13:30:27 -0700 Subject: [PATCH 08/43] PKCS10 loading, part 1 --- .../ref/System.Security.Cryptography.cs | 3 + .../X509Certificates/CertificateRequest.cs | 363 +++++++++++++++++- .../X509Certificates/PublicKey.cs | 27 +- .../X509Certificates/X509Certificate2.cs | 6 +- 4 files changed, 392 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index b4b3f26bcfb47..fb2a462103b39 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2425,10 +2425,12 @@ public sealed partial class CertificateRequest public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding) { } public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding? rsaSignaturePadding) { } public CertificateRequest(string subjectName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } public CertificateRequest(string subjectName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding) { } public System.Collections.ObjectModel.Collection CertificateExtensions { get { throw null; } } public System.Security.Cryptography.HashAlgorithmName HashAlgorithm { get { throw null; } } + public System.Collections.ObjectModel.Collection OtherRequestAttributes { get { throw null; } } public System.Security.Cryptography.X509Certificates.PublicKey PublicKey { get { throw null; } } public System.Security.Cryptography.X509Certificates.X500DistinguishedName SubjectName { get { throw null; } } public System.Security.Cryptography.X509Certificates.X509Certificate2 Create(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.DateTimeOffset notBefore, System.DateTimeOffset notAfter, byte[] serialNumber) { throw null; } @@ -2438,6 +2440,7 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public System.Security.Cryptography.X509Certificates.X509Certificate2 CreateSelfSigned(System.DateTimeOffset notBefore, System.DateTimeOffset notAfter) { throw null; } public byte[] CreateSigningRequest() { throw null; } public byte[] CreateSigningRequest(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(System.ReadOnlySpan pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } } public sealed partial class CertificateRevocationListBuilder { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index ca3c1ac642511..81bf36afbcff3 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -34,6 +35,16 @@ public sealed class CertificateRequest /// public Collection CertificateExtensions { get; } = new Collection(); + /// + /// Gets a collection representing attributes, other than the extension request attribute, to include + /// in a certificate request. + /// + /// + /// A collection representing attributes, other than the extension request attribute, to include + /// in a certificate request + /// + public Collection OtherRequestAttributes { get; } = new Collection(); + /// /// A representation of the public key for the certificate or certificate request. /// @@ -191,6 +202,38 @@ public CertificateRequest(X500DistinguishedName subjectName, PublicKey publicKey HashAlgorithm = hashAlgorithm; } + /// + /// Create a CertificateRequest for the specified subject name, encoded public key, hash algorithm, + /// and RSA signature padding. + /// + /// + /// The parsed representation of the subject name for the certificate or certificate request. + /// + /// + /// The encoded representation of the public key to include in the certificate or certificate request. + /// + /// + /// The hash algorithm to use when signing the certificate or certificate request. + /// + /// + /// The RSA signature padding to use when signing this request with an RSA certificate. + /// + public CertificateRequest( + X500DistinguishedName subjectName, + PublicKey publicKey, + HashAlgorithmName hashAlgorithm, + RSASignaturePadding? rsaSignaturePadding) + { + ArgumentNullException.ThrowIfNull(subjectName); + ArgumentNullException.ThrowIfNull(publicKey); + ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + + SubjectName = subjectName; + PublicKey = publicKey; + HashAlgorithm = hashAlgorithm; + _rsaPadding = rsaSignaturePadding; + } + /// /// Create an ASN.1 DER-encoded PKCS#10 CertificationRequest object representing the current state /// of this object. @@ -249,10 +292,40 @@ public byte[] CreateSigningRequest(X509SignatureGenerator signatureGenerator) ArgumentNullException.ThrowIfNull(signatureGenerator); X501Attribute[] attributes = Array.Empty(); + bool hasExtensions = CertificateExtensions.Count > 0; - if (CertificateExtensions.Count > 0) + if (OtherRequestAttributes.Count > 0 || hasExtensions) + { + attributes = new X501Attribute[OtherRequestAttributes.Count + (hasExtensions ? 1 : 0)]; + } + + int attrCount = 0; + + foreach (AsnEncodedData attr in OtherRequestAttributes) + { + if (attr is null) + { + throw new InvalidOperationException("Something about a null attribute"); + } + + if (attr.Oid is null || attr.Oid.Value is null) + { + throw new InvalidOperationException("Something about a null attr OID"); + } + + if (attr.Oid.Value == Oids.Pkcs9ExtensionRequest) + { + throw new InvalidOperationException("Something about can't redefine the extensions attr"); + } + + Helpers.ValidateDer(attr.RawData); + attributes[attrCount] = new X501Attribute(attr.Oid.Value, attr.RawData); + attrCount++; + } + + if (hasExtensions) { - attributes = new X501Attribute[] { new Pkcs9ExtensionRequest(CertificateExtensions) }; + attributes[attrCount] = new Pkcs9ExtensionRequest(CertificateExtensions); } var requestInfo = new Pkcs10CertificationRequestInfo(SubjectName, PublicKey, attributes); @@ -685,6 +758,139 @@ public X509Certificate2 Create( return ret; } + public static unsafe CertificateRequest LoadCertificateRequest( + ReadOnlySpan pkcs10, + HashAlgorithmName signerHashAlgorithm, + out int bytesConsumed, + bool skipSignatureValidation = false, + RSASignaturePadding? signerSignaturePadding = null) + { + try + { + AsnValueReader outer = new AsnValueReader(pkcs10, AsnEncodingRules.DER); + int encodedLength = outer.PeekEncodedValue().Length; + + AsnValueReader pkcs10Asn = outer.ReadSequence(); + CertificateRequest req; + + fixed (byte* p10ptr = pkcs10) + { + using (PointerMemoryManager manager = new PointerMemoryManager(p10ptr, encodedLength)) + { + ReadOnlyMemory rebind = manager.Memory; + ReadOnlySpan encodedRequestInfo = pkcs10Asn.PeekEncodedValue(); + CertificationRequestInfoAsn requestInfo; + AlgorithmIdentifierAsn algorithmIdentifier; + ReadOnlySpan signature; + int signatureUnusedBitCount; + + CertificationRequestInfoAsn.Decode(ref pkcs10Asn, rebind, out requestInfo); + AlgorithmIdentifierAsn.Decode(ref pkcs10Asn, rebind, out algorithmIdentifier); + + if (!pkcs10Asn.TryReadPrimitiveBitString(out signatureUnusedBitCount, out signature)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + pkcs10Asn.ThrowIfNotEmpty(); + + if (requestInfo.Version != 0) + { + throw new CryptographicException("Something about a request from the future"); + } + + PublicKey publicKey = PublicKey.DecodeSubjectPublicKeyInfo(ref requestInfo.SubjectPublicKeyInfo); + + if (!skipSignatureValidation) + { + // None of the supported signature algorithms support signatures that are not full bytes. + // So, shortcut the verification on the bit length + if (signatureUnusedBitCount != 0 || + !VerifyX509Signature(encodedRequestInfo, signature, publicKey, algorithmIdentifier)) + { + throw new CryptographicException("Signature didn't work"); + } + } + + X500DistinguishedName subject = new X500DistinguishedName(requestInfo.Subject.Span); + + req = new CertificateRequest( + subject, + publicKey, + signerHashAlgorithm, + signerSignaturePadding); + + if (requestInfo.Attributes is not null) + { + bool foundCertExt = false; + + foreach (AttributeAsn attr in requestInfo.Attributes) + { + if (attr.AttrType == Oids.Pkcs9ExtensionRequest) + { + if (foundCertExt) + { + throw new CryptographicException("Too many certificate extension requests"); + } + + foundCertExt = true; + + if (attr.AttrValues.Length != 1) + { + throw new CryptographicException("Invalid certificate extensions request"); + } + + AsnValueReader extsReader = new AsnValueReader( + attr.AttrValues[0].Span, + AsnEncodingRules.DER); + + AsnValueReader exts = extsReader.ReadSequence(); + extsReader.ThrowIfNotEmpty(); + + while (exts.HasData) + { + X509ExtensionAsn.Decode(ref exts, rebind, out X509ExtensionAsn extAsn); + + X509Extension ext = new X509Extension( + extAsn.ExtnId, + extAsn.ExtnValue.Span, + extAsn.Critical); + + X509Extension? rich = X509Certificate2.CreateCustomExtensionIfAny(extAsn.ExtnId); + + if (rich is not null) + { + rich.CopyFrom(ext); + req.CertificateExtensions.Add(rich); + } + else + { + req.CertificateExtensions.Add(ext); + } + } + } + else + { + foreach (ReadOnlyMemory val in attr.AttrValues) + { + req.OtherRequestAttributes.Add( + new AsnEncodedData(attr.AttrType, val.Span)); + } + } + } + } + } + } + + bytesConsumed = encodedLength; + return req; + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + private static ArraySegment NormalizeSerialNumber(ReadOnlySpan serialNumber) { byte[] newSerialNumber; @@ -710,5 +916,158 @@ private static ArraySegment NormalizeSerialNumber(ReadOnlySpan seria serialNumber.Slice(leadingZeros).CopyTo(newSerialNumber); return new ArraySegment(newSerialNumber, 0, contentLength); } + + private static bool VerifyX509Signature( + ReadOnlySpan toBeSigned, + ReadOnlySpan signature, + PublicKey publicKey, + AlgorithmIdentifierAsn algorithmIdentifier) + { + RSA? rsa = publicKey.GetRSAPublicKey(); + ECDsa? ecdsa = publicKey.GetECDsaPublicKey(); + + try + { + HashAlgorithmName hashAlg; + + if (algorithmIdentifier.Algorithm == Oids.RsaPss) + { + if (rsa is null || !algorithmIdentifier.Parameters.HasValue) + { + return false; + } + + PssParamsAsn pssParams = PssParamsAsn.Decode( + algorithmIdentifier.Parameters.GetValueOrDefault(), + AsnEncodingRules.DER); + + if (pssParams.TrailerField != 1 || + !pssParams.HashAlgorithm.HasNullEquivalentParameters() || + pssParams.MaskGenAlgorithm.Algorithm != Oids.Mgf1 || + !pssParams.MaskGenAlgorithm.Parameters.HasValue) + { + return false; + } + + AlgorithmIdentifierAsn mgfParams = AlgorithmIdentifierAsn.Decode( + pssParams.MaskGenAlgorithm.Parameters.GetValueOrDefault(), + AsnEncodingRules.DER); + + if (mgfParams.Algorithm != pssParams.HashAlgorithm.Algorithm || + !mgfParams.HasNullEquivalentParameters()) + { + return false; + } + + switch (pssParams.HashAlgorithm.Algorithm) + { + case Oids.Sha256: + if (pssParams.SaltLength != SHA256.HashSizeInBytes) + { + return false; + } + + hashAlg = HashAlgorithmName.SHA256; + break; + case Oids.Sha384: + if (pssParams.SaltLength != SHA384.HashSizeInBytes) + { + return false; + } + + hashAlg = HashAlgorithmName.SHA384; + break; + case Oids.Sha512: + if (pssParams.SaltLength != SHA512.HashSizeInBytes) + { + return false; + } + + hashAlg = HashAlgorithmName.SHA512; + break; + case Oids.Sha1: + if (pssParams.SaltLength != SHA1.HashSizeInBytes) + { + return false; + } + + hashAlg = HashAlgorithmName.SHA1; + break; + default: + return false; + } + + return rsa.VerifyData( + toBeSigned, + signature, + hashAlg, + RSASignaturePadding.Pss); + } + + // All remaining algorithms have no defined parameters + if (!algorithmIdentifier.HasNullEquivalentParameters()) + { + return false; + } + + switch (algorithmIdentifier.Algorithm) + { + case Oids.RsaPkcs1Sha256: + case Oids.ECDsaWithSha256: + hashAlg = HashAlgorithmName.SHA256; + break; + case Oids.RsaPkcs1Sha384: + case Oids.ECDsaWithSha384: + hashAlg = HashAlgorithmName.SHA384; + break; + case Oids.RsaPkcs1Sha512: + case Oids.ECDsaWithSha512: + hashAlg = HashAlgorithmName.SHA512; + break; + case Oids.RsaPkcs1Sha1: + case Oids.ECDsaWithSha1: + hashAlg = HashAlgorithmName.SHA1; + break; + default: + return false; + } + + switch (algorithmIdentifier.Algorithm) + { + case Oids.RsaPkcs1Sha256: + case Oids.RsaPkcs1Sha384: + case Oids.RsaPkcs1Sha512: + case Oids.RsaPkcs1Sha1: + if (rsa is null) + { + return false; + } + + return rsa.VerifyData(toBeSigned, signature, hashAlg, RSASignaturePadding.Pkcs1); + case Oids.ECDsaWithSha256: + case Oids.ECDsaWithSha384: + case Oids.ECDsaWithSha512: + case Oids.ECDsaWithSha1: + if (ecdsa is null) + { + return false; + } + + return ecdsa.VerifyData(toBeSigned, signature, hashAlg); + default: + Debug.Fail($"Algorithm ID {algorithmIdentifier.Algorithm} was in the first switch, but not the second"); + return false; + } + } + catch (AsnContentException) + { + return false; + } + finally + { + rsa?.Dispose(); + ecdsa?.Dispose(); + } + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs index b6612d0798a1a..2befa008777e9 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs @@ -299,11 +299,32 @@ private static unsafe int DecodeSubjectPublicKeyInfo( throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } - oid = new Oid(spki.Algorithm.Algorithm, null); - parameters = new AsnEncodedData(spki.Algorithm.Parameters.GetValueOrDefault().Span); - keyValue = new AsnEncodedData(spki.SubjectPublicKey.Span); + DecodeSubjectPublicKeyInfo(ref spki, out oid, out parameters, out keyValue); return read; } } + + internal static PublicKey DecodeSubjectPublicKeyInfo(ref SubjectPublicKeyInfoAsn spki) + { + DecodeSubjectPublicKeyInfo( + ref spki, + out Oid oid, + out AsnEncodedData parameters, + out AsnEncodedData keyValue); + + + return new PublicKey(oid, keyValue, parameters); + } + + private static void DecodeSubjectPublicKeyInfo( + ref SubjectPublicKeyInfoAsn spki, + out Oid oid, + out AsnEncodedData parameters, + out AsnEncodedData keyValue) + { + oid = new Oid(spki.Algorithm.Algorithm, null); + parameters = new AsnEncodedData(spki.Algorithm.Parameters.GetValueOrDefault().Span); + keyValue = new AsnEncodedData(spki.SubjectPublicKey.Span); + } } } 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 0143355f8ba79..2d5916bfac315 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 @@ -1231,8 +1231,10 @@ private static X509Certificate2 ExtractKeyFromEncryptedPem( throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey); } - private static X509Extension? CreateCustomExtensionIfAny(Oid oid) => - oid.Value switch + private static X509Extension? CreateCustomExtensionIfAny(Oid oid) => CreateCustomExtensionIfAny(oid.Value); + + internal static X509Extension? CreateCustomExtensionIfAny(string? oidValue) => + oidValue switch { Oids.BasicConstraints => X509Pal.Instance.SupportsLegacyBasicConstraintsExtension ? new X509BasicConstraintsExtension() : null, Oids.BasicConstraints2 => new X509BasicConstraintsExtension(), From a9f179eef0edc4904c7043c57f9765ee6c8d910f Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 3 Jun 2022 15:39:54 -0700 Subject: [PATCH 09/43] X509BasicConstraintsExtension, Quality of Life --- ...rity.Cryptography.X509Certificates.Tests.csproj | 1 + .../ref/System.Security.Cryptography.cs | 2 ++ .../X509BasicConstraintsExtension.cs | 14 ++++++++++++++ 3 files changed, 17 insertions(+) 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 efb493e37bced..5957a2d0173ff 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 @@ -69,6 +69,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index fb2a462103b39..90f617a991eb4 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2623,6 +2623,8 @@ public X509BasicConstraintsExtension(System.Security.Cryptography.AsnEncodedData public bool HasPathLengthConstraint { get { throw null; } } public int PathLengthConstraint { get { throw null; } } public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEncodedData) { } + public static System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension CreateForCertificateAuthority(int? pathLengthConstraint = default(int?)) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension CreateForEndEntity(bool critical = false) { throw null; } } public partial class X509Certificate : System.IDisposable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509BasicConstraintsExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509BasicConstraintsExtension.cs index fd7d5373af68d..a72f82be12b50 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509BasicConstraintsExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509BasicConstraintsExtension.cs @@ -60,6 +60,20 @@ public override void CopyFrom(AsnEncodedData asnEncodedData) _decoded = false; } + public static X509BasicConstraintsExtension CreateForCertificateAuthority(int? pathLengthConstraint = null) + { + return new X509BasicConstraintsExtension( + true, + pathLengthConstraint.HasValue, + pathLengthConstraint.GetValueOrDefault(), + true); + } + + public static X509BasicConstraintsExtension CreateForEndEntity(bool critical = false) + { + return new X509BasicConstraintsExtension(false, false, 0, critical); + } + private static byte[] EncodeExtension(bool certificateAuthority, bool hasPathLengthConstraint, int pathLengthConstraint) { if (hasPathLengthConstraint && pathLengthConstraint < 0) From 6d5dc06a1ff7d85db2d4aebda4382fd96926f0ec Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 3 Jun 2022 15:40:19 -0700 Subject: [PATCH 10/43] Don't Be A CA. --- .../tests/CertificateCreation/DontBeACA.cs | 484 ++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs new file mode 100644 index 0000000000000..e4374f29e1253 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -0,0 +1,484 @@ +// Licensed to the.NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Formats.Asn1; +using System.Net; +using System.Net.Sockets; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests.CertificateCreation +{ + public static class DontBeACA + { + [Fact] + public static void EndToEnd() + { + X500DistinguishedName reqName = + new X500DistinguishedName("CN=Apple, CN=Banana, OU=Cherry, O=Date, L=Elderberry"); + + byte[] pkcs10 = BuildRequest(reqName, "Fig"); + + using (X509Certificate2 issuerCert = MakeCA()) + { + CertificateRequest req = IngestRequest(pkcs10, issuerCert); + + byte[] serial = { 1, 1, 2, 3, 5, 8, 13, 21 }; + DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddMinutes(-2); + DateTimeOffset notAfter = notBefore.AddMinutes(20); + + using (X509Certificate2 issued = req.Create(issuerCert, notBefore, notAfter, serial)) + { + Assert.NotNull(issued); + AssertExtensions.SequenceEqual(serial, issued.SerialNumberBytes.Span); + AssertExtensions.SequenceEqual(reqName.RawData, issued.SubjectName.RawData); + AssertExtensions.SequenceEqual(issuerCert.SubjectName.RawData, issued.IssuerName.RawData); + Assert.Equal(notBefore.DateTime, issued.NotBefore.ToUniversalTime(), TimeSpan.FromSeconds(1)); + Assert.Equal(notAfter.DateTime, issued.NotAfter.ToUniversalTime(), TimeSpan.FromSeconds(1)); + + X509ExtensionCollection extensions = issued.Extensions; + Assert.Equal(7, extensions.Count); + + // Extensions are a SEQUENCE OF, not a SET OF, so the order the CA wrote them is the order they appear. + Assert.IsType(extensions[0]); + Assert.IsType(extensions[1]); + Assert.IsType(extensions[2]); + Assert.Equal("2.5.29.31", extensions[3].Oid.Value); + Assert.IsType(extensions[4]); + Assert.IsType(extensions[5]); + Assert.IsType(extensions[6]); + } + } + + static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCert) + { + CertificateRequest req = CertificateRequest.LoadCertificateRequest( + pkcs10, + HashAlgorithmName.SHA256, + out int bytesConsumed, + skipSignatureValidation: true, + signerSignaturePadding: RSASignaturePadding.Pss); + + // TODO: How to validate the Subject? + + AsnEncodedData? challengePassword = null; + + foreach (AsnEncodedData otherAttribute in req.OtherRequestAttributes) + { + if (otherAttribute?.Oid?.Value == "1.2.840.113549.1.9.7") + { + if (challengePassword is not null) + { + throw new InvalidOperationException("Two challenge passwords provided"); + } + + challengePassword = otherAttribute; + } + else + { + throw new InvalidOperationException( + $"Unsupported attribute provided: {otherAttribute.Oid?.Value ?? "(unknown)"}"); + } + } + + X509BasicConstraintsExtension? basicConstraints = null; + X509SubjectKeyIdentifierExtension? skid = null; + X509EnhancedKeyUsageExtension? eku = null; + X509KeyUsageExtension? ku = null; + X509Extension? san = null; + + foreach (X509Extension reqExt in req.CertificateExtensions) + { + if (reqExt is X509BasicConstraintsExtension bcLocal) + { + if (basicConstraints is not null) + { + throw new InvalidOperationException("Duplicate Basic Constraints Extension"); + } + + if (bcLocal.CertificateAuthority) + { + throw new InvalidOperationException("Not Authorized to create subordinate CA"); + } + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 + // + // CAs MUST NOT include the pathLenConstraint field unless the cA + // boolean is asserted + + if (bcLocal.HasPathLengthConstraint) + { + throw new InvalidOperationException("Invalid Basic Constraints Extension"); + } + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 + // + // This extension MAY appear as a critical or + // non-critical extension in end entity certificates. + + basicConstraints = bcLocal; + } + else if (reqExt is X509SubjectKeyIdentifierExtension skidLocal) + { + if (skid is not null) + { + throw new InvalidOperationException("Duplicate Subject Key Identifier Extension"); + } + + // Note: Another good way to handle this is to just ignore it and replace it. + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 + // + // Conforming CAs MUST mark this extension as non-critical. + skidLocal.Critical = false; + skid = skidLocal; + } + else if (reqExt is X509EnhancedKeyUsageExtension ekuLocal) + { + if (eku is not null) + { + throw new InvalidOperationException("Duplicate EKU Extension"); + } + + foreach (Oid requestedUsage in ekuLocal.EnhancedKeyUsages) + { + switch (requestedUsage.Value) + { + // tls-server + case "1.3.6.1.5.5.7.3.1": + // tls-client + case "1.3.6.1.5.5.7.3.2": + break; + default: + throw new InvalidOperationException( + $"Unauthorized EKU requested {requestedUsage.Value}"); + } + } + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12 + // + // This extension MAY, at the option of the certificate issuer, be + // either critical or non-critical. + // ... + // Conforming CAs + // SHOULD NOT mark this extension as critical if the anyExtendedKeyUsage + // KeyPurposeId is present. + eku = ekuLocal; + } + else if (reqExt is X509KeyUsageExtension kuLocal) + { + if (ku is not null) + { + throw new InvalidOperationException("Duplicate Key Usage Extension"); + } + + X509KeyUsageFlags requestedUsages = kuLocal.KeyUsages; + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 + // + // When the + // keyUsage extension appears in a certificate, at least one of the bits + // MUST be set to 1. + + if (requestedUsages == 0) + { + throw new InvalidOperationException("Key Usage contains no usages"); + } + + const X509KeyUsageFlags KeyAgreeRestrictions = + X509KeyUsageFlags.EncipherOnly | + X509KeyUsageFlags.DecipherOnly; + + const X509KeyUsageFlags PermittedFlags = + KeyAgreeRestrictions | + X509KeyUsageFlags.KeyAgreement | + X509KeyUsageFlags.DataEncipherment | + X509KeyUsageFlags.KeyEncipherment | + //Deprecated, discouraged to accept. + //X509KeyUsageFlags.NonRepudiation | + X509KeyUsageFlags.DigitalSignature; + + if ((requestedUsages & PermittedFlags) != requestedUsages) + { + throw new InvalidOperationException("Key Usage contains other than permitted flags"); + } + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 + // + // The meaning of the encipherOnly bit is undefined in the absence of + // the keyAgreement bit. + // ... + // The meaning of the decipherOnly bit is undefined in the absence of + // the keyAgreement bit. + + if ((requestedUsages & KeyAgreeRestrictions) != 0 && + (requestedUsages & X509KeyUsageFlags.KeyAgreement) == 0) + { + throw new InvalidOperationException("Key Usage contains invalid values"); + } + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 + // + // When present, conforming CAs + // SHOULD mark this extension as critical. + kuLocal.Critical = true; + ku = kuLocal; + } + else if (reqExt is X509SubjectAlternativeNameExtension sanLocal) + { + if (san is not null) + { + throw new InvalidOperationException("Duplicate Subject Alternative Name Extension"); + } + + // CAUTION: DO NOT ACCEPT THIS EXTENSION AS-IS, ALWAYS RE-ENCODE IT. + // + // This is because the .NET X509SubjectAlternativeNameExtension cannot + // describe certain kinds of requested alternative name. + // + // Instead, loop over the requested names to validate them and build a new extension. + // + // Unsupported name types are simply ignored. + + SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); + bool added = false; + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 + // + // Because the subject alternative name is considered to be definitively + // bound to the public key, all parts of the subject alternative name + // MUST be verified by the CA. + + foreach (string dnsName in sanLocal.EnumerateDnsNames()) + { + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 + // + // Unlike the subject field, conforming CAs MUST + // NOT issue certificates with subjectAltNames containing empty + // GeneralName fields. + + if (string.IsNullOrWhiteSpace(dnsName)) + { + throw new InvalidOperationException("Invalid Subject Alternative Name Extension"); + } + + // Sanity: .. is never allowed. + if (dnsName.Contains("..")) + { + throw new InvalidOperationException("Invalid Subject Alternative Name Extension"); + } + + string trimmed = dnsName.Trim(); + + // If you want to support requests using "fully-qualified DNS" and convert them to + // "normal" DNS: + if (trimmed.EndsWith('.')) + { + trimmed = trimmed.Substring(0, trimmed.Length - 1); + } + + // This is a standin for a contextual acceptance test. + // It mirrors an authorization for any subdomain of fruit.example, + // but not fruit.example itself. + if (!trimmed.EndsWith(".fruit.example")) + { + throw new InvalidOperationException($"Unauthorized requested DNS Name via SAN: {trimmed}"); + } + + // Sanity: Don't allow * anywhere after the first position. + if (trimmed.IndexOf('*', 1) > 0) + { + throw new InvalidOperationException("Invalid Subject Alternative Name Extension"); + } + + // Sanity: If the first position is '*' then the second is '.'. + if (trimmed.StartsWith('*') && trimmed[1] != '.') + { + throw new InvalidOperationException("Invalid Subject Alternative Name Extension"); + } + + // Sanity: Cannot start with '.' + if (trimmed.StartsWith('.')) + { + throw new InvalidOperationException("Invalid Subject Alternative Name Extension"); + } + + builder.AddDnsName(trimmed); + added = true; + } + + foreach (IPAddress ipAddr in sanLocal.EnumerateIPAddresses()) + { + // This policy represents accepting only values in 10.1.13.0/24, and no IPv6. + + if (ipAddr.AddressFamily != AddressFamily.InterNetwork) + { + throw new InvalidOperationException($"Unauthorized requested IP Address via SAN: {ipAddr}"); + } + + byte[] addr = ipAddr.GetAddressBytes(); + + if (addr[0] != 10 || addr[1] != 1 || addr[2] != 13) + { + throw new InvalidOperationException($"Unauthorized requested IP Address via SAN: {ipAddr}"); + } + + builder.AddIpAddress(ipAddr); + added = true; + } + + // For extra goodness, both the DnsName and IP Address values could/should be checked in a hash set + // (or whatever) to filter out duplicates. For DnsName values duplicates should be determined + // after IDNA normalization. + // + // Yeah, being a CA is hard. + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 + // + // If the subjectAltName extension is present, the sequence MUST contain + // at least one entry. + + if (!added) + { + throw new InvalidOperationException("SAN extension contained no supported addresses"); + } + + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 + // + // If the subject field + // contains an empty sequence, then the issuing CA MUST include a + // subjectAltName extension that is marked as critical.When including + // the subjectAltName extension in a certificate that has a non-empty + // subject distinguished name, conforming CAs SHOULD mark the + // subjectAltName extension as non-critical. + san = builder.Build(req.SubjectName.RawData.Length == 0); + } + else + { + // What else would the client request? + // + // Authority Key Identifier? That comes from you. + // Authority Information Access? Same. + // CRL Distribution Points? Same. + // Signed Certificate Timestamp? Seems pretty hard without your involvement. + // et cetera. + // + // Anything else in this bucket is just a bad client. + // + // You're free to ignore AKId/AIA/CDP as "I just requested everything I already had", + // but most unknown things should probably be rejected over ignored. + // + // (Unless you're taking after StartSSL and ignoring literally everything except the + // public key because you got all the SAN data and subject and such from elsewhere, + // in which case, rock on.) + + throw new InvalidOperationException( + $"Unsupported extension {reqExt.Oid?.Value ?? "(unknown)"} requested."); + } + } + + // Let's fill in what's missing. + skid ??= new X509SubjectKeyIdentifierExtension(req.PublicKey, false); + basicConstraints ??= X509BasicConstraintsExtension.CreateForEndEntity(); + + // Let's fill in CA responsibilities. + // For Authority Key Identifier, we'll chain to our Subject Key Identifier if we have one, + // or our Issuer+Serial if not. + bool weHaveSkid = issuerCert.Extensions["2.5.29.14"] is not null; + X509Extension akid = + X509AuthorityKeyIdentifierExtension.CreateFromCertificate(issuerCert, weHaveSkid, !weHaveSkid); + + X509Extension cdp = + CertificateRevocationListBuilder.BuildCrlDistributionPointExtension( + new[] { "http://issuer.ca.example/shard.crl" }); + + X509Extension aia = + new X509AuthorityInformationAccessExtension( + new[] { "http://ocsp.issuer.ca.example/ocsp/" }, + new[] { "http://issuer.ca.example/issuer.cer" }); + + // Because of the above validation we technically know we only need to remove the SAN extension, + // but let's just build clean, for safety. + req.CertificateExtensions.Clear(); + + // There may be a standard order these get written in. + // This order is mostly arbitrary, except the conditional ones went last. + req.CertificateExtensions.Add(skid); + req.CertificateExtensions.Add(basicConstraints); + req.CertificateExtensions.Add(akid); + req.CertificateExtensions.Add(cdp); + req.CertificateExtensions.Add(aia); + + if (eku is not null) + { + req.CertificateExtensions.Add(eku); + } + + if (ku is not null) + { + req.CertificateExtensions.Add(ku); + } + + if (san is not null) + { + req.CertificateExtensions.Add(san); + } + + return req; + } + + static byte[] BuildRequest(X500DistinguishedName reqName, string? challengePassword) + { + using (RSA key = RSA.Create()) + { + CertificateRequest req = new CertificateRequest( + reqName, + key, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true)); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("grapefruit.fruit.example"); + sanBuilder.AddDnsName("honeydew.fruit.example"); + sanBuilder.AddDnsName("honeydew.fruit.example"); + sanBuilder.AddIpAddress(IPAddress.Parse("10.1.13.5")); + sanBuilder.AddEmailAddress("email@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + if (!string.IsNullOrWhiteSpace(challengePassword)) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.WriteCharacterString(UniversalTagNumber.UTF8String, challengePassword.Trim()); + + req.OtherRequestAttributes.Add( + new AsnEncodedData("1.2.840.113549.1.9.7", writer.Encode())); + } + + return req.CreateSigningRequest(); + } + } + + static X509Certificate2 MakeCA() + { + using (RSA key = RSA.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=Issuing Authority", + key, + HashAlgorithmName.SHA384, + RSASignaturePadding.Pkcs1); + + req.CertificateExtensions.Add(X509BasicConstraintsExtension.CreateForCertificateAuthority()); + req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); + req.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true)); + + DateTimeOffset now = DateTimeOffset.UtcNow; + return req.CreateSelfSigned(now.AddHours(-1), now.AddHours(1)); + } + } + } + } +} From 79961fd69f439472f6524f2159bcbaaa49117602 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 6 Jun 2022 15:24:15 -0700 Subject: [PATCH 11/43] Support CRLs numbered to (practically) infinity... and beyond. --- .../ref/System.Security.Cryptography.cs | 16 +++++------ .../CertificateRevocationListBuilder.cs | 28 ++++++++----------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 90f617a991eb4..13e561351da46 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2453,16 +2453,16 @@ public void AddEntry(System.ReadOnlySpan serialNumber) { } public void AddEntry(System.ReadOnlySpan serialNumber, System.DateTimeOffset revocationTime) { } public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.DateTimeOffset revocationTime) { } - public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } - public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, int crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } - public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate) { throw null; } - public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, int crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate) { throw null; } public static System.Security.Cryptography.X509Certificates.X509Extension BuildCrlDistributionPointExtension(System.Collections.Generic.IEnumerable uris, bool critical = false) { throw null; } public void ExpireEntries(System.DateTimeOffset oldestRevocationTimeToKeep) { } - public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(byte[] currentCrl, out int currentCrlNumber) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(System.ReadOnlySpan currentCrl, out int currentCrlNumber, out int bytesConsumed) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(System.ReadOnlySpan currentCrl, out int currentCrlNumber) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(string currentCrl, out int currentCrlNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(byte[] currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(System.ReadOnlySpan currentCrl, out System.Numerics.BigInteger currentCrlNumber, out int bytesConsumed) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(System.ReadOnlySpan currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(string currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index e9cc4bb86bd46..a5678bf92d969 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Formats.Asn1; +using System.Numerics; using System.Security.Cryptography.Asn1; namespace System.Security.Cryptography.X509Certificates @@ -59,13 +60,13 @@ private CertificateRevocationListBuilder(List revoked) _revoked = revoked; } - public static CertificateRevocationListBuilder Load(byte[] currentCrl, out int currentCrlNumber) + public static CertificateRevocationListBuilder Load(byte[] currentCrl, out BigInteger currentCrlNumber) { ArgumentNullException.ThrowIfNull(currentCrl); CertificateRevocationListBuilder ret = Load( new ReadOnlySpan(currentCrl), - out int crlNumber, + out BigInteger crlNumber, out int bytesConsumed); if (bytesConsumed != currentCrl.Length) @@ -79,11 +80,11 @@ public static CertificateRevocationListBuilder Load(byte[] currentCrl, out int c public static CertificateRevocationListBuilder Load( ReadOnlySpan currentCrl, - out int currentCrlNumber, + out BigInteger currentCrlNumber, out int bytesConsumed) { List list = new(); - int crlNumber = 0; + BigInteger crlNumber = 0; int payloadLength; try @@ -155,12 +156,7 @@ public static CertificateRevocationListBuilder Load( extnValue, AsnEncodingRules.DER); - // The CRL Number is out of range for this method. - if (!crlNumberReader.TryReadInt32(out crlNumber)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - + crlNumber = crlNumberReader.ReadInteger(); crlNumberReader.ThrowIfNotEmpty(); break; @@ -195,14 +191,14 @@ public static CertificateRevocationListBuilder Load( return new CertificateRevocationListBuilder(list); } - public static CertificateRevocationListBuilder LoadPem(string currentCrl, out int currentCrlNumber) + public static CertificateRevocationListBuilder LoadPem(string currentCrl, out BigInteger currentCrlNumber) { ArgumentNullException.ThrowIfNull(currentCrl); return LoadPem(currentCrl.AsSpan(), out currentCrlNumber); } - public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan currentCrl, out int currentCrlNumber) + public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan currentCrl, out BigInteger currentCrlNumber) { ReadOnlySpan source = currentCrl; @@ -280,14 +276,14 @@ public void ExpireEntries(DateTimeOffset oldestRevocationTimeToKeep) _revoked.RemoveAll(rc => rc.RevocationTime < oldestRevocationTimeToKeep); } - public byte[] Build(X509Certificate2 issuerCertificate, int crlNumber, DateTimeOffset nextUpdate) + public byte[] Build(X509Certificate2 issuerCertificate, BigInteger crlNumber, DateTimeOffset nextUpdate) { return Build(issuerCertificate, crlNumber, nextUpdate, DateTimeOffset.UtcNow); } public byte[] Build( X509Certificate2 issuerCertificate, - int crlNumber, + BigInteger crlNumber, DateTimeOffset nextUpdate, DateTimeOffset thisUpdate) { @@ -365,7 +361,7 @@ public byte[] Build( public byte[] Build( X500DistinguishedName issuerName, X509SignatureGenerator generator, - int crlNumber, + BigInteger crlNumber, DateTimeOffset nextUpdate, X509AuthorityKeyIdentifierExtension akid) { @@ -375,7 +371,7 @@ public byte[] Build( public byte[] Build( X500DistinguishedName issuerName, X509SignatureGenerator generator, - int crlNumber, + BigInteger crlNumber, DateTimeOffset nextUpdate, DateTimeOffset thisUpdate, X509AuthorityKeyIdentifierExtension akid) From f62cdbf64a8de853018e7eb8b28d696eba08d4c6 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 6 Jun 2022 17:10:44 -0700 Subject: [PATCH 12/43] Respond to early review feedback --- .../System/Security/Cryptography/Helpers.cs | 63 +++++++++++++++++++ ...icateRevocationListBuilder.CdpExtension.cs | 6 +- .../CertificateRevocationListBuilder.cs | 14 ++--- .../X509Certificates/PublicKey.cs | 1 - .../SubjectAlternativeNameBuilder.cs | 10 ++- .../X509SubjectAlternativeNameExtension.cs | 5 ++ 6 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs index f2a4addbca6a5..91ba2a513e7e6 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs @@ -320,5 +320,68 @@ public static int GetPaddingSize(this SymmetricAlgorithm algorithm, CipherMode m { return (mode == CipherMode.CFB ? feedbackSizeInBits : algorithm.BlockSize) / 8; } + + internal static TReturn EncodeToResult(this AsnWriter writer, LocalReadOnlySpanFunc func) + { + const int MaxStackAlloc = 64; + + Span stackTemp = stackalloc byte[MaxStackAlloc]; + Span temp = stackTemp; + byte[]? rentTemp = null; + + if (writer.GetEncodedLength() > MaxStackAlloc) + { + rentTemp = CryptoPool.Rent(writer.GetEncodedLength()); + temp = rentTemp; + } + + int written = 0; + + try + { + written = writer.Encode(temp); + return func(temp.Slice(0, written)); + } + finally + { + if (rentTemp is not null) + { + CryptoPool.Return(rentTemp, written); + } + } + } + + internal static TReturn EncodeToResult(this AsnWriter writer, LocalReadOnlySpanFunc func, TArg1 arg1) + { + const int MaxStackAlloc = 64; + + Span stackTemp = stackalloc byte[MaxStackAlloc]; + Span temp = stackTemp; + byte[]? rentTemp = null; + + if (writer.GetEncodedLength() > MaxStackAlloc) + { + rentTemp = CryptoPool.Rent(writer.GetEncodedLength()); + temp = rentTemp; + } + + int written = 0; + + try + { + written = writer.Encode(temp); + return func(temp.Slice(0, written), arg1); + } + finally + { + if (rentTemp is not null) + { + CryptoPool.Return(rentTemp, written); + } + } + } + + internal delegate TReturn LocalReadOnlySpanFunc(ReadOnlySpan source); + internal delegate TReturn LocalReadOnlySpanFunc(ReadOnlySpan source, TArg1 arg1); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs index d66989f5477f4..7270978e4a1f5 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Formats.Asn1; +using Internal.Cryptography; namespace System.Security.Cryptography.X509Certificates { @@ -63,7 +64,10 @@ public static X509Extension BuildCrlDistributionPointExtension(IEnumerable new X509Extension(Oids.CrlDistributionPoints, span, crit), + critical); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index a5678bf92d969..2dc8362a293f1 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -51,7 +51,7 @@ internal RevokedCertificate(ref AsnValueReader reader, int version) public CertificateRevocationListBuilder() { - _revoked = new(); + _revoked = new List(); } private CertificateRevocationListBuilder(List revoked) @@ -200,15 +200,13 @@ public static CertificateRevocationListBuilder LoadPem(string currentCrl, out Bi public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan currentCrl, out BigInteger currentCrlNumber) { - ReadOnlySpan source = currentCrl; - - while (PemEncoding.TryFind(source, out PemFields fields)) + foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(currentCrl)) { - if (source[fields.Label].SequenceEqual("CRL")) + if (contents[fields.Label].SequenceEqual("CRL")) { byte[] rented = ArrayPool.Shared.Rent(fields.DecodedDataLength); - if (!Convert.TryFromBase64Chars(source[fields.Base64Data], rented, out int bytesWritten)) + if (!Convert.TryFromBase64Chars(contents[fields.Base64Data], rented, out int bytesWritten)) { Debug.Fail("Base64Decode failed, but PemEncoding said it was legal"); throw new UnreachableException(); @@ -223,8 +221,6 @@ public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan curren ArrayPool.Shared.Return(rented); return ret; } - - source = source.Slice(fields.Location.End.GetOffset(source.Length)); } throw new CryptographicException("No PEM-encoded CRL was found"); @@ -396,6 +392,7 @@ public byte[] Build( byte[] signatureAlgId = generator.GetSignatureAlgorithmIdentifier(hashAlgorithm); AsnWriter writer = (_writer ??= new AsnWriter(AsnEncodingRules.DER)); + writer.Reset(); // TBSCertList using (writer.PushSequence()) @@ -488,7 +485,6 @@ public byte[] Build( } byte[] crl = writer.Encode(); - writer.Reset(); return crl; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs index 2befa008777e9..726e87a6697de 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs @@ -312,7 +312,6 @@ internal static PublicKey DecodeSubjectPublicKeyInfo(ref SubjectPublicKeyInfoAsn out AsnEncodedData parameters, out AsnEncodedData keyValue); - return new PublicKey(oid, keyValue, parameters); } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs index 25d463df011e4..97b725f358e9e 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/SubjectAlternativeNameBuilder.cs @@ -7,6 +7,7 @@ using System.Net; using System.Security.Cryptography.Asn1; using System.Text; +using Internal.Cryptography; namespace System.Security.Cryptography.X509Certificates { @@ -56,7 +57,6 @@ public void AddUserPrincipalName(string upn) _writer.Reset(); _writer.WriteCharacterString(UniversalTagNumber.UTF8String, upn); byte[] otherNameValue = _writer.Encode(); - _writer.Reset(); OtherNameAsn otherName = new OtherNameAsn { @@ -79,10 +79,9 @@ public X509Extension Build(bool critical = false) } } - byte[] encoded = _writer.Encode(); - _writer.Reset(); - - return new X509Extension(Oids.SubjectAltName, encoded, critical); + return _writer.EncodeToResult( + static (span, crit) => new X509Extension(Oids.SubjectAltName, span, crit), + critical); } private void AddGeneralName(GeneralNameAsn generalName) @@ -93,7 +92,6 @@ private void AddGeneralName(GeneralNameAsn generalName) _writer.Reset(); generalName.Encode(_writer); _encodedNames.Add(_writer.Encode()); - _writer.Reset(); } catch (EncoderFallbackException) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs index 98fb355deef02..3e4d6a993c484 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs @@ -163,6 +163,11 @@ private static IEnumerable EnumerateIPAddresses(List { yield return new IPAddress(value); } + else + { + throw new CryptographicException( + "Subject Alternative Name has an IP Address that is neither IPv4 nor IPv6"); + } } } } From ff72707d4da05bbe5558ce83ebc3decfa9b388a1 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 6 Jun 2022 17:23:27 -0700 Subject: [PATCH 13/43] Words matter, apparently --- .../X509Certificates/CertificateRevocationListBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 2dc8362a293f1..0f16a80855ee0 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -202,7 +202,7 @@ public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan curren { foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(currentCrl)) { - if (contents[fields.Label].SequenceEqual("CRL")) + if (contents[fields.Label].SequenceEqual("X509 CRL")) { byte[] rented = ArrayPool.Shared.Rent(fields.DecodedDataLength); From 0569f636c76a43ec0f5f7517f388ffb92988c381 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 6 Jun 2022 17:56:02 -0700 Subject: [PATCH 14/43] Early draft iterator for X.500 name AttributeTypeAndValue values --- .../Cryptography/Asn1Reader/AsnValueReader.cs | 3 +- .../tests/CertificateCreation/DontBeACA.cs | 67 ++++++++++++++-- .../ref/System.Security.Cryptography.cs | 1 + .../src/System.Security.Cryptography.csproj | 4 +- .../X500DictionaryStringHelper.cs | 27 +++++++ .../X509Certificates/X500DistinguishedName.cs | 80 ++++++++++++++++++- 6 files changed, 172 insertions(+), 10 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs b/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs index 67e9d869a68db..8c23228315df7 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1Reader/AsnValueReader.cs @@ -173,7 +173,7 @@ internal AsnValueReader ReadSequence(Asn1Tag? expectedTag = default) return new AsnValueReader(content, _ruleSet); } - internal AsnValueReader ReadSetOf(Asn1Tag? expectedTag = default) + internal AsnValueReader ReadSetOf(Asn1Tag? expectedTag = default, bool skipSortOrderValidation = false) { AsnDecoder.ReadSetOf( _span, @@ -181,6 +181,7 @@ internal AsnValueReader ReadSetOf(Asn1Tag? expectedTag = default) out int contentOffset, out int contentLength, out int bytesConsumed, + skipSortOrderValidation: skipSortOrderValidation, expectedTag: expectedTag); ReadOnlySpan content = _span.Slice(contentOffset, contentLength); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs index e4374f29e1253..d686215d0d41e 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -1,6 +1,7 @@ // Licensed to the.NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Formats.Asn1; using System.Net; using System.Net.Sockets; @@ -58,8 +59,6 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe skipSignatureValidation: true, signerSignaturePadding: RSASignaturePadding.Pss); - // TODO: How to validate the Subject? - AsnEncodedData? challengePassword = null; foreach (AsnEncodedData otherAttribute in req.OtherRequestAttributes) @@ -396,9 +395,67 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe new[] { "http://ocsp.issuer.ca.example/ocsp/" }, new[] { "http://issuer.ca.example/issuer.cer" }); - // Because of the above validation we technically know we only need to remove the SAN extension, - // but let's just build clean, for safety. - req.CertificateExtensions.Clear(); + + bool acceptSubject = true; + string? cn = null; + + if (san is not null) + { + if (req.SubjectName.RawData.Length == 2) + { + throw new InvalidOperationException("A name is required when no SAN extension is present"); + } + } + else + { + foreach ((Oid typeId, string value) in req.SubjectName.EnumerateSimpleAttributes()) + { + switch (typeId.Value) + { + case "2.5.4.3": + if (cn is not null) + { + throw new InvalidOperationException("CN was specified more than once"); + } + + if (value.Length == 0 || value.IndexOfAny(new[] { ' ', '*' }) > -1 || + !value.EndsWith(".fruit.example")) + { + throw new InvalidOperationException("CN is unauthorized"); + } + + cn = value; + break; + default: + acceptSubject = false; + break; + } + } + } + + if (!acceptSubject) + { + if (cn is null) + { + throw new InvalidOperationException("No CN provided"); + } + + // Rewrite the subject name. + X500DistinguishedNameBuilder nameBuilder = new X500DistinguishedNameBuilder(); + nameBuilder.AddCommonName(cn); + + req = new CertificateRequest( + nameBuilder.Build(), + req.PublicKey, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pss); + } + else + { + // Because of the above validation we technically know we only need to remove the SAN extension, + // but let's just build clean, for safety. + req.CertificateExtensions.Clear(); + } // There may be a standard order these get written in. // This order is mostly arbitrary, except the conditional ones went last. diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 13e561351da46..2f8c5b1cae37a 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2552,6 +2552,7 @@ public X500DistinguishedName(string distinguishedName) { } public X500DistinguishedName(string distinguishedName, System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags flag) { } public string Name { get { throw null; } } public string Decode(System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags flag) { throw null; } + public System.Collections.Generic.IEnumerable<(System.Security.Cryptography.Oid AttributeType, string Value)> EnumerateSimpleAttributes(bool reversed = true) { throw null; } public override string Format(bool multiLine) { throw null; } } public sealed partial class X500DistinguishedNameBuilder diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index c88c3b663af7c..de3f9bc10c64c 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -443,6 +443,7 @@ + @@ -850,7 +851,6 @@ - @@ -1042,7 +1042,6 @@ - @@ -1207,7 +1206,6 @@ - diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DictionaryStringHelper.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DictionaryStringHelper.cs index 4edef81da00f2..2ef7a8cb4426b 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DictionaryStringHelper.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DictionaryStringHelper.cs @@ -33,5 +33,32 @@ internal static string ReadAnyAsnString(this AsnReader tavReader) throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } } + + internal static string ReadAnyAsnString(ref this AsnValueReader tavReader) + { + Asn1Tag tag = tavReader.PeekTag(); + + if (tag.TagClass != TagClass.Universal) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + switch ((UniversalTagNumber)tag.TagValue) + { + case UniversalTagNumber.BMPString: + case UniversalTagNumber.IA5String: + case UniversalTagNumber.NumericString: + case UniversalTagNumber.PrintableString: + case UniversalTagNumber.UTF8String: + case UniversalTagNumber.T61String: + // .NET's string comparisons start by checking the length, so a trailing + // NULL character which was literally embedded in the DER would cause a + // failure in .NET whereas it wouldn't have with strcmp. + return tavReader.ReadCharacterString((UniversalTagNumber)tag.TagValue).TrimEnd('\0'); + + default: + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs index 1da4b68c8fa37..cc61b84820167 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs @@ -1,10 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Formats.Asn1; + namespace System.Security.Cryptography.X509Certificates { public sealed class X500DistinguishedName : AsnEncodedData { + private volatile string? _lazyDistinguishedName; + private List<(Oid, string)>? _parsedAttributes; + public X500DistinguishedName(byte[] encodedDistinguishedName) : base(new Oid(null, null), encodedDistinguishedName) { @@ -69,6 +75,28 @@ public override string Format(bool multiLine) return X509Pal.Instance.X500DistinguishedNameFormat(RawData, multiLine); } + /// + /// Enumerates over the X500DistinguishedName, showing the attribute type identifier and attribute value + /// at each step in the enumeration. + /// + /// + /// to enumerate in the order used by ; + /// to enumerate in the declared order. + /// + /// + /// An enumerator that iterates over the attributes in the X.500 Dinstinguished Name. + /// + /// + /// The X.500 Name is not a proper DER-encoded X.500 Name value, or the X.500 Name contains + /// multiple-value Relative Distinguished Names. + /// + public IEnumerable<(Oid AttributeType, string Value)> EnumerateSimpleAttributes(bool reversed = true) + { + List<(Oid, string)> parsedAttributes = _parsedAttributes ??= ParseAttributes(RawData); + + return EnumerateParsedAttributes(parsedAttributes, reversed); + } + private static byte[] Encode(string distinguishedName, X500DistinguishedNameFlags flags) { ArgumentNullException.ThrowIfNull(distinguishedName); @@ -87,6 +115,56 @@ private static void ThrowIfInvalid(X500DistinguishedNameFlags flags) throw new ArgumentException(SR.Format(SR.Arg_EnumIllegalVal, "flag")); } - private volatile string? _lazyDistinguishedName; + private static IEnumerable<(Oid AttributeType, string AttributeValue)> EnumerateParsedAttributes( + List<(Oid, string)> parsedAttributes, + bool reversed) + { + if (reversed) + { + for (int i = parsedAttributes.Count - 1; i >= 0; i--) + { + yield return parsedAttributes[i]; + } + } + else + { + for (int i = 0; i < parsedAttributes.Count; i++) + { + yield return parsedAttributes[i]; + } + } + } + + private static List<(Oid, string)> ParseAttributes(byte[] rawData) + { + List<(Oid, string)>? parsedAttributes = null; + + try + { + AsnValueReader outer = new AsnValueReader(rawData, AsnEncodingRules.DER); + AsnValueReader sequence = outer.ReadSequence(); + outer.ThrowIfNotEmpty(); + + while (sequence.HasData) + { + // If the set has multiple values we're going to throw, so don't bother checking that they're sorted. + AsnValueReader set = sequence.ReadSetOf(skipSortOrderValidation: true); + AsnValueReader typeAndValue = set.ReadSequence(); + set.ThrowIfNotEmpty(); + + string type = typeAndValue.ReadObjectIdentifier(); + string value = typeAndValue.ReadAnyAsnString(); + typeAndValue.ThrowIfNotEmpty(); + + (parsedAttributes ??= new List<(Oid, string)>()).Add((new Oid(type, null), value)); + } + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + + return parsedAttributes ?? new List<(Oid, string)>(); + } } } From 0c0123e9b606b33f790f7f0606e4865d21ee08ee Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Tue, 7 Jun 2022 18:22:02 -0700 Subject: [PATCH 15/43] Start moving MatchesTlsHostname to X509Certificate2 --- .../Security/Cryptography/Oids.Shared.cs | 152 ++++++++---- .../tests/CertificateCreation/DontBeACA.cs | 16 +- .../ref/System.Security.Cryptography.cs | 11 +- .../X509Certificates/X500DistinguishedName.cs | 89 +++++-- .../X509Certificates/X509Certificate2.cs | 230 ++++++++++++++++++ 5 files changed, 425 insertions(+), 73 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs index 2fc4276104754..2dec336df8f58 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs @@ -2,55 +2,58 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Security.Cryptography; +using System.Formats.Asn1; namespace System.Security.Cryptography { internal static partial class Oids { - private static volatile Oid? _rsaOid; - private static volatile Oid? _ecPublicKeyOid; - private static volatile Oid? _tripleDesCbcOid; - private static volatile Oid? _aes256CbcOid; - private static volatile Oid? _secp256r1Oid; - private static volatile Oid? _secp384r1Oid; - private static volatile Oid? _secp521r1Oid; - private static volatile Oid? _sha256Oid; - private static volatile Oid? _pkcs7DataOid; - private static volatile Oid? _contentTypeOid; - private static volatile Oid? _documentDescriptionOid; - private static volatile Oid? _documentNameOid; - private static volatile Oid? _localKeyIdOid; - private static volatile Oid? _messageDigestOid; - private static volatile Oid? _signingTimeOid; - private static volatile Oid? _pkcs9ExtensionRequestOid; - private static volatile Oid? _basicConstraints2Oid; - private static volatile Oid? _enhancedKeyUsageOid; - private static volatile Oid? _keyUsageOid; - private static volatile Oid? _subjectKeyIdentifierOid; - - internal static Oid RsaOid => _rsaOid ??= InitializeOid(Rsa); - internal static Oid EcPublicKeyOid => _ecPublicKeyOid ??= InitializeOid(EcPublicKey); - internal static Oid TripleDesCbcOid => _tripleDesCbcOid ??= InitializeOid(TripleDesCbc); - internal static Oid Aes256CbcOid => _aes256CbcOid ??= InitializeOid(Aes256Cbc); - internal static Oid secp256r1Oid => _secp256r1Oid ??= new Oid(secp256r1, nameof(ECCurve.NamedCurves.nistP256)); - internal static Oid secp384r1Oid => _secp384r1Oid ??= new Oid(secp384r1, nameof(ECCurve.NamedCurves.nistP384)); - internal static Oid secp521r1Oid => _secp521r1Oid ??= new Oid(secp521r1, nameof(ECCurve.NamedCurves.nistP521)); - internal static Oid Sha256Oid => _sha256Oid ??= InitializeOid(Sha256); - - internal static Oid Pkcs7DataOid => _pkcs7DataOid ??= InitializeOid(Pkcs7Data); - internal static Oid ContentTypeOid => _contentTypeOid ??= InitializeOid(ContentType); - internal static Oid DocumentDescriptionOid => _documentDescriptionOid ??= InitializeOid(DocumentDescription); - internal static Oid DocumentNameOid => _documentNameOid ??= InitializeOid(DocumentName); - internal static Oid LocalKeyIdOid => _localKeyIdOid ??= InitializeOid(LocalKeyId); - internal static Oid MessageDigestOid => _messageDigestOid ??= InitializeOid(MessageDigest); - internal static Oid SigningTimeOid => _signingTimeOid ??= InitializeOid(SigningTime); - internal static Oid Pkcs9ExtensionRequestOid => _pkcs9ExtensionRequestOid ??= InitializeOid(Pkcs9ExtensionRequest); - - internal static Oid BasicConstraints2Oid => _basicConstraints2Oid ??= InitializeOid(BasicConstraints2); - internal static Oid EnhancedKeyUsageOid => _enhancedKeyUsageOid ??= InitializeOid(EnhancedKeyUsage); - internal static Oid KeyUsageOid => _keyUsageOid ??= InitializeOid(KeyUsage); - internal static Oid SubjectKeyIdentifierOid => _subjectKeyIdentifierOid ??= InitializeOid(SubjectKeyIdentifier); + private static volatile Oid? s_rsaOid; + private static volatile Oid? s_ecPublicKeyOid; + private static volatile Oid? s_tripleDesCbcOid; + private static volatile Oid? s_aes256CbcOid; + private static volatile Oid? s_secp256r1Oid; + private static volatile Oid? s_secp384r1Oid; + private static volatile Oid? s_secp521r1Oid; + private static volatile Oid? s_sha256Oid; + private static volatile Oid? s_pkcs7DataOid; + private static volatile Oid? s_contentTypeOid; + private static volatile Oid? s_documentDescriptionOid; + private static volatile Oid? s_documentNameOid; + private static volatile Oid? s_localKeyIdOid; + private static volatile Oid? s_messageDigestOid; + private static volatile Oid? s_signingTimeOid; + private static volatile Oid? s_pkcs9ExtensionRequestOid; + private static volatile Oid? s_basicConstraints2Oid; + private static volatile Oid? s_enhancedKeyUsageOid; + private static volatile Oid? s_keyUsageOid; + private static volatile Oid? s_subjectKeyIdentifierOid; + private static volatile Oid? s_commonNameOid; + + internal static Oid RsaOid => s_rsaOid ??= InitializeOid(Rsa); + internal static Oid EcPublicKeyOid => s_ecPublicKeyOid ??= InitializeOid(EcPublicKey); + internal static Oid TripleDesCbcOid => s_tripleDesCbcOid ??= InitializeOid(TripleDesCbc); + internal static Oid Aes256CbcOid => s_aes256CbcOid ??= InitializeOid(Aes256Cbc); + internal static Oid secp256r1Oid => s_secp256r1Oid ??= new Oid(secp256r1, nameof(ECCurve.NamedCurves.nistP256)); + internal static Oid secp384r1Oid => s_secp384r1Oid ??= new Oid(secp384r1, nameof(ECCurve.NamedCurves.nistP384)); + internal static Oid secp521r1Oid => s_secp521r1Oid ??= new Oid(secp521r1, nameof(ECCurve.NamedCurves.nistP521)); + internal static Oid Sha256Oid => s_sha256Oid ??= InitializeOid(Sha256); + + internal static Oid Pkcs7DataOid => s_pkcs7DataOid ??= InitializeOid(Pkcs7Data); + internal static Oid ContentTypeOid => s_contentTypeOid ??= InitializeOid(ContentType); + internal static Oid DocumentDescriptionOid => s_documentDescriptionOid ??= InitializeOid(DocumentDescription); + internal static Oid DocumentNameOid => s_documentNameOid ??= InitializeOid(DocumentName); + internal static Oid LocalKeyIdOid => s_localKeyIdOid ??= InitializeOid(LocalKeyId); + internal static Oid MessageDigestOid => s_messageDigestOid ??= InitializeOid(MessageDigest); + internal static Oid SigningTimeOid => s_signingTimeOid ??= InitializeOid(SigningTime); + internal static Oid Pkcs9ExtensionRequestOid => s_pkcs9ExtensionRequestOid ??= InitializeOid(Pkcs9ExtensionRequest); + + internal static Oid BasicConstraints2Oid => s_basicConstraints2Oid ??= InitializeOid(BasicConstraints2); + internal static Oid EnhancedKeyUsageOid => s_enhancedKeyUsageOid ??= InitializeOid(EnhancedKeyUsage); + internal static Oid KeyUsageOid => s_keyUsageOid ??= InitializeOid(KeyUsage); + internal static Oid SubjectKeyIdentifierOid => s_subjectKeyIdentifierOid ??= InitializeOid(SubjectKeyIdentifier); + + internal static Oid CommonNameOid => s_commonNameOid ??= InitializeOid(CommonName); private static Oid InitializeOid(string oidValue) { @@ -65,5 +68,66 @@ private static Oid InitializeOid(string oidValue) return oid; } + internal static Oid GetSharedOrNewOid(ref AsnValueReader asnValueReader) + { + Oid? ret = GetSharedOrNullOid(ref asnValueReader); + + if (ret is not null) + { + return ret; + } + + string oidValue = asnValueReader.ReadObjectIdentifier(); + return new Oid(oidValue, null); + } + + internal static Oid? GetSharedOrNullOid(ref AsnValueReader asnValueReader) + { + Asn1Tag tag = asnValueReader.PeekTag(); + + // Any of these cases are going to be an invalid OID. We'll let that get thrown naturally, the same as if + // it wasn't a match. + if (!tag.IsConstructed && + (tag.TagClass != TagClass.Universal || tag.TagValue == (int)UniversalTagNumber.ObjectIdentifier)) + { + ReadOnlySpan contentBytes = asnValueReader.PeekContentBytes(); + Oid? ret = null; + + switch (contentBytes.Length) + { + case 3: + switch (contentBytes[0]) + { + case 0x55: + switch (contentBytes[1]) + { + case 0x04: + switch (contentBytes[2]) + { + case 0x03: + ret = CommonNameOid; + break; + } + + break; + } + + break; + } + + break; + } + + if (ret is not null) + { + // Move to the next item. + asnValueReader.ReadEncodedValue(); + } + + return ret; + } + + return null; + } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs index d686215d0d41e..aa3f62e6e7635 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -408,9 +408,15 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe } else { - foreach ((Oid typeId, string value) in req.SubjectName.EnumerateSimpleAttributes()) + foreach (X500DistinguishedName.RelativeDistinguishedName rdn in + req.SubjectName.EnumerateRelativeDistinguishedNames()) { - switch (typeId.Value) + if (rdn.HasMultipleValues) + { + throw new InvalidOperationException("Multi-value RDNs are not accepted"); + } + + switch (rdn.SingleValueType.Value) { case "2.5.4.3": if (cn is not null) @@ -418,13 +424,13 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe throw new InvalidOperationException("CN was specified more than once"); } - if (value.Length == 0 || value.IndexOfAny(new[] { ' ', '*' }) > -1 || - !value.EndsWith(".fruit.example")) + if (rdn.SingleValueValue.Length == 0 || rdn.SingleValueValue.IndexOfAny(new[] { ' ', '*' }) > -1 || + !rdn.SingleValueValue.EndsWith(".fruit.example")) { throw new InvalidOperationException("CN is unauthorized"); } - cn = value; + cn = rdn.SingleValueValue; break; default: acceptSubject = false; diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 2f8c5b1cae37a..9683aa8683a3c 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2552,8 +2552,16 @@ public X500DistinguishedName(string distinguishedName) { } public X500DistinguishedName(string distinguishedName, System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags flag) { } public string Name { get { throw null; } } public string Decode(System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags flag) { throw null; } - public System.Collections.Generic.IEnumerable<(System.Security.Cryptography.Oid AttributeType, string Value)> EnumerateSimpleAttributes(bool reversed = true) { throw null; } + public System.Collections.Generic.IEnumerable EnumerateRelativeDistinguishedNames(bool reversed = true) { throw null; } public override string Format(bool multiLine) { throw null; } + public sealed partial class RelativeDistinguishedName + { + internal RelativeDistinguishedName() { } + public bool HasMultipleValues { get { throw null; } } + public System.ReadOnlyMemory RawData { get { throw null; } } + public System.Security.Cryptography.Oid? SingleValueType { get { throw null; } } + public string? SingleValueValue { get { throw null; } } + } } public sealed partial class X500DistinguishedNameBuilder { @@ -2812,6 +2820,7 @@ public override void Import(string fileName) { } public override void Import(string fileName, System.Security.SecureString? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags) { } [System.ObsoleteAttribute("X509Certificate and X509Certificate2 are immutable. Use the appropriate constructor to create a new certificate.", DiagnosticId="SYSLIB0026", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public override void Import(string fileName, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags) { } + public bool MatchesTlsHostname(string hostname, bool allowWildcards = true, bool allowCommonName = true) { throw null; } public override void Reset() { } public override string ToString() { throw null; } public override string ToString(bool verbose) { throw null; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs index cc61b84820167..59bef117fdf58 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Formats.Asn1; namespace System.Security.Cryptography.X509Certificates @@ -9,7 +10,7 @@ namespace System.Security.Cryptography.X509Certificates public sealed class X500DistinguishedName : AsnEncodedData { private volatile string? _lazyDistinguishedName; - private List<(Oid, string)>? _parsedAttributes; + private List? _parsedAttributes; public X500DistinguishedName(byte[] encodedDistinguishedName) : base(new Oid(null, null), encodedDistinguishedName) @@ -76,25 +77,23 @@ public override string Format(bool multiLine) } /// - /// Enumerates over the X500DistinguishedName, showing the attribute type identifier and attribute value - /// at each step in the enumeration. + /// Iterates over the RelativeDistinguishedName values within this distinguished name value. /// /// /// to enumerate in the order used by ; /// to enumerate in the declared order. /// /// - /// An enumerator that iterates over the attributes in the X.500 Dinstinguished Name. + /// An enumerator that iterates over the relative distinguished names in the X.500 Dinstinguished Name. /// /// - /// The X.500 Name is not a proper DER-encoded X.500 Name value, or the X.500 Name contains - /// multiple-value Relative Distinguished Names. + /// The X.500 Name is not a proper DER-encoded X.500 Name value. /// - public IEnumerable<(Oid AttributeType, string Value)> EnumerateSimpleAttributes(bool reversed = true) + public IEnumerable EnumerateRelativeDistinguishedNames(bool reversed = true) { - List<(Oid, string)> parsedAttributes = _parsedAttributes ??= ParseAttributes(RawData); + List parsedAttributes = _parsedAttributes ??= ParseAttributes(RawData); - return EnumerateParsedAttributes(parsedAttributes, reversed); + return EnumerateRelativeDistinguishedNames(parsedAttributes, reversed); } private static byte[] Encode(string distinguishedName, X500DistinguishedNameFlags flags) @@ -115,8 +114,8 @@ private static void ThrowIfInvalid(X500DistinguishedNameFlags flags) throw new ArgumentException(SR.Format(SR.Arg_EnumIllegalVal, "flag")); } - private static IEnumerable<(Oid AttributeType, string AttributeValue)> EnumerateParsedAttributes( - List<(Oid, string)> parsedAttributes, + private static IEnumerable EnumerateRelativeDistinguishedNames( + List parsedAttributes, bool reversed) { if (reversed) @@ -135,28 +134,31 @@ private static void ThrowIfInvalid(X500DistinguishedNameFlags flags) } } - private static List<(Oid, string)> ParseAttributes(byte[] rawData) + private static List ParseAttributes(byte[] rawData) { - List<(Oid, string)>? parsedAttributes = null; + List? parsedAttributes = null; + ReadOnlyMemory rawDataMemory = rawData; + ReadOnlySpan rawDataSpan = rawData; try { - AsnValueReader outer = new AsnValueReader(rawData, AsnEncodingRules.DER); + AsnValueReader outer = new AsnValueReader(rawDataSpan, AsnEncodingRules.DER); AsnValueReader sequence = outer.ReadSequence(); outer.ThrowIfNotEmpty(); while (sequence.HasData) { - // If the set has multiple values we're going to throw, so don't bother checking that they're sorted. - AsnValueReader set = sequence.ReadSetOf(skipSortOrderValidation: true); - AsnValueReader typeAndValue = set.ReadSequence(); - set.ThrowIfNotEmpty(); + ReadOnlySpan encodedValue = sequence.PeekEncodedValue(); - string type = typeAndValue.ReadObjectIdentifier(); - string value = typeAndValue.ReadAnyAsnString(); - typeAndValue.ThrowIfNotEmpty(); + if (!rawDataSpan.Overlaps(encodedValue, out int offset)) + { + Debug.Fail("AsnValueReader produced a span outside of the original bounds"); + throw new UnreachableException(); + } - (parsedAttributes ??= new List<(Oid, string)>()).Add((new Oid(type, null), value)); + var rdn = new RelativeDistinguishedName(rawDataMemory.Slice(offset, encodedValue.Length)); + sequence.ReadEncodedValue(); + (parsedAttributes ??= new List()).Add(rdn); } } catch (AsnContentException e) @@ -164,7 +166,48 @@ private static void ThrowIfInvalid(X500DistinguishedNameFlags flags) throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } - return parsedAttributes ?? new List<(Oid, string)>(); + return parsedAttributes ?? new List(); + } + + public sealed class RelativeDistinguishedName + { + public ReadOnlyMemory RawData { get; } + public bool HasMultipleValues { get; } + public Oid? SingleValueType { get; } + public string? SingleValueValue { get; } + + internal RelativeDistinguishedName(ReadOnlyMemory rawData) + { + RawData = rawData; + + AsnValueReader outer = new AsnValueReader(rawData.Span, AsnEncodingRules.DER); + + // Windows does not enforce the sort order on multi-value RDNs. + AsnValueReader rdn = outer.ReadSetOf(skipSortOrderValidation: true); + AsnValueReader typeAndValue = rdn.ReadSequence(); + + Oid firstType = Oids.GetSharedOrNewOid(ref typeAndValue); + string firstValue = typeAndValue.ReadAnyAsnString(); + typeAndValue.ThrowIfNotEmpty(); + + if (rdn.HasData) + { + HasMultipleValues = true; + + while (rdn.HasData) + { + typeAndValue = rdn.ReadSequence(); + Oids.GetSharedOrNewOid(ref typeAndValue); + typeAndValue.ReadAnyAsnString(); + typeAndValue.ThrowIfNotEmpty(); + } + } + else + { + SingleValueType = firstType; + SingleValueValue = firstValue; + } + } } } } 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 2d5916bfac315..5a0b6c5dcf759 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 @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Formats.Asn1; using System.IO; +using System.Net; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security.Cryptography.X509Certificates.Asn1; @@ -1169,6 +1170,235 @@ public bool TryExportCertificatePem(Span destination, out int charsWritten return PemEncoding.TryWrite(PemLabels.X509Certificate, RawDataMemory.Span, destination, out charsWritten); } + /// + /// Checks to see if the certificate matches the provided hostname. + /// + /// The host name to match against. + /// + /// to allow wildcard matching for dNSName values in the + /// Subject Alternative Name extension; otherwise, . + /// + /// + /// to allow matching against the subject Common Name value; + /// otherwise, . + /// + /// + /// if the certificate is a match for the requested hostname; + /// otherwise, + /// + /// + /// + /// This method is a platform neutral implementation of IETF RFC 6125 host matching logic. + /// The SslStream class uses the hostname validator from the operating system, which may + /// result in different values from this implementation. + /// + /// + /// The logical flow of this method is: + /// + /// + /// If the hostname parses as an then IPAddress matching is done; + /// otherwise, DNS Name matching is done. + /// + /// + /// For IPAddress matching, the value must be an exact match against an iPAddress value in an + /// entry of the Subject Alternative Name extension. + /// + /// + /// For DNS Name matching, the value must be an exact match against a dNSName value in an + /// entry of the Subject Alternative Name extension, or a wildcard match against the same. + /// + /// + /// For wildcard matching, the wildcard must be the first character in the dNSName entry, + /// the second character must be a period (.), and the entry must have a length greater than two. + /// The wildcard will only match the value up to the first period (.), + /// remaining characters must be an exact match. + /// + /// + /// If there is no Subject Alternative Name extension, or the extension does not have any entries + /// of the appropriate type, then Common Name matching is used as a fallback. + /// + /// + /// For Common Name matching, if the Subject Name contains a single Common Name, and that attribute + /// is not defined as part of a multi-valued Relative Distinguished Name, then the hostname is matched + /// against the Common Name attribute's value. + /// Note that wildcards are not used in Common Name matching. + /// + /// + /// + /// + /// This method does not convert non-ASCII hostnames to the IDNA representation. For Unicode domains, + /// the caller must make use of or an equivalent IDNA mapper. + /// + /// + /// The "exact" matches performed by this routine are , + /// as domain names are not case-sensitive. + /// + /// + /// This method does not determine if the hostname is authorized by a trusted authority. A trust + /// decision cannot be made without additionally checking for trust via . + /// + /// + /// This method does not check that the certificate has an id-kp-serverAuth (1.3.6.1.5.5.7.3.1) + /// extended key usage. + /// + /// + public bool MatchesTlsHostname(string hostname, bool allowWildcards = true, bool allowCommonName = true) + { + ArgumentNullException.ThrowIfNull(hostname); + + if (hostname.Length == 0) + { + return false; + } + + X509Extension? rawSAN = null; + + foreach (X509Extension extension in Pal.Extensions) + { + if (extension.Oid!.Value == Oids.CommonName) + { + if (rawSAN is null) + { + rawSAN = extension; + } + else + { + throw new CryptographicException("Multiple Subject Alternative Name extensions present"); + } + } + } + + if (rawSAN is not null) + { + var san = new X509SubjectAlternativeNameExtension(); + san.CopyFrom(rawSAN); + + bool hadAny = false; + + if (IPAddress.TryParse(hostname, out IPAddress? ipAddress)) + { + foreach (IPAddress sanEntry in san.EnumerateIPAddresses()) + { + if (sanEntry.Equals(ipAddress)) + { + return true; + } + + hadAny = true; + } + } + else + { + ReadOnlySpan match = hostname; + + // Treat "something.example.org." as "something.example.org" + if (hostname.EndsWith('.')) + { + match = match.Slice(0, match.Length - 1); + + if (match.IsEmpty) + { + return false; + } + } + + ReadOnlySpan afterFirstDot = default; + int firstDot = match.IndexOf('.'); + + // ".something.example.org" always fails to match. + if (firstDot == 0) + { + return false; + } + + if (firstDot > 0) + { + afterFirstDot = match.Slice(firstDot + 1); + } + + foreach (string embedded in san.EnumerateDnsNames()) + { + hadAny = true; + + if (embedded.Length == 0) + { + continue; + } + + ReadOnlySpan embeddedSpan = embedded; + + // Convert embedded "something.example.org." to "something.example.org" + if (embedded.EndsWith('.')) + { + embeddedSpan = embeddedSpan.Slice(0, embeddedSpan.Length - 1); + } + + if (allowWildcards && embeddedSpan.StartsWith("*.") && embeddedSpan.Length > 2) + { + if (embeddedSpan.Slice(2).Equals(afterFirstDot, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + else if (embeddedSpan.Equals(match, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + if (hadAny) + { + return false; + } + } + + if (allowCommonName) + { + X500DistinguishedName.RelativeDistinguishedName? cn = null; + + foreach (X500DistinguishedName.RelativeDistinguishedName rdn in + SubjectName.EnumerateRelativeDistinguishedNames()) + { + if (rdn.HasMultipleValues) + { + // Since X500DN already read it with DER, we can use BER here and save on some ifs. + AsnValueReader reader = new AsnValueReader(rdn.RawData.Span, AsnEncodingRules.BER); + AsnValueReader set = reader.ReadSetOf(); + + while (set.HasData) + { + AsnValueReader attributeTypeAndValue = set.ReadSequence(); + Oid? type = Oids.GetSharedOrNullOid(ref attributeTypeAndValue); + + if (ReferenceEquals(type, Oids.CommonNameOid)) + { + return false; + } + } + } + else if (ReferenceEquals(rdn.SingleValueType, Oids.CommonNameOid)) + { + if (cn is null) + { + cn = rdn; + } + else + { + return false; + } + } + } + + if (cn is not null) + { + return hostname.Equals(cn.SingleValueValue, StringComparison.OrdinalIgnoreCase); + } + } + + return false; + } + private static X509Certificate2 ExtractKeyFromPem( ReadOnlySpan keyPem, string[] labels, From 8d686cd8b7cae824566d9703c6e52f104f2f047c Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Wed, 8 Jun 2022 13:08:18 -0700 Subject: [PATCH 16/43] Rename to MatchesHostname, add first wave of tests --- .../SubjectAlternativeNameTests.cs | 60 ---- .../tests/MatchesHostnameTests.cs | 301 ++++++++++++++++++ ...Cryptography.X509Certificates.Tests.csproj | 1 + .../ref/System.Security.Cryptography.cs | 3 +- .../X509Certificates/X509Certificate2.cs | 4 +- .../X509SubjectAlternativeNameExtension.cs | 92 ------ 6 files changed, 305 insertions(+), 156 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs index 1dd6cc11124ff..27e9ddb914559 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectAlternativeNameTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using System.Security.Cryptography.X509Certificates; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests.ExtensionsTests @@ -48,64 +47,5 @@ public static void EnumerateIPAddresses() Assert.Equal(new[] { IPAddress.Loopback, IPAddress.IPv6Loopback }, ext.EnumerateIPAddresses()); } - - [Fact] - public static void MatchesIpAddress() - { - SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); - builder.AddDnsName("foo"); - builder.AddIpAddress(IPAddress.Loopback); - builder.AddUserPrincipalName("user@some.domain"); - builder.AddIpAddress(IPAddress.IPv6Loopback); - builder.AddDnsName("*.foo"); - X509Extension built = builder.Build(true); - - X509SubjectAlternativeNameExtension ext = new(); - ext.CopyFrom(built); - - Assert.False(ext.MatchesHostname(IPAddress.Broadcast.ToString()), "Matches IPAddress.Broadcast"); - Assert.False(ext.MatchesHostname(IPAddress.IPv6Any.ToString()), "Matches IPAddress.IPv6Any"); - Assert.False(ext.MatchesHostname(IPAddress.Any.ToString()), "Matches IPAddress.Any"); - Assert.False(ext.MatchesHostname(IPAddress.None.ToString()), "Matches IPAddress.None"); - Assert.True(ext.MatchesHostname(IPAddress.IPv6Loopback.ToString()), "Matches IPAddress.IPv6Loopback"); - Assert.True(ext.MatchesHostname(IPAddress.Loopback.ToString()), "Matches IPAddress.Loopback"); - } - - [Fact] - public static void MatchesDnsName() - { - SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); - builder.AddDnsName("foo"); - builder.AddIpAddress(IPAddress.Loopback); - builder.AddUserPrincipalName("user@some.domain"); - builder.AddIpAddress(IPAddress.IPv6Loopback); - builder.AddDnsName("*.foo"); - X509Extension built = builder.Build(true); - - X509SubjectAlternativeNameExtension ext = new(); - ext.CopyFrom(built); - - static void AssertMatches(X509SubjectAlternativeNameExtension ext, string target, bool expected) - { - if (expected) - { - Assert.True(ext.MatchesHostname(target), $"Matches '{target}'"); - } - else - { - Assert.False(ext.MatchesHostname(target), $"Matches '{target}'"); - } - } - - AssertMatches(ext, "foo", true); - AssertMatches(ext, "fOo", true); - AssertMatches(ext, "fOo.", true); - AssertMatches(ext, ".fOo.", false); - AssertMatches(ext, "BAR.fOo.", true); - AssertMatches(ext, "BAR.foo", true); - AssertMatches(ext, "baz.BAR.foo", false); - AssertMatches(ext, "baz.BAR.foo.", false); - AssertMatches(ext, "example.com", false); - } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs new file mode 100644 index 0000000000000..341f0568116b4 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs @@ -0,0 +1,301 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public static class MatchesHostnameTests + { + [Theory] + [InlineData("fruit.example", false)] + [InlineData("127.0.0.1", false)] + [InlineData("microsoft.com", true)] + [InlineData("www.microsoft.com", true)] + [InlineData("wwwqa.microsoft.com", true)] + [InlineData("wwwqa2.microsoft.com", false)] + [InlineData("staticview.microsoft.com", true)] + [InlineData("c.s-microsoft.com", true)] + [InlineData("i.s-microsoft.com", true)] + [InlineData("j.s-microsoft.com", false)] + [InlineData("s-microsoft.com", false)] + [InlineData("privacy.microsoft.com", true)] + [InlineData("more.privacy.microsoft.com", false)] + [InlineData("moreprivacy.microsoft.com", false)] + public static void MicrosoftDotComSslMatchesHostname(string candidate, bool expected) + { + using (X509Certificate2 cert = new X509Certificate2(TestData.MicrosoftDotComSslCertBytes)) + { + AssertMatch(expected, cert, candidate); + } + } + + [Fact] + public static void SanDnsMeansNoCommonNameFallback() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=zalzalak.fruit.example", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("yumberry.fruit.example"); + sanBuilder.AddDnsName("*.pome.fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "yumberry.fruit.example"); + AssertMatch(true, cert, "zalzalak.pome.fruit.example"); + + // zalzalak is a pome, and our fake DNS knows that, but the certificate doesn't. + AssertMatch(false, cert, "zalzalak.fruit.example"); + } + } + } + + [Fact] + public static void SanWithNoDnsMeansDoCommonNameFallback() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=zalzalak.fruit.example", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "yumberry.fruit.example"); + AssertMatch(true, cert, "127.0.0.1"); + + // Since the SAN contains no dNSName values, we fall back to the CN. + AssertMatch(true, cert, "zalzalak.fruit.example"); + AssertMatch(false, cert, "zalzalak.fruit.example", allowCommonName: false); + } + } + } + + [Fact] + public static void SanDoesNotMatchIPAddressInDnsName() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("127.0.0.1"); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + // 127.0.0.1 is an IP Address, but the SAN calls it a dNSName, so it won't match. + AssertMatch(false, cert, "127.0.0.1"); + + // Since the SAN contains no iPAddress values, we fall back to the CN. + AssertMatch(true, cert, "10.0.0.1"); + } + } + } + + [Fact] + public static void CommonNameDoesNotUseWildcards() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=*.fruit.example", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "papaya.fruit.example"); + + AssertMatch(true, cert, "*.fruit.example"); + } + } + } + + [Fact] + public static void NoPartialWildcards() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("*berry.fruit.example"); + sanBuilder.AddDnsName("cran*.fruit.example"); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "cranberry.fruit.example"); + + // Since we don't consider the partial wildcards as wildcards, they do match unexpanded. + AssertMatch(true, cert, "*berry.fruit.example"); + AssertMatch(true, cert, "cran*.fruit.example"); + } + } + } + + [Fact] + public static void WildcardsDoNotMatchThroughPeriods() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("fruit.example"); + sanBuilder.AddDnsName("*.fruit.example"); + sanBuilder.AddDnsName("rambutan.fruit.example"); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "apple.fruit.example"); + AssertMatch(true, cert, "blackberry.fruit.example"); + AssertMatch(true, cert, "pome.fruit.example"); + AssertMatch(true, cert, "pomme.fruit.example"); + AssertMatch(true, cert, "rambutan.fruit.example"); + AssertMatch(false, cert, "apple.pome.fruit.example"); + AssertMatch(false, cert, "apple.pomme.fruit.example"); + + AssertMatch(true, cert, "*.fruit.example"); + AssertMatch(true, cert, "*.fruit.example", allowWildcards: false); + + AssertMatch(false, cert, "apple.fruit.example", allowWildcards: false); + AssertMatch(false, cert, "blackberry.fruit.example", allowWildcards: false); + AssertMatch(false, cert, "pome.fruit.example", allowWildcards: false); + AssertMatch(false, cert, "pomme.fruit.example", allowWildcards: false); + // This one has a redundant dNSName after the wildcard + AssertMatch(true, cert, "rambutan.fruit.example", allowWildcards: false); + + AssertMatch(true, cert, "fruit.example"); + AssertMatch(true, cert, "fruit.example", allowWildcards: false); + } + } + } + + [Theory] + [InlineData("aPPlE.fruit.example", true)] + [InlineData("tOmaTO.FRUIT.example", true)] + [InlineData("tOmaTO.vegetable.example", false)] + [InlineData("FRUit.example", true)] + [InlineData("VEGetaBlE.example", false)] + public static void DnsMatchNotCaseSensitive(string target, bool expected) + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("fruit.EXAMPLE"); + sanBuilder.AddDnsName("*.FrUIt.eXaMpLE"); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(expected, cert, target); + } + } + } + + private static void AssertMatch( + bool expected, + X509Certificate2 cert, + string hostname, + bool allowWildcards = true, + bool allowCommonName = true) + { + bool match = cert.MatchesHostname(hostname, allowWildcards, allowCommonName); + + if (match != expected) + { + string display = $"Matches {(hostname.Contains('*') ? "(literal) " : "")}'{hostname}'"; + + if (!allowWildcards && !allowCommonName) + { + display += " with no wildcards or common name fallback"; + } + else if (!allowWildcards) + { + display += " with no wildcards"; + } + else if (!allowCommonName) + { + display += " with no common name fallback"; + } + + if (expected) + { + Assert.True(match, display); + } + else + { + Assert.False(match, display); + } + } + } + } +} 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 5957a2d0173ff..d3ea6e4713186 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 @@ -45,6 +45,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 9683aa8683a3c..31eaceadeff76 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2820,7 +2820,7 @@ public override void Import(string fileName) { } public override void Import(string fileName, System.Security.SecureString? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags) { } [System.ObsoleteAttribute("X509Certificate and X509Certificate2 are immutable. Use the appropriate constructor to create a new certificate.", DiagnosticId="SYSLIB0026", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public override void Import(string fileName, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags) { } - public bool MatchesTlsHostname(string hostname, bool allowWildcards = true, bool allowCommonName = true) { throw null; } + public bool MatchesHostname(string hostname, bool allowWildcards = true, bool allowCommonName = true) { throw null; } public override void Reset() { } public override string ToString() { throw null; } public override string ToString(bool verbose) { throw null; } @@ -3186,7 +3186,6 @@ public X509SubjectAlternativeNameExtension(System.ReadOnlySpan rawData, bo public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEncodedData) { } public System.Collections.Generic.IEnumerable EnumerateDnsNames() { throw null; } public System.Collections.Generic.IEnumerable EnumerateIPAddresses() { throw null; } - public bool MatchesHostname(string hostname) { throw null; } } public sealed partial class X509SubjectKeyIdentifierExtension : System.Security.Cryptography.X509Certificates.X509Extension { 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 5a0b6c5dcf759..9f1a5dfeabf1b 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 @@ -1242,7 +1242,7 @@ public bool TryExportCertificatePem(Span destination, out int charsWritten /// extended key usage. /// /// - public bool MatchesTlsHostname(string hostname, bool allowWildcards = true, bool allowCommonName = true) + public bool MatchesHostname(string hostname, bool allowWildcards = true, bool allowCommonName = true) { ArgumentNullException.ThrowIfNull(hostname); @@ -1255,7 +1255,7 @@ public bool MatchesTlsHostname(string hostname, bool allowWildcards = true, bool foreach (X509Extension extension in Pal.Extensions) { - if (extension.Oid!.Value == Oids.CommonName) + if (extension.Oid!.Value == Oids.SubjectAltName) { if (rawSAN is null) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs index 3e4d6a993c484..d12e20e8e4ca1 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectAlternativeNameExtension.cs @@ -34,98 +34,6 @@ public override void CopyFrom(AsnEncodedData asnEncodedData) _decoded = null; } - public bool MatchesHostname(string hostname) - { - ArgumentNullException.ThrowIfNull(hostname); - - if (hostname.Length == 0) - { - return false; - } - - if (IPAddress.TryParse(hostname, out IPAddress? ipAddress)) - { - // Big enough for IPv6 - Span encodedAddr = stackalloc byte[16]; - - if (!ipAddress.TryWriteBytes(encodedAddr, out int written)) - { - return false; - } - - ReadOnlySpan match = encodedAddr.Slice(0, written); - - List decoded = (_decoded ??= Decode(RawData)); - - foreach (GeneralNameAsn item in decoded) - { - if (item.IPAddress.HasValue) - { - if (item.IPAddress.GetValueOrDefault().Span.SequenceEqual(match)) - { - return true; - } - } - } - } - else - { - ReadOnlySpan match = hostname; - - if (hostname.EndsWith('.')) - { - match = match.Slice(0, match.Length - 1); - - if (match.IsEmpty) - { - return false; - } - } - - ReadOnlySpan afterFirstDot = default; - int firstDot = match.IndexOf('.'); - - if (firstDot == 0) - { - return false; - } - - if (firstDot > 0) - { - afterFirstDot = match.Slice(firstDot + 1); - } - - foreach (string embedded in EnumerateDnsNames()) - { - if (embedded.Length == 0) - { - continue; - } - - ReadOnlySpan embeddedSpan = embedded; - - if (embedded.EndsWith('.')) - { - embeddedSpan = embeddedSpan.Slice(0, embeddedSpan.Length - 1); - } - - if (embeddedSpan.StartsWith("*.") && embeddedSpan.Length > 2) - { - if (embeddedSpan.Slice(2).Equals(afterFirstDot, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - else if (embeddedSpan.Equals(match, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - public IEnumerable EnumerateDnsNames() { List decoded = (_decoded ??= Decode(RawData)); From e9eba9b4e7ea03563eda1e6121d7b594c281f96e Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Wed, 8 Jun 2022 14:47:44 -0700 Subject: [PATCH 17/43] Use list pattern in GetSharedOrNullOid --- .../Security/Cryptography/Oids.Shared.cs | 64 +++++++------------ 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs index 2dec336df8f58..0616dd81b2fad 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs @@ -81,53 +81,37 @@ internal static Oid GetSharedOrNewOid(ref AsnValueReader asnValueReader) return new Oid(oidValue, null); } - internal static Oid? GetSharedOrNullOid(ref AsnValueReader asnValueReader) + internal static Oid? GetSharedOrNullOid(ref AsnValueReader asnValueReader, Asn1Tag? expectedTag = null) { Asn1Tag tag = asnValueReader.PeekTag(); - // Any of these cases are going to be an invalid OID. We'll let that get thrown naturally, the same as if - // it wasn't a match. - if (!tag.IsConstructed && - (tag.TagClass != TagClass.Universal || tag.TagValue == (int)UniversalTagNumber.ObjectIdentifier)) + // This isn't a valid OID, so return null and let whatever's going to happen happen. + if (tag.IsConstructed) { - ReadOnlySpan contentBytes = asnValueReader.PeekContentBytes(); - Oid? ret = null; - - switch (contentBytes.Length) - { - case 3: - switch (contentBytes[0]) - { - case 0x55: - switch (contentBytes[1]) - { - case 0x04: - switch (contentBytes[2]) - { - case 0x03: - ret = CommonNameOid; - break; - } - - break; - } - - break; - } - - break; - } - - if (ret is not null) - { - // Move to the next item. - asnValueReader.ReadEncodedValue(); - } + return null; + } - return ret; + // Not the tag we're expecting, so don't match. + if (!tag.HasSameClassAndValue(expectedTag.GetValueOrDefault(Asn1Tag.ObjectIdentifier))) + { + return null; + } + + ReadOnlySpan contentBytes = asnValueReader.PeekContentBytes(); + + Oid? ret = contentBytes switch + { + [0x55, 0x04, 0x03] => CommonNameOid, + _ => null, + }; + + if (ret is not null) + { + // Move to the next item. + asnValueReader.ReadEncodedValue(); } - return null; + return ret; } } } From 778ddfde208c838ef55df65f4873b7e2f9f46e61 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Wed, 8 Jun 2022 14:55:09 -0700 Subject: [PATCH 18/43] Be more stringent with acceptable tag values --- .../src/System/Security/Cryptography/Oids.Shared.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs index 0616dd81b2fad..f6354ce48b91a 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Formats.Asn1; +using System.Runtime.CompilerServices; namespace System.Security.Cryptography { @@ -91,8 +92,15 @@ internal static Oid GetSharedOrNewOid(ref AsnValueReader asnValueReader) return null; } + Asn1Tag expected = expectedTag.GetValueOrDefault(Asn1Tag.ObjectIdentifier); + + Debug.Assert( + expected.TagClass != TagClass.Universal || + expected.TagValue == (int)UniversalTagNumber.ObjectIdentifier, + $"{nameof(GetSharedOrNullOid)} was called with the wrong Universal class tag: {expectedTag}"); + // Not the tag we're expecting, so don't match. - if (!tag.HasSameClassAndValue(expectedTag.GetValueOrDefault(Asn1Tag.ObjectIdentifier))) + if (!tag.HasSameClassAndValue(expected)) { return null; } From 4c9c0493f6891ecd553ca70ddb8b38ee07ae173d Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Wed, 8 Jun 2022 15:43:49 -0700 Subject: [PATCH 19/43] Push MatchesHostname coverage to 100% --- .../tests/MatchesHostnameTests.cs | 473 +++++++++++++++++- 1 file changed, 465 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs index 341f0568116b4..9cf21577fb21e 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/MatchesHostnameTests.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using System.Net; +using Test.Cryptography; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests @@ -94,6 +96,36 @@ public static void SanWithNoDnsMeansDoCommonNameFallback() } } + [Fact] + public static void SanWithIPAddressMeansNoCommonNameFallback() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "127.0.0.1"); + + // Since the SAN has an iPAddress value, we do not fall back to the CN. + AssertMatch(false, cert, "10.0.0.1"); + } + } + } + [Fact] public static void SanDoesNotMatchIPAddressInDnsName() { @@ -228,13 +260,8 @@ public static void WildcardsDoNotMatchThroughPeriods() } } - [Theory] - [InlineData("aPPlE.fruit.example", true)] - [InlineData("tOmaTO.FRUIT.example", true)] - [InlineData("tOmaTO.vegetable.example", false)] - [InlineData("FRUit.example", true)] - [InlineData("VEGetaBlE.example", false)] - public static void DnsMatchNotCaseSensitive(string target, bool expected) + [Fact] + public static void DnsMatchNotCaseSensitive() { using (ECDsa key = ECDsa.Create()) { @@ -256,11 +283,441 @@ public static void DnsMatchNotCaseSensitive(string target, bool expected) using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) { - AssertMatch(expected, cert, target); + AssertMatch(true, cert, "aPPlE.fruit.example"); + AssertMatch(true, cert, "tOmaTO.FRUIT.example"); + AssertMatch(false, cert, "tOmaTO.vegetable.example"); + AssertMatch(true, cert, "FRUit.example"); + AssertMatch(false, cert, "VEGetaBlE.example"); + } + } + } + + [Fact] + public static void DnsNameIgnoresTrailingPeriod() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("fruit.example."); + sanBuilder.AddDnsName("*.FrUIt.eXaMpLE."); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "aPPlE.fruit.example"); + AssertMatch(true, cert, "tOmaTO.FRUIT.example"); + AssertMatch(false, cert, "tOmaTO.vegetable.example"); + AssertMatch(true, cert, "FRUit.example"); + AssertMatch(false, cert, "VEGetaBlE.example"); + } + } + } + + [Fact] + public static void DnsNameMatchIgnoresTrailingPeriodFromParameter() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("fruit.EXAMPLE"); + sanBuilder.AddDnsName("*.FrUIt.eXaMpLE"); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "aPPlE.fruit.example."); + AssertMatch(true, cert, "tOmaTO.FRUIT.example."); + AssertMatch(false, cert, "tOmaTO.vegetable.example."); + AssertMatch(true, cert, "FRUit.example."); + AssertMatch(false, cert, "VEGetaBlE.example."); + } + } + } + + [Fact] + public static void DnsNameMatchRejectsLeadingPeriodFromParameter() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=10.0.0.1", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("fruit.EXAMPLE"); + sanBuilder.AddDnsName("*.FrUIt.eXaMpLE"); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "aPPlE.fruit.example."); + AssertMatch(true, cert, "tOmaTO.FRUIT.example."); + AssertMatch(false, cert, "tOmaTO.vegetable.example."); + AssertMatch(true, cert, "FRUit.example."); + AssertMatch(false, cert, ".FRUit.example."); + AssertMatch(false, cert, "VEGetaBlE.example."); + } + } + } + + [Fact] + public static void CommonNameMatchDoesNotIgnoreTrailingPeriodFromParameter() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=fruit.example", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "FRUit.example"); + AssertMatch(false, cert, "FRUit.example", allowCommonName: false); + AssertMatch(false, cert, "FRUit.example."); + } + } + } + + [Fact] + public static void CommonNameMatchDoesNotIgnoreTrailingPeriodFromValue() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=fruit.example.", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(true, cert, "FRUit.example."); + AssertMatch(false, cert, "FRUit.example.", allowCommonName: false); + AssertMatch(false, cert, "FRUit.example"); + } + } + } + + [Fact] + public static void NoMatchIfMultipleCommonNames() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=fruit.example, CN=potato.vegetable.example", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "FRUit.example"); + AssertMatch(false, cert, "potato.vegetable.example"); + } + } + } + + [Fact] + public static void NoMatchIfMultipleCommonNamesWithMultiRDN() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=fruit.example, CN=potato.vegetable.example+ST=Idaho", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "FRUit.example"); + AssertMatch(false, cert, "potato.vegetable.example"); + } + } + } + + [Fact] + public static void NoMatchIfCommonNamesInMultiRDN() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=potato.vegetable.example+ST=Idaho", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "FRUit.example"); + AssertMatch(false, cert, "potato.vegetable.example"); + } + } + } + + [Fact] + public static void MultiRdnWithNoCommonNameIsOK() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=potato.vegetable.example,ST=Idaho+ST=Utah", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "FRUit.example"); + AssertMatch(true, cert, "potato.vegetable.example"); + } + } + } + + [Fact] + public static void NoMatchAndNoCommonName() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "ST=Idaho", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, ""); + AssertMatch(false, cert, "FRUit.example"); + AssertMatch(false, cert, "potato.vegetable.example"); + } + } + } + + [Fact] + public static void NoMatchAndEmptyCommonName() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=,ST=Idaho", + key, + HashAlgorithmName.SHA256); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, ""); + AssertMatch(false, cert, "FRUit.example"); + AssertMatch(false, cert, "potato.vegetable.example"); + } + } + } + + [Fact] + public static void NoMatchOnEmptyDnsName() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=potato.vegetable.example", + key, + HashAlgorithmName.SHA256); + + req.CertificateExtensions.Add( + new X509Extension("2.5.29.17", "30028200".HexToByteArray(), false)); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + X509SubjectAlternativeNameExtension sanExt = + (X509SubjectAlternativeNameExtension)cert.Extensions[0]; + + if (sanExt.EnumerateDnsNames().Single() != "") + { + throw new InvalidOperationException("Invalid test data"); + } + + AssertMatch(false, cert, "example"); + AssertMatch(false, cert, "example."); + AssertMatch(false, cert, "."); + AssertMatch(false, cert, "*"); + AssertMatch(false, cert, "*."); + AssertMatch(false, cert, ""); + } + } + } + + [Fact] + public static void NoMatchOnDnsNameWithLeadingPeriod() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=potato.vegetable.example", + key, + HashAlgorithmName.SHA256); + + req.CertificateExtensions.Add( + new X509Extension( + "2.5.29.17", + "301682142E70656163682E66727569742E6578616D706C65".HexToByteArray(), + false)); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + X509SubjectAlternativeNameExtension sanExt = + (X509SubjectAlternativeNameExtension)cert.Extensions[0]; + + if (sanExt.EnumerateDnsNames().Single() != ".peach.fruit.example") + { + throw new InvalidOperationException("Invalid test data"); + } + + AssertMatch(false, cert, "peach.fruit.example"); + AssertMatch(false, cert, ""); + } + } + } + + [Fact] + public static void WildcardRequiresSuffixToMatch() + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + "CN=potato.vegetable.example", + key, + HashAlgorithmName.SHA256); + + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("*"); + sanBuilder.AddDnsName("*."); + sanBuilder.AddEmailAddress("it@fruit.example"); + + req.CertificateExtensions.Add(sanBuilder.Build()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMinutes(-1); + DateTimeOffset notAfter = now.AddMinutes(1); + + using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter)) + { + AssertMatch(false, cert, "example"); + AssertMatch(false, cert, "example."); + AssertMatch(false, cert, "."); + AssertMatch(true, cert, "*"); + AssertMatch(true, cert, "*."); } } } + [Fact] + public static void TooManySANsThrows() + { + byte[] tooManySans = ( + "3082021430820175A00302010202083C883E44C34DA5CB300A06082A8648CE3D" + + "04030230233121301F06035504031318706F7461746F2E766567657461626C65" + + "2E6578616D706C65301E170D3232303630383232333530365A170D3232303630" + + "383232333730365A30233121301F06035504031318706F7461746F2E76656765" + + "7461626C652E6578616D706C6530819B301006072A8648CE3D020106052B8104" + + "0023038186000400BA92930960C2C98D81F4DEAB62E75C0F768B5518A8FF58C2" + + "1D43B453AA2D1C73FA6BB0586349DDD61D0C25DC46B444BF5806F72F0F83546C" + + "B27583AE0007B101780007B7AE5717D4343C85D168212F2C2E4EC8F8B9F1953F" + + "A159C5E74A191B609E6A38FAAC404E3A0C094DD39A6732673545EE8C195A2B9B" + + "600420E9F55C145232304EA350304E30180603551D110411300F820D66727569" + + "742E6578616D706C6530320603551D11042B302982152A2E64727570652E6672" + + "7569742E6578616D706C65811069744066727569742E6578616D706C65300A06" + + "082A8648CE3D04030203818C003081880242009DA8DF6009D12EC733ADEE7479" + + "18B4611E185E478BA1D33AB7150A6A29F21FF31B48846B132868934A9F989C88" + + "39C7B8955A70DD5D4E9E1BB7C0D78F6AD8C3C6DC024200958482B9444D1AD2D3" + + "F67B51AD13064F2FDD4EC2F64ECB352D3F11BE8066F9021DD0CF309654351781" + + "69E940B767111BB2D28119EB3A2461617792F1CDF131F794").HexToByteArray(); + + using (X509Certificate2 cert = new X509Certificate2(tooManySans)) + { + Assert.Throws( + () => cert.MatchesHostname("fruit.example")); + + Assert.Throws( + () => cert.MatchesHostname("fruit.example", allowWildcards: false)); + + Assert.Throws( + () => cert.MatchesHostname("fruit.example", allowCommonName: false)); + + Assert.Throws( + () => cert.MatchesHostname("fruit.example", false, false)); + + // But argument validation comes first. + Assert.Throws("hostname", () => cert.MatchesHostname(null)); + Assert.Throws("hostname", () => cert.MatchesHostname(null, false, true)); + Assert.Throws("hostname", () => cert.MatchesHostname(null, true, false)); + Assert.Throws("hostname", () => cert.MatchesHostname(null, false, false)); + } + } + private static void AssertMatch( bool expected, X509Certificate2 cert, From 2046ebf51e67d11bcfd59d1aca8a83d06be29cb0 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 9 Jun 2022 09:30:28 -0700 Subject: [PATCH 20/43] Add CreateSigningRequestPem, some code cleanup --- .../Security/Cryptography/Oids.Shared.cs | 18 +++++- .../System/Security/Cryptography/PemLabels.cs | 2 + .../ref/System.Security.Cryptography.cs | 2 + .../X509Certificates/CertificateRequest.cs | 64 ++++++++++--------- .../CertificateRevocationListBuilder.cs | 2 +- .../X509Certificates/X509Certificate2.cs | 4 +- 6 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs index f6354ce48b91a..93d575a006749 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Formats.Asn1; -using System.Runtime.CompilerServices; namespace System.Security.Cryptography { @@ -121,5 +120,22 @@ internal static Oid GetSharedOrNewOid(ref AsnValueReader asnValueReader) return ret; } + + internal static bool ValueEquals(this Oid oid, Oid? other) + { + Debug.Assert(oid is not null); + + if (ReferenceEquals(oid, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + return oid.Value is not null && oid.Value.Equals(other.Value); + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs b/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs index 9373bbb48349f..a654db6cf6ecf 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs @@ -13,5 +13,7 @@ internal static class PemLabels internal const string EcPrivateKey = "EC PRIVATE KEY"; internal const string X509Certificate = "CERTIFICATE"; internal const string Pkcs7Certificate = "PKCS7"; + internal const string Pkcs10CertificateRequest = "CERTIFICATE REQUEST"; + internal const string X509CertificateRevocationList = "X509 CRL"; } } diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 31eaceadeff76..e9ab176902d0a 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2441,6 +2441,8 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public byte[] CreateSigningRequest() { throw null; } public byte[] CreateSigningRequest(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(System.ReadOnlySpan pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } + public string CreateSigningRequestPem() { throw null; } + public string CreateSigningRequestPem(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } } public sealed partial class CertificateRevocationListBuilder { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index 81bf36afbcff3..ae8fd27658a15 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -242,36 +242,9 @@ public CertificateRequest( /// /// When submitting a certificate signing request via a web browser, or other graphical or textual /// interface, the input is frequently expected to be in the PEM (Privacy Enhanced Mail) format, - /// instead of the DER binary format. To convert the return value to PEM format, make a string - /// consisting of -----BEGIN CERTIFICATE REQUEST-----, a newline, the Base-64-encoded - /// representation of the request (by convention, linewrapped at 64 characters), a newline, - /// and -----END CERTIFICATE REQUEST-----. - /// - /// + /// instead of the DER binary format. /// + /// public byte[] CreateSigningRequest() { if (_generator == null) @@ -287,6 +260,12 @@ public byte[] CreateSigningRequest() /// /// A with which to sign the request. /// + /// + /// When submitting a certificate signing request via a web browser, or other graphical or textual + /// interface, the input is frequently expected to be in the PEM (Privacy Enhanced Mail) format, + /// instead of the DER binary format. + /// + /// public byte[] CreateSigningRequest(X509SignatureGenerator signatureGenerator) { ArgumentNullException.ThrowIfNull(signatureGenerator); @@ -332,6 +311,33 @@ public byte[] CreateSigningRequest(X509SignatureGenerator signatureGenerator) return requestInfo.ToPkcs10Request(signatureGenerator, HashAlgorithm); } + /// + /// Create a PEM-encoded PKCS#10 CertificationRequest representing the current state + /// of this object using the provided signature generator. + /// + /// + public string CreateSigningRequestPem() + { + byte[] der = CreateSigningRequest(); + return PemEncoding.WriteString(PemLabels.Pkcs10CertificateRequest, der); + } + + /// + /// Create a PEM-encoded PKCS#10 CertificationRequest representing the current state + /// of this object using the provided signature generator. + /// + /// + /// A with which to sign the request. + /// + /// + public string CreateSigningRequestPem(X509SignatureGenerator signatureGenerator) + { + ArgumentNullException.ThrowIfNull(signatureGenerator); + + byte[] der = CreateSigningRequest(signatureGenerator); + return PemEncoding.WriteString(PemLabels.Pkcs10CertificateRequest, der); + } + /// /// Create a self-signed certificate using the established subject, key, and optional /// extensions. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 0f16a80855ee0..5bbe09abcb4e9 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -202,7 +202,7 @@ public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan curren { foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(currentCrl)) { - if (contents[fields.Label].SequenceEqual("X509 CRL")) + if (contents[fields.Label].SequenceEqual(PemLabels.X509CertificateRevocationList)) { byte[] rented = ArrayPool.Shared.Rent(fields.DecodedDataLength); 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 9f1a5dfeabf1b..1ba8fa2aa3942 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 @@ -1371,13 +1371,13 @@ public bool MatchesHostname(string hostname, bool allowWildcards = true, bool al AsnValueReader attributeTypeAndValue = set.ReadSequence(); Oid? type = Oids.GetSharedOrNullOid(ref attributeTypeAndValue); - if (ReferenceEquals(type, Oids.CommonNameOid)) + if (Oids.CommonNameOid.ValueEquals(type)) { return false; } } } - else if (ReferenceEquals(rdn.SingleValueType, Oids.CommonNameOid)) + else if (Oids.CommonNameOid.ValueEquals(rdn.SingleValueType)) { if (cn is null) { From f38d49c8639708d5265d290709483d18f189c164 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 9 Jun 2022 09:31:05 -0700 Subject: [PATCH 21/43] Add unsafeLoadCertificateExtensions parameter to LoadCertificateRequest --- .../tests/CertificateCreation/DontBeACA.cs | 1 + .../ref/System.Security.Cryptography.cs | 2 +- .../X509Certificates/CertificateRequest.cs | 33 +++++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs index aa3f62e6e7635..7f5901e8c1815 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -57,6 +57,7 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe HashAlgorithmName.SHA256, out int bytesConsumed, skipSignatureValidation: true, + unsafeLoadCertificateExtensions: true, signerSignaturePadding: RSASignaturePadding.Pss); AsnEncodedData? challengePassword = null; diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index e9ab176902d0a..e286e7d50394d 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2440,9 +2440,9 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public System.Security.Cryptography.X509Certificates.X509Certificate2 CreateSelfSigned(System.DateTimeOffset notBefore, System.DateTimeOffset notAfter) { throw null; } public byte[] CreateSigningRequest() { throw null; } public byte[] CreateSigningRequest(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(System.ReadOnlySpan pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } public string CreateSigningRequestPem() { throw null; } public string CreateSigningRequestPem(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(System.ReadOnlySpan pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } } public sealed partial class CertificateRevocationListBuilder { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index ae8fd27658a15..691c22e432280 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -769,6 +769,7 @@ public static unsafe CertificateRequest LoadCertificateRequest( HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, + bool unsafeLoadCertificateExtensions = false, RSASignaturePadding? signerSignaturePadding = null) { try @@ -857,21 +858,25 @@ public static unsafe CertificateRequest LoadCertificateRequest( { X509ExtensionAsn.Decode(ref exts, rebind, out X509ExtensionAsn extAsn); - X509Extension ext = new X509Extension( - extAsn.ExtnId, - extAsn.ExtnValue.Span, - extAsn.Critical); - - X509Extension? rich = X509Certificate2.CreateCustomExtensionIfAny(extAsn.ExtnId); - - if (rich is not null) - { - rich.CopyFrom(ext); - req.CertificateExtensions.Add(rich); - } - else + if (unsafeLoadCertificateExtensions) { - req.CertificateExtensions.Add(ext); + X509Extension ext = new X509Extension( + extAsn.ExtnId, + extAsn.ExtnValue.Span, + extAsn.Critical); + + X509Extension? rich = + X509Certificate2.CreateCustomExtensionIfAny(extAsn.ExtnId); + + if (rich is not null) + { + rich.CopyFrom(ext); + req.CertificateExtensions.Add(rich); + } + else + { + req.CertificateExtensions.Add(ext); + } } } } From 4386438b8a76b99e8b34243aa2de3fe74cc27457 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 9 Jun 2022 14:11:58 -0700 Subject: [PATCH 22/43] Share PssParamsAsn to RSASignaturePadding conversions --- .../Cryptography/Asn1/PssParamsAsn.manual.cs | 64 +++++++++++++++++++ .../System/Security/Cryptography/Helpers.cs | 14 ++++ .../Security/Cryptography/Oids.Shared.cs | 5 ++ .../src/Resources/Strings.resx | 2 +- .../System.Security.Cryptography.Pkcs.csproj | 4 ++ .../Cryptography/Pkcs/CmsSignature.RSA.cs | 43 +------------ .../src/Resources/Strings.resx | 15 +++++ .../src/System.Security.Cryptography.csproj | 4 ++ .../X509Certificates/CertificateRequest.cs | 59 +---------------- 9 files changed, 112 insertions(+), 98 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/Asn1/PssParamsAsn.manual.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1/PssParamsAsn.manual.cs b/src/libraries/Common/src/System/Security/Cryptography/Asn1/PssParamsAsn.manual.cs new file mode 100644 index 0000000000000..b80004f5efa50 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1/PssParamsAsn.manual.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Formats.Asn1; +using Internal.Cryptography; + +namespace System.Security.Cryptography.Asn1 +{ + internal partial struct PssParamsAsn + { + internal RSASignaturePadding GetSignaturePadding( + int? digestValueLength = null) + { + if (TrailerField != 1) + { + throw new CryptographicException(SR.Cryptography_Pkcs_InvalidSignatureParameters); + } + + if (MaskGenAlgorithm.Algorithm != Oids.Mgf1) + { + throw new CryptographicException( + SR.Cryptography_Pkcs_PssParametersMgfNotSupported, + MaskGenAlgorithm.Algorithm); + } + + if (MaskGenAlgorithm.Parameters == null) + { + throw new CryptographicException(SR.Cryptography_Pkcs_InvalidSignatureParameters); + } + + AlgorithmIdentifierAsn mgfParams = AlgorithmIdentifierAsn.Decode( + MaskGenAlgorithm.Parameters.Value, + AsnEncodingRules.DER); + + if (mgfParams.Algorithm != HashAlgorithm.Algorithm) + { + throw new CryptographicException( + SR.Format( + SR.Cryptography_Pkcs_PssParametersMgfHashMismatch, + mgfParams.Algorithm, + HashAlgorithm.Algorithm)); + } + + int saltSize = digestValueLength.GetValueOrDefault(); + + if (!digestValueLength.HasValue) + { + saltSize = Helpers.HashOidToByteLength(HashAlgorithm.Algorithm); + } + + if (SaltLength != saltSize) + { + throw new CryptographicException( + SR.Format( + SR.Cryptography_Pkcs_PssParametersSaltMismatch, + SaltLength, + HashAlgorithm.Algorithm)); + } + + // When RSASignaturePadding supports custom salt sizes this return will look different. + return RSASignaturePadding.Pss; + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs b/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs index 3d1b8de29d435..1b24b59857873 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs @@ -72,5 +72,19 @@ internal static bool TryCopyToDestination(this ReadOnlySpan source, Span 256 >> 3, + Oids.Sha384 => 384 >> 3, + Oids.Sha512 => 512 >> 3, + Oids.Sha1 => 160 >> 3, + Oids.Md5 => 128 >> 3, + _ => throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashOid)), + }; + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs index 93d575a006749..1a7bac8faeae4 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs @@ -83,6 +83,7 @@ internal static Oid GetSharedOrNewOid(ref AsnValueReader asnValueReader) internal static Oid? GetSharedOrNullOid(ref AsnValueReader asnValueReader, Asn1Tag? expectedTag = null) { +#if NET Asn1Tag tag = asnValueReader.PeekTag(); // This isn't a valid OID, so return null and let whatever's going to happen happen. @@ -119,6 +120,10 @@ internal static Oid GetSharedOrNewOid(ref AsnValueReader asnValueReader) } return ret; +#else + // Cannot use list patterns on older TFMs. + return null; +#endif } internal static bool ValueEquals(this Oid oid, Oid? other) diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography.Pkcs/src/Resources/Strings.resx index 2bdcfe05da60d..7055b2beec656 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/Resources/Strings.resx @@ -206,7 +206,7 @@ This verification operation applies to 'Pkcs12IntegrityMode.{0}', but the target object is in 'Pkcs12IntegrityMode.{1}'. - Invalid signature paramters. + Invalid signature parameters. The EncryptedPrivateKeyInfo structure was decoded but was not successfully interpreted, the password may be incorrect. diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj b/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj index 1bbf74d2fe669..be33237e61641 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj @@ -415,6 +415,10 @@ System.Security.Cryptography.Pkcs.EnvelopedCms Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml + + Common\System\Security\Cryptography\Asn1\PssParamsAsn.manual.cs + Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml + Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml.cs Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/CmsSignature.RSA.cs b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/CmsSignature.RSA.cs index 1ac9a248d35e7..12adeda2360f6 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/CmsSignature.RSA.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/CmsSignature.RSA.cs @@ -309,47 +309,8 @@ protected override RSASignaturePadding GetSignaturePadding( digestAlgorithmOid)); } - if (pssParams.TrailerField != 1) - { - throw new CryptographicException(SR.Cryptography_Pkcs_InvalidSignatureParameters); - } - - if (pssParams.SaltLength != digestValueLength) - { - throw new CryptographicException( - SR.Format( - SR.Cryptography_Pkcs_PssParametersSaltMismatch, - pssParams.SaltLength, - digestAlgorithmName.Name)); - } - - if (pssParams.MaskGenAlgorithm.Algorithm != Oids.Mgf1) - { - throw new CryptographicException( - SR.Cryptography_Pkcs_PssParametersMgfNotSupported, - pssParams.MaskGenAlgorithm.Algorithm); - } - - if (pssParams.MaskGenAlgorithm.Parameters == null) - { - throw new CryptographicException(SR.Cryptography_Pkcs_InvalidSignatureParameters); - } - - AlgorithmIdentifierAsn mgfParams = AlgorithmIdentifierAsn.Decode( - pssParams.MaskGenAlgorithm.Parameters.Value, - AsnEncodingRules.DER); - - if (mgfParams.Algorithm != digestAlgorithmOid) - { - throw new CryptographicException( - SR.Format( - SR.Cryptography_Pkcs_PssParametersMgfHashMismatch, - mgfParams.Algorithm, - digestAlgorithmOid)); - } - - // When RSASignaturePadding supports custom salt sizes this return will look different. - return RSASignaturePadding.Pss; + RSASignaturePadding padding = pssParams.GetSignaturePadding(digestValueLength); + return padding; } protected override bool Sign( diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 4a48215d74260..4b5baf3e37589 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -513,6 +513,21 @@ The provided PFX data contains no certificates. + + Invalid signature parameters. + + + PSS parameters were not present. + + + This platform does not support the MGF hash algorithm ({0}) being different from the signature hash algorithm ({1}). + + + Mask generation function '{0}' is not supported by this platform. + + + PSS salt size {0} is not supported by this platform with hash algorithm {1}. + The EncryptedPrivateKeyInfo structure was decoded but was not successfully interpreted, the password may be incorrect. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index de3f9bc10c64c..54aa2f040549f 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -167,6 +167,10 @@ Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml + + Common\System\Security\Cryptography\Asn1\PssParamsAsn.manual.cs + Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml + Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml.cs Common\System\Security\Cryptography\Asn1\PssParamsAsn.xml diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index 691c22e432280..7aac7b782cc60 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -952,67 +952,14 @@ private static bool VerifyX509Signature( algorithmIdentifier.Parameters.GetValueOrDefault(), AsnEncodingRules.DER); - if (pssParams.TrailerField != 1 || - !pssParams.HashAlgorithm.HasNullEquivalentParameters() || - pssParams.MaskGenAlgorithm.Algorithm != Oids.Mgf1 || - !pssParams.MaskGenAlgorithm.Parameters.HasValue) - { - return false; - } - - AlgorithmIdentifierAsn mgfParams = AlgorithmIdentifierAsn.Decode( - pssParams.MaskGenAlgorithm.Parameters.GetValueOrDefault(), - AsnEncodingRules.DER); - - if (mgfParams.Algorithm != pssParams.HashAlgorithm.Algorithm || - !mgfParams.HasNullEquivalentParameters()) - { - return false; - } - - switch (pssParams.HashAlgorithm.Algorithm) - { - case Oids.Sha256: - if (pssParams.SaltLength != SHA256.HashSizeInBytes) - { - return false; - } - - hashAlg = HashAlgorithmName.SHA256; - break; - case Oids.Sha384: - if (pssParams.SaltLength != SHA384.HashSizeInBytes) - { - return false; - } - - hashAlg = HashAlgorithmName.SHA384; - break; - case Oids.Sha512: - if (pssParams.SaltLength != SHA512.HashSizeInBytes) - { - return false; - } - - hashAlg = HashAlgorithmName.SHA512; - break; - case Oids.Sha1: - if (pssParams.SaltLength != SHA1.HashSizeInBytes) - { - return false; - } - - hashAlg = HashAlgorithmName.SHA1; - break; - default: - return false; - } + RSASignaturePadding padding = pssParams.GetSignaturePadding(); + hashAlg = HashAlgorithmName.FromOid(pssParams.HashAlgorithm.Algorithm); return rsa.VerifyData( toBeSigned, signature, hashAlg, - RSASignaturePadding.Pss); + padding); } // All remaining algorithms have no defined parameters From 1fe525b8521a11c13e476c8495fff15360c13e55 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 9 Jun 2022 15:39:42 -0700 Subject: [PATCH 23/43] Get PKCS10 signature verification happy --- .../CertificateRequestLoadTests.cs | 170 ++++++++++++++++++ .../tests/CertificateCreation/DontBeACA.cs | 1 - .../PrivateKeyAssociationTests.cs | 23 --- .../X509Sha1SignatureGenerators.cs | 76 ++++++++ ...Cryptography.X509Certificates.Tests.csproj | 2 + .../src/Resources/Strings.resx | 3 + .../X509Certificates/CertificateRequest.cs | 19 +- .../X509Certificates/PublicKey.cs | 2 +- 8 files changed, 262 insertions(+), 34 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/X509Sha1SignatureGenerators.cs diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs new file mode 100644 index 0000000000000..02dc2583e7f9e --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests.CertificateCreation +{ + public static class CertificateRequestLoadTests + { + [Theory] + [InlineData("SHA256")] + [InlineData("SHA384")] + [InlineData("SHA512")] + [InlineData("SHA1")] + public static void VerifySignature_ECDsa(string hashAlgorithm) + { + HashAlgorithmName hashAlgorithmName = new HashAlgorithmName(hashAlgorithm); + + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest first = new CertificateRequest( + "CN=Test", + key, + hashAlgorithmName); + + byte[] pkcs10; + + if (hashAlgorithm == "SHA1") + { + pkcs10 = first.CreateSigningRequest(new ECDsaSha1SignatureGenerator(key)); + } + else + { + pkcs10 = first.CreateSigningRequest(); + } + + // Assert.NoThrow + CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _); + + pkcs10[^1] ^= 0xFF; + + Assert.Throws( + () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + } + } + + [Theory] + [InlineData("SHA256")] + [InlineData("SHA384")] + [InlineData("SHA512")] + [InlineData("SHA1")] + public static void VerifySignature_RSA_PKCS1(string hashAlgorithm) + { + HashAlgorithmName hashAlgorithmName = new HashAlgorithmName(hashAlgorithm); + + using (RSA key = RSA.Create()) + { + CertificateRequest first = new CertificateRequest( + "CN=Test", + key, + hashAlgorithmName, + RSASignaturePadding.Pkcs1); + + byte[] pkcs10; + + if (hashAlgorithm == "SHA1") + { + pkcs10 = first.CreateSigningRequest(new RSASha1Pkcs1SignatureGenerator(key)); + } + else + { + pkcs10 = first.CreateSigningRequest(); + } + + // Assert.NoThrow + CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _); + + pkcs10[^1] ^= 0xFF; + + Assert.Throws( + () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + + // Assert.NoThrow + CertificateRequest.LoadCertificateRequest( + pkcs10, + hashAlgorithmName, + out _, + skipSignatureValidation: true); + } + } + + [Theory] + [InlineData("SHA256")] + [InlineData("SHA384")] + [InlineData("SHA512")] + [InlineData("SHA1")] + public static void VerifySignature_RSA_PSS(string hashAlgorithm) + { + HashAlgorithmName hashAlgorithmName = new HashAlgorithmName(hashAlgorithm); + + using (RSA key = RSA.Create()) + { + CertificateRequest first = new CertificateRequest( + "CN=Test", + key, + hashAlgorithmName, + RSASignaturePadding.Pss); + + byte[] pkcs10; + + if (hashAlgorithm == "SHA1") + { + pkcs10 = first.CreateSigningRequest(new RSASha1PssSignatureGenerator(key)); + } + else + { + pkcs10 = first.CreateSigningRequest(); + } + + // Assert.NoThrow + CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _); + + pkcs10[^1] ^= 0xFF; + + Assert.Throws( + () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + + // Assert.NoThrow + CertificateRequest.LoadCertificateRequest( + pkcs10, + hashAlgorithmName, + out _, + skipSignatureValidation: true); + } + } + + [Theory] + [InlineData("SHA256")] + [InlineData("SHA1")] + public static void VerifySignature_DSA(string hashAlgorithm) + { + HashAlgorithmName hashAlgorithmName = new HashAlgorithmName(hashAlgorithm); + + using (DSA key = DSA.Create(TestData.GetDSA1024Params())) + { + DSAX509SignatureGenerator generator = new DSAX509SignatureGenerator(key); + + CertificateRequest first = new CertificateRequest( + new X500DistinguishedName("CN=Test"), + generator.PublicKey, + hashAlgorithmName); + + byte[] pkcs10 = first.CreateSigningRequest(generator); + + // The inbox version doesn't support DSA + Assert.Throws( + () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + + // Assert.NoThrow + CertificateRequest.LoadCertificateRequest( + pkcs10, + hashAlgorithmName, + out _, + skipSignatureValidation: true); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs index 7f5901e8c1815..ca993c15d3ea1 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -56,7 +56,6 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe pkcs10, HashAlgorithmName.SHA256, out int bytesConsumed, - skipSignatureValidation: true, unsafeLoadCertificateExtensions: true, signerSignaturePadding: RSASignaturePadding.Pss); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/PrivateKeyAssociationTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/PrivateKeyAssociationTests.cs index 6763e700d6984..83c075cf67d4e 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/PrivateKeyAssociationTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/PrivateKeyAssociationTests.cs @@ -549,28 +549,5 @@ public static void ThirdPartyProvider_ECDsa() Assert.True(ecdsaOther.VerifyData(data, signature, hashAlgorithm)); } } - - private sealed class RSASha1Pkcs1SignatureGenerator : X509SignatureGenerator - { - private readonly X509SignatureGenerator _realRsaGenerator; - - internal RSASha1Pkcs1SignatureGenerator(RSA rsa) - { - _realRsaGenerator = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); - } - - protected override PublicKey BuildPublicKey() => _realRsaGenerator.PublicKey; - - public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) - { - if (hashAlgorithm == HashAlgorithmName.SHA1) - return "300D06092A864886F70D0101050500".HexToByteArray(); - - throw new InvalidOperationException(); - } - - public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) => - _realRsaGenerator.SignData(data, hashAlgorithm); - } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/X509Sha1SignatureGenerators.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/X509Sha1SignatureGenerators.cs new file mode 100644 index 0000000000000..0ed6da88215eb --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/X509Sha1SignatureGenerators.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Test.Cryptography; + +namespace System.Security.Cryptography.X509Certificates.Tests.CertificateCreation +{ + internal sealed class ECDsaSha1SignatureGenerator : X509SignatureGenerator + { + private readonly X509SignatureGenerator _realGenerator; + + internal ECDsaSha1SignatureGenerator(ECDsa ecdsa) + { + _realGenerator = CreateForECDsa(ecdsa); + } + + protected override PublicKey BuildPublicKey() => _realGenerator.PublicKey; + + public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) + { + if (hashAlgorithm == HashAlgorithmName.SHA1) + return "300906072A8648CE3D0401".HexToByteArray(); + + throw new InvalidOperationException(); + } + + public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) => + _realGenerator.SignData(data, hashAlgorithm); + } + + internal sealed class RSASha1Pkcs1SignatureGenerator : X509SignatureGenerator + { + private readonly X509SignatureGenerator _realRsaGenerator; + + internal RSASha1Pkcs1SignatureGenerator(RSA rsa) + { + _realRsaGenerator = CreateForRSA(rsa, RSASignaturePadding.Pkcs1); + } + + protected override PublicKey BuildPublicKey() => _realRsaGenerator.PublicKey; + + public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) + { + if (hashAlgorithm == HashAlgorithmName.SHA1) + return "300D06092A864886F70D0101050500".HexToByteArray(); + + throw new InvalidOperationException(); + } + + public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) => + _realRsaGenerator.SignData(data, hashAlgorithm); + } + + internal sealed class RSASha1PssSignatureGenerator : X509SignatureGenerator + { + private readonly X509SignatureGenerator _realRsaGenerator; + + internal RSASha1PssSignatureGenerator(RSA rsa) + { + _realRsaGenerator = CreateForRSA(rsa, RSASignaturePadding.Pss); + } + + protected override PublicKey BuildPublicKey() => _realRsaGenerator.PublicKey; + + public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) + { + if (hashAlgorithm == HashAlgorithmName.SHA1) + return "300D06092A864886F70D01010A3000".HexToByteArray(); + + throw new InvalidOperationException(); + } + + public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) => + _realRsaGenerator.SignData(data, hashAlgorithm); + } +} 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 d3ea6e4713186..378e05d159061 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 @@ -68,6 +68,7 @@ + @@ -78,6 +79,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 4b5baf3e37589..81026b36bd70f 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -243,6 +243,9 @@ The issuer certificate uses an RSA key but no RSASignaturePadding was provided to a constructor. If one cannot be provided, use the X509SignatureGenerator overload. + + The public key within the PKCS#10 Certification Signing Request did not verify the embedded signature. + The specified feedback size '{0}' for CipherMode '{1}' is not supported. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index 7aac7b782cc60..282531518bb67 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -815,7 +815,7 @@ public static unsafe CertificateRequest LoadCertificateRequest( if (signatureUnusedBitCount != 0 || !VerifyX509Signature(encodedRequestInfo, signature, publicKey, algorithmIdentifier)) { - throw new CryptographicException("Signature didn't work"); + throw new CryptographicException(SR.Cryptography_CertReq_SignatureVerificationFailed); } } @@ -962,12 +962,6 @@ private static bool VerifyX509Signature( padding); } - // All remaining algorithms have no defined parameters - if (!algorithmIdentifier.HasNullEquivalentParameters()) - { - return false; - } - switch (algorithmIdentifier.Algorithm) { case Oids.RsaPkcs1Sha256: @@ -987,7 +981,14 @@ private static bool VerifyX509Signature( hashAlg = HashAlgorithmName.SHA1; break; default: - return false; + throw new NotSupportedException( + SR.Format(SR.Cryptography_UnknownKeyAlgorithm, algorithmIdentifier.Algorithm)); + } + + // All remaining algorithms have no defined parameters + if (!algorithmIdentifier.HasNullEquivalentParameters()) + { + return false; } switch (algorithmIdentifier.Algorithm) @@ -1011,7 +1012,7 @@ private static bool VerifyX509Signature( return false; } - return ecdsa.VerifyData(toBeSigned, signature, hashAlg); + return ecdsa.VerifyData(toBeSigned, signature, hashAlg, DSASignatureFormat.Rfc3279DerSequence); default: Debug.Fail($"Algorithm ID {algorithmIdentifier.Algorithm} was in the first switch, but not the second"); return false; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs index 726e87a6697de..e033386a4afdb 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs @@ -312,7 +312,7 @@ internal static PublicKey DecodeSubjectPublicKeyInfo(ref SubjectPublicKeyInfoAsn out AsnEncodedData parameters, out AsnEncodedData keyValue); - return new PublicKey(oid, keyValue, parameters); + return new PublicKey(oid, parameters, keyValue); } private static void DecodeSubjectPublicKeyInfo( From 68971747f43a98b9b0c88391f046a8a13a30cdff Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 9 Jun 2022 16:03:16 -0700 Subject: [PATCH 24/43] Add a static data load test --- .../CertificateRequestLoadTests.cs | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs index 02dc2583e7f9e..e7c60b74e0f0b 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; +using System.Net; using Test.Cryptography; using Xunit; @@ -9,6 +9,66 @@ namespace System.Security.Cryptography.X509Certificates.Tests.CertificateCreatio { public static class CertificateRequestLoadTests { + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public static void LoadBigExponentRequest(bool loadExtensions, bool oversizedRequest) + { + byte[] pkcs10 = TestData.BigExponentPkcs10Bytes; + + if (oversizedRequest) + { + byte[] temp = new byte[pkcs10.Length + 22]; + pkcs10.AsSpan().CopyTo(temp); + pkcs10 = temp; + } + + CertificateRequest req = CertificateRequest.LoadCertificateRequest( + pkcs10, + HashAlgorithmName.SHA256, + out int bytesConsumed, + unsafeLoadCertificateExtensions: loadExtensions); + + Assert.Equal(TestData.BigExponentPkcs10Bytes.Length, bytesConsumed); + Assert.Equal("1.2.840.113549.1.1.1", req.PublicKey.Oid.Value); + Assert.Equal("0500", req.PublicKey.EncodedParameters.RawData.ByteArrayToHex()); + Assert.Null(req.PublicKey.EncodedParameters.Oid); + Assert.Null(req.PublicKey.EncodedKeyValue.Oid); + + Assert.Equal( + "3082010C0282010100AF81C1CBD8203F624A539ED6608175372393A2837D4890" + + "E48A19DED36973115620968D6BE0D3DAA38AA777BE02EE0B6B93B724E8DCC12B" + + "632B4FA80BBC925BCE624F4CA7CC606306B39403E28C932D24DD546FFE4EF6A3" + + "7F10770B2215EA8CBB5BF427E8C4D89B79EB338375100C5F83E55DE9B4466DDF" + + "BEEE42539AEF33EF187B7760C3B1A1B2103C2D8144564A0C1039A09C85CF6B59" + + "74EB516FC8D6623C94AE3A5A0BB3B4C792957D432391566CF3E2A52AFB0C142B" + + "9E0681B8972671AF2B82DD390A39B939CF719568687E4990A63050CA7768DCD6" + + "B378842F18FDB1F6D9FF096BAF7BEB98DCF930D66FCFD503F58D41BFF46212E2" + + "4E3AFC45EA42BD884702050200000441", + req.PublicKey.EncodedKeyValue.RawData.ByteArrayToHex()); + + Assert.Equal( + "CN=localhost, OU=.NET Framework (CoreFX), O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + req.SubjectName.Name); + + if (loadExtensions) + { + Assert.Equal(1, req.CertificateExtensions.Count); + + X509SubjectAlternativeNameExtension san = + Assert.IsType(req.CertificateExtensions[0]); + + Assert.Equal(new[] { IPAddress.Loopback, IPAddress.IPv6Loopback }, san.EnumerateIPAddresses()); + Assert.Equal(new[] { "localhost" }, san.EnumerateDnsNames()); + } + else + { + Assert.Empty(req.CertificateExtensions); + } + } + [Theory] [InlineData("SHA256")] [InlineData("SHA384")] From e0f81402f18b436f215bd221c54a0c612029ace8 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 9 Jun 2022 16:18:10 -0700 Subject: [PATCH 25/43] Add PKCS10PEM export tests --- .../CertificateRequestUsageTests.cs | 6 ++++++ .../tests/TestData.cs | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestUsageTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestUsageTests.cs index d753f8e477da7..7787c9fc437a6 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestUsageTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestUsageTests.cs @@ -19,6 +19,8 @@ public static void ReproduceBigExponentCsr() byte[] autoCsr; byte[] csr; + string csrPem; + string autoCsrPem; using (RSA rsa = RSA.Create()) { @@ -33,13 +35,17 @@ public static void ReproduceBigExponentCsr() request.CertificateExtensions.Add(sanExtension); autoCsr = request.CreateSigningRequest(); + autoCsrPem = request.CreateSigningRequestPem(); X509SignatureGenerator generator = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); csr = request.CreateSigningRequest(generator); + csrPem = request.CreateSigningRequestPem(generator); } Assert.Equal(TestData.BigExponentPkcs10Bytes.ByteArrayToHex(), autoCsr.ByteArrayToHex()); Assert.Equal(TestData.BigExponentPkcs10Bytes.ByteArrayToHex(), csr.ByteArrayToHex()); + Assert.Equal(TestData.BigExponentPkcs10Pem, autoCsrPem); + Assert.Equal(TestData.BigExponentPkcs10Pem, csrPem); } [Fact] diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs index a0151384fe999..92d8606e3093b 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/TestData.cs @@ -2334,6 +2334,27 @@ internal struct ECDsaCngKeyValues "699B3C957C6DD22E9B63DBAE3B5AE62919F0EA3DF304C7DD9E0BBA0E7053605F" + "D066A788426159BB937C58E5A110461DC9364CA7CA").HexToByteArray(); + internal const string BigExponentPkcs10Pem = + "-----BEGIN CERTIFICATE REQUEST-----\n" + + "MIIDETCCAfkCAQAwgYoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u\n" + + "MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp\n" + + "b24xIDAeBgNVBAsTFy5ORVQgRnJhbWV3b3JrIChDb3JlRlgpMRIwEAYDVQQDEwls\n" + + "b2NhbGhvc3QwggEkMA0GCSqGSIb3DQEBAQUAA4IBEQAwggEMAoIBAQCvgcHL2CA/\n" + + "YkpTntZggXU3I5Oig31IkOSKGd7TaXMRViCWjWvg09qjiqd3vgLuC2uTtyTo3MEr\n" + + "YytPqAu8klvOYk9Mp8xgYwazlAPijJMtJN1Ub/5O9qN/EHcLIhXqjLtb9CfoxNib\n" + + "eeszg3UQDF+D5V3ptEZt377uQlOa7zPvGHt3YMOxobIQPC2BRFZKDBA5oJyFz2tZ\n" + + "dOtRb8jWYjyUrjpaC7O0x5KVfUMjkVZs8+KlKvsMFCueBoG4lyZxryuC3TkKObk5\n" + + "z3GVaGh+SZCmMFDKd2jc1rN4hC8Y/bH22f8Ja69765jc+TDWb8/VA/WNQb/0YhLi\n" + + "Tjr8RepCvYhHAgUCAAAEQaA/MD0GCSqGSIb3DQEJDjEwMC4wLAYDVR0RBCUwI4cE\n" + + "fwAAAYcQAAAAAAAAAAAAAAAAAAAAAYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUA\n" + + "A4IBAQA7yufgLTqChDURDIplGX/xoCfsWso36+KbbnCTpL3Km9qOJE3AWEaqnxht\n" + + "LrvfZHS7Cez1o8EfOn5W2dSJw9SuLc9dUqv83+1tRiOvfH0uUqGJvEoL/F65bsFY\n" + + "qWspLfbkrcrlIzp+FZhETiP3MlJrcRciZuRXBvkO+rCUWnXURvCmVHx4jdga1vTR\n" + + "5/0OiIQIOvUgA9nNOLOhQPLlUs8/vwtMdx5XRcbabybc/Q/rh7n90vRySgneH7TF\n" + + "XkOfQ8bjeoZroZSUshDSlGmbPJV8bdIum2Pbrjta5ikZ8Oo98wTH3Z4Lug5wU2Bf\n" + + "0GaniEJhWbuTfFjloRBGHck2TKfK\n" + + "-----END CERTIFICATE REQUEST-----"; + internal static byte[] EmptySubjectCertificate = ( "308202A73082018FA003020102020103300D06092A864886F70D01010B050030" + "1F311D301B06035504031314456D707479205375626A65637420497373756572" + From 5ccef315bff5c18aae3484ea02b2d5a3a15bdabc Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 9 Jun 2022 16:54:46 -0700 Subject: [PATCH 26/43] Add PEM-based PKCS10 loading --- .../CertificateRequestLoadTests.cs | 115 +++++++++++++++++- .../ref/System.Security.Cryptography.cs | 3 + .../src/Resources/Strings.resx | 3 + .../X509Certificates/CertificateRequest.cs | 100 +++++++++++++++ .../CertificateRevocationListBuilder.cs | 2 +- 5 files changed, 216 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs index e7c60b74e0f0b..a385a5d78f4fb 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs @@ -14,24 +14,127 @@ public static class CertificateRequestLoadTests [InlineData(false, false)] [InlineData(true, true)] [InlineData(false, true)] - public static void LoadBigExponentRequest(bool loadExtensions, bool oversizedRequest) + public static void LoadBigExponentRequest_Span(bool loadExtensions, bool oversized) { byte[] pkcs10 = TestData.BigExponentPkcs10Bytes; - if (oversizedRequest) + if (oversized) { - byte[] temp = new byte[pkcs10.Length + 22]; - pkcs10.AsSpan().CopyTo(temp); - pkcs10 = temp; + Array.Resize(ref pkcs10, pkcs10.Length + 22); } CertificateRequest req = CertificateRequest.LoadCertificateRequest( - pkcs10, + new ReadOnlySpan(pkcs10), HashAlgorithmName.SHA256, out int bytesConsumed, unsafeLoadCertificateExtensions: loadExtensions); Assert.Equal(TestData.BigExponentPkcs10Bytes.Length, bytesConsumed); + VerifyBigExponentRequest(req, loadExtensions); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void LoadBigExponentRequest_Bytes(bool loadExtensions) + { + CertificateRequest req = CertificateRequest.LoadCertificateRequest( + TestData.BigExponentPkcs10Bytes, + HashAlgorithmName.SHA256, + unsafeLoadCertificateExtensions: loadExtensions); + + VerifyBigExponentRequest(req, loadExtensions); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void LoadBigExponentRequest_Bytes_Oversized(bool loadExtensions) + { + byte[] pkcs10 = TestData.BigExponentPkcs10Bytes; + Array.Resize(ref pkcs10, pkcs10.Length + 2); + + Assert.Throws( + () => CertificateRequest.LoadCertificateRequest( + pkcs10, + HashAlgorithmName.SHA256, + unsafeLoadCertificateExtensions: loadExtensions)); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public static void LoadBigExponentRequest_PemString(bool loadExtensions, bool multiPem) + { + string pem = TestData.BigExponentPkcs10Pem; + + if (multiPem) + { + pem = $@" +-----BEGIN UNRELATED----- +abcd +-----END UNRELATED----- +-----BEGIN CERTIFICATE REQUEST----- +!!!!!INVALID!!!!! +-----END CERTIFICATE REQUEST----- +-----BEGIN MORE UNRELATED----- +efgh +-----END MORE UNRELATED----- +{pem} +-----BEGIN CERTIFICATE REQUEST----- +!!!!!INVALID!!!!! +-----END CERTIFICATE REQUEST-----"; + } + + CertificateRequest req = CertificateRequest.LoadCertificateRequestPem( + pem, + HashAlgorithmName.SHA256, + unsafeLoadCertificateExtensions: loadExtensions); + + VerifyBigExponentRequest(req, loadExtensions); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public static void LoadBigExponentRequest_PemSpam(bool loadExtensions, bool multiPem) + { + string pem = TestData.BigExponentPkcs10Pem; + + if (multiPem) + { + pem = $@" +-----BEGIN UNRELATED----- +abcd +-----END UNRELATED----- +Free Floating Text +-----BEGIN CERTIFICATE REQUEST----- +!!!!!INVALID!!!!! +-----END CERTIFICATE REQUEST----- +-----BEGIN MORE UNRELATED----- +efgh +-----END MORE UNRELATED----- +More Text. +{pem} +-----BEGIN CERTIFICATE REQUEST----- +!!!!!INVALID!!!!! +-----END CERTIFICATE REQUEST-----"; + } + + CertificateRequest req = CertificateRequest.LoadCertificateRequestPem( + pem.AsSpan(), + HashAlgorithmName.SHA256, + unsafeLoadCertificateExtensions: loadExtensions); + + VerifyBigExponentRequest(req, loadExtensions); + } + + private static void VerifyBigExponentRequest(CertificateRequest req, bool loadExtensions) + { Assert.Equal("1.2.840.113549.1.1.1", req.PublicKey.Oid.Value); Assert.Equal("0500", req.PublicKey.EncodedParameters.RawData.ByteArrayToHex()); Assert.Null(req.PublicKey.EncodedParameters.Oid); diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index e286e7d50394d..4b51014006956 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2442,7 +2442,10 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public byte[] CreateSigningRequest(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } public string CreateSigningRequestPem() { throw null; } public string CreateSigningRequestPem(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(byte[] pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(System.ReadOnlySpan pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequestPem(System.ReadOnlySpan pkcs10Pem, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequestPem(string pkcs10Pem, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } } public sealed partial class CertificateRevocationListBuilder { diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 81026b36bd70f..b7cc8edbbc954 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -459,6 +459,9 @@ TransformBlock may only process bytes in block sized increments. + + The contents do not contain a PEM with a '{0}' label, or the content is malformed. + Key is not a valid private key. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index 282531518bb67..52cccab987a18 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -764,6 +764,82 @@ public X509Certificate2 Create( return ret; } + public static unsafe CertificateRequest LoadCertificateRequestPem( + string pkcs10Pem, + HashAlgorithmName signerHashAlgorithm, + bool skipSignatureValidation = false, + bool unsafeLoadCertificateExtensions = false, + RSASignaturePadding? signerSignaturePadding = null) + { + ArgumentNullException.ThrowIfNull(pkcs10Pem); + + return LoadCertificateRequestPem( + pkcs10Pem.AsSpan(), + signerHashAlgorithm, + skipSignatureValidation, + unsafeLoadCertificateExtensions, + signerSignaturePadding); + } + + public static unsafe CertificateRequest LoadCertificateRequestPem( + ReadOnlySpan pkcs10Pem, + HashAlgorithmName signerHashAlgorithm, + bool skipSignatureValidation = false, + bool unsafeLoadCertificateExtensions = false, + RSASignaturePadding? signerSignaturePadding = null) + { + foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(pkcs10Pem)) + { + if (contents[fields.Label].SequenceEqual(PemLabels.Pkcs10CertificateRequest)) + { + byte[] rented = ArrayPool.Shared.Rent(fields.DecodedDataLength); + + if (!Convert.TryFromBase64Chars(contents[fields.Base64Data], rented, out int bytesWritten)) + { + Debug.Fail("Base64Decode failed, but PemEncoding said it was legal"); + throw new UnreachableException(); + } + + try + { + return LoadCertificateRequest( + rented.AsSpan(0, bytesWritten), + permitTrailingData: false, + signerHashAlgorithm, + out _, + skipSignatureValidation, + unsafeLoadCertificateExtensions, + signerSignaturePadding); + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + } + + throw new CryptographicException(SR.Cryptography_NoPemOfLabel, PemLabels.Pkcs10CertificateRequest); + } + + public static unsafe CertificateRequest LoadCertificateRequest( + byte[] pkcs10, + HashAlgorithmName signerHashAlgorithm, + bool skipSignatureValidation = false, + bool unsafeLoadCertificateExtensions = false, + RSASignaturePadding? signerSignaturePadding = null) + { + ArgumentNullException.ThrowIfNull(pkcs10); + + return LoadCertificateRequest( + pkcs10, + permitTrailingData: false, + signerHashAlgorithm, + out _, + skipSignatureValidation, + unsafeLoadCertificateExtensions, + signerSignaturePadding); + } + public static unsafe CertificateRequest LoadCertificateRequest( ReadOnlySpan pkcs10, HashAlgorithmName signerHashAlgorithm, @@ -771,6 +847,25 @@ public static unsafe CertificateRequest LoadCertificateRequest( bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, RSASignaturePadding? signerSignaturePadding = null) + { + return LoadCertificateRequest( + pkcs10, + permitTrailingData: true, + signerHashAlgorithm, + out bytesConsumed, + skipSignatureValidation, + unsafeLoadCertificateExtensions, + signerSignaturePadding); + } + + private static unsafe CertificateRequest LoadCertificateRequest( + ReadOnlySpan pkcs10, + bool permitTrailingData, + HashAlgorithmName signerHashAlgorithm, + out int bytesConsumed, + bool skipSignatureValidation = false, + bool unsafeLoadCertificateExtensions = false, + RSASignaturePadding? signerSignaturePadding = null) { try { @@ -780,6 +875,11 @@ public static unsafe CertificateRequest LoadCertificateRequest( AsnValueReader pkcs10Asn = outer.ReadSequence(); CertificateRequest req; + if (!permitTrailingData) + { + outer.ThrowIfNotEmpty(); + } + fixed (byte* p10ptr = pkcs10) { using (PointerMemoryManager manager = new PointerMemoryManager(p10ptr, encodedLength)) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 5bbe09abcb4e9..90b05e13af778 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -223,7 +223,7 @@ public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan curren } } - throw new CryptographicException("No PEM-encoded CRL was found"); + throw new CryptographicException(SR.Cryptography_NoPemOfLabel, PemLabels.X509CertificateRevocationList); } public void AddEntry(X509Certificate2 certificate) From b7d980347b2d0bf3ee8d834e09957436b0372d1f Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 10 Jun 2022 09:54:50 -0700 Subject: [PATCH 27/43] Make SKI.SubjectKeyIdentifierBytes nullable --- .../SubjectKeyIdentifierTests.cs | 25 ++++++++++++++++++- .../ref/System.Security.Cryptography.cs | 2 +- .../X509AuthorityKeyIdentifierExtension.cs | 16 +++++++++--- .../X509SubjectKeyIdentifierExtension.cs | 7 +++++- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs index cf968f210dea2..f3f0477dcfcd6 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs @@ -20,6 +20,8 @@ public static void DefaultConstructor() string skid = e.SubjectKeyIdentifier; Assert.Null(skid); + + Assert.False(e.SubjectKeyIdentifierBytes.HasValue, "SubjectKeyIdentifierBytes.HasValue"); } [Theory] @@ -55,6 +57,10 @@ public static void EncodeFromBytes(bool fromSpan) string skid = e.SubjectKeyIdentifier; Assert.Equal("01020304", skid); + + AssertExtensions.SequenceEqual( + new byte[] { 1, 2, 3, 4 }, + e.SubjectKeyIdentifierBytes.GetValueOrDefault().Span); } [Fact] @@ -69,6 +75,10 @@ public static void EncodeFromString() e = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), false); string skid = e.SubjectKeyIdentifier; Assert.Equal("01ABCD", skid); + + AssertExtensions.SequenceEqual( + new byte[] { 0x01, 0xAB, 0xCD }, + e.SubjectKeyIdentifierBytes.GetValueOrDefault().Span); } [Fact] @@ -89,6 +99,11 @@ public static void EncodeFromPublicKey() e = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), false); string skid = e.SubjectKeyIdentifier; Assert.Equal("5971A65A334DDA980780FF841EBE87F9723241F2", skid); + + Assert.Equal( + "5971A65A334DDA980780FF841EBE87F9723241F2", + e.SubjectKeyIdentifierBytes.GetValueOrDefault().ByteArrayToHex()); + } [Fact] @@ -134,8 +149,12 @@ public static void DecodeFromBER() ext = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), false); string skid = ext.SubjectKeyIdentifier; Assert.Equal("5971A65A334DDA980780FF841EBE87F9723241F2", skid); + + Assert.Equal( + "5971A65A334DDA980780FF841EBE87F9723241F2", + ext.SubjectKeyIdentifierBytes.GetValueOrDefault().ByteArrayToHex()); } - + private static void EncodeDecode( byte[] certBytes, X509SubjectKeyIdentifierHashAlgorithm algorithm, @@ -158,6 +177,10 @@ private static void EncodeDecode( ext = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), critical); Assert.Equal(expectedIdentifier, ext.SubjectKeyIdentifier); + + Assert.Equal( + expectedIdentifier, + ext.SubjectKeyIdentifierBytes.GetValueOrDefault().ByteArrayToHex()); } } } diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 4b51014006956..709fabf7ba052 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3202,7 +3202,7 @@ public X509SubjectKeyIdentifierExtension(System.Security.Cryptography.X509Certif public X509SubjectKeyIdentifierExtension(System.Security.Cryptography.X509Certificates.PublicKey key, System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierHashAlgorithm algorithm, bool critical) { } public X509SubjectKeyIdentifierExtension(string subjectKeyIdentifier, bool critical) { } public string? SubjectKeyIdentifier { get { throw null; } } - public System.ReadOnlyMemory SubjectKeyIdentifierBytes { get { throw null; } } + public System.ReadOnlyMemory? SubjectKeyIdentifierBytes { get { throw null; } } public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEncodedData) { } } public enum X509SubjectKeyIdentifierHashAlgorithm diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs index a96906a3915c3..6c9448008f175 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs @@ -138,7 +138,13 @@ public static X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier { ArgumentNullException.ThrowIfNull(subjectKeyIdentifier); - return CreateFromSubjectKeyIdentifier(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span); + if (!subjectKeyIdentifier.SubjectKeyIdentifierBytes.HasValue) + { + throw new ArgumentException("Something about the extension has not had a value provided to it"); + } + + return CreateFromSubjectKeyIdentifier( + subjectKeyIdentifier.SubjectKeyIdentifierBytes.GetValueOrDefault().Span); } public static X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier( @@ -265,15 +271,19 @@ public static X509AuthorityKeyIdentifierExtension CreateFromCertificate( throw new CryptographicException("Provided certificate does not have a subject key identifier"); } + // Only the default constructor for the X509SubjectKeyIdentifierExtension produces null + Debug.Assert(skid.SubjectKeyIdentifierBytes.HasValue); + ReadOnlySpan skidBytes = skid.SubjectKeyIdentifierBytes.GetValueOrDefault().Span; + if (includeIssuerAndSerial) { return Create( - skid.SubjectKeyIdentifierBytes.Span, + skidBytes, certificate.IssuerName, certificate.SerialNumberBytes.Span); } - return CreateFromSubjectKeyIdentifier(skid.SubjectKeyIdentifierBytes.Span); + return CreateFromSubjectKeyIdentifier(skidBytes); } else if (includeIssuerAndSerial) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs index 1edcbbad26125..827548ecd9510 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs @@ -61,7 +61,7 @@ public string? SubjectKeyIdentifier } } - public ReadOnlyMemory SubjectKeyIdentifierBytes + public ReadOnlyMemory? SubjectKeyIdentifierBytes { get { @@ -70,6 +70,11 @@ public ReadOnlyMemory SubjectKeyIdentifierBytes Decode(RawData); } + if (_subjectKeyIdentifier is null) + { + return default; + } + return _subjectKeyIdentifier; } } From 99c1835302baba9338c0737d891d4c56e8c1757e Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 13 Jun 2022 06:29:18 -0700 Subject: [PATCH 28/43] SubjectKeyIdentifierBytes back to not-nullable --- .../ExtensionsTests/SubjectKeyIdentifierTests.cs | 15 ++++++--------- .../ref/System.Security.Cryptography.cs | 2 +- .../X509AuthorityKeyIdentifierExtension.cs | 11 ++--------- .../X509SubjectKeyIdentifierExtension.cs | 12 +++++------- 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs index f3f0477dcfcd6..7718ab832776a 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/SubjectKeyIdentifierTests.cs @@ -21,7 +21,7 @@ public static void DefaultConstructor() string skid = e.SubjectKeyIdentifier; Assert.Null(skid); - Assert.False(e.SubjectKeyIdentifierBytes.HasValue, "SubjectKeyIdentifierBytes.HasValue"); + Assert.Throws(() => e.SubjectKeyIdentifierBytes); } [Theory] @@ -60,7 +60,7 @@ public static void EncodeFromBytes(bool fromSpan) AssertExtensions.SequenceEqual( new byte[] { 1, 2, 3, 4 }, - e.SubjectKeyIdentifierBytes.GetValueOrDefault().Span); + e.SubjectKeyIdentifierBytes.Span); } [Fact] @@ -78,7 +78,7 @@ public static void EncodeFromString() AssertExtensions.SequenceEqual( new byte[] { 0x01, 0xAB, 0xCD }, - e.SubjectKeyIdentifierBytes.GetValueOrDefault().Span); + e.SubjectKeyIdentifierBytes.Span); } [Fact] @@ -102,7 +102,7 @@ public static void EncodeFromPublicKey() Assert.Equal( "5971A65A334DDA980780FF841EBE87F9723241F2", - e.SubjectKeyIdentifierBytes.GetValueOrDefault().ByteArrayToHex()); + e.SubjectKeyIdentifierBytes.ByteArrayToHex()); } @@ -152,7 +152,7 @@ public static void DecodeFromBER() Assert.Equal( "5971A65A334DDA980780FF841EBE87F9723241F2", - ext.SubjectKeyIdentifierBytes.GetValueOrDefault().ByteArrayToHex()); + ext.SubjectKeyIdentifierBytes.ByteArrayToHex()); } private static void EncodeDecode( @@ -177,10 +177,7 @@ private static void EncodeDecode( ext = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), critical); Assert.Equal(expectedIdentifier, ext.SubjectKeyIdentifier); - - Assert.Equal( - expectedIdentifier, - ext.SubjectKeyIdentifierBytes.GetValueOrDefault().ByteArrayToHex()); + Assert.Equal(expectedIdentifier, ext.SubjectKeyIdentifierBytes.ByteArrayToHex()); } } } diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 709fabf7ba052..4b51014006956 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3202,7 +3202,7 @@ public X509SubjectKeyIdentifierExtension(System.Security.Cryptography.X509Certif public X509SubjectKeyIdentifierExtension(System.Security.Cryptography.X509Certificates.PublicKey key, System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierHashAlgorithm algorithm, bool critical) { } public X509SubjectKeyIdentifierExtension(string subjectKeyIdentifier, bool critical) { } public string? SubjectKeyIdentifier { get { throw null; } } - public System.ReadOnlyMemory? SubjectKeyIdentifierBytes { get { throw null; } } + public System.ReadOnlyMemory SubjectKeyIdentifierBytes { get { throw null; } } public override void CopyFrom(System.Security.Cryptography.AsnEncodedData asnEncodedData) { } } public enum X509SubjectKeyIdentifierHashAlgorithm diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs index 6c9448008f175..6530099204414 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs @@ -138,13 +138,8 @@ public static X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier { ArgumentNullException.ThrowIfNull(subjectKeyIdentifier); - if (!subjectKeyIdentifier.SubjectKeyIdentifierBytes.HasValue) - { - throw new ArgumentException("Something about the extension has not had a value provided to it"); - } - return CreateFromSubjectKeyIdentifier( - subjectKeyIdentifier.SubjectKeyIdentifierBytes.GetValueOrDefault().Span); + subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span); } public static X509AuthorityKeyIdentifierExtension CreateFromSubjectKeyIdentifier( @@ -271,9 +266,7 @@ public static X509AuthorityKeyIdentifierExtension CreateFromCertificate( throw new CryptographicException("Provided certificate does not have a subject key identifier"); } - // Only the default constructor for the X509SubjectKeyIdentifierExtension produces null - Debug.Assert(skid.SubjectKeyIdentifierBytes.HasValue); - ReadOnlySpan skidBytes = skid.SubjectKeyIdentifierBytes.GetValueOrDefault().Span; + ReadOnlySpan skidBytes = skid.SubjectKeyIdentifierBytes.Span; if (includeIssuerAndSerial) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs index 827548ecd9510..6519a3d10f009 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs @@ -61,18 +61,16 @@ public string? SubjectKeyIdentifier } } - public ReadOnlyMemory? SubjectKeyIdentifierBytes + public ReadOnlyMemory SubjectKeyIdentifierBytes { get { - if (!_decoded) - { - Decode(RawData); - } - + // Rather than check _decoded, this property checks for a null _subjectKeyIdentifier so that using + // the default constructor, not calling CopyFrom, and then calling this property will throw + // instead of using Nullable to talk about that degenerate state. if (_subjectKeyIdentifier is null) { - return default; + Decode(RawData); } return _subjectKeyIdentifier; From aa1b125827991a883d8289cb3f071b514f139b17 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 13 Jun 2022 07:09:09 -0700 Subject: [PATCH 29/43] LoadCertificateRequest => LoadSigningRequest --- .../ref/System.Security.Cryptography.cs | 8 ++++---- .../X509Certificates/CertificateRequest.cs | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 4b51014006956..dfab67fe254b5 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2442,10 +2442,10 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public byte[] CreateSigningRequest(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } public string CreateSigningRequestPem() { throw null; } public string CreateSigningRequestPem(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(byte[] pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequest(System.ReadOnlySpan pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequestPem(System.ReadOnlySpan pkcs10Pem, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } - public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadCertificateRequestPem(string pkcs10Pem, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadSigningRequest(byte[] pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadSigningRequest(System.ReadOnlySpan pkcs10, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadSigningRequestPem(System.ReadOnlySpan pkcs10Pem, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.CertificateRequest LoadSigningRequestPem(string pkcs10Pem, System.Security.Cryptography.HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, bool unsafeLoadCertificateExtensions = false, System.Security.Cryptography.RSASignaturePadding? signerSignaturePadding = null) { throw null; } } public sealed partial class CertificateRevocationListBuilder { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index 52cccab987a18..7b2001390ac97 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -764,7 +764,7 @@ public X509Certificate2 Create( return ret; } - public static unsafe CertificateRequest LoadCertificateRequestPem( + public static unsafe CertificateRequest LoadSigningRequestPem( string pkcs10Pem, HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, @@ -773,7 +773,7 @@ public static unsafe CertificateRequest LoadCertificateRequestPem( { ArgumentNullException.ThrowIfNull(pkcs10Pem); - return LoadCertificateRequestPem( + return LoadSigningRequestPem( pkcs10Pem.AsSpan(), signerHashAlgorithm, skipSignatureValidation, @@ -781,7 +781,7 @@ public static unsafe CertificateRequest LoadCertificateRequestPem( signerSignaturePadding); } - public static unsafe CertificateRequest LoadCertificateRequestPem( + public static unsafe CertificateRequest LoadSigningRequestPem( ReadOnlySpan pkcs10Pem, HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, @@ -802,7 +802,7 @@ public static unsafe CertificateRequest LoadCertificateRequestPem( try { - return LoadCertificateRequest( + return LoadSigningRequest( rented.AsSpan(0, bytesWritten), permitTrailingData: false, signerHashAlgorithm, @@ -821,7 +821,7 @@ public static unsafe CertificateRequest LoadCertificateRequestPem( throw new CryptographicException(SR.Cryptography_NoPemOfLabel, PemLabels.Pkcs10CertificateRequest); } - public static unsafe CertificateRequest LoadCertificateRequest( + public static unsafe CertificateRequest LoadSigningRequest( byte[] pkcs10, HashAlgorithmName signerHashAlgorithm, bool skipSignatureValidation = false, @@ -830,7 +830,7 @@ public static unsafe CertificateRequest LoadCertificateRequest( { ArgumentNullException.ThrowIfNull(pkcs10); - return LoadCertificateRequest( + return LoadSigningRequest( pkcs10, permitTrailingData: false, signerHashAlgorithm, @@ -840,7 +840,7 @@ public static unsafe CertificateRequest LoadCertificateRequest( signerSignaturePadding); } - public static unsafe CertificateRequest LoadCertificateRequest( + public static unsafe CertificateRequest LoadSigningRequest( ReadOnlySpan pkcs10, HashAlgorithmName signerHashAlgorithm, out int bytesConsumed, @@ -848,7 +848,7 @@ public static unsafe CertificateRequest LoadCertificateRequest( bool unsafeLoadCertificateExtensions = false, RSASignaturePadding? signerSignaturePadding = null) { - return LoadCertificateRequest( + return LoadSigningRequest( pkcs10, permitTrailingData: true, signerHashAlgorithm, @@ -858,7 +858,7 @@ public static unsafe CertificateRequest LoadCertificateRequest( signerSignaturePadding); } - private static unsafe CertificateRequest LoadCertificateRequest( + private static unsafe CertificateRequest LoadSigningRequest( ReadOnlySpan pkcs10, bool permitTrailingData, HashAlgorithmName signerHashAlgorithm, From 97791b90e89db57c24cf789f8297ba9a4cb9aac2 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 13 Jun 2022 11:34:42 -0700 Subject: [PATCH 30/43] Commit missing test changes --- .../CertificateRequestLoadTests.cs | 30 +++++++++---------- .../tests/CertificateCreation/DontBeACA.cs | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs index a385a5d78f4fb..c2128357b5e5b 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CertificateRequestLoadTests.cs @@ -23,7 +23,7 @@ public static void LoadBigExponentRequest_Span(bool loadExtensions, bool oversiz Array.Resize(ref pkcs10, pkcs10.Length + 22); } - CertificateRequest req = CertificateRequest.LoadCertificateRequest( + CertificateRequest req = CertificateRequest.LoadSigningRequest( new ReadOnlySpan(pkcs10), HashAlgorithmName.SHA256, out int bytesConsumed, @@ -38,7 +38,7 @@ public static void LoadBigExponentRequest_Span(bool loadExtensions, bool oversiz [InlineData(false)] public static void LoadBigExponentRequest_Bytes(bool loadExtensions) { - CertificateRequest req = CertificateRequest.LoadCertificateRequest( + CertificateRequest req = CertificateRequest.LoadSigningRequest( TestData.BigExponentPkcs10Bytes, HashAlgorithmName.SHA256, unsafeLoadCertificateExtensions: loadExtensions); @@ -55,7 +55,7 @@ public static void LoadBigExponentRequest_Bytes_Oversized(bool loadExtensions) Array.Resize(ref pkcs10, pkcs10.Length + 2); Assert.Throws( - () => CertificateRequest.LoadCertificateRequest( + () => CertificateRequest.LoadSigningRequest( pkcs10, HashAlgorithmName.SHA256, unsafeLoadCertificateExtensions: loadExtensions)); @@ -88,7 +88,7 @@ public static void LoadBigExponentRequest_PemString(bool loadExtensions, bool mu -----END CERTIFICATE REQUEST-----"; } - CertificateRequest req = CertificateRequest.LoadCertificateRequestPem( + CertificateRequest req = CertificateRequest.LoadSigningRequestPem( pem, HashAlgorithmName.SHA256, unsafeLoadCertificateExtensions: loadExtensions); @@ -125,7 +125,7 @@ More Text. -----END CERTIFICATE REQUEST-----"; } - CertificateRequest req = CertificateRequest.LoadCertificateRequestPem( + CertificateRequest req = CertificateRequest.LoadSigningRequestPem( pem.AsSpan(), HashAlgorithmName.SHA256, unsafeLoadCertificateExtensions: loadExtensions); @@ -200,12 +200,12 @@ public static void VerifySignature_ECDsa(string hashAlgorithm) } // Assert.NoThrow - CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _); + CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _); pkcs10[^1] ^= 0xFF; Assert.Throws( - () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + () => CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _)); } } @@ -238,15 +238,15 @@ public static void VerifySignature_RSA_PKCS1(string hashAlgorithm) } // Assert.NoThrow - CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _); + CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _); pkcs10[^1] ^= 0xFF; Assert.Throws( - () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + () => CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _)); // Assert.NoThrow - CertificateRequest.LoadCertificateRequest( + CertificateRequest.LoadSigningRequest( pkcs10, hashAlgorithmName, out _, @@ -283,15 +283,15 @@ public static void VerifySignature_RSA_PSS(string hashAlgorithm) } // Assert.NoThrow - CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _); + CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _); pkcs10[^1] ^= 0xFF; Assert.Throws( - () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + () => CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _)); // Assert.NoThrow - CertificateRequest.LoadCertificateRequest( + CertificateRequest.LoadSigningRequest( pkcs10, hashAlgorithmName, out _, @@ -319,10 +319,10 @@ public static void VerifySignature_DSA(string hashAlgorithm) // The inbox version doesn't support DSA Assert.Throws( - () => CertificateRequest.LoadCertificateRequest(pkcs10, hashAlgorithmName, out _)); + () => CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _)); // Assert.NoThrow - CertificateRequest.LoadCertificateRequest( + CertificateRequest.LoadSigningRequest( pkcs10, hashAlgorithmName, out _, diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs index ca993c15d3ea1..226b57eec5103 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -52,7 +52,7 @@ public static void EndToEnd() static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCert) { - CertificateRequest req = CertificateRequest.LoadCertificateRequest( + CertificateRequest req = CertificateRequest.LoadSigningRequest( pkcs10, HashAlgorithmName.SHA256, out int bytesConsumed, From b6f2a10d60746fba099a1e9727ed18cede49e932 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 13 Jun 2022 11:35:23 -0700 Subject: [PATCH 31/43] Move RelativeDistinguishedName to non-nested-level type --- .../tests/CertificateCreation/DontBeACA.cs | 3 +- .../ref/System.Security.Cryptography.cs | 18 +++--- .../src/System.Security.Cryptography.csproj | 1 + .../X509Certificates/X500DistinguishedName.cs | 61 +++---------------- .../X500RelativeDistinguishedName.cs | 50 +++++++++++++++ .../X509Certificates/X509Certificate2.cs | 5 +- 6 files changed, 73 insertions(+), 65 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs index 226b57eec5103..e6298ed521830 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -408,8 +408,7 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe } else { - foreach (X500DistinguishedName.RelativeDistinguishedName rdn in - req.SubjectName.EnumerateRelativeDistinguishedNames()) + foreach (X500RelativeDistinguishedName rdn in req.SubjectName.EnumerateRelativeDistinguishedNames()) { if (rdn.HasMultipleValues) { diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index dfab67fe254b5..d1e0624079326 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2557,16 +2557,8 @@ public X500DistinguishedName(string distinguishedName) { } public X500DistinguishedName(string distinguishedName, System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags flag) { } public string Name { get { throw null; } } public string Decode(System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags flag) { throw null; } - public System.Collections.Generic.IEnumerable EnumerateRelativeDistinguishedNames(bool reversed = true) { throw null; } + public System.Collections.Generic.IEnumerable EnumerateRelativeDistinguishedNames(bool reversed = true) { throw null; } public override string Format(bool multiLine) { throw null; } - public sealed partial class RelativeDistinguishedName - { - internal RelativeDistinguishedName() { } - public bool HasMultipleValues { get { throw null; } } - public System.ReadOnlyMemory RawData { get { throw null; } } - public System.Security.Cryptography.Oid? SingleValueType { get { throw null; } } - public string? SingleValueValue { get { throw null; } } - } } public sealed partial class X500DistinguishedNameBuilder { @@ -2597,6 +2589,14 @@ public enum X500DistinguishedNameFlags UseT61Encoding = 8192, ForceUTF8Encoding = 16384, } + public sealed partial class X500RelativeDistinguishedName + { + internal X500RelativeDistinguishedName() { } + public bool HasMultipleValues { get { throw null; } } + public System.ReadOnlyMemory RawData { get { throw null; } } + public System.Security.Cryptography.Oid? SingleValueType { get { throw null; } } + public string? SingleValueValue { get { throw null; } } + } public sealed partial class X509AuthorityInformationAccessExtension : System.Security.Cryptography.X509Certificates.X509Extension { public X509AuthorityInformationAccessExtension() { } diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 54aa2f040549f..c217c4f5a1d42 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -451,6 +451,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs index 59bef117fdf58..4311fe06d9568 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedName.cs @@ -10,7 +10,7 @@ namespace System.Security.Cryptography.X509Certificates public sealed class X500DistinguishedName : AsnEncodedData { private volatile string? _lazyDistinguishedName; - private List? _parsedAttributes; + private List? _parsedAttributes; public X500DistinguishedName(byte[] encodedDistinguishedName) : base(new Oid(null, null), encodedDistinguishedName) @@ -89,9 +89,9 @@ public override string Format(bool multiLine) /// /// The X.500 Name is not a proper DER-encoded X.500 Name value. /// - public IEnumerable EnumerateRelativeDistinguishedNames(bool reversed = true) + public IEnumerable EnumerateRelativeDistinguishedNames(bool reversed = true) { - List parsedAttributes = _parsedAttributes ??= ParseAttributes(RawData); + List parsedAttributes = _parsedAttributes ??= ParseAttributes(RawData); return EnumerateRelativeDistinguishedNames(parsedAttributes, reversed); } @@ -114,8 +114,8 @@ private static void ThrowIfInvalid(X500DistinguishedNameFlags flags) throw new ArgumentException(SR.Format(SR.Arg_EnumIllegalVal, "flag")); } - private static IEnumerable EnumerateRelativeDistinguishedNames( - List parsedAttributes, + private static IEnumerable EnumerateRelativeDistinguishedNames( + List parsedAttributes, bool reversed) { if (reversed) @@ -134,9 +134,9 @@ private static IEnumerable EnumerateRelativeDistingui } } - private static List ParseAttributes(byte[] rawData) + private static List ParseAttributes(byte[] rawData) { - List? parsedAttributes = null; + List? parsedAttributes = null; ReadOnlyMemory rawDataMemory = rawData; ReadOnlySpan rawDataSpan = rawData; @@ -156,9 +156,9 @@ private static List ParseAttributes(byte[] rawData) throw new UnreachableException(); } - var rdn = new RelativeDistinguishedName(rawDataMemory.Slice(offset, encodedValue.Length)); + var rdn = new X500RelativeDistinguishedName(rawDataMemory.Slice(offset, encodedValue.Length)); sequence.ReadEncodedValue(); - (parsedAttributes ??= new List()).Add(rdn); + (parsedAttributes ??= new List()).Add(rdn); } } catch (AsnContentException e) @@ -166,48 +166,7 @@ private static List ParseAttributes(byte[] rawData) throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } - return parsedAttributes ?? new List(); - } - - public sealed class RelativeDistinguishedName - { - public ReadOnlyMemory RawData { get; } - public bool HasMultipleValues { get; } - public Oid? SingleValueType { get; } - public string? SingleValueValue { get; } - - internal RelativeDistinguishedName(ReadOnlyMemory rawData) - { - RawData = rawData; - - AsnValueReader outer = new AsnValueReader(rawData.Span, AsnEncodingRules.DER); - - // Windows does not enforce the sort order on multi-value RDNs. - AsnValueReader rdn = outer.ReadSetOf(skipSortOrderValidation: true); - AsnValueReader typeAndValue = rdn.ReadSequence(); - - Oid firstType = Oids.GetSharedOrNewOid(ref typeAndValue); - string firstValue = typeAndValue.ReadAnyAsnString(); - typeAndValue.ThrowIfNotEmpty(); - - if (rdn.HasData) - { - HasMultipleValues = true; - - while (rdn.HasData) - { - typeAndValue = rdn.ReadSequence(); - Oids.GetSharedOrNewOid(ref typeAndValue); - typeAndValue.ReadAnyAsnString(); - typeAndValue.ThrowIfNotEmpty(); - } - } - else - { - SingleValueType = firstType; - SingleValueValue = firstValue; - } - } + return parsedAttributes ?? new List(); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs new file mode 100644 index 0000000000000..f255d34c1601e --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Asn1; + +namespace System.Security.Cryptography.X509Certificates +{ + public sealed class X500RelativeDistinguishedName + { + public ReadOnlyMemory RawData { get; } + public bool HasMultipleValues { get; } + public Oid? SingleValueType { get; } + public string? SingleValueValue { get; } + + internal X500RelativeDistinguishedName(ReadOnlyMemory rawData) + { + RawData = rawData; + + AsnValueReader outer = new AsnValueReader(rawData.Span, AsnEncodingRules.DER); + + // Windows does not enforce the sort order on multi-value RDNs. + AsnValueReader rdn = outer.ReadSetOf(skipSortOrderValidation: true); + AsnValueReader typeAndValue = rdn.ReadSequence(); + + Oid firstType = Oids.GetSharedOrNewOid(ref typeAndValue); + string firstValue = typeAndValue.ReadAnyAsnString(); + typeAndValue.ThrowIfNotEmpty(); + + if (rdn.HasData) + { + HasMultipleValues = true; + + while (rdn.HasData) + { + typeAndValue = rdn.ReadSequence(); + Oids.GetSharedOrNewOid(ref typeAndValue); + typeAndValue.ReadAnyAsnString(); + typeAndValue.ThrowIfNotEmpty(); + } + } + else + { + SingleValueType = firstType; + SingleValueValue = firstValue; + } + } + } +} 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 1ba8fa2aa3942..b1fe57d92b812 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 @@ -1355,10 +1355,9 @@ public bool MatchesHostname(string hostname, bool allowWildcards = true, bool al if (allowCommonName) { - X500DistinguishedName.RelativeDistinguishedName? cn = null; + X500RelativeDistinguishedName? cn = null; - foreach (X500DistinguishedName.RelativeDistinguishedName rdn in - SubjectName.EnumerateRelativeDistinguishedNames()) + foreach (X500RelativeDistinguishedName rdn in SubjectName.EnumerateRelativeDistinguishedNames()) { if (rdn.HasMultipleValues) { From 3322643b204a66028faadd2cf1ea763e7e0f3c39 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 13 Jun 2022 15:43:48 -0700 Subject: [PATCH 32/43] Delay the processing of RDN's Single-Value-Value --- .../tests/CertificateCreation/DontBeACA.cs | 9 +- .../tests/X500DistinguishedNameTests.cs | 63 +++++++++- .../ref/System.Security.Cryptography.cs | 2 +- .../X500RelativeDistinguishedName.cs | 111 ++++++++++++++++-- .../X509Certificates/X509Certificate2.cs | 2 +- 5 files changed, 174 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs index e6298ed521830..b035116b5824e 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/DontBeACA.cs @@ -423,13 +423,16 @@ static CertificateRequest IngestRequest(byte[] pkcs10, X509Certificate2 issuerCe throw new InvalidOperationException("CN was specified more than once"); } - if (rdn.SingleValueValue.Length == 0 || rdn.SingleValueValue.IndexOfAny(new[] { ' ', '*' }) > -1 || - !rdn.SingleValueValue.EndsWith(".fruit.example")) + string? cnValue = rdn.GetSingleValueValue(); + + if (string.IsNullOrEmpty(cnValue) || + cnValue.IndexOfAny(new[] { ' ', '*' }) > -1 || + !cnValue.EndsWith(".fruit.example")) { throw new InvalidOperationException("CN is unauthorized"); } - cn = rdn.SingleValueValue; + cn = cnValue; break; default: acceptSubject = false; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs index e364614d4ec93..78c871d636a76 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.InteropServices; +using System.Collections.Generic; using Test.Cryptography; using Xunit; @@ -230,6 +230,67 @@ public static void NameWithSTIdentifierForState() Assert.Equal("C=US, S=VA", dn.Decode(X500DistinguishedNameFlags.None)); } + [Fact] + public static void EnumeratorWithNonTextualData() + { + // OID.2.5.4.106=#06032A0304, CN=localhost, OU=.NET Framework (CoreFX), O=Microsoft Corporation, + // L=Redmond, S=Washington, C=US + byte[] encoded = + { + 0x30, 0x81, 0x98, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, + 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x0A, 0x57, 0x61, 0x73, 0x68, 0x69, + 0x6E, 0x67, 0x74, 0x6F, 0x6E, 0x31, 0x10, 0x30, 0x0E, 0x06, 0x03, 0x55, 0x04, 0x07, 0x13, 0x07, + 0x52, 0x65, 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x31, 0x1E, 0x30, 0x1C, 0x06, 0x03, 0x55, 0x04, 0x0A, + 0x13, 0x15, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, 0x43, 0x6F, 0x72, 0x70, + 0x6F, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x31, 0x20, 0x30, 0x1E, 0x06, 0x03, 0x55, 0x04, 0x0B, + 0x13, 0x17, 0x2E, 0x4E, 0x45, 0x54, 0x20, 0x46, 0x72, 0x61, 0x6D, 0x65, 0x77, 0x6F, 0x72, 0x6B, + 0x20, 0x28, 0x43, 0x6F, 0x72, 0x65, 0x46, 0x58, 0x29, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, + 0x04, 0x03, 0x13, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 0x73, 0x74, 0x31, 0x0C, 0x30, + 0x0A, 0x06, 0x03, 0x55, 0x04, 0x6A, 0x06, 0x03, 0x2A, 0x03, 0x04, + }; + + X500DistinguishedName dn = new X500DistinguishedName(encoded); + IEnumerable able = dn.EnumerateRelativeDistinguishedNames(false); + IEnumerator ator = able.GetEnumerator(); + + Assert.True(ator.MoveNext(), "ator.MoveNext() 1"); + Assert.False(ator.Current.HasMultipleValues, "ator.Current.HasMultipleValues 1"); + Assert.Equal("2.5.4.6", ator.Current.SingleValueType.Value); + Assert.Equal("US", ator.Current.GetSingleValueValue()); + + Assert.True(ator.MoveNext(), "ator.MoveNext() 2"); + Assert.False(ator.Current.HasMultipleValues, "ator.Current.HasMultipleValues 2"); + Assert.Equal("2.5.4.8", ator.Current.SingleValueType.Value); + Assert.Equal("Washington", ator.Current.GetSingleValueValue()); + + Assert.True(ator.MoveNext(), "ator.MoveNext() 3"); + Assert.False(ator.Current.HasMultipleValues, "ator.Current.HasMultipleValues 3"); + Assert.Equal("2.5.4.7", ator.Current.SingleValueType.Value); + Assert.Equal("Redmond", ator.Current.GetSingleValueValue()); + + Assert.True(ator.MoveNext(), "ator.MoveNext() 4"); + Assert.False(ator.Current.HasMultipleValues, "ator.Current.HasMultipleValues 4"); + Assert.Equal("2.5.4.10", ator.Current.SingleValueType.Value); + Assert.Equal("Microsoft Corporation", ator.Current.GetSingleValueValue()); + + Assert.True(ator.MoveNext(), "ator.MoveNext() 5"); + Assert.False(ator.Current.HasMultipleValues, "ator.Current.HasMultipleValues 5"); + Assert.Equal("2.5.4.11", ator.Current.SingleValueType.Value); + Assert.Equal(".NET Framework (CoreFX)", ator.Current.GetSingleValueValue()); + + Assert.True(ator.MoveNext(), "ator.MoveNext() 6"); + Assert.False(ator.Current.HasMultipleValues, "ator.Current.HasMultipleValues 6"); + Assert.Equal("2.5.4.3", ator.Current.SingleValueType.Value); + Assert.Equal("localhost", ator.Current.GetSingleValueValue()); + + Assert.True(ator.MoveNext(), "ator.MoveNext() 7"); + Assert.False(ator.Current.HasMultipleValues, "ator.Current.HasMultipleValues 7"); + Assert.Equal("2.5.4.106", ator.Current.SingleValueType.Value); + Assert.Null(ator.Current.GetSingleValueValue()); + + Assert.False(ator.MoveNext(), "ator.MoveNext() 8"); + } + public static readonly object[][] WhitespaceBeforeCases = { // Regular space. diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index d1e0624079326..0bc8da8eeff28 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2595,7 +2595,7 @@ internal X500RelativeDistinguishedName() { } public bool HasMultipleValues { get { throw null; } } public System.ReadOnlyMemory RawData { get { throw null; } } public System.Security.Cryptography.Oid? SingleValueType { get { throw null; } } - public string? SingleValueValue { get { throw null; } } + public string? GetSingleValueValue() { throw null; } } public sealed partial class X509AuthorityInformationAccessExtension : System.Security.Cryptography.X509Certificates.X509Extension { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs index f255d34c1601e..554664cc01309 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs @@ -1,31 +1,60 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.Diagnostics; using System.Formats.Asn1; namespace System.Security.Cryptography.X509Certificates { + /// + /// Represents a Relative Distinguished Name component of an X.500 Distinguished Name. + /// + /// public sealed class X500RelativeDistinguishedName { - public ReadOnlyMemory RawData { get; } + private readonly ReadOnlyMemory _singleValueValue; + + /// + /// Gets a value that indicates whether this Relative Distinguished Name is composed + /// of multiple attributes or only a single attribute. + /// + /// + /// if the Relative Distinguished Name is composed of multiple + /// attributes; if it is composed of only a single attribute. + /// public bool HasMultipleValues { get; } + + /// + /// Gets the encoded representation of this Relative Distinguished Name. + /// + /// + /// The encoded representation of this Relative Distinguished Name. + /// + public ReadOnlyMemory RawData { get; } + + /// + /// When applicable, gets the object identifier (OID) identifying the single attribute + /// value for this Relative Distinguished Name. + /// + /// + /// The object identifier (OID) identifying the single attribute value for this Relative + /// Distinguished Name (RDN), or if the RDN has multiple attributes. + /// public Oid? SingleValueType { get; } - public string? SingleValueValue { get; } internal X500RelativeDistinguishedName(ReadOnlyMemory rawData) { RawData = rawData; - AsnValueReader outer = new AsnValueReader(rawData.Span, AsnEncodingRules.DER); + ReadOnlySpan rawDataSpan = rawData.Span; + AsnValueReader outer = new AsnValueReader(rawDataSpan, AsnEncodingRules.DER); // Windows does not enforce the sort order on multi-value RDNs. AsnValueReader rdn = outer.ReadSetOf(skipSortOrderValidation: true); AsnValueReader typeAndValue = rdn.ReadSequence(); Oid firstType = Oids.GetSharedOrNewOid(ref typeAndValue); - string firstValue = typeAndValue.ReadAnyAsnString(); + ReadOnlySpan firstValue = typeAndValue.ReadEncodedValue(); typeAndValue.ThrowIfNotEmpty(); if (rdn.HasData) @@ -35,7 +64,12 @@ internal X500RelativeDistinguishedName(ReadOnlyMemory rawData) while (rdn.HasData) { typeAndValue = rdn.ReadSequence(); - Oids.GetSharedOrNewOid(ref typeAndValue); + + if (Oids.GetSharedOrNullOid(ref typeAndValue) is null) + { + typeAndValue.ReadObjectIdentifier(); + } + typeAndValue.ReadAnyAsnString(); typeAndValue.ThrowIfNotEmpty(); } @@ -43,8 +77,71 @@ internal X500RelativeDistinguishedName(ReadOnlyMemory rawData) else { SingleValueType = firstType; - SingleValueValue = firstValue; + + if (!rawDataSpan.Overlaps(firstValue, out int offset)) + { + Debug.Fail("AsnValueReader.ReadEncodedValue returned data outside its bounds"); + throw new UnreachableException(); + } + + _singleValueValue = rawData.Slice(offset, firstValue.Length); + } + } + + /// + /// Gets the textual representation of the value for the Relative Distinguished Name (RDN), + /// when the RDN only contains one attribute. + /// + /// + /// if the encoded value is not a textual data type, such as an + /// OCTET STRING; otherwise, the textual representation of the value. + /// + /// + /// The attribute is identified as a textual value, but the value did not successfully decode. + /// + /// + /// The Relative Distinguished Name has multiple attributes ( + /// is ). + /// + public string? GetSingleValueValue() + { + if (_singleValueValue.IsEmpty) + { + throw new InvalidOperationException( + "Something about multi-valued RDNs not being Single-Value"); } + + // X.520 defines a few non-textual attributes, such as objectIdentifier (2.5.4.106), + // which Windows renders textually as the bytes in hexadecimal preceded by an octothorpe, + // e.g. #06032A0304 for an objectIdentifier attribute whose value is the OID 1.2.3.4 + // + // For these, we return null, and then let the X500Name.Format code handle the hex fallback. + + try + { + AsnValueReader reader = new AsnValueReader(_singleValueValue.Span, AsnEncodingRules.BER); + Asn1Tag tag = reader.PeekTag(); + + if (tag.TagClass == TagClass.Universal) + { + switch ((UniversalTagNumber)tag.TagValue) + { + case UniversalTagNumber.BMPString: + case UniversalTagNumber.UTF8String: + case UniversalTagNumber.IA5String: + case UniversalTagNumber.PrintableString: + case UniversalTagNumber.NumericString: + case UniversalTagNumber.T61String: + return reader.ReadAnyAsnString(); + } + } + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + + return null; } } } 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 b1fe57d92b812..9f47357c17987 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 @@ -1391,7 +1391,7 @@ public bool MatchesHostname(string hostname, bool allowWildcards = true, bool al if (cn is not null) { - return hostname.Equals(cn.SingleValueValue, StringComparison.OrdinalIgnoreCase); + return hostname.Equals(cn.GetSingleValueValue(), StringComparison.OrdinalIgnoreCase); } } From 34e14b3ac97aba2d775fbcea47e024177cb7c06d Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Tue, 14 Jun 2022 06:23:32 -0700 Subject: [PATCH 33/43] Make HasMultipleValues computed, add MemberNotNullWhen --- .../ref/System.Security.Cryptography.cs | 1 + .../X500RelativeDistinguishedName.cs | 24 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 0bc8da8eeff28..a3a8fb93a7c59 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2592,6 +2592,7 @@ public enum X500DistinguishedNameFlags public sealed partial class X500RelativeDistinguishedName { internal X500RelativeDistinguishedName() { } + [System.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute(false, "SingleValueType")] public bool HasMultipleValues { get { throw null; } } public System.ReadOnlyMemory RawData { get { throw null; } } public System.Security.Cryptography.Oid? SingleValueType { get { throw null; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs index 554664cc01309..396273fbda2a5 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500RelativeDistinguishedName.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Formats.Asn1; namespace System.Security.Cryptography.X509Certificates @@ -14,16 +15,6 @@ public sealed class X500RelativeDistinguishedName { private readonly ReadOnlyMemory _singleValueValue; - /// - /// Gets a value that indicates whether this Relative Distinguished Name is composed - /// of multiple attributes or only a single attribute. - /// - /// - /// if the Relative Distinguished Name is composed of multiple - /// attributes; if it is composed of only a single attribute. - /// - public bool HasMultipleValues { get; } - /// /// Gets the encoded representation of this Relative Distinguished Name. /// @@ -59,8 +50,6 @@ internal X500RelativeDistinguishedName(ReadOnlyMemory rawData) if (rdn.HasData) { - HasMultipleValues = true; - while (rdn.HasData) { typeAndValue = rdn.ReadSequence(); @@ -88,6 +77,17 @@ internal X500RelativeDistinguishedName(ReadOnlyMemory rawData) } } + /// + /// Gets a value that indicates whether this Relative Distinguished Name is composed + /// of multiple attributes or only a single attribute. + /// + /// + /// if the Relative Distinguished Name is composed of multiple + /// attributes; if it is composed of only a single attribute. + /// + [MemberNotNullWhen(false, nameof(SingleValueType))] + public bool HasMultipleValues => SingleValueType is null; + /// /// Gets the textual representation of the value for the Relative Distinguished Name (RDN), /// when the RDN only contains one attribute. From af0b92260c94f14e1e804b322f0444fb751d0fb0 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Tue, 14 Jun 2022 07:23:18 -0700 Subject: [PATCH 34/43] Add more RDN tests --- .../tests/X500DistinguishedNameTests.cs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs index 78c871d636a76..62d071a5e0713 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X500DistinguishedNameTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Formats.Asn1; +using System.Text; using Test.Cryptography; using Xunit; @@ -291,6 +293,129 @@ public static void EnumeratorWithNonTextualData() Assert.False(ator.MoveNext(), "ator.MoveNext() 8"); } + [Fact] + public static void EnumeratorWithInvalidData() + { + // A variety of encodings of a country name, except it as two versions of + // the CountryCode3n attribute, both say they are NumericString (as matching the spec), + // but the latter one has the correct value (840) and the earlier one has the 3c text (USA). + X500DistinguishedName dn = new X500DistinguishedName(( + "304C31133011060B2B0601040182373C02010313025553310C300A0603550462" + + "1303555341310C300A06035504631203555341310C300A060355046312033834" + + "30310B3009060355040613025553").HexToByteArray()); + + int index = 0; + + foreach (X500RelativeDistinguishedName rdn in dn.EnumerateRelativeDistinguishedNames(reversed: false)) + { + switch (index) + { + case 0: + Assert.False(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Equal("1.3.6.1.4.1.311.60.2.1.3", rdn.SingleValueType.Value); + Assert.Equal("US", rdn.GetSingleValueValue()); + break; + case 1: + Assert.False(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Equal("2.5.4.98", rdn.SingleValueType.Value); + Assert.Equal("USA", rdn.GetSingleValueValue()); + break; + case 2: + Assert.False(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Equal("2.5.4.99", rdn.SingleValueType.Value); + + CryptographicException ex = Assert.Throws( + () => rdn.GetSingleValueValue()); + + Assert.IsType(ex.InnerException); + Assert.IsType(ex.InnerException.InnerException); + break; + case 3: + Assert.False(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Equal("2.5.4.99", rdn.SingleValueType.Value); + Assert.Equal("840", rdn.GetSingleValueValue()); + break; + case 4: + Assert.False(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Equal("2.5.4.6", rdn.SingleValueType.Value); + Assert.Equal("US", rdn.GetSingleValueValue()); + break; + default: + Assert.Fail($"Enumeration produced an unexpected {index}th result"); + break; + } + + index++; + } + + Assert.Equal(5, index); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void EnumerateWithMultiValueRdn(bool fromSpan) + { + ReadOnlySpan encoded = stackalloc byte[] + { + 0x30, 0x3D, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x20, 0x30, 0x0C, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x05, 0x4A, 0x61, 0x6D, 0x65, 0x73, 0x30, + 0x10, 0x06, 0x03, 0x55, 0x04, 0x0A, 0x13, 0x09, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, + 0x74, 0x31, 0x0C, 0x30, 0x0A, 0x06, 0x03, 0x55, 0x04, 0x63, 0x12, 0x03, 0x38, 0x34, 0x30, + }; + + X500DistinguishedName dn; + + if (fromSpan) + { + dn = new X500DistinguishedName(encoded); + } + else + { + byte[] tmp = encoded.ToArray(); + dn = new X500DistinguishedName(tmp); + + // Updating encoded here is important for the !Overlaps test. + encoded = tmp; + } + + int index = 0; + + foreach (X500RelativeDistinguishedName rdn in dn.EnumerateRelativeDistinguishedNames(reversed: false)) + { + switch (index) + { + case 0: + Assert.False(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Equal("2.5.4.6", rdn.SingleValueType.Value); + Assert.Equal("US", rdn.GetSingleValueValue()); + break; + case 1: + Assert.True(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Null(rdn.SingleValueType); + Assert.Throws(() => rdn.GetSingleValueValue()); + + ReadOnlySpan expected = encoded.Slice(15, 34); + AssertExtensions.SequenceEqual(expected, rdn.RawData.Span); + Assert.False(expected.Overlaps(rdn.RawData.Span), "expected.Overlaps(rdn.RawData.Span)"); + + break; + case 2: + Assert.False(rdn.HasMultipleValues, $"rdn.HasMultipleValues {index}"); + Assert.Equal("2.5.4.99", rdn.SingleValueType.Value); + Assert.Equal("840", rdn.GetSingleValueValue()); + break; + default: + Assert.Fail($"Enumeration produced an unexpected {index}th result"); + break; + } + + index++; + } + + Assert.Equal(3, index); + } + public static readonly object[][] WhitespaceBeforeCases = { // Regular space. From 80035815d59d9154a937ce07cc9e4ecd133ab70d Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Tue, 14 Jun 2022 15:57:25 -0700 Subject: [PATCH 35/43] Move CRL HashAlgorithm and RSAPadding to parameters And start breaking up the cert overload Build argument validaton --- .../X509Certificates/CertificateAuthority.cs | 5 +- .../CertificateCreation/CrlBuilderTests.cs | 188 ++++++++++-------- .../ref/System.Security.Cryptography.cs | 10 +- .../CertificateRevocationListBuilder.cs | 136 ++++++++----- 4 files changed, 196 insertions(+), 143 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index d6ab6817338b3..c0e20a035ae77 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -308,8 +308,6 @@ internal byte[] GetCrl() } CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); - builder.RSASignaturePadding = RSASignaturePadding.Pkcs1; - builder.HashAlgorithm = HashAlgorithmName.SHA256; if (_revocationList is not null) { @@ -337,10 +335,11 @@ internal byte[] GetCrl() { crl = builder.Build( CorruptRevocationIssuerName ? s_nonParticipatingName : _cert.SubjectName, - X509SignatureGenerator.CreateForRSA(key, builder.RSASignaturePadding), + X509SignatureGenerator.CreateForRSA(key, RSASignaturePadding.Pkcs1), _crlNumber, nextUpdate, thisUpdate, + HashAlgorithmName.SHA256, _akidExtension ??= CreateAkidExtension()); if (CorruptRevocationSignature) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index c66c9a87cb902..40c453e24ec5a 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -1,6 +1,9 @@ // Licensed to the.NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; using Test.Cryptography; using Xunit; @@ -8,6 +11,8 @@ namespace System.Security.Cryptography.X509Certificates.Tests.CertificateCreatio { public static class CrlBuilderTests { + private const string CertParam = "issuerCertificate"; + [Fact] public static void AddEntryArgumentValidation() { @@ -25,99 +30,118 @@ public static void AddEntryArgumentValidation() } [Fact] - public static void BuildWithIssuerCertArgumentValidation() + public static void BuildWithNullCertificate() { DateTimeOffset now = DateTimeOffset.UtcNow; - DateTimeOffset notBefore = now.AddMinutes(-5); - DateTimeOffset notAfter = now.AddMinutes(5); - DateTimeOffset thisUpdate = now; - DateTimeOffset nextUpdate = now.AddMinutes(1); - - const string ParamName = "issuerCertificate"; CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); - Assert.Throws(ParamName, () => builder.Build(null, 0, now)); + Assert.Throws(CertParam, () => builder.Build(null, 0, now, HashAlgorithmName.SHA256)); + Assert.Throws(CertParam, () => builder.Build(null, 0, now, now, HashAlgorithmName.SHA256)); + } - using (ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP384)) + [Fact] + public static void BuildWithNoPrivateKeyCertificate() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + using (X509Certificate2 cert = new X509Certificate2(TestData.MsCertificatePemBytes)) { - CertificateRequest certReq = new CertificateRequest("CN=Bad CA", key, HashAlgorithmName.SHA384); + ArgumentException e; + + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now, HashAlgorithmName.SHA256)); + + Assert.Contains("private key", e.Message); + + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now, now, HashAlgorithmName.SHA256)); - using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + Assert.Contains("private key", e.Message); + } + } + + [Fact] + public static void BuildWithCertificateWithNoBasicConstraints() + { + BuildCertificateAndRun( + Enumerable.Empty(), + static (cert, now) => { - ArgumentException ex; - - using (X509Certificate2 pubOnly = new X509Certificate2(cert.RawDataMemory.Span)) - { - ex = Assert.Throws(ParamName, () => builder.Build(pubOnly, 0, nextUpdate)); - Assert.Contains("private key", ex.Message); - - ex = Assert.Throws(ParamName, () => builder.Build(pubOnly, 0, nextUpdate, thisUpdate)); - Assert.Contains("private key", ex.Message); - } - - ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate)); - Assert.Contains("Basic Constraints", ex.Message); - Assert.DoesNotContain("appropriate", ex.Message); - - ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate, thisUpdate)); - Assert.Contains("Basic Constraints", ex.Message); - Assert.DoesNotContain("appropriate", ex.Message); - } + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + ArgumentException e; + + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now.AddMinutes(5), HashAlgorithmName.SHA256)); + + Assert.Contains("Basic Constraints", e.Message); + Assert.DoesNotContain("appropriate", e.Message); - certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now.AddMinutes(5), now, HashAlgorithmName.SHA256)); - using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + Assert.Contains("Basic Constraints", e.Message); + Assert.DoesNotContain("appropriate", e.Message); + }); + } + + [Fact] + public static void BuildWithCertificateWithBadBasicConstraints() + { + BuildCertificateAndRun( + new X509Extension[] { - ArgumentException ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate)); - Assert.Contains("Basic Constraints", ex.Message); - Assert.Contains("appropriate", ex.Message); + X509BasicConstraintsExtension.CreateForEndEntity(), + }, + static (cert, now) => + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); - ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate, thisUpdate)); - Assert.Contains("Basic Constraints", ex.Message); - Assert.Contains("appropriate", ex.Message); - } + ArgumentException e; + + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now.AddMinutes(5), HashAlgorithmName.SHA256)); + + Assert.Contains("Basic Constraints", e.Message); + Assert.Contains("appropriate", e.Message); + + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now.AddMinutes(5), now, HashAlgorithmName.SHA256)); - certReq.CertificateExtensions.Clear(); - certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); - certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign, true)); + Assert.Contains("Basic Constraints", e.Message); + Assert.Contains("appropriate", e.Message); + }); + } + + private static void BuildCertificateAndRun( + IEnumerable extensions, + Action action, + [CallerMemberName] string callerName = null) + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + $"CN=\"{callerName}\"", + key, + HashAlgorithmName.SHA384); - using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + foreach (X509Extension ext in extensions) { - ArgumentException ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate)); - Assert.Contains("Key Usage", ex.Message); - Assert.Contains("CrlSign", ex.Message); - Assert.DoesNotContain("KeyCertSign", ex.Message); - - ex = Assert.Throws(ParamName, () => builder.Build(cert, 0, nextUpdate, thisUpdate)); - Assert.Contains("Key Usage", ex.Message); - Assert.Contains("CrlSign", ex.Message); - Assert.DoesNotContain("KeyCertSign", ex.Message); + req.CertificateExtensions.Add(ext); } - certReq.CertificateExtensions.RemoveAt(1); - certReq.CertificateExtensions.Add( - new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true)); + DateTimeOffset now = DateTimeOffset.UtcNow; - // The certificate is acceptable now, move on to other arguments. - using (X509Certificate2 cert = certReq.CreateSelfSigned(notBefore, notAfter)) + using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMonths(-1), now.AddMonths(1))) { - Assert.Throws( - "crlNumber", - () => builder.Build(cert, -1, nextUpdate)); - - Assert.Throws( - "crlNumber", - () => builder.Build(cert, -1, nextUpdate, thisUpdate)); - - ArgumentException ex = Assert.Throws(() => builder.Build(cert, 0, now.AddYears(-10))); - Assert.Null(ex.ParamName); - Assert.Contains("thisUpdate", ex.Message); - Assert.Contains("nextUpdate", ex.Message); - - ex = Assert.Throws(() => builder.Build(cert, 0, thisUpdate, nextUpdate)); - Assert.Null(ex.ParamName); - Assert.Contains("thisUpdate", ex.Message); - Assert.Contains("nextUpdate", ex.Message); + action(cert, now); } } } @@ -133,19 +157,19 @@ public static void BuildWithGeneratorArgumentValidation() Assert.Throws( "issuerName", - () => builder.Build((X500DistinguishedName)null, default, 0, nextUpdate, default)); + () => builder.Build((X500DistinguishedName)null, default, 0, nextUpdate, default, default)); Assert.Throws( "issuerName", - () => builder.Build((X500DistinguishedName)null, default, 0, nextUpdate, thisUpdate, default)); + () => builder.Build((X500DistinguishedName)null, default, 0, nextUpdate, thisUpdate, default, default)); X500DistinguishedName issuerName = new X500DistinguishedName("CN=Bad CA"); Assert.Throws( "generator", - () => builder.Build(issuerName, default, 0, nextUpdate, default)); + () => builder.Build(issuerName, default, 0, nextUpdate, default, default)); Assert.Throws( "generator", - () => builder.Build(issuerName, default, 0, nextUpdate, thisUpdate, default)); + () => builder.Build(issuerName, default, 0, nextUpdate, thisUpdate, default, default)); using (ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP384)) { @@ -153,19 +177,19 @@ public static void BuildWithGeneratorArgumentValidation() Assert.Throws( "crlNumber", - () => builder.Build(issuerName, generator, -1, nextUpdate, default)); + () => builder.Build(issuerName, generator, -1, nextUpdate, default, default)); Assert.Throws( "crlNumber", - () => builder.Build(issuerName, generator, -1, nextUpdate, thisUpdate, default)); + () => builder.Build(issuerName, generator, -1, nextUpdate, thisUpdate, default, default)); ArgumentException ex = Assert.Throws( - () => builder.Build(issuerName, generator, 0, now.AddYears(-10), default)); + () => builder.Build(issuerName, generator, 0, now.AddYears(-10), default, default)); Assert.Null(ex.ParamName); Assert.Contains("thisUpdate", ex.Message); Assert.Contains("nextUpdate", ex.Message); ex = Assert.Throws( - () => builder.Build(issuerName, generator, 0, thisUpdate, nextUpdate, default)); + () => builder.Build(issuerName, generator, 0, thisUpdate, nextUpdate, default, default)); Assert.Null(ex.ParamName); Assert.Contains("thisUpdate", ex.Message); Assert.Contains("nextUpdate", ex.Message); diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index a3a8fb93a7c59..7b21d56d2bc31 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2450,18 +2450,16 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public sealed partial class CertificateRevocationListBuilder { public CertificateRevocationListBuilder() { } - public System.Security.Cryptography.HashAlgorithmName? HashAlgorithm { get { throw null; } set { } } - public System.Security.Cryptography.RSASignaturePadding? RSASignaturePadding { get { throw null; } set { } } public void AddEntry(byte[] serialNumber) { } public void AddEntry(byte[] serialNumber, System.DateTimeOffset revocationTime) { } public void AddEntry(System.ReadOnlySpan serialNumber) { } public void AddEntry(System.ReadOnlySpan serialNumber, System.DateTimeOffset revocationTime) { } public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.DateTimeOffset revocationTime) { } - public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } - public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } - public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate) { throw null; } - public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding? rsaSignaturePadding = null) { throw null; } + public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding? rsaSignaturePadding = null) { throw null; } public static System.Security.Cryptography.X509Certificates.X509Extension BuildCrlDistributionPointExtension(System.Collections.Generic.IEnumerable uris, bool critical = false) { throw null; } public void ExpireEntries(System.DateTimeOffset oldestRevocationTimeToKeep) { } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(byte[] currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 90b05e13af778..58e7b3dcd77fb 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -12,43 +12,9 @@ namespace System.Security.Cryptography.X509Certificates { public sealed partial class CertificateRevocationListBuilder { - private struct RevokedCertificate - { - internal byte[] Serial; - internal DateTimeOffset RevocationTime; - internal byte[]? Extensions; - - internal RevokedCertificate(ref AsnValueReader reader, int version) - { - AsnValueReader revokedCertificate = reader.ReadSequence(); - Serial = revokedCertificate.ReadIntegerBytes().ToArray(); - RevocationTime = ReadX509Time(ref revokedCertificate); - Extensions = null; - - if (version > 0 && revokedCertificate.HasData) - { - AsnValueReader crlExtensionsExplicit = - revokedCertificate.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); - - if (!crlExtensionsExplicit.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - Extensions = crlExtensionsExplicit.ReadEncodedValue().ToArray(); - crlExtensionsExplicit.ThrowIfNotEmpty(); - } - - revokedCertificate.ThrowIfNotEmpty(); - } - } - private List _revoked; private AsnWriter? _writer; - public HashAlgorithmName? HashAlgorithm { get; set; } - public RSASignaturePadding? RSASignaturePadding { get; set; } - public CertificateRevocationListBuilder() { _revoked = new List(); @@ -272,16 +238,29 @@ public void ExpireEntries(DateTimeOffset oldestRevocationTimeToKeep) _revoked.RemoveAll(rc => rc.RevocationTime < oldestRevocationTimeToKeep); } - public byte[] Build(X509Certificate2 issuerCertificate, BigInteger crlNumber, DateTimeOffset nextUpdate) + public byte[] Build( + X509Certificate2 issuerCertificate, + BigInteger crlNumber, + DateTimeOffset nextUpdate, + HashAlgorithmName hashAlgorithm, + RSASignaturePadding? rsaSignaturePadding = null) { - return Build(issuerCertificate, crlNumber, nextUpdate, DateTimeOffset.UtcNow); + return Build( + issuerCertificate, + crlNumber, + nextUpdate, + DateTimeOffset.UtcNow, + hashAlgorithm, + rsaSignaturePadding); } public byte[] Build( X509Certificate2 issuerCertificate, BigInteger crlNumber, DateTimeOffset nextUpdate, - DateTimeOffset thisUpdate) + DateTimeOffset thisUpdate, + HashAlgorithmName hashAlgorithm, + RSASignaturePadding? rsaSignaturePadding = null) { ArgumentNullException.ThrowIfNull(issuerCertificate); @@ -293,6 +272,7 @@ public byte[] Build( throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); if (nextUpdate <= thisUpdate) throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); + ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); // Check the Basic Constraints and Key Usage extensions to help identify inappropriate certificates. // Note that this is not a security check. The system library backing X509Chain will use these same criteria @@ -301,7 +281,8 @@ public byte[] Build( // chosen the wrong cert. var basicConstraints = (X509BasicConstraintsExtension?)issuerCertificate.Extensions[Oids.BasicConstraints2]; var keyUsage = (X509KeyUsageExtension?)issuerCertificate.Extensions[Oids.KeyUsage]; - //var akid = issuerCertificate.Extensions["Oids.Autho"]; + var subjectKeyIdentifier = + (X509SubjectKeyIdentifierExtension?)issuerCertificate.Extensions[Oids.SubjectKeyIdentifier]; if (basicConstraints == null) throw new ArgumentException( @@ -313,8 +294,6 @@ public byte[] Build( nameof(issuerCertificate)); if (keyUsage != null && (keyUsage.KeyUsages & X509KeyUsageFlags.CrlSign) == 0) throw new ArgumentException(SR.Cryptography_CRLBuilder_IssuerKeyUsageInvalid, nameof(issuerCertificate)); - //if (akid is null) - // throw new ArgumentException("AKID needed", nameof(issuerCertificate)); AsymmetricAlgorithm? key = null; string keyAlgorithm = issuerCertificate.GetKeyAlgorithm(); @@ -325,7 +304,7 @@ public byte[] Build( switch (keyAlgorithm) { case Oids.Rsa: - if (RSASignaturePadding is null) + if (rsaSignaturePadding is null) { throw new InvalidOperationException( "The issuer certificate uses an RSA key, but no RSASignaturePadding value was provided."); @@ -333,7 +312,7 @@ public byte[] Build( RSA? rsa = issuerCertificate.GetRSAPrivateKey(); key = rsa; - generator = X509SignatureGenerator.CreateForRSA(rsa!, RSASignaturePadding); + generator = X509SignatureGenerator.CreateForRSA(rsa!, rsaSignaturePadding); break; case Oids.EcPublicKey: ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey(); @@ -346,7 +325,27 @@ public byte[] Build( nameof(issuerCertificate)); } - return Build(issuerCertificate.SubjectName, generator, crlNumber, nextUpdate, thisUpdate, null!); + X509AuthorityKeyIdentifierExtension akid; + + if (subjectKeyIdentifier is not null) + { + akid = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectKeyIdentifier); + } + else + { + akid = X509AuthorityKeyIdentifierExtension.CreateFromIssuerNameAndSerialNumber( + issuerCertificate.SubjectName, + issuerCertificate.SerialNumberBytes.Span); + } + + return Build( + issuerCertificate.SubjectName, + generator, + crlNumber, + nextUpdate, + thisUpdate, + hashAlgorithm, + akid); } finally { @@ -359,9 +358,17 @@ public byte[] Build( X509SignatureGenerator generator, BigInteger crlNumber, DateTimeOffset nextUpdate, + HashAlgorithmName hashAlgorithm, X509AuthorityKeyIdentifierExtension akid) { - return Build(issuerName, generator, crlNumber, nextUpdate, DateTimeOffset.UtcNow, akid); + return Build( + issuerName, + generator, + crlNumber, + nextUpdate, + DateTimeOffset.UtcNow, + hashAlgorithm, + akid); } public byte[] Build( @@ -370,6 +377,7 @@ public byte[] Build( BigInteger crlNumber, DateTimeOffset nextUpdate, DateTimeOffset thisUpdate, + HashAlgorithmName hashAlgorithm, X509AuthorityKeyIdentifierExtension akid) { ArgumentNullException.ThrowIfNull(issuerName); @@ -380,16 +388,9 @@ public byte[] Build( if (nextUpdate <= thisUpdate) throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); + ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); ArgumentNullException.ThrowIfNull(akid); - HashAlgorithmName hashAlgorithm = HashAlgorithm.GetValueOrDefault(); - - if (string.IsNullOrEmpty(hashAlgorithm.Name)) - { - throw new InvalidOperationException( - "The hash algorithm to use during signing must be specified via the HashAlgorithm property."); - } - byte[] signatureAlgId = generator.GetSignatureAlgorithmIdentifier(hashAlgorithm); AsnWriter writer = (_writer ??= new AsnWriter(AsnEncodingRules.DER)); writer.Reset(); @@ -527,5 +528,36 @@ private static void WriteX509Time(AsnWriter writer, DateTimeOffset time) writer.WriteGeneralizedTime(time, omitFractionalSeconds: true); } } + + private struct RevokedCertificate + { + internal byte[] Serial; + internal DateTimeOffset RevocationTime; + internal byte[]? Extensions; + + internal RevokedCertificate(ref AsnValueReader reader, int version) + { + AsnValueReader revokedCertificate = reader.ReadSequence(); + Serial = revokedCertificate.ReadIntegerBytes().ToArray(); + RevocationTime = ReadX509Time(ref revokedCertificate); + Extensions = null; + + if (version > 0 && revokedCertificate.HasData) + { + AsnValueReader crlExtensionsExplicit = + revokedCertificate.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); + + if (!crlExtensionsExplicit.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + Extensions = crlExtensionsExplicit.ReadEncodedValue().ToArray(); + crlExtensionsExplicit.ThrowIfNotEmpty(); + } + + revokedCertificate.ThrowIfNotEmpty(); + } + } } } From 3c33e17d8063bc371c87ff0dd4f0d1899a4893aa Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Wed, 15 Jun 2022 08:27:30 -0700 Subject: [PATCH 36/43] Argument validation to CRLBuilder.Build --- .../CertificateCreation/CrlBuilderTests.cs | 291 ++++++++++++++++++ .../CertificateRevocationListBuilder.cs | 4 +- 2 files changed, 293 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index 40c453e24ec5a..71b60b5a62e9d 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -120,6 +120,295 @@ public static void BuildWithCertificateWithBadBasicConstraints() }); } + [Fact] + public static void BuildWithCertificateWithBadKeyUsage() + { + BuildCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign, true), + }, + static (cert, now) => + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + ArgumentException e; + + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now.AddMinutes(5), HashAlgorithmName.SHA256)); + + Assert.Contains("CrlSign", e.Message); + + e = Assert.Throws( + CertParam, + () => builder.Build(cert, 0, now.AddMinutes(5), now, HashAlgorithmName.SHA256)); + + Assert.Contains("CrlSign", e.Message); + }); + } + + [Fact] + public static void BuildWithNextUpdateBeforeThisUpdate() + { + BuildCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + new X509KeyUsageExtension( + X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, + true), + }, + static (cert, now) => + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + ArgumentException e; + + e = Assert.Throws( + () => builder.Build(cert, 0, now.AddMinutes(-5), HashAlgorithmName.SHA256)); + + Assert.Null(e.ParamName); + Assert.Contains("thisUpdate", e.Message); + Assert.Contains("nextUpdate", e.Message); + + e = Assert.Throws( + () => builder.Build(cert, 0, now, now.AddSeconds(1), HashAlgorithmName.SHA256)); + + Assert.Null(e.ParamName); + Assert.Contains("thisUpdate", e.Message); + Assert.Contains("nextUpdate", e.Message); + + using (ECDsa key = cert.GetECDsaPrivateKey()) + { + X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(key); + X500DistinguishedName dn = cert.SubjectName; + + e = Assert.Throws( + () => builder.Build(dn, gen, 0, now.AddMinutes(-5), HashAlgorithmName.SHA256, null)); + + Assert.Null(e.ParamName); + Assert.Contains("thisUpdate", e.Message); + Assert.Contains("nextUpdate", e.Message); + + e = Assert.Throws( + () => builder.Build(dn, gen, 0, now, now.AddSeconds(1), HashAlgorithmName.SHA256, null)); + + Assert.Null(e.ParamName); + Assert.Contains("thisUpdate", e.Message); + Assert.Contains("nextUpdate", e.Message); + } + }); + } + + [Fact] + public static void BuildWithNoHashAlgorithm() + { + BuildCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (cert, now) => + { + HashAlgorithmName hashAlg = default; + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + Assert.Throws( + "hashAlgorithm", + () => builder.Build(cert, 0, now.AddMinutes(5), hashAlg)); + + Assert.Throws( + "hashAlgorithm", + () => builder.Build(cert, 0, now.AddMinutes(5), now, hashAlg)); + + using (ECDsa key = cert.GetECDsaPrivateKey()) + { + X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(key); + X500DistinguishedName dn = cert.SubjectName; + + Assert.Throws( + "hashAlgorithm", + () => builder.Build(dn, gen, 0, now.AddMinutes(5), hashAlg, null)); + + Assert.Throws( + "hashAlgorithm", + () => builder.Build(dn, gen, 0, now.AddMinutes(5), now, hashAlg, null)); + } + }); + } + + [Fact] + public static void BuildWithEmptyHashAlgorithm() + { + BuildCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (cert, now) => + { + HashAlgorithmName hashAlg = new HashAlgorithmName(""); + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + ArgumentException e; + + e = Assert.Throws( + "hashAlgorithm", + () => builder.Build(cert, 0, now.AddMinutes(5), hashAlg)); + + Assert.Contains("empty", e.Message); + + e = Assert.Throws( + "hashAlgorithm", + () => builder.Build(cert, 0, now.AddMinutes(5), now, hashAlg)); + + Assert.Contains("empty", e.Message); + + using (ECDsa key = cert.GetECDsaPrivateKey()) + { + X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(key); + X500DistinguishedName dn = cert.SubjectName; + + e = Assert.Throws( + "hashAlgorithm", + () => builder.Build(dn, gen, 0, now.AddMinutes(5), hashAlg, null)); + + Assert.Contains("empty", e.Message); + + e = Assert.Throws( + "hashAlgorithm", + () => builder.Build(dn, gen, 0, now.AddMinutes(5), now, hashAlg, null)); + + Assert.Contains("empty", e.Message); + } + }); + } + + [Fact] + public static void BuildWithNegativeCrlNumber() + { + BuildCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (cert, now) => + { + HashAlgorithmName hashAlg = new HashAlgorithmName(""); + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + Assert.Throws( + "crlNumber", + () => builder.Build(cert, -1, now.AddMinutes(5), hashAlg)); + + Assert.Throws( + "crlNumber", + () => builder.Build(cert, -1, now.AddMinutes(5), now, hashAlg)); + + using (ECDsa key = cert.GetECDsaPrivateKey()) + { + X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(key); + X500DistinguishedName dn = cert.SubjectName; + + Assert.Throws( + "crlNumber", + () => builder.Build(dn, gen, -1, now.AddMinutes(5), hashAlg, null)); + + Assert.Throws( + "crlNumber", + () => builder.Build(dn, gen, -1, now.AddMinutes(5), now, hashAlg, null)); + } + }); + } + + [Fact] + public static void BuildWithGeneratorNullName() + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + DateTimeOffset now = DateTimeOffset.UtcNow; + + Assert.Throws( + "issuerName", + () => builder.Build(null, null, 0, now.AddMinutes(5), HashAlgorithmName.SHA256, null)); + + Assert.Throws( + "issuerName", + () => builder.Build(null, null, 0, now.AddMinutes(5), now, HashAlgorithmName.SHA256, null)); + } + + [Fact] + public static void BuildWithGeneratorNullGenerator() + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + DateTimeOffset now = DateTimeOffset.UtcNow; + X500DistinguishedName dn = new X500DistinguishedName("CN=Name"); + + Assert.Throws( + "generator", + () => builder.Build(dn, null, 0, now.AddMinutes(5), HashAlgorithmName.SHA256, null)); + + Assert.Throws( + "generator", + () => builder.Build(dn, null, 0, now.AddMinutes(5), now, HashAlgorithmName.SHA256, null)); + } + + [Fact] + public static void BuildWithGeneratorNullAkid() + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + DateTimeOffset now = DateTimeOffset.UtcNow; + X500DistinguishedName dn = new X500DistinguishedName("CN=Name"); + + using (RSA rsa = RSA.Create(TestData.RsaBigExponentParams)) + { + X509SignatureGenerator gen = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); + + Assert.Throws( + "akid", + () => builder.Build(dn, gen, 0, now.AddMinutes(5), HashAlgorithmName.SHA256, null)); + + Assert.Throws( + "akid", + () => builder.Build(dn, gen, 0, now.AddMinutes(5), now, HashAlgorithmName.SHA256, null)); + } + } + + [Fact] + public static void BuildWithRSACertificateAndNoPadding() + { + using (RSA key = RSA.Create(TestData.RsaBigExponentParams)) + { + CertificateRequest req = new CertificateRequest( + "CN=RSA Test", + key, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); + req.CertificateExtensions.Add(X509BasicConstraintsExtension.CreateForCertificateAuthority()); + + DateTimeOffset now = DateTimeOffset.UtcNow; + + using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMonths(-1), now.AddMonths(1))) + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + ArgumentException e; + + e = Assert.Throws( + () => builder.Build(cert, 0, now.AddMinutes(5), HashAlgorithmName.SHA256)); + + Assert.Null(e.ParamName); + Assert.Contains(nameof(RSASignaturePadding), e.Message); + + e = Assert.Throws( + () => builder.Build(cert, 0, now.AddMinutes(5), now, HashAlgorithmName.SHA256)); + + Assert.Null(e.ParamName); + Assert.Contains(nameof(RSASignaturePadding), e.Message); + } + } + } + private static void BuildCertificateAndRun( IEnumerable extensions, Action action, @@ -132,6 +421,8 @@ private static void BuildCertificateAndRun( key, HashAlgorithmName.SHA384); + req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); + foreach (X509Extension ext in extensions) { req.CertificateExtensions.Add(ext); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 58e7b3dcd77fb..f3092e2bd0c16 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -306,7 +306,7 @@ public byte[] Build( case Oids.Rsa: if (rsaSignaturePadding is null) { - throw new InvalidOperationException( + throw new ArgumentException( "The issuer certificate uses an RSA key, but no RSASignaturePadding value was provided."); } @@ -334,7 +334,7 @@ public byte[] Build( else { akid = X509AuthorityKeyIdentifierExtension.CreateFromIssuerNameAndSerialNumber( - issuerCertificate.SubjectName, + issuerCertificate.IssuerName, issuerCertificate.SerialNumberBytes.Span); } From 62ffa1bad77e0cc4fb74a2971c5d873783f77a27 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Wed, 15 Jun 2022 15:56:30 -0700 Subject: [PATCH 37/43] Fix AKID with Issuer+Serial --- .../AuthorityKeyIdentifierTests.cs | 36 +++++++++++++++++++ .../X509AuthorityKeyIdentifierExtension.cs | 5 +-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs index bbb664dd9d985..1d60d75cfb0d5 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/AuthorityKeyIdentifierTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Test.Cryptography; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests.ExtensionsTests @@ -20,5 +21,40 @@ public static void DefaultConstructor() Assert.False(e.RawIssuer.HasValue, "e.RawIssuer.HasValue"); Assert.False(e.SerialNumber.HasValue, "e.SerialNumber.HasValue"); } + + [Fact] + public static void RoundtripFull() + { + byte[] encoded = ( + "303C80140235857ED35BD13609F22DE8A71F93DFEBD3F495A11AA41830163114" + + "301206035504030C0B49737375696E6743657274820852E6DEFA1D32A969").HexToByteArray(); + + X509AuthorityKeyIdentifierExtension akid = new(encoded); + Assert.True(akid.KeyIdentifier.HasValue, "akid.KeyIdentifier.HasValue"); + + Assert.Equal( + "0235857ED35BD13609F22DE8A71F93DFEBD3F495", + akid.KeyIdentifier.Value.ByteArrayToHex()); + + Assert.True(akid.RawIssuer.HasValue, "akid.RawIssuer.HasValue"); + Assert.NotNull(akid.SimpleIssuer); + + Assert.Equal( + "A11AA41830163114301206035504030C0B49737375696E6743657274", + akid.RawIssuer.Value.ByteArrayToHex()); + Assert.Equal( + "30163114301206035504030C0B49737375696E6743657274", + akid.SimpleIssuer.RawData.ByteArrayToHex()); + + Assert.True(akid.SerialNumber.HasValue, "akid.SerialNumber.HasValue"); + Assert.Equal("52E6DEFA1D32A969", akid.SerialNumber.Value.ByteArrayToHex()); + + X509AuthorityKeyIdentifierExtension akid2 = X509AuthorityKeyIdentifierExtension.Create( + akid.KeyIdentifier.Value.Span, + akid.SimpleIssuer, + akid.SerialNumber.Value.Span); + + AssertExtensions.SequenceEqual(akid.RawData, akid2.RawData); + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs index 6530099204414..8db49a8a29d7f 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs @@ -238,6 +238,7 @@ public static X509AuthorityKeyIdentifierExtension Create( writer.WriteOctetString(keyIdentifier, new Asn1Tag(TagClass.ContextSpecific, 0)); using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1))) + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 4))) { writer.WriteEncodedValue(issuerName.RawData); } @@ -329,10 +330,10 @@ private void Decode(ReadOnlySpan rawData) if (nextTag.HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 1))) { - byte[] rawIssuer = aki.ReadOctetString(nextTag); + byte[] rawIssuer = aki.PeekEncodedValue().ToArray(); _rawIssuer = rawIssuer; - AsnValueReader generalNames = new AsnValueReader(rawIssuer, AsnEncodingRules.DER); + AsnValueReader generalNames = aki.ReadSequence(nextTag); bool foundIssuer = false; // Walk all of the entities to make sure they decode legally, so no early abort. From 944ed03b4f5209a84adc404f810ab265b6d1ac01 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 16 Jun 2022 07:37:38 -0700 Subject: [PATCH 38/43] Add some tests that build CRLs --- .../CertificateCreation/CrlBuilderTests.cs | 174 +++++++++++++++--- 1 file changed, 145 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index 71b60b5a62e9d..c330487186130 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -408,35 +408,7 @@ public static void BuildWithRSACertificateAndNoPadding() } } } - - private static void BuildCertificateAndRun( - IEnumerable extensions, - Action action, - [CallerMemberName] string callerName = null) - { - using (ECDsa key = ECDsa.Create()) - { - CertificateRequest req = new CertificateRequest( - $"CN=\"{callerName}\"", - key, - HashAlgorithmName.SHA384); - - req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); - - foreach (X509Extension ext in extensions) - { - req.CertificateExtensions.Add(ext); - } - - DateTimeOffset now = DateTimeOffset.UtcNow; - - using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMonths(-1), now.AddMonths(1))) - { - action(cert, now); - } - } - } - + [Fact] public static void BuildWithGeneratorArgumentValidation() { @@ -487,6 +459,93 @@ public static void BuildWithGeneratorArgumentValidation() } } + [Fact] + public static void BuildEmpty() + { + BuildRsaCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (cert, notNow) => + { + HashAlgorithmName hashAlg = HashAlgorithmName.SHA256; + RSASignaturePadding pad = RSASignaturePadding.Pkcs1; + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + DateTimeOffset now = new DateTimeOffset(2013, 4, 6, 7, 58, 9, TimeSpan.Zero); + + byte[] built = builder.Build(cert, 123, now.AddMinutes(5), now, hashAlg, pad); + + // The length of the output depends on a number of factors, but they're all stable + // for this test (since it doesn't use ECDSA's variable-length, non-deterministic, signature) + // + // In fact, because RSASSA-PKCS1 is a deterministic algorithm, we can check it for a fixed output. + + byte[] expected = ( + "3082018E3078020101300D06092A864886F70D01010B05003015311330110603" + + "550403130A4275696C64456D707479170D3133303430363037353830395A170D" + + "3133303430363038303330395AA02F302D301F0603551D2304183016801478A5" + + "C75D51667331D5A96924114C9B5FA00D7BCB300A0603551D14040302017B300D" + + "06092A864886F70D01010B0500038201010005A249671E2EE8780CBE17180493" + + "094A5EB576465A9B2674666B762184AD992832556B36CC8320264FE45A6B4981" + + "439ED9CFB87EAD10D4A95769713A0442B2D3A5FD20487DA5B33BCFBE10ED921C" + + "8B9896B69EA443D8D9F0AF5E0EB789361655C80EC3C7C7C84F5127C6A29C27BE" + + "8437CE0182BD16CF697169121C2BBFAADC4EDE17C8BB76949D25376F2739E03C" + + "DA0609D03C024CD5A911B342571F385B3B8A782B62C5375E1D674E43447FE2EB" + + "9EFFCAF71CCCECBAE600C74F6FD6CB36A87C5786603501EA43794144142E8557" + + "EC2EBC2F7357DB050440FD97F233441E2BE981ED6309CE7C8B1C97BCE658FCEC" + + "6BD63004A1D3D4EA0043783E55E7ECBCF6E6").HexToByteArray(); + + Assert.Equal(expected, built); + }); + } + + [Fact] + public static void BuildSingleEntry() + { + BuildRsaCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (cert, notNow) => + { + HashAlgorithmName hashAlg = HashAlgorithmName.SHA256; + RSASignaturePadding pad = RSASignaturePadding.Pkcs1; + DateTimeOffset now = new DateTimeOffset(2013, 4, 6, 7, 58, 9, TimeSpan.Zero); + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + builder.AddEntry( + stackalloc byte[] { 0x01, 0x01, 0x02, 0x03, 0x05, 0x08, 0x0C, 0x15 }, + now.AddSeconds(-1812)); + + byte[] explicitUpdateTime = builder.Build(cert, 123, now.AddMinutes(5), now, hashAlg, pad); + + // The length of the output depends on a number of factors, but they're all stable + // for this test (since it doesn't use ECDSA's variable-length, non-deterministic, signature) + // + // In fact, because RSASSA-PKCS1 is a deterministic algorithm, we can check it for a fixed output. + + byte[] expected = ( + "308201B230819B020101300D06092A864886F70D01010B0500301B3119301706" + + "0355040313104275696C6453696E676C65456E747279170D3133303430363037" + + "353830395A170D3133303430363038303330395A301B30190208010102030508" + + "0C15170D3133303430363037323735375AA02F302D301F0603551D2304183016" + + "801478A5C75D51667331D5A96924114C9B5FA00D7BCB300A0603551D14040302" + + "017B300D06092A864886F70D01010B05000382010100A9E1D03571B1E4BF7670" + + "EC32459A74B11482741FD973FF5040D57B133B5B6C783DC9ED105C4CF5DDE8FC" + + "8B767C6034253D749A834622034A669AA4C6EFDB93C82EB15B69E6DC43F05BAE" + + "7E9E21B0351A720C5E79F3BE65304658EBDFE196269BC285D653E7ACD97811D6" + + "4E08792034B47D83BF9D37851116023BDF7460C5BF1492CFA486AD7B2F277870" + + "82E6A3C05C0E43BB7D62B234C0E6C5BA2E0103E1CCBDAE15F9CD6DB989DED687" + + "0915AB164EB2FC2ADA00D4980574FC2C3C0905C1BFC9F42DBF0F800FF7F9D92C" + + "1F99C443EFC32593C749E18C41282E0EF232643846D204A6BC23C55605299225" + + "6323F7BD75DEE733C9FD011B6D3B85395422046B5573").HexToByteArray(); + + AssertExtensions.SequenceEqual(expected, explicitUpdateTime); + }); + } + [Fact] public static void BuildSimpleCdp() { @@ -500,5 +559,62 @@ public static void BuildSimpleCdp() Assert.Equal(expected, ext.RawData); } + + private static void BuildCertificateAndRun( + IEnumerable extensions, + Action action, + [CallerMemberName] string callerName = null) + { + using (ECDsa key = ECDsa.Create()) + { + CertificateRequest req = new CertificateRequest( + $"CN=\"{callerName}\"", + key, + HashAlgorithmName.SHA384); + + req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); + + foreach (X509Extension ext in extensions) + { + req.CertificateExtensions.Add(ext); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + + using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMonths(-1), now.AddMonths(1))) + { + action(cert, now); + } + } + } + + private static void BuildRsaCertificateAndRun( + IEnumerable extensions, + Action action, + [CallerMemberName] string callerName = null) + { + using (RSA key = RSA.Create(TestData.RsaBigExponentParams)) + { + CertificateRequest req = new CertificateRequest( + $"CN=\"{callerName}\"", + key, + HashAlgorithmName.SHA384, + RSASignaturePadding.Pkcs1); + + req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); + + foreach (X509Extension ext in extensions) + { + req.CertificateExtensions.Add(ext); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + + using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMonths(-1), now.AddMonths(1))) + { + action(cert, now); + } + } + } } } From 0283f9dd9fac9eb75f475a51241b3cd6db910ff2 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 30 Jun 2022 17:22:04 -0700 Subject: [PATCH 39/43] Split CRLBuilder into partial files --- .../src/System.Security.Cryptography.csproj | 2 + .../CertificateRevocationListBuilder.Build.cs | 264 +++++++++++ .../CertificateRevocationListBuilder.Load.cs | 181 ++++++++ .../CertificateRevocationListBuilder.cs | 420 +----------------- 4 files changed, 449 insertions(+), 418 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Load.cs diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index c217c4f5a1d42..415953d6a70b6 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -421,7 +421,9 @@ + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs new file mode 100644 index 0000000000000..862f311cf30a0 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs @@ -0,0 +1,264 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Formats.Asn1; +using System.Numerics; + +namespace System.Security.Cryptography.X509Certificates +{ + public sealed partial class CertificateRevocationListBuilder + { + public byte[] Build( + X509Certificate2 issuerCertificate, + BigInteger crlNumber, + DateTimeOffset nextUpdate, + HashAlgorithmName hashAlgorithm, + RSASignaturePadding? rsaSignaturePadding = null) + { + return Build( + issuerCertificate, + crlNumber, + nextUpdate, + DateTimeOffset.UtcNow, + hashAlgorithm, + rsaSignaturePadding); + } + + public byte[] Build( + X509Certificate2 issuerCertificate, + BigInteger crlNumber, + DateTimeOffset nextUpdate, + DateTimeOffset thisUpdate, + HashAlgorithmName hashAlgorithm, + RSASignaturePadding? rsaSignaturePadding = null) + { + ArgumentNullException.ThrowIfNull(issuerCertificate); + + if (!issuerCertificate.HasPrivateKey) + throw new ArgumentException( + SR.Cryptography_CertReq_IssuerRequiresPrivateKey, + nameof(issuerCertificate)); + if (crlNumber < 0) + throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); + if (nextUpdate <= thisUpdate) + throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); + + ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + + // Check the Basic Constraints and Key Usage extensions to help identify inappropriate certificates. + // Note that this is not a security check. The system library backing X509Chain will use these same criteria + // to determine if a chain is valid; and a user can easily call the X509SignatureGenerator overload to + // bypass this validation. We're simply helping them at signing time understand that they've + // chosen the wrong cert. + var basicConstraints = (X509BasicConstraintsExtension?)issuerCertificate.Extensions[Oids.BasicConstraints2]; + var keyUsage = (X509KeyUsageExtension?)issuerCertificate.Extensions[Oids.KeyUsage]; + var subjectKeyIdentifier = + (X509SubjectKeyIdentifierExtension?)issuerCertificate.Extensions[Oids.SubjectKeyIdentifier]; + + if (basicConstraints == null) + throw new ArgumentException( + SR.Cryptography_CertReq_BasicConstraintsRequired, + nameof(issuerCertificate)); + if (!basicConstraints.CertificateAuthority) + throw new ArgumentException( + SR.Cryptography_CertReq_IssuerBasicConstraintsInvalid, + nameof(issuerCertificate)); + if (keyUsage != null && (keyUsage.KeyUsages & X509KeyUsageFlags.CrlSign) == 0) + throw new ArgumentException(SR.Cryptography_CRLBuilder_IssuerKeyUsageInvalid, nameof(issuerCertificate)); + + AsymmetricAlgorithm? key = null; + string keyAlgorithm = issuerCertificate.GetKeyAlgorithm(); + X509SignatureGenerator generator; + + try + { + switch (keyAlgorithm) + { + case Oids.Rsa: + if (rsaSignaturePadding is null) + { + throw new ArgumentException( + "The issuer certificate uses an RSA key, but no RSASignaturePadding value was provided."); + } + + RSA? rsa = issuerCertificate.GetRSAPrivateKey(); + key = rsa; + generator = X509SignatureGenerator.CreateForRSA(rsa!, rsaSignaturePadding); + break; + case Oids.EcPublicKey: + ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey(); + key = ecdsa; + generator = X509SignatureGenerator.CreateForECDsa(ecdsa!); + break; + default: + throw new ArgumentException( + SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm), + nameof(issuerCertificate)); + } + + X509AuthorityKeyIdentifierExtension akid; + + if (subjectKeyIdentifier is not null) + { + akid = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectKeyIdentifier); + } + else + { + akid = X509AuthorityKeyIdentifierExtension.CreateFromIssuerNameAndSerialNumber( + issuerCertificate.IssuerName, + issuerCertificate.SerialNumberBytes.Span); + } + + return Build( + issuerCertificate.SubjectName, + generator, + crlNumber, + nextUpdate, + thisUpdate, + hashAlgorithm, + akid); + } + finally + { + key?.Dispose(); + } + } + + public byte[] Build( + X500DistinguishedName issuerName, + X509SignatureGenerator generator, + BigInteger crlNumber, + DateTimeOffset nextUpdate, + HashAlgorithmName hashAlgorithm, + X509AuthorityKeyIdentifierExtension akid) + { + return Build( + issuerName, + generator, + crlNumber, + nextUpdate, + DateTimeOffset.UtcNow, + hashAlgorithm, + akid); + } + + public byte[] Build( + X500DistinguishedName issuerName, + X509SignatureGenerator generator, + BigInteger crlNumber, + DateTimeOffset nextUpdate, + DateTimeOffset thisUpdate, + HashAlgorithmName hashAlgorithm, + X509AuthorityKeyIdentifierExtension akid) + { + ArgumentNullException.ThrowIfNull(issuerName); + ArgumentNullException.ThrowIfNull(generator); + + if (crlNumber < 0) + throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); + if (nextUpdate <= thisUpdate) + throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); + + ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + ArgumentNullException.ThrowIfNull(akid); + + byte[] signatureAlgId = generator.GetSignatureAlgorithmIdentifier(hashAlgorithm); + AsnWriter writer = (_writer ??= new AsnWriter(AsnEncodingRules.DER)); + writer.Reset(); + + // TBSCertList + using (writer.PushSequence()) + { + // version v2(1) + writer.WriteInteger(1); + + // signature (AlgorithmIdentifier) + writer.WriteEncodedValue(signatureAlgId); + + // issuer + writer.WriteEncodedValue(issuerName.RawData); + + // thisUpdate + WriteX509Time(writer, thisUpdate); + + // nextUpdate + WriteX509Time(writer, nextUpdate); + + // revokedCertificates (don't write down if empty) + if (_revoked.Count > 0) + { + // SEQUENCE OF + using (writer.PushSequence()) + { + foreach (RevokedCertificate revoked in _revoked) + { + // Anonymous CRL Entry type + using (writer.PushSequence()) + { + writer.WriteInteger(revoked.Serial); + WriteX509Time(writer, revoked.RevocationTime); + + if (revoked.Extensions is not null) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) + { + writer.WriteEncodedValue(revoked.Extensions); + } + } + } + } + } + } + + // extensions [0] EXPLICIT Extensions + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) + { + // Extensions (SEQUENCE OF) + using (writer.PushSequence()) + { + // Authority Key Identifier Extension + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(akid.Oid!.Value!); + + if (akid.Critical) + { + writer.WriteBoolean(true); + } + + writer.WriteOctetString(akid.RawData); + } + + // CRL Number Extension + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier("2.5.29.20"); + + using (writer.PushOctetString()) + { + writer.WriteInteger(crlNumber); + } + } + } + } + } + + byte[] tbsCertList = writer.Encode(); + writer.Reset(); + + byte[] signature = generator.SignData(tbsCertList, hashAlgorithm); + + // CertificateList + using (writer.PushSequence()) + { + writer.WriteEncodedValue(tbsCertList); + writer.WriteEncodedValue(signatureAlgId); + writer.WriteBitString(signature); + } + + byte[] crl = writer.Encode(); + return crl; + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Load.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Load.cs new file mode 100644 index 0000000000000..f88d44e48a48a --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Load.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Numerics; +using System.Security.Cryptography.Asn1; + +namespace System.Security.Cryptography.X509Certificates +{ + public sealed partial class CertificateRevocationListBuilder + { + public static CertificateRevocationListBuilder Load(byte[] currentCrl, out BigInteger currentCrlNumber) + { + ArgumentNullException.ThrowIfNull(currentCrl); + + CertificateRevocationListBuilder ret = Load( + new ReadOnlySpan(currentCrl), + out BigInteger crlNumber, + out int bytesConsumed); + + if (bytesConsumed != currentCrl.Length) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + currentCrlNumber = crlNumber; + return ret; + } + + public static CertificateRevocationListBuilder Load( + ReadOnlySpan currentCrl, + out BigInteger currentCrlNumber, + out int bytesConsumed) + { + List list = new(); + BigInteger crlNumber = 0; + int payloadLength; + + try + { + AsnValueReader reader = new AsnValueReader(currentCrl, AsnEncodingRules.DER); + payloadLength = reader.PeekEncodedValue().Length; + + AsnValueReader certificateList = reader.ReadSequence(); + AsnValueReader tbsCertList = certificateList.ReadSequence(); + AlgorithmIdentifierAsn.Decode(ref certificateList, ReadOnlyMemory.Empty, out _); + + if (!certificateList.TryReadPrimitiveBitString(out _, out _)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + certificateList.ThrowIfNotEmpty(); + + int version = 0; + + if (tbsCertList.PeekTag().HasSameClassAndValue(Asn1Tag.Integer)) + { + // https://datatracker.ietf.org/doc/html/rfc5280#section-5.1 says the only + // version values are v1 (0) and v2 (1). + if (!tbsCertList.TryReadInt32(out version) || version != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + AlgorithmIdentifierAsn.Decode(ref tbsCertList, ReadOnlyMemory.Empty, out _); + // X500DN + tbsCertList.ReadSequence(); + + // thisUpdate + ReadX509Time(ref tbsCertList); + + // nextUpdate + ReadX509TimeOpt(ref tbsCertList); + + AsnValueReader revokedCertificates = tbsCertList.ReadSequence(); + + if (version > 0 && tbsCertList.HasData) + { + AsnValueReader crlExtensionsExplicit = tbsCertList.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); + AsnValueReader crlExtensions = crlExtensionsExplicit.ReadSequence(); + crlExtensionsExplicit.ThrowIfNotEmpty(); + + while (crlExtensions.HasData) + { + AsnValueReader extension = crlExtensions.ReadSequence(); + string extnId = extension.ReadObjectIdentifier(); + + if (extension.PeekTag().HasSameClassAndValue(Asn1Tag.Boolean)) + { + extension.ReadBoolean(); + } + + if (!extension.TryReadPrimitiveOctetString(out ReadOnlySpan extnValue)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + switch (extnId) + { + case Oids.CrlNumber: + { + AsnValueReader crlNumberReader = new AsnValueReader( + extnValue, + AsnEncodingRules.DER); + + crlNumber = crlNumberReader.ReadInteger(); + crlNumberReader.ThrowIfNotEmpty(); + + break; + } + + case Oids.AuthorityInformationAccess: + { + AsnValueReader aiaReader = new AsnValueReader(extnValue, AsnEncodingRules.DER); + aiaReader.ReadSequence(); + aiaReader.ThrowIfNotEmpty(); + break; + } + } + } + } + + tbsCertList.ThrowIfNotEmpty(); + + while (revokedCertificates.HasData) + { + RevokedCertificate revokedCertificate = new RevokedCertificate(ref revokedCertificates, version); + list.Add(revokedCertificate); + } + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + + bytesConsumed = payloadLength; + currentCrlNumber = crlNumber; + return new CertificateRevocationListBuilder(list); + } + + public static CertificateRevocationListBuilder LoadPem(string currentCrl, out BigInteger currentCrlNumber) + { + ArgumentNullException.ThrowIfNull(currentCrl); + + return LoadPem(currentCrl.AsSpan(), out currentCrlNumber); + } + + public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan currentCrl, out BigInteger currentCrlNumber) + { + foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(currentCrl)) + { + if (contents[fields.Label].SequenceEqual(PemLabels.X509CertificateRevocationList)) + { + byte[] rented = ArrayPool.Shared.Rent(fields.DecodedDataLength); + + if (!Convert.TryFromBase64Chars(contents[fields.Base64Data], rented, out int bytesWritten)) + { + Debug.Fail("Base64Decode failed, but PemEncoding said it was legal"); + throw new UnreachableException(); + } + + CertificateRevocationListBuilder ret = Load( + rented.AsSpan(0, bytesWritten), + out currentCrlNumber, + out int bytesConsumed); + + Debug.Assert(bytesConsumed == bytesWritten); + ArrayPool.Shared.Return(rented); + return ret; + } + } + + throw new CryptographicException(SR.Cryptography_NoPemOfLabel, PemLabels.X509CertificateRevocationList); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index f3092e2bd0c16..19179581baf1d 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -12,7 +12,7 @@ namespace System.Security.Cryptography.X509Certificates { public sealed partial class CertificateRevocationListBuilder { - private List _revoked; + private readonly List _revoked; private AsnWriter? _writer; public CertificateRevocationListBuilder() @@ -26,172 +26,6 @@ private CertificateRevocationListBuilder(List revoked) _revoked = revoked; } - public static CertificateRevocationListBuilder Load(byte[] currentCrl, out BigInteger currentCrlNumber) - { - ArgumentNullException.ThrowIfNull(currentCrl); - - CertificateRevocationListBuilder ret = Load( - new ReadOnlySpan(currentCrl), - out BigInteger crlNumber, - out int bytesConsumed); - - if (bytesConsumed != currentCrl.Length) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - currentCrlNumber = crlNumber; - return ret; - } - - public static CertificateRevocationListBuilder Load( - ReadOnlySpan currentCrl, - out BigInteger currentCrlNumber, - out int bytesConsumed) - { - List list = new(); - BigInteger crlNumber = 0; - int payloadLength; - - try - { - AsnValueReader reader = new AsnValueReader(currentCrl, AsnEncodingRules.DER); - payloadLength = reader.PeekEncodedValue().Length; - - AsnValueReader certificateList = reader.ReadSequence(); - AsnValueReader tbsCertList = certificateList.ReadSequence(); - AlgorithmIdentifierAsn.Decode(ref certificateList, ReadOnlyMemory.Empty, out _); - - if (!certificateList.TryReadPrimitiveBitString(out _, out _)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - certificateList.ThrowIfNotEmpty(); - - int version = 0; - - if (tbsCertList.PeekTag().HasSameClassAndValue(Asn1Tag.Integer)) - { - // https://datatracker.ietf.org/doc/html/rfc5280#section-5.1 says the only - // version values are v1 (0) and v2 (1). - if (!tbsCertList.TryReadInt32(out version) || version != 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - } - - AlgorithmIdentifierAsn.Decode(ref tbsCertList, ReadOnlyMemory.Empty, out _); - // X500DN - tbsCertList.ReadSequence(); - - // thisUpdate - ReadX509Time(ref tbsCertList); - - // nextUpdate - ReadX509TimeOpt(ref tbsCertList); - - AsnValueReader revokedCertificates = tbsCertList.ReadSequence(); - - if (version > 0 && tbsCertList.HasData) - { - AsnValueReader crlExtensionsExplicit = tbsCertList.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); - AsnValueReader crlExtensions = crlExtensionsExplicit.ReadSequence(); - crlExtensionsExplicit.ThrowIfNotEmpty(); - - while (crlExtensions.HasData) - { - AsnValueReader extension = crlExtensions.ReadSequence(); - string extnId = extension.ReadObjectIdentifier(); - - if (extension.PeekTag().HasSameClassAndValue(Asn1Tag.Boolean)) - { - extension.ReadBoolean(); - } - - if (!extension.TryReadPrimitiveOctetString(out ReadOnlySpan extnValue)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - switch (extnId) - { - case Oids.CrlNumber: - { - AsnValueReader crlNumberReader = new AsnValueReader( - extnValue, - AsnEncodingRules.DER); - - crlNumber = crlNumberReader.ReadInteger(); - crlNumberReader.ThrowIfNotEmpty(); - - break; - } - - case Oids.AuthorityInformationAccess: - { - AsnValueReader aiaReader = new AsnValueReader(extnValue, AsnEncodingRules.DER); - aiaReader.ReadSequence(); - aiaReader.ThrowIfNotEmpty(); - break; - } - } - } - } - - tbsCertList.ThrowIfNotEmpty(); - - while (revokedCertificates.HasData) - { - RevokedCertificate revokedCertificate = new RevokedCertificate(ref revokedCertificates, version); - list.Add(revokedCertificate); - } - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - - bytesConsumed = payloadLength; - currentCrlNumber = crlNumber; - return new CertificateRevocationListBuilder(list); - } - - public static CertificateRevocationListBuilder LoadPem(string currentCrl, out BigInteger currentCrlNumber) - { - ArgumentNullException.ThrowIfNull(currentCrl); - - return LoadPem(currentCrl.AsSpan(), out currentCrlNumber); - } - - public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan currentCrl, out BigInteger currentCrlNumber) - { - foreach ((ReadOnlySpan contents, PemFields fields) in new PemEnumerator(currentCrl)) - { - if (contents[fields.Label].SequenceEqual(PemLabels.X509CertificateRevocationList)) - { - byte[] rented = ArrayPool.Shared.Rent(fields.DecodedDataLength); - - if (!Convert.TryFromBase64Chars(contents[fields.Base64Data], rented, out int bytesWritten)) - { - Debug.Fail("Base64Decode failed, but PemEncoding said it was legal"); - throw new UnreachableException(); - } - - CertificateRevocationListBuilder ret = Load( - rented.AsSpan(0, bytesWritten), - out currentCrlNumber, - out int bytesConsumed); - - Debug.Assert(bytesConsumed == bytesWritten); - ArrayPool.Shared.Return(rented); - return ret; - } - } - - throw new CryptographicException(SR.Cryptography_NoPemOfLabel, PemLabels.X509CertificateRevocationList); - } - public void AddEntry(X509Certificate2 certificate) { AddEntry(certificate, DateTimeOffset.UtcNow); @@ -200,6 +34,7 @@ public void AddEntry(X509Certificate2 certificate) public void AddEntry(X509Certificate2 certificate, DateTimeOffset revocationTime) { ArgumentNullException.ThrowIfNull(certificate); + AddEntry(certificate.SerialNumberBytes.Span, revocationTime); } @@ -238,257 +73,6 @@ public void ExpireEntries(DateTimeOffset oldestRevocationTimeToKeep) _revoked.RemoveAll(rc => rc.RevocationTime < oldestRevocationTimeToKeep); } - public byte[] Build( - X509Certificate2 issuerCertificate, - BigInteger crlNumber, - DateTimeOffset nextUpdate, - HashAlgorithmName hashAlgorithm, - RSASignaturePadding? rsaSignaturePadding = null) - { - return Build( - issuerCertificate, - crlNumber, - nextUpdate, - DateTimeOffset.UtcNow, - hashAlgorithm, - rsaSignaturePadding); - } - - public byte[] Build( - X509Certificate2 issuerCertificate, - BigInteger crlNumber, - DateTimeOffset nextUpdate, - DateTimeOffset thisUpdate, - HashAlgorithmName hashAlgorithm, - RSASignaturePadding? rsaSignaturePadding = null) - { - ArgumentNullException.ThrowIfNull(issuerCertificate); - - if (!issuerCertificate.HasPrivateKey) - throw new ArgumentException( - SR.Cryptography_CertReq_IssuerRequiresPrivateKey, - nameof(issuerCertificate)); - if (crlNumber < 0) - throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); - if (nextUpdate <= thisUpdate) - throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); - ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); - - // Check the Basic Constraints and Key Usage extensions to help identify inappropriate certificates. - // Note that this is not a security check. The system library backing X509Chain will use these same criteria - // to determine if a chain is valid; and a user can easily call the X509SignatureGenerator overload to - // bypass this validation. We're simply helping them at signing time understand that they've - // chosen the wrong cert. - var basicConstraints = (X509BasicConstraintsExtension?)issuerCertificate.Extensions[Oids.BasicConstraints2]; - var keyUsage = (X509KeyUsageExtension?)issuerCertificate.Extensions[Oids.KeyUsage]; - var subjectKeyIdentifier = - (X509SubjectKeyIdentifierExtension?)issuerCertificate.Extensions[Oids.SubjectKeyIdentifier]; - - if (basicConstraints == null) - throw new ArgumentException( - SR.Cryptography_CertReq_BasicConstraintsRequired, - nameof(issuerCertificate)); - if (!basicConstraints.CertificateAuthority) - throw new ArgumentException( - SR.Cryptography_CertReq_IssuerBasicConstraintsInvalid, - nameof(issuerCertificate)); - if (keyUsage != null && (keyUsage.KeyUsages & X509KeyUsageFlags.CrlSign) == 0) - throw new ArgumentException(SR.Cryptography_CRLBuilder_IssuerKeyUsageInvalid, nameof(issuerCertificate)); - - AsymmetricAlgorithm? key = null; - string keyAlgorithm = issuerCertificate.GetKeyAlgorithm(); - X509SignatureGenerator generator; - - try - { - switch (keyAlgorithm) - { - case Oids.Rsa: - if (rsaSignaturePadding is null) - { - throw new ArgumentException( - "The issuer certificate uses an RSA key, but no RSASignaturePadding value was provided."); - } - - RSA? rsa = issuerCertificate.GetRSAPrivateKey(); - key = rsa; - generator = X509SignatureGenerator.CreateForRSA(rsa!, rsaSignaturePadding); - break; - case Oids.EcPublicKey: - ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey(); - key = ecdsa; - generator = X509SignatureGenerator.CreateForECDsa(ecdsa!); - break; - default: - throw new ArgumentException( - SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm), - nameof(issuerCertificate)); - } - - X509AuthorityKeyIdentifierExtension akid; - - if (subjectKeyIdentifier is not null) - { - akid = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectKeyIdentifier); - } - else - { - akid = X509AuthorityKeyIdentifierExtension.CreateFromIssuerNameAndSerialNumber( - issuerCertificate.IssuerName, - issuerCertificate.SerialNumberBytes.Span); - } - - return Build( - issuerCertificate.SubjectName, - generator, - crlNumber, - nextUpdate, - thisUpdate, - hashAlgorithm, - akid); - } - finally - { - key?.Dispose(); - } - } - - public byte[] Build( - X500DistinguishedName issuerName, - X509SignatureGenerator generator, - BigInteger crlNumber, - DateTimeOffset nextUpdate, - HashAlgorithmName hashAlgorithm, - X509AuthorityKeyIdentifierExtension akid) - { - return Build( - issuerName, - generator, - crlNumber, - nextUpdate, - DateTimeOffset.UtcNow, - hashAlgorithm, - akid); - } - - public byte[] Build( - X500DistinguishedName issuerName, - X509SignatureGenerator generator, - BigInteger crlNumber, - DateTimeOffset nextUpdate, - DateTimeOffset thisUpdate, - HashAlgorithmName hashAlgorithm, - X509AuthorityKeyIdentifierExtension akid) - { - ArgumentNullException.ThrowIfNull(issuerName); - ArgumentNullException.ThrowIfNull(generator); - - if (crlNumber < 0) - throw new ArgumentOutOfRangeException(nameof(crlNumber), SR.ArgumentOutOfRange_NeedNonNegNum); - if (nextUpdate <= thisUpdate) - throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); - - ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); - ArgumentNullException.ThrowIfNull(akid); - - byte[] signatureAlgId = generator.GetSignatureAlgorithmIdentifier(hashAlgorithm); - AsnWriter writer = (_writer ??= new AsnWriter(AsnEncodingRules.DER)); - writer.Reset(); - - // TBSCertList - using (writer.PushSequence()) - { - // version v2(1) - writer.WriteInteger(1); - - // signature (AlgorithmIdentifier) - writer.WriteEncodedValue(signatureAlgId); - - // issuer - writer.WriteEncodedValue(issuerName.RawData); - - // thisUpdate - WriteX509Time(writer, thisUpdate); - - // nextUpdate - WriteX509Time(writer, nextUpdate); - - // revokedCertificates (don't write down if empty) - if (_revoked.Count > 0) - { - // SEQUENCE OF - using (writer.PushSequence()) - { - foreach (RevokedCertificate revoked in _revoked) - { - // Anonymous CRL Entry type - using (writer.PushSequence()) - { - writer.WriteInteger(revoked.Serial); - WriteX509Time(writer, revoked.RevocationTime); - - if (revoked.Extensions is not null) - { - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) - { - writer.WriteEncodedValue(revoked.Extensions); - } - } - } - } - } - } - - // extensions [0] EXPLICIT Extensions - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) - { - // Extensions (SEQUENCE OF) - using (writer.PushSequence()) - { - // Authority Key Identifier Extension - using (writer.PushSequence()) - { - writer.WriteObjectIdentifier(akid.Oid!.Value!); - - if (akid.Critical) - { - writer.WriteBoolean(true); - } - - writer.WriteOctetString(akid.RawData); - } - - // CRL Number Extension - using (writer.PushSequence()) - { - writer.WriteObjectIdentifier("2.5.29.20"); - - using (writer.PushOctetString()) - { - writer.WriteInteger(crlNumber); - } - } - } - } - } - - byte[] tbsCertList = writer.Encode(); - writer.Reset(); - - byte[] signature = generator.SignData(tbsCertList, hashAlgorithm); - - // CertificateList - using (writer.PushSequence()) - { - writer.WriteEncodedValue(tbsCertList); - writer.WriteEncodedValue(signatureAlgId); - writer.WriteBitString(signature); - } - - byte[] crl = writer.Encode(); - return crl; - } - private static DateTimeOffset ReadX509Time(ref AsnValueReader reader) { if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.UtcTime)) From f0f3c737dcaa8462cda85edc26445f49dfd77d64 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 30 Jun 2022 17:39:54 -0700 Subject: [PATCH 40/43] Add CrlBuilder.RemoveEntry --- .../CertificateCreation/CrlBuilderTests.cs | 52 +++++++++++++++++++ .../ref/System.Security.Cryptography.cs | 2 + .../CertificateRevocationListBuilder.cs | 19 +++++++ 3 files changed, 73 insertions(+) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index c330487186130..d05b5feeceb37 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -546,6 +546,58 @@ public static void BuildSingleEntry() }); } + [Fact] + public static void AddTwiceRemoveOnce() + { + BuildRsaCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (cert, notNow) => + { + HashAlgorithmName hashAlg = HashAlgorithmName.SHA256; + RSASignaturePadding pad = RSASignaturePadding.Pkcs1; + DateTimeOffset now = new DateTimeOffset(2013, 4, 6, 7, 58, 9, TimeSpan.Zero); + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + ReadOnlySpan serial = new byte[] { 0x01, 0x01, 0x02, 0x03, 0x05, 0x08, 0x0C, 0x15 }; + + builder.AddEntry(serial, now.AddSeconds(-1812)); + + // This entry will have a revocation time based on DateTimeOffset.UtcNow, so it's unpredictable. + // But, that's OK, because we're going to remove it. + builder.AddEntry(serial); + + // Remove only the last one. + builder.RemoveEntry(serial); + + byte[] explicitUpdateTime = builder.Build(cert, 123, now.AddMinutes(5), now, hashAlg, pad); + + // The length of the output depends on a number of factors, but they're all stable + // for this test (since it doesn't use ECDSA's variable-length, non-deterministic, signature) + // + // In fact, because RSASSA-PKCS1 is a deterministic algorithm, we can check it for a fixed output. + + byte[] expected = ( + "308201B430819D020101300D06092A864886F70D01010B0500301D311B301906" + + "035504031312416464547769636552656D6F76654F6E6365170D313330343036" + + "3037353830395A170D3133303430363038303330395A301B3019020801010203" + + "05080C15170D3133303430363037323735375AA02F302D301F0603551D230418" + + "3016801478A5C75D51667331D5A96924114C9B5FA00D7BCB300A0603551D1404" + + "0302017B300D06092A864886F70D01010B050003820101005B959102271A96F4" + + "4EF37B6C7D1BC566875C6CB2B45B5F32CE474155890047EAD9CF74A97E89CA4B" + + "2139417167B0EDC537300A5271F399820E1D2B326DF85FD4F3249B4D0AE0B067" + + "5662986E44E2041E1DADC4A3F557FFE6E50DB12E12BE5A6734BD3EBD537D348D" + + "DD454C2310AEFC586722730252AA63F20CCF8E5127E5A2E5FDD0F16E1296E831" + + "03730D6ACA32584D33DC51B6075000507A808EDC012C982BF9969970C115D0BB" + + "BEDB56089C5E3A51FD1E6180088BDEC343976E42BE4F04798E19B043D5295E1D" + + "A9C0371F6E62CED8626E65804E13A9D28D5A9458AAE6DEC3E06B43E236EDEA55" + + "6AAA7E7A32930C2E8289D62E1CBF7AFAB632FF260B1B49F9").HexToByteArray(); + + AssertExtensions.SequenceEqual(expected, explicitUpdateTime); + }); + } + [Fact] public static void BuildSimpleCdp() { diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 7b21d56d2bc31..43be5a3e791da 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2466,6 +2466,8 @@ public void ExpireEntries(System.DateTimeOffset oldestRevocationTimeToKeep) { } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(System.ReadOnlySpan currentCrl, out System.Numerics.BigInteger currentCrlNumber, out int bytesConsumed) { throw null; } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(System.ReadOnlySpan currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(string currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } + public void RemoveEntry(byte[] serialNumber) { } + public void RemoveEntry(System.ReadOnlySpan serialNumber) { } } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 19179581baf1d..01281689ae4ec 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -73,6 +73,25 @@ public void ExpireEntries(DateTimeOffset oldestRevocationTimeToKeep) _revoked.RemoveAll(rc => rc.RevocationTime < oldestRevocationTimeToKeep); } + public void RemoveEntry(byte[] serialNumber) + { + ArgumentNullException.ThrowIfNull(serialNumber); + + RemoveEntry(new ReadOnlySpan(serialNumber)); + } + + public void RemoveEntry(ReadOnlySpan serialNumber) + { + for (int i = _revoked.Count - 1; i >= 0; i--) + { + if (serialNumber.SequenceEqual(_revoked[i].Serial)) + { + _revoked.RemoveAt(i); + return; + } + } + } + private static DateTimeOffset ReadX509Time(ref AsnValueReader reader) { if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.UtcTime)) From 19e35eceebbaef99c948a958ceb7a148e33830e3 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 30 Jun 2022 18:23:53 -0700 Subject: [PATCH 41/43] Apply feedback --- .../tests/CertificateCreation/CrlBuilderTests.cs | 2 +- .../ref/System.Security.Cryptography.cs | 6 +++--- .../X509Certificates/CertificateRequest.cs | 2 +- .../CertificateRevocationListBuilder.cs | 10 ++++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index d05b5feeceb37..570419352d190 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -569,7 +569,7 @@ public static void AddTwiceRemoveOnce() builder.AddEntry(serial); // Remove only the last one. - builder.RemoveEntry(serial); + Assert.True(builder.RemoveEntry(serial), "builder.RemoveEntry(serial)"); byte[] explicitUpdateTime = builder.Build(cert, 123, now.AddMinutes(5), now, hashAlg, pad); diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 43be5a3e791da..b59998b7d8d4f 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2425,7 +2425,7 @@ public sealed partial class CertificateRequest public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding) { } public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } - public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding? rsaSignaturePadding) { } + public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding? rsaSignaturePadding = null) { } public CertificateRequest(string subjectName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } public CertificateRequest(string subjectName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding) { } public System.Collections.ObjectModel.Collection CertificateExtensions { get { throw null; } } @@ -2466,8 +2466,8 @@ public void ExpireEntries(System.DateTimeOffset oldestRevocationTimeToKeep) { } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder Load(System.ReadOnlySpan currentCrl, out System.Numerics.BigInteger currentCrlNumber, out int bytesConsumed) { throw null; } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(System.ReadOnlySpan currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(string currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; } - public void RemoveEntry(byte[] serialNumber) { } - public void RemoveEntry(System.ReadOnlySpan serialNumber) { } + public bool RemoveEntry(byte[] serialNumber) { throw null; } + public bool RemoveEntry(System.ReadOnlySpan serialNumber) { throw null; } } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index 7b2001390ac97..f960f19b8001b 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -222,7 +222,7 @@ public CertificateRequest( X500DistinguishedName subjectName, PublicKey publicKey, HashAlgorithmName hashAlgorithm, - RSASignaturePadding? rsaSignaturePadding) + RSASignaturePadding? rsaSignaturePadding = default) { ArgumentNullException.ThrowIfNull(subjectName); ArgumentNullException.ThrowIfNull(publicKey); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 01281689ae4ec..9da8fdaaed4ac 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -73,23 +73,25 @@ public void ExpireEntries(DateTimeOffset oldestRevocationTimeToKeep) _revoked.RemoveAll(rc => rc.RevocationTime < oldestRevocationTimeToKeep); } - public void RemoveEntry(byte[] serialNumber) + public bool RemoveEntry(byte[] serialNumber) { ArgumentNullException.ThrowIfNull(serialNumber); - RemoveEntry(new ReadOnlySpan(serialNumber)); + return RemoveEntry(new ReadOnlySpan(serialNumber)); } - public void RemoveEntry(ReadOnlySpan serialNumber) + public bool RemoveEntry(ReadOnlySpan serialNumber) { for (int i = _revoked.Count - 1; i >= 0; i--) { if (serialNumber.SequenceEqual(_revoked[i].Serial)) { _revoked.RemoveAt(i); - return; + return true; } } + + return false; } private static DateTimeOffset ReadX509Time(ref AsnValueReader reader) From 0840313b62cc6a7d7112f9149ebaca8fa66dd13b Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 1 Jul 2022 11:07:45 -0700 Subject: [PATCH 42/43] Add X509RevocationReason support --- .../src/System/Security/Cryptography/Oids.cs | 1 + .../CertificateCreation/CrlBuilderTests.cs | 48 ++++++++++ .../ref/System.Security.Cryptography.cs | 23 +++-- .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 1 + .../CertificateRevocationListBuilder.Build.cs | 5 +- .../CertificateRevocationListBuilder.cs | 93 +++++++++++++------ .../X509Certificates/X509RevocationReason.cs | 82 ++++++++++++++++ 8 files changed, 219 insertions(+), 37 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509RevocationReason.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs index 913270624a9e7..40e761dab09d4 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs @@ -107,6 +107,7 @@ internal static partial class Oids internal const string IssuerAltName = "2.5.29.18"; internal const string BasicConstraints2 = "2.5.29.19"; internal const string CrlNumber = "2.5.29.20"; + internal const string CrlReasons = "2.5.29.21"; internal const string CrlDistributionPoints = "2.5.29.31"; internal const string CertPolicies = "2.5.29.32"; internal const string AnyCertPolicy = "2.5.29.32.0"; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index 570419352d190..fedd99f802c1c 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -546,6 +546,54 @@ public static void BuildSingleEntry() }); } + [Fact] + public static void BuildSingleEntryWithReason() + { + BuildRsaCertificateAndRun( + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (cert, notNow) => + { + HashAlgorithmName hashAlg = HashAlgorithmName.SHA256; + RSASignaturePadding pad = RSASignaturePadding.Pkcs1; + DateTimeOffset now = new DateTimeOffset(2013, 4, 6, 7, 58, 9, TimeSpan.Zero); + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + + builder.AddEntry( + stackalloc byte[] { 0x01, 0x01, 0x02, 0x03, 0x05, 0x08, 0x0C, 0x15 }, + now.AddSeconds(-1812), + X509RevocationReason.KeyCompromise); + + byte[] explicitUpdateTime = builder.Build(cert, 123, now.AddMinutes(5), now, hashAlg, pad); + + // The length of the output depends on a number of factors, but they're all stable + // for this test (since it doesn't use ECDSA's variable-length, non-deterministic, signature) + // + // In fact, because RSASSA-PKCS1 is a deterministic algorithm, we can check it for a fixed output. + + byte[] expected = ( + "308201CA3081B3020101300D06092A864886F70D01010B050030253123302106" + + "03550403131A4275696C6453696E676C65456E74727957697468526561736F6E" + + "170D3133303430363037353830395A170D3133303430363038303330395A3029" + + "302702080101020305080C15170D3133303430363037323735375A300C300A06" + + "03551D1504030A0101A02F302D301F0603551D2304183016801478A5C75D5166" + + "7331D5A96924114C9B5FA00D7BCB300A0603551D14040302017B300D06092A86" + + "4886F70D01010B0500038201010055283C97666765D19AABFFDAA36112781957" + + "1FCA3CE68AA00DAFDFF784F8F34D0EFF4EC8659A26A254DDDC9BBD7D664E0160" + + "4D3696209B5A4B0FFF57102BC8AA17FED0D33AD3452BE5E22269E78BB4084698" + + "28E2814EA8E6B8003EBB7AC727DAD912580F941C6D2616195C083218F997D682" + + "966CC6EEB810B815ABA991135469E2CD2915EE7C0FCB387C0B6169E0F1F2CFD8" + + "2274D134DB2C27826E04138FF8C7AB4B8678AF53C3904C09F1F9589D5325E5D4" + + "3F2A7F2EF81BD19DE5362181B9E0603DE98F664F98A6599A3BFB9AAFA2DC3491" + + "9305B8812BC11BFA06C6550A257396766B750D10B6C6BEA7A193E4D3F4C2FEFD" + + "FC2B875D1A2BFDB849EBBCFC767B").HexToByteArray(); + + AssertExtensions.SequenceEqual(expected, explicitUpdateTime); + }); + } + [Fact] public static void AddTwiceRemoveOnce() { diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index b59998b7d8d4f..49d446c54f248 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2450,12 +2450,9 @@ public CertificateRequest(string subjectName, System.Security.Cryptography.RSA k public sealed partial class CertificateRevocationListBuilder { public CertificateRevocationListBuilder() { } - public void AddEntry(byte[] serialNumber) { } - public void AddEntry(byte[] serialNumber, System.DateTimeOffset revocationTime) { } - public void AddEntry(System.ReadOnlySpan serialNumber) { } - public void AddEntry(System.ReadOnlySpan serialNumber, System.DateTimeOffset revocationTime) { } - public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { } - public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.DateTimeOffset revocationTime) { } + public void AddEntry(byte[] serialNumber, System.DateTimeOffset? revocationTime = default(System.DateTimeOffset?), System.Security.Cryptography.X509Certificates.X509RevocationReason? reason = default(System.Security.Cryptography.X509Certificates.X509RevocationReason?)) { } + public void AddEntry(System.ReadOnlySpan serialNumber, System.DateTimeOffset? revocationTime = default(System.DateTimeOffset?), System.Security.Cryptography.X509Certificates.X509RevocationReason? reason = default(System.Security.Cryptography.X509Certificates.X509RevocationReason?)) { } + public void AddEntry(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.DateTimeOffset? revocationTime = default(System.DateTimeOffset?), System.Security.Cryptography.X509Certificates.X509RevocationReason? reason = default(System.Security.Cryptography.X509Certificates.X509RevocationReason?)) { } public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } public byte[] Build(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension akid) { throw null; } public byte[] Build(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.Numerics.BigInteger crlNumber, System.DateTimeOffset nextUpdate, System.DateTimeOffset thisUpdate, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding? rsaSignaturePadding = null) { throw null; } @@ -3150,6 +3147,20 @@ public enum X509RevocationMode Online = 1, Offline = 2, } + public enum X509RevocationReason + { + Unspecified = 0, + KeyCompromise = 1, + CACompromise = 2, + AffiliationChanged = 3, + Superseded = 4, + CessationOfOperation = 5, + CertificateHold = 6, + RemoveFromCrl = 8, + PrivilegeWithdrawn = 9, + AACompromise = 10, + WeakAlgorithmOrKey = 11, + } public abstract partial class X509SignatureGenerator { protected X509SignatureGenerator() { } diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index b7cc8edbbc954..1d592c6214105 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -264,6 +264,9 @@ The issuer certificate's Key Usage extension is present but does not contain the CrlSign flag. + + The specified revocation reason is not supported. + FlushFinalBlock() method was called twice on a CryptoStream. It can only be called once. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 415953d6a70b6..c5788e3a86073 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -486,6 +486,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs index 862f311cf30a0..6174d5467b3ad 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs @@ -201,10 +201,7 @@ public byte[] Build( if (revoked.Extensions is not null) { - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) - { - writer.WriteEncodedValue(revoked.Extensions); - } + writer.WriteEncodedValue(revoked.Extensions); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs index 9da8fdaaed4ac..dba0226525d44 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.cs @@ -26,45 +26,88 @@ private CertificateRevocationListBuilder(List revoked) _revoked = revoked; } - public void AddEntry(X509Certificate2 certificate) - { - AddEntry(certificate, DateTimeOffset.UtcNow); - } - - public void AddEntry(X509Certificate2 certificate, DateTimeOffset revocationTime) + public void AddEntry( + X509Certificate2 certificate, + DateTimeOffset? revocationTime = null, + X509RevocationReason? reason = null) { ArgumentNullException.ThrowIfNull(certificate); - AddEntry(certificate.SerialNumberBytes.Span, revocationTime); + AddEntry(certificate.SerialNumberBytes.Span, revocationTime, reason); } - public void AddEntry(byte[] serialNumber) - { - AddEntry(serialNumber, DateTimeOffset.UtcNow); - } - - public void AddEntry(byte[] serialNumber, DateTimeOffset revocationTime) + public void AddEntry( + byte[] serialNumber, + DateTimeOffset? revocationTime = null, + X509RevocationReason? reason = null) { ArgumentNullException.ThrowIfNull(serialNumber); - AddEntry(new ReadOnlySpan(serialNumber), revocationTime); + AddEntry(new ReadOnlySpan(serialNumber), revocationTime, reason); } - public void AddEntry(ReadOnlySpan serialNumber) - { - AddEntry(serialNumber, DateTimeOffset.UtcNow); - } - - public void AddEntry(ReadOnlySpan serialNumber, DateTimeOffset revocationTime) + public void AddEntry( + ReadOnlySpan serialNumber, + DateTimeOffset? revocationTime = null, + X509RevocationReason? reason = null) { if (serialNumber.IsEmpty) throw new ArgumentException(SR.Arg_EmptyOrNullArray, nameof(serialNumber)); + byte[]? extensions = null; + + if (reason.HasValue) + { + X509RevocationReason reasonValue = reason.GetValueOrDefault(); + + switch (reasonValue) + { + case X509RevocationReason.Unspecified: + case X509RevocationReason.KeyCompromise: + case X509RevocationReason.CACompromise: + case X509RevocationReason.AffiliationChanged: + case X509RevocationReason.Superseded: + case X509RevocationReason.CessationOfOperation: + case X509RevocationReason.CertificateHold: + case X509RevocationReason.PrivilegeWithdrawn: + case X509RevocationReason.WeakAlgorithmOrKey: + break; + default: + // Includes RemoveFromCrl (no delta CRL support) + // Includes AaCompromise (no support for attribute certificates) + throw new ArgumentOutOfRangeException( + nameof(reason), + reasonValue, + SR.Cryptography_CRLBuilder_ReasonNotSupported); + } + + AsnWriter writer = (_writer ??= new AsnWriter(AsnEncodingRules.DER)); + writer.Reset(); + + // SEQUENCE OF Extension + using (writer.PushSequence()) + { + // Extension + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(Oids.CrlReasons); + + using (writer.PushOctetString()) + { + writer.WriteEnumeratedValue(reasonValue); + } + } + } + + extensions = writer.Encode(); + } + _revoked.Add( new RevokedCertificate { Serial = serialNumber.ToArray(), - RevocationTime = revocationTime.ToUniversalTime(), + RevocationTime = (revocationTime ?? DateTimeOffset.UtcNow).ToUniversalTime(), + Extensions = extensions, }); } @@ -149,16 +192,12 @@ internal RevokedCertificate(ref AsnValueReader reader, int version) if (version > 0 && revokedCertificate.HasData) { - AsnValueReader crlExtensionsExplicit = - revokedCertificate.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); - - if (!crlExtensionsExplicit.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence)) + if (!revokedCertificate.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence)) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - Extensions = crlExtensionsExplicit.ReadEncodedValue().ToArray(); - crlExtensionsExplicit.ThrowIfNotEmpty(); + Extensions = revokedCertificate.ReadEncodedValue().ToArray(); } revokedCertificate.ThrowIfNotEmpty(); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509RevocationReason.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509RevocationReason.cs new file mode 100644 index 0000000000000..42c4b5b098222 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509RevocationReason.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace System.Security.Cryptography.X509Certificates +{ + /// + /// Specifies the reason a certificate was revoked. + /// + /// + /// This enum represents the CRLReason enum from IETF RFC 5280 and + /// ITU T-REC X.509. + /// + public enum X509RevocationReason + { + /// + /// Revocation occurred for a reason that has no more specific value. + /// + Unspecified = 0, + + /// + /// The private key, or another validated portion of an end-entity certificate, + /// is suspected to have been compromised. + /// + KeyCompromise = 1, + + /// + /// The private key, or another validated portion of a Certificate Authority (CA) certificate, + /// is suspected to have been compromised. + /// + CACompromise = 2, + + /// + /// The subject's name, or other validated information in the certificate, has changed without + /// anything being compromised. + /// + AffiliationChanged = 3, + + /// + /// The certificate has been superseded, but without anything being compromised. + /// + Superseded = 4, + + /// + /// The certificate is no longer needed, but nothing is suspected to be compromised. + /// + CessationOfOperation = 5, + + /// + /// The certificate is temporarily suspended, and may either return to service or + /// become permanently revoked in the future. + /// + CertificateHold = 6, + + // There is no 7 + + /// + /// The certificate was revoked with on a base + /// Certificate Revocation List (CRL) and is being returned to service on a delta CRL. + /// + RemoveFromCrl = 8, + + /// + /// A privilege contained within the certificate has been withdrawn. + /// + PrivilegeWithdrawn = 9, + + /// + /// It is known, or suspected, that aspects of the Attribute Authority (AA) validated in + /// the attribute certificate have been compromised. + /// + AACompromise = 10, + + /// + /// The certificate key uses a weak cryptographic algorithm, or the + /// key is too short, or the key was generated in an unsafe manner. + /// + WeakAlgorithmOrKey = 11, + } +} From 936a95a07c9a824bf9de81bcd288814114faacf4 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 1 Jul 2022 12:09:35 -0700 Subject: [PATCH 43/43] Add an argument validation test for reason codes --- .../CertificateCreation/CrlBuilderTests.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs index fedd99f802c1c..f117966d0597a 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/CrlBuilderTests.cs @@ -659,6 +659,38 @@ public static void BuildSimpleCdp() Assert.Equal(expected, ext.RawData); } + + [Fact] + public static void UnsupportedRevocationReasons() + { + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); + byte[] serial = { 1, 2, 3 }; + const string ParamName = "reason"; + + Assert.Throws( + ParamName, + () => builder.AddEntry(serial, reason: (X509RevocationReason)(-1))); + + Assert.Throws( + ParamName, + () => builder.AddEntry(serial, reason: (X509RevocationReason)(-2))); + + Assert.Throws( + ParamName, + () => builder.AddEntry(serial, reason: (X509RevocationReason)7)); + + Assert.Throws( + ParamName, + () => builder.AddEntry(serial, reason: (X509RevocationReason)12)); + + Assert.Throws( + ParamName, + () => builder.AddEntry(serial, reason: X509RevocationReason.AACompromise)); + + Assert.Throws( + ParamName, + () => builder.AddEntry(serial, reason: X509RevocationReason.RemoveFromCrl)); + } private static void BuildCertificateAndRun( IEnumerable extensions,