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 c674bf61427d16..7b8abe41935b65 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -142,8 +142,7 @@ internal void Revoke(X509Certificate2 certificate, DateTimeOffset revocationTime _revocationList = new List<(byte[], DateTimeOffset)>(); } - byte[] serial = certificate.GetSerialNumber(); - Array.Reverse(serial); + byte[] serial = certificate.SerialNumberBytes.ToArray(); _revocationList.Add((serial, revocationTime)); _crl = null; } @@ -219,8 +218,7 @@ private void RebuildRootWithRevocation(X509Extension cdpExtension, X509Extension req.CertificateExtensions.Add(cdpExtension); req.CertificateExtensions.Add(aiaExtension); - byte[] serial = _cert.GetSerialNumber(); - Array.Reverse(serial); + byte[] serial = _cert.SerialNumberBytes.ToArray(); X509Certificate2 dispose = _cert; @@ -762,9 +760,7 @@ private X509Extension CreateAkidExtension() } // authorityCertSerialNumber [2] CertificateSerialNumber (INTEGER) - byte[] serial = _cert.GetSerialNumber(); - Array.Reverse(serial); - writer.WriteInteger(serial, s_context2); + writer.WriteInteger(_cert.SerialNumberBytes.Span, s_context2); } else { diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertTests.cs index 0d58ca8c994f47..a1e39d7d74b3af 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertTests.cs @@ -67,18 +67,19 @@ public static void X509CertTest() Assert.Equal(certSubjectObsolete, cert.GetIssuerName()); #pragma warning restore CS0618 - int snlen = cert.GetSerialNumber().Length; - Assert.Equal(16, snlen); - - byte[] serialNumber = new byte[snlen]; - Buffer.BlockCopy(cert.GetSerialNumber(), 0, - serialNumber, 0, - snlen); - - Assert.Equal(0xF6, serialNumber[0]); - Assert.Equal(0xB3, serialNumber[snlen / 2]); - Assert.Equal(0x2A, serialNumber[snlen - 1]); - + byte[] serial1 = cert.GetSerialNumber(); + byte[] serial2 = cert.GetSerialNumber(); + Assert.NotSame(serial1, serial2); + AssertExtensions.SequenceEqual(serial1, serial2); + + // Big-endian value + byte[] expectedSerial = "2A98A8770374E7B34195EBE04D9B17F6".HexToByteArray(); + AssertExtensions.SequenceEqual(expectedSerial, cert.SerialNumberBytes.Span); + + // GetSerialNumber() returns in little-endian order. + Array.Reverse(expectedSerial); + AssertExtensions.SequenceEqual(expectedSerial, serial1); + Assert.Equal("1.2.840.113549.1.1.1", cert.GetKeyAlgorithm()); int pklen = cert.GetPublicKey().Length; @@ -377,6 +378,7 @@ public static void UseAfterDispose() Assert.ThrowsAny(() => c.GetKeyAlgorithmParametersString()); Assert.ThrowsAny(() => c.GetPublicKey()); Assert.ThrowsAny(() => c.GetSerialNumber()); + Assert.ThrowsAny(() => c.SerialNumberBytes); Assert.ThrowsAny(() => c.Issuer); Assert.ThrowsAny(() => c.Subject); Assert.ThrowsAny(() => c.NotBefore); @@ -433,6 +435,7 @@ public static void X509Certificate2WithT61String() Assert.Equal(certSubject, cert.Issuer); Assert.Equal("9E7A5CCC9F951A8700", cert.GetSerialNumber().ByteArrayToHex()); + Assert.Equal("00871A959FCC5C7A9E", cert.SerialNumberBytes.ByteArrayToHex()); Assert.Equal("1.2.840.113549.1.1.1", cert.GetKeyAlgorithm()); Assert.Equal(74, cert.GetPublicKey().Length); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs index 254eef6c5b057e..d9f28b979b0c07 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs @@ -30,6 +30,7 @@ private static void VerifyDefaultConstructor(X509Certificate2 c) Assert.ThrowsAny(() => c.GetKeyAlgorithmParametersString()); Assert.ThrowsAny(() => c.GetPublicKey()); Assert.ThrowsAny(() => c.GetSerialNumber()); + Assert.ThrowsAny(() => c.SerialNumberBytes); Assert.ThrowsAny(() => ignored = c.Issuer); Assert.ThrowsAny(() => ignored = c.Subject); Assert.ThrowsAny(() => ignored = c.RawData); @@ -155,6 +156,7 @@ public static void TestCopyConstructor_Pal() Assert.Equal(c1.GetKeyAlgorithmParametersString(), c2.GetKeyAlgorithmParametersString()); Assert.Equal(c1.GetPublicKey(), c2.GetPublicKey()); Assert.Equal(c1.GetSerialNumber(), c2.GetSerialNumber()); + AssertExtensions.SequenceEqual(c1.SerialNumberBytes.Span, c1.SerialNumberBytes.Span); Assert.Equal(c1.Issuer, c2.Issuer); Assert.Equal(c1.Subject, c2.Subject); Assert.Equal(c1.RawData, c2.RawData); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/BasicConstraintsTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/BasicConstraintsTests.cs index 59e08af509c66d..a23d97fec0d7c3 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/BasicConstraintsTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/ExtensionsTests/BasicConstraintsTests.cs @@ -39,6 +39,23 @@ public static void Encode( byte[] expectedDer = expectedDerString.HexToByteArray(); Assert.Equal(expectedDer, ext.RawData); + Assert.Equal(critical, ext.Critical); + + if (certificateAuthority) + { + ext = X509BasicConstraintsExtension.CreateForCertificateAuthority( + hasPathLengthConstraint ? pathLengthConstraint : null); + + AssertExtensions.SequenceEqual(expectedDer, ext.RawData); + Assert.True(ext.Critical, "ext.Critical"); + } + else if (!hasPathLengthConstraint) + { + ext = X509BasicConstraintsExtension.CreateForEndEntity(critical); + + AssertExtensions.SequenceEqual(expectedDer, ext.RawData); + Assert.Equal(critical, ext.Critical); + } } [Theory] 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 cf968f210dea2c..56b6f1bb4e385d 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.Throws(() => e.SubjectKeyIdentifierBytes); } [Theory] @@ -55,6 +57,8 @@ public static void EncodeFromBytes(bool fromSpan) string skid = e.SubjectKeyIdentifier; Assert.Equal("01020304", skid); + + AssertExtensions.SequenceEqual(sk, e.SubjectKeyIdentifierBytes.Span); } [Fact] @@ -69,6 +73,11 @@ public static void EncodeFromString() e = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), false); string skid = e.SubjectKeyIdentifier; Assert.Equal("01ABCD", skid); + Assert.Equal(skid, Convert.ToHexString(e.SubjectKeyIdentifierBytes.Span)); + + ReadOnlyMemory ski1 = e.SubjectKeyIdentifierBytes; + ReadOnlyMemory ski2 = e.SubjectKeyIdentifierBytes; + Assert.True(ski1.Span == ski2.Span, "Two calls to SubjectKeyIdentifierBytes return the same buffer"); } [Fact] @@ -89,6 +98,7 @@ public static void EncodeFromPublicKey() e = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), false); string skid = e.SubjectKeyIdentifier; Assert.Equal("5971A65A334DDA980780FF841EBE87F9723241F2", skid); + Assert.Equal(skid, Convert.ToHexString(e.SubjectKeyIdentifierBytes.Span)); } [Fact] @@ -134,8 +144,9 @@ public static void DecodeFromBER() ext = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), false); string skid = ext.SubjectKeyIdentifier; Assert.Equal("5971A65A334DDA980780FF841EBE87F9723241F2", skid); + Assert.Equal(skid, Convert.ToHexString(ext.SubjectKeyIdentifierBytes.Span)); } - + private static void EncodeDecode( byte[] certBytes, X509SubjectKeyIdentifierHashAlgorithm algorithm, @@ -158,6 +169,11 @@ private static void EncodeDecode( ext = new X509SubjectKeyIdentifierExtension(new AsnEncodedData(rawData), critical); Assert.Equal(expectedIdentifier, ext.SubjectKeyIdentifier); + Assert.Equal(expectedIdentifier, Convert.ToHexString(ext.SubjectKeyIdentifierBytes.Span)); + + ReadOnlyMemory ski1 = ext.SubjectKeyIdentifierBytes; + ReadOnlyMemory ski2 = ext.SubjectKeyIdentifierBytes; + Assert.True(ski1.Span == ski2.Span, "Two calls to SubjectKeyIdentifierBytes return the same buffer"); } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/LoadFromFileTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/LoadFromFileTests.cs index 9740830d7eb0fb..275d024a63efc8 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/LoadFromFileTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/LoadFromFileTests.cs @@ -49,6 +49,8 @@ public static void TestSerial() Assert.Equal(expectedSerialHex, serialHex); serialHex = c.SerialNumber; Assert.Equal(expectedSerialHex, serialHex); + + Assert.Equal(expectedSerialHex, c.SerialNumberBytes.ByteArrayToHex()); } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PropsTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PropsTests.cs index a2759f5126dd46..b57a2361c008bb 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PropsTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PropsTests.cs @@ -35,15 +35,35 @@ public static void TestSubject() public static void TestSerialBytes() { byte[] expectedSerialBytes = "b00000000100dd9f3bd08b0aaf11b000000033".HexToByteArray(); - string expectedSerialString = "33000000B011AF0A8BD03B9FDD0001000000B0"; + const string ExpectedSerialString = "33000000B011AF0A8BD03B9FDD0001000000B0"; using (var c = new X509Certificate2(TestData.MsCertificate)) { byte[] serial = c.GetSerialNumber(); Assert.Equal(expectedSerialBytes, serial); - Assert.Equal(expectedSerialString, c.SerialNumber); + Assert.Equal(ExpectedSerialString, c.SerialNumber); + Assert.Equal(ExpectedSerialString, c.SerialNumberBytes.ByteArrayToHex()); + + ReadOnlyMemory serial1 = c.SerialNumberBytes; + ReadOnlyMemory serial2 = c.SerialNumberBytes; + Assert.True(serial1.Span == serial2.Span, "Two calls to SerialNumberBytes return the same buffer"); + } + } + + [Fact] + public static void SerialNumberBytes_LifetimeIndependentOfCert() + { + const string ExpectedSerialString = "33000000B011AF0A8BD03B9FDD0001000000B0"; + + ReadOnlyMemory serial; + + using (X509Certificate2 cert = new X509Certificate2(TestData.MsCertificate)) + { + serial = cert.SerialNumberBytes; } + + Assert.Equal(ExpectedSerialString, serial.ByteArrayToHex()); } [Theory] 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 564c43813a04c8..c4140da251d35f 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2614,6 +2614,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 { @@ -2651,6 +2653,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; } @@ -3178,6 +3181,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/X509Certificates/X509BasicConstraintsExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509BasicConstraintsExtension.cs index 29b29e4652e197..f109bc0bcfcc87 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,50 @@ public override void CopyFrom(AsnEncodedData asnEncodedData) _decoded = false; } + /// + /// Creates an instance of appropriate for + /// a certificate authority, optionally including a path length constraint value. + /// + /// + /// The longest valid length of a certificate chain between the certificate containing + /// this extension and an end-entity certificate. + /// The default is , an unconstrained length. + /// + /// + /// The configured basic constraints extension. + /// + /// + /// Following the guidance from IETF RFC 3280, the extension returned from this method + /// will have the property set to . + /// + /// + /// is a non-null value less than zero. + /// + public static X509BasicConstraintsExtension CreateForCertificateAuthority(int? pathLengthConstraint = null) + { + return new X509BasicConstraintsExtension( + true, + pathLengthConstraint.HasValue, + pathLengthConstraint.GetValueOrDefault(), + true); + } + + /// + /// Creates an instance of appropriate for + /// an end-entity certificate, optionally marking the extension as critical. + /// + /// + /// to mark the extension as critical; otherwise. + /// The default is . + /// + /// + /// The configured basic constraints extension. + /// + 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) 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 bb925f247304b6..59a82d473dc575 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/X509SubjectKeyIdentifierExtension.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierExtension.cs index 3e15957705bd61..bd4ded85922a0e 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 @@ -8,10 +8,13 @@ namespace System.Security.Cryptography.X509Certificates { public sealed class X509SubjectKeyIdentifierExtension : X509Extension { + private byte[]? _subjectKeyIdentifierBytes; + private string? _subjectKeyIdentifierString; + private bool _decoded; + public X509SubjectKeyIdentifierExtension() : base(Oids.SubjectKeyIdentifierOid) { - _subjectKeyIdentifier = null; _decoded = true; } @@ -51,12 +54,32 @@ public string? SubjectKeyIdentifier { if (!_decoded) { - byte[] subjectKeyIdentifierValue; - X509Pal.Instance.DecodeX509SubjectKeyIdentifierExtension(RawData, out subjectKeyIdentifierValue); - _subjectKeyIdentifier = subjectKeyIdentifierValue.ToHexStringUpper(); - _decoded = true; + Decode(RawData); + } + + return _subjectKeyIdentifierString; + } + } + + /// + /// Gets a value whose contents represent the subject key identifier (SKI) for a certificate. + /// + /// + /// The subject key identifier (SKI) for a certificate. + /// + public ReadOnlyMemory SubjectKeyIdentifierBytes + { + get + { + // Rather than check _decoded, this property checks for a null _subjectKeyIdentifierBytes 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 (_subjectKeyIdentifierBytes is null) + { + Decode(RawData); } - return _subjectKeyIdentifier; + + return _subjectKeyIdentifierBytes; } } @@ -66,6 +89,13 @@ public override void CopyFrom(AsnEncodedData asnEncodedData) _decoded = false; } + private void Decode(byte[] rawData) + { + X509Pal.Instance.DecodeX509SubjectKeyIdentifierExtension(rawData, out _subjectKeyIdentifierBytes); + _subjectKeyIdentifierString = _subjectKeyIdentifierBytes.ToHexStringUpper(); + _decoded = true; + } + private static byte[] EncodeExtension(ReadOnlySpan subjectKeyIdentifier) { if (subjectKeyIdentifier.Length == 0) @@ -121,8 +151,5 @@ private static byte[] GenerateSubjectKeyIdentifierFromPublicKey(PublicKey key, X throw new ArgumentException(SR.Format(SR.Arg_EnumIllegalVal, algorithm), nameof(algorithm)); } } - - private string? _subjectKeyIdentifier; - private bool _decoded; } }