Add BuildCrlDistributionPointExtension
authorJeremy Barton <jbarton@microsoft.com>
Wed, 27 Jul 2022 16:56:05 +0000 (09:56 -0700)
committerGitHub <noreply@github.com>
Wed, 27 Jul 2022 16:56:05 +0000 (09:56 -0700)
src/libraries/Common/src/System/Security/Cryptography/Oids.Shared.cs
src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs
src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs
src/libraries/System.Security.Cryptography/src/Resources/Strings.resx
src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.CdpExtension.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
src/libraries/System.Security.Cryptography/tests/X509Certificates/CrlDistPointBuilderTests.cs [new file with mode: 0644]

index 87cb468..59ad3a9 100644 (file)
@@ -32,6 +32,7 @@ namespace System.Security.Cryptography
         private static volatile Oid? s_authorityKeyIdentifierOid;
         private static volatile Oid? s_authorityInformationAccessOid;
         private static volatile Oid? s_crlNumberOid;
+        private static volatile Oid? s_crlDistributionPointOid;
         private static volatile Oid? s_commonNameOid;
         private static volatile Oid? s_countryOrRegionOid;
         private static volatile Oid? s_localityNameOid;
@@ -66,6 +67,7 @@ namespace System.Security.Cryptography
         internal static Oid SubjectAltNameOid => s_subjectAltNameOid ??= InitializeOid(SubjectAltName);
         internal static Oid AuthorityInformationAccessOid => s_authorityInformationAccessOid ??= InitializeOid(AuthorityInformationAccess);
         internal static Oid CrlNumberOid => s_crlNumberOid ??= InitializeOid(CrlNumber);
+        internal static Oid CrlDistributionPointsOid => s_crlDistributionPointOid ??= InitializeOid(CrlDistributionPoints);
 
         internal static Oid CommonNameOid => s_commonNameOid ??= InitializeOid(CommonName);
         internal static Oid CountryOrRegionNameOid => s_countryOrRegionOid ??= InitializeOid(CountryOrRegionName);
index 0140132..4fed67e 100644 (file)
@@ -703,33 +703,7 @@ SingleResponse ::= SEQUENCE {
 
         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 X509Extension CreateAkidExtension()
index 8321a27..1e63f64 100644 (file)
@@ -2473,6 +2473,7 @@ namespace System.Security.Cryptography.X509Certificates
         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.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.X509Certificates.X509AuthorityKeyIdentifierExtension authorityKeyIdentifier, System.DateTimeOffset? thisUpdate = default(System.DateTimeOffset?)) { 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, System.DateTimeOffset? thisUpdate = default(System.DateTimeOffset?)) { throw null; }
+        public static System.Security.Cryptography.X509Certificates.X509Extension BuildCrlDistributionPointExtension(System.Collections.Generic.IEnumerable<string> uris, bool critical = false) { 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<byte> currentCrl, out System.Numerics.BigInteger currentCrlNumber, out int bytesConsumed) { throw null; }
         public static System.Security.Cryptography.X509Certificates.CertificateRevocationListBuilder LoadPem(System.ReadOnlySpan<char> currentCrl, out System.Numerics.BigInteger currentCrlNumber) { throw null; }
index fb9fdee..1c1b430 100644 (file)
   <data name="Cryptography_X509_AKID_NoSKID" xml:space="preserve">
     <value>The provided certificate does not have a Subject Key Identifier extension.</value>
   </data>
+  <data name="Cryptography_X509_CDP_MustNotBuildEmpty" xml:space="preserve">
+    <value>The collection of distribution URIs must be non-empty.</value>
+  </data>
+  <data name="Cryptography_X509_CDP_NullValue" xml:space="preserve">
+    <value>One of the provided CRL Distribution Point URIs is a null value.</value>
+  </data>
   <data name="Cryptography_X509_CertificateCorrupted" xml:space="preserve">
     <value>Certificate '{0}' is corrupted.</value>
   </data>
index 8b7ade8..438f9a7 100644 (file)
     <Compile Include="System\Security\Cryptography\X509Certificates\CertificateRequest.cs" />
     <Compile Include="System\Security\Cryptography\X509Certificates\CertificateRevocationListBuilder.cs" />
     <Compile Include="System\Security\Cryptography\X509Certificates\CertificateRevocationListBuilder.Build.cs" />
+    <Compile Include="System\Security\Cryptography\X509Certificates\CertificateRevocationListBuilder.CdpExtension.cs" />
     <Compile Include="System\Security\Cryptography\X509Certificates\CertificateRevocationListBuilder.Load.cs" />
     <Compile Include="System\Security\Cryptography\X509Certificates\ChainPal.cs" />
     <Compile Include="System\Security\Cryptography\X509Certificates\DSACertificateExtensions.cs" />
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 (file)
index 0000000..a6b2380
--- /dev/null
@@ -0,0 +1,115 @@
+// 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 Internal.Cryptography;
+
+namespace System.Security.Cryptography.X509Certificates
+{
+    public sealed partial class CertificateRevocationListBuilder
+    {
+        /// <summary>
+        ///   Builds a CRL Distribution Point Extension with the specified retrieval URIs.
+        /// </summary>
+        /// <param name="uris">
+        ///   The URIs to include as distribution points for the relevant Certificate
+        ///   Revocation List (CRL).
+        /// </param>
+        /// <param name="critical">
+        ///   <see langword="true" /> to mark the extension as critical;
+        ///   otherwise, <see langword="false" />.
+        ///   The default is <see langword="false" />.
+        /// </param>
+        /// <returns>
+        ///   An object suitable for use as a CRL Distribution Point Extension.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        ///   <paramref name="uris"/> is <see langword="null" />.
+        /// </exception>
+        /// <exception cref="ArgumentException">
+        ///   <para>
+        ///     <paramref name="uris"/> contains a <see langword="null" /> value.
+        ///   </para>
+        ///   <para>- or -</para>
+        ///   <para>
+        ///     <paramref name="uris"/> is empty.
+        ///   </para>
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   One of the values in <paramref name="uris"/>
+        ///   contains characters outside of the International Alphabet 5 (IA5) character space
+        ///   (which is equivalent to 7-bit US-ASCII).
+        /// </exception>
+        public static X509Extension BuildCrlDistributionPointExtension(
+            IEnumerable<string> uris,
+            bool critical = false)
+        {
+            ArgumentNullException.ThrowIfNull(uris);
+
+            // 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
+                            try
+                            {
+                                writer.WriteCharacterString(
+                                    UniversalTagNumber.IA5String,
+                                    uri,
+                                    new Asn1Tag(TagClass.ContextSpecific, 6));
+                            }
+                            catch (System.Text.EncoderFallbackException e)
+                            {
+                                throw new CryptographicException(SR.Cryptography_Invalid_IA5String, e);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (writer is null)
+            {
+                throw new ArgumentException(SR.Cryptography_X509_CDP_MustNotBuildEmpty, nameof(uris));
+            }
+
+            // CRLDistributionPoints
+            writer.PopSequence();
+
+            byte[] encoded = writer.Encode();
+            return new X509Extension(Oids.CrlDistributionPointsOid, encoded, critical);
+        }
+    }
+}
index f65e905..f2d2bc4 100644 (file)
     <Compile Include="TripleDesTests.cs" />
     <Compile Include="ZeroMemoryTests.cs" />
     <Compile Include="X509Certificates\ChainPolicyTests.cs" />
+    <Compile Include="X509Certificates\CrlDistPointBuilderTests.cs" />
     <Compile Include="X509Certificates\X500DistinguishedNameBuilderTests.cs" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CrlDistPointBuilderTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CrlDistPointBuilderTests.cs
new file mode 100644 (file)
index 0000000..239cf6c
--- /dev/null
@@ -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 Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.X509Certificates.Tests
+{
+    public static class CrlDistPointBuilderTests
+    {
+        [Fact]
+        public static void NullEnumerable()
+        {
+            Assert.Throws<ArgumentNullException>(
+                "uris",
+                () => CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(null));
+        }
+
+        [Fact]
+        public static void NullUriInEnumerable()
+        {
+            Assert.Throws<ArgumentException>(
+                "uris",
+                () => CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(
+                    new[]
+                    {
+                        "http://cert.example/ca1.crl",
+                        null,
+                        "http://cdn.cert.example/ca1.crl",
+                    }));
+        }
+
+        [Fact]
+        public static void BuildEmpty()
+        {
+            Assert.Throws<ArgumentException>(
+                "uris",
+                () => CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(
+                    System.Linq.Enumerable.Empty<string>()));
+        }
+
+        [Fact]
+        public static void BuildOneEntry()
+        {
+            X509Extension ext = CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(
+                new[]
+                {
+                    "http://crl.microsoft.com/pki/crl/products/MicCodSigPCA_08-31-2010.crl",
+                });
+
+            Assert.False(ext.Critical, "ext.Critical");
+            Assert.Equal("2.5.29.31", ext.Oid.Value);
+
+            byte[] expected = (
+                "304d304ba049a0478645687474703a2f2f63726c2e6d6963726f736f" +
+                "66742e636f6d2f706b692f63726c2f70726f64756374732f4d696343" +
+                "6f645369675043415f30382d33312d323031302e63726c").HexToByteArray();
+
+            AssertExtensions.SequenceEqual(expected, ext.RawData);
+        }
+
+        [Fact]
+        public static void BuildTwoEntries()
+        {
+            // Recreate the encoding of the CDP extension from https://crt.sh/?id=3777044
+            // (the original wasn't marked as critical, but that doesn't affect the RawData value)
+            X509Extension ext = CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(
+                new[]
+                {
+                    "http://crl3.digicert.com/sha2-ev-server-g1.crl",
+                    "http://crl4.digicert.com/sha2-ev-server-g1.crl",
+                },
+                critical: true);
+
+            Assert.True(ext.Critical, "ext.Critical");
+            Assert.Equal("2.5.29.31", ext.Oid.Value);
+
+            byte[] expected = (
+                "306C3034A032A030862E687474703A2F2F63726C332E64696769636572742E63" +
+                "6F6D2F736861322D65762D7365727665722D67312E63726C3034A032A030862E" +
+                "687474703A2F2F63726C342E64696769636572742E636F6D2F736861322D6576" +
+                "2D7365727665722D67312E63726C").HexToByteArray();
+
+            AssertExtensions.SequenceEqual(expected, ext.RawData);
+        }
+
+        [Fact]
+        public static void UriNotValidated()
+        {
+            X509Extension ext = CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(
+                new[]
+                {
+                    "!!!!",
+                });
+
+            Assert.Equal("300C300AA008A006860421212121", ext.RawData.ByteArrayToHex());
+        }
+
+        [Fact]
+        public static void OnlyAscii7Permitted()
+        {
+            Assert.Throws<CryptographicException>(
+                () => CertificateRevocationListBuilder.BuildCrlDistributionPointExtension(
+                    new[]
+                    {
+                        // http://[nihongo].example/ca4.crl
+                        "http://\u65E5\u672C\u8A8E.example/ca4.crl",
+                    }));
+        }
+    }
+}