From 3e4b0b39bfc2232e4baadde3bd1a5abbad92fa8b Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 13 Feb 2024 16:00:16 -0500 Subject: [PATCH 1/3] Implement functionality and limited tests from RFC --- .../ref/System.Security.Cryptography.cs | 6 ++ .../X509SubjectKeyIdentifierExtension.cs | 21 ++++++ .../X509SubjectKeyIdentifierHashAlgorithm.cs | 30 ++++++++ .../SubjectKeyIdentifierTests.cs | 74 +++++++++++++++++-- 4 files changed, 126 insertions(+), 5 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 6220729b74a45c..a6ee388c8ceca2 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3612,6 +3612,12 @@ public enum X509SubjectKeyIdentifierHashAlgorithm Sha1 = 0, ShortSha1 = 1, CapiSha1 = 2, + Sha256 = 3, + Sha384 = 4, + Sha512 = 5, + ShortSha256 = 6, + ShortSha384 = 7, + ShortSha512 = 8, } [System.FlagsAttribute] public enum X509VerificationFlags 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 52c4ad6f8b9513..a97061a885f774 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 @@ -186,11 +186,32 @@ private static byte[] GenerateSubjectKeyIdentifierFromPublicKey(PublicKey key, X case X509SubjectKeyIdentifierHashAlgorithm.CapiSha1: // CAPI SHA1 is the SHA-1 hash over the whole SubjectPublicKeyInfo return HashSubjectPublicKeyInfo(key, HashAlgorithmName.SHA1); + case X509SubjectKeyIdentifierHashAlgorithm.Sha256: + return HashSubjectPublicKeyInfo(key, HashAlgorithmName.SHA256); + case X509SubjectKeyIdentifierHashAlgorithm.Sha384: + return HashSubjectPublicKeyInfo(key, HashAlgorithmName.SHA384); + case X509SubjectKeyIdentifierHashAlgorithm.Sha512: + return HashSubjectPublicKeyInfo(key, HashAlgorithmName.SHA512); + case X509SubjectKeyIdentifierHashAlgorithm.ShortSha256: + return HashSubjectPublicKeyLeft160Bits(key, HashAlgorithmName.SHA256); + case X509SubjectKeyIdentifierHashAlgorithm.ShortSha384: + return HashSubjectPublicKeyLeft160Bits(key, HashAlgorithmName.SHA384); + case X509SubjectKeyIdentifierHashAlgorithm.ShortSha512: + return HashSubjectPublicKeyLeft160Bits(key, HashAlgorithmName.SHA512); default: throw new ArgumentException(SR.Format(SR.Arg_EnumIllegalVal, algorithm), nameof(algorithm)); } } + private static byte[] HashSubjectPublicKeyLeft160Bits(PublicKey key, HashAlgorithmName hashAlgorithmName) + { + const int TruncateSize = 160 / 8; + Span hash = stackalloc byte[512 / 8]; // Largest known hash is 512-bits. + int written = CryptographicOperations.HashData(hashAlgorithmName, key.EncodedKeyValue.RawData, hash); + Debug.Assert(written >= TruncateSize); + return hash.Slice(0, TruncateSize).ToArray(); + } + private static byte[] HashSubjectPublicKeyInfo(PublicKey key, HashAlgorithmName hashAlgorithmName) { Span hash = stackalloc byte[512 / 8]; // Largest known hash is 512-bits. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierHashAlgorithm.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierHashAlgorithm.cs index 5210a244455ebe..efe5777cc5b8c2 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierHashAlgorithm.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SubjectKeyIdentifierHashAlgorithm.cs @@ -11,5 +11,35 @@ public enum X509SubjectKeyIdentifierHashAlgorithm Sha1 = 0, ShortSha1 = 1, CapiSha1 = 2, + + /// + /// The SHA-256 hash over the SubjectPublicKeyInfo as described in RFC 7093. + /// + Sha256 = 3, + + /// + /// The SHA-384 hash over the SubjectPublicKeyInfo as described in RFC 7093. + /// + Sha384 = 4, + + /// + /// The SHA-512 hash over the SubjectPublicKeyInfo as described in RFC 7093. + /// + Sha512 = 5, + + /// + /// The SHA-256 hash over the subjectPublicKey truncated to the leftmost 160-bits as described in RFC 7093. + /// + ShortSha256 = 6, + + /// + /// The SHA-384 hash over the subjectPublicKey truncated to the leftmost 160-bits as described in RFC 7093. + /// + ShortSha384 = 7, + + /// + /// The SHA-512 hash over the subjectPublicKey truncated to the leftmost 160-bits as described in RFC 7093. + /// + ShortSha512 = 8, } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs index e4d743a00e015b..fc0fd4abb86411 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.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 Test.Cryptography; using Xunit; @@ -148,6 +149,52 @@ public static void DecodeFromBER() Assert.Equal(skid, Convert.ToHexString(ext.SubjectKeyIdentifierBytes.Span)); } + [Theory] + [MemberData(nameof(Rfc7093Examples))] + public static void EncodeDecode_Rfc7093Examples( + byte[] subjectPublicKeyInfo, + X509SubjectKeyIdentifierHashAlgorithm algorithm, + byte[] expectedDer, + string expectedIdentifier) + { + EncodeDecodeSubjectPublicKeyInfo(subjectPublicKeyInfo, algorithm, false, expectedDer, expectedIdentifier); + } + + public static IEnumerable Rfc7093Examples() + { + byte[] example = + [ + 0x30, 0x59, + 0x30, 0x13, + 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, + 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, + 0x03, 0x42, 0x00, + 0x04, + 0x7F, 0x7F, 0x35, 0xA7, 0x97, 0x94, 0xC9, 0x50, 0x06, 0x0B, 0x80, 0x29, 0xFC, 0x8F, 0x36, 0x3A, + 0x28, 0xF1, 0x11, 0x59, 0x69, 0x2D, 0x9D, 0x34, 0xE6, 0xAC, 0x94, 0x81, 0x90, 0x43, 0x47, 0x35, + 0xF8, 0x33, 0xB1, 0xA6, 0x66, 0x52, 0xDC, 0x51, 0x43, 0x37, 0xAF, 0xF7, 0xF5, 0xC9, 0xC7, 0x5D, + 0x67, 0x0C, 0x01, 0x9D, 0x95, 0xA5, 0xD6, 0x39, 0xB7, 0x27, 0x44, 0xC6, 0x4A, 0x91, 0x28, 0xBB, + ]; + + // Method 1 example from RFC 7093 + yield return new object[] + { + example, + X509SubjectKeyIdentifierHashAlgorithm.ShortSha256, + Convert.FromHexString("0414BF37B3E5808FD46D54B28E846311BCCE1CAD2E1A"), + "BF37B3E5808FD46D54B28E846311BCCE1CAD2E1A", + }; + + // Method 4 example from RFC 7093 + yield return new object[] + { + example, + X509SubjectKeyIdentifierHashAlgorithm.Sha256, + Convert.FromHexString("04206D20896AB8BD833B6B66554BD59B20225D8A75A296088148399D7BF763D57405"), + "6D20896AB8BD833B6B66554BD59B20225D8A75A296088148399D7BF763D57405", + }; + } + private static void EncodeDecode( byte[] certBytes, X509SubjectKeyIdentifierHashAlgorithm algorithm, @@ -155,15 +202,32 @@ private static void EncodeDecode( byte[] expectedDer, string expectedIdentifier) { - PublicKey pk; - using (var cert = new X509Certificate2(certBytes)) { - pk = cert.PublicKey; + EncodeDecodePublicKey(cert.PublicKey, algorithm, critical, expectedDer, expectedIdentifier); } + } + + private static void EncodeDecodeSubjectPublicKeyInfo( + byte[] spkiBytes, + X509SubjectKeyIdentifierHashAlgorithm algorithm, + bool critical, + byte[] expectedDer, + string expectedIdentifier) + { + PublicKey publicKey = PublicKey.CreateFromSubjectPublicKeyInfo(spkiBytes, out _); + EncodeDecodePublicKey(publicKey, algorithm, critical, expectedDer, expectedIdentifier); + } + - X509SubjectKeyIdentifierExtension ext = - new X509SubjectKeyIdentifierExtension(pk, algorithm, critical); + private static void EncodeDecodePublicKey( + PublicKey publicKey, + X509SubjectKeyIdentifierHashAlgorithm algorithm, + bool critical, + byte[] expectedDer, + string expectedIdentifier) + { + X509SubjectKeyIdentifierExtension ext = new X509SubjectKeyIdentifierExtension(publicKey, algorithm, critical); byte[] rawData = ext.RawData; Assert.Equal(expectedDer, rawData); From c0ec71eaf3f7c2b52fe164695c786710fa60a4c2 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 16 Feb 2024 16:11:33 -0500 Subject: [PATCH 2/3] Finish tests --- .../TestUtilities/System/AssertExtensions.cs | 13 ++ .../SubjectKeyIdentifierTests.cs | 146 +++++++++++++++++- 2 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs b/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs index 32dd05facb0313..a9f962072cdc66 100644 --- a/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs +++ b/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs @@ -490,6 +490,19 @@ public static void CollectionEqual(IEnumerable expected, IEnumerable ac } } + /// + /// Validates that the actual span is not equal to the expected span. + /// + /// The sequence that should be not be equal to. + /// The actual sequence. + public static void SequenceNotEqual(ReadOnlySpan expected, ReadOnlySpan actual) where T : IEquatable + { + if (expected.SequenceEqual(actual)) + { + throw new XunitException($"Expected: Contents of expected to differ from actual but were the same."); + } + } + /// /// Validates that the actual span is equal to the expected span. /// If this fails, determine where the differences are and create an exception with that information. diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs index fc0fd4abb86411..52e36fdf86ac92 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs @@ -10,6 +10,10 @@ namespace System.Security.Cryptography.X509Certificates.Tests.ExtensionsTests [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] public static class SubjectKeyIdentifierTests { + private const string EcPublicKey = "1.2.840.10045.2.1"; + private static ReadOnlySpan NistP256r1 => [0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + private static ReadOnlySpan BrainpoolP256r1 => [0x06, 0x09, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07]; + [Fact] public static void DefaultConstructor() { @@ -151,13 +155,85 @@ public static void DecodeFromBER() [Theory] [MemberData(nameof(Rfc7093Examples))] - public static void EncodeDecode_Rfc7093Examples( + public static void EncodeDecode_Rfc7093_Examples( byte[] subjectPublicKeyInfo, X509SubjectKeyIdentifierHashAlgorithm algorithm, - byte[] expectedDer, + string expectedDer, string expectedIdentifier) { - EncodeDecodeSubjectPublicKeyInfo(subjectPublicKeyInfo, algorithm, false, expectedDer, expectedIdentifier); + EncodeDecodeSubjectPublicKeyInfo( + subjectPublicKeyInfo, + algorithm, + false, + Convert.FromHexString(expectedDer), + expectedIdentifier); + } + + [Theory] + [MemberData(nameof(Rfc7093Vectors))] + public static void EncodeDecode_Rfc7093_TestVectors( + byte[] key, + byte[] parameters, + X509SubjectKeyIdentifierHashAlgorithm algorithm, + string expectedDer, + string expectedIdentifier) + { + EncodeDecodePublicKey( + new PublicKey(new Oid("1.2.3.4"), new AsnEncodedData(parameters), new AsnEncodedData(key)), + algorithm, + false, + Convert.FromHexString(expectedDer), + expectedIdentifier); + } + + [Theory] + [InlineData(X509SubjectKeyIdentifierHashAlgorithm.ShortSha256)] + [InlineData(X509SubjectKeyIdentifierHashAlgorithm.ShortSha384)] + [InlineData(X509SubjectKeyIdentifierHashAlgorithm.ShortSha512)] + public static void Rfc7093_Truncated_SubjectPublicKeyOnly(X509SubjectKeyIdentifierHashAlgorithm algorithm) + { + ReadOnlySpan ecKey = + [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ]; + + PublicKey nistP256Key = new PublicKey(new Oid(EcPublicKey), new AsnEncodedData(NistP256r1), new AsnEncodedData(ecKey)); + PublicKey brainboolP256Key = new PublicKey(new Oid(EcPublicKey), new AsnEncodedData(BrainpoolP256r1), new AsnEncodedData(ecKey)); + + X509SubjectKeyIdentifierExtension nistP256Extension = new(nistP256Key, algorithm, critical: false); + X509SubjectKeyIdentifierExtension brainpoolP256Extension = new(brainboolP256Key, algorithm, critical: false); + + // Although the PublicKeys have different parameters by their curve, the key material is the same, so the + // hash should not differ. + AssertExtensions.SequenceEqual( + nistP256Extension.SubjectKeyIdentifierBytes.Span, + brainpoolP256Extension.SubjectKeyIdentifierBytes.Span); + } + + [Theory] + [InlineData(X509SubjectKeyIdentifierHashAlgorithm.Sha256)] + [InlineData(X509SubjectKeyIdentifierHashAlgorithm.Sha384)] + [InlineData(X509SubjectKeyIdentifierHashAlgorithm.Sha512)] + public static void Rfc7093_SubjectPublicKeyInfo(X509SubjectKeyIdentifierHashAlgorithm algorithm) + { + ReadOnlySpan ecKey = + [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ]; + + PublicKey nistP256Key = new PublicKey(new Oid(EcPublicKey), new AsnEncodedData(NistP256r1), new AsnEncodedData(ecKey)); + PublicKey brainboolP256Key = new PublicKey(new Oid(EcPublicKey), new AsnEncodedData(BrainpoolP256r1), new AsnEncodedData(ecKey)); + + X509SubjectKeyIdentifierExtension nistP256Extension = new(nistP256Key, algorithm, critical: false); + X509SubjectKeyIdentifierExtension brainpoolP256Extension = new(brainboolP256Key, algorithm, critical: false); + + // Although the PublicKeys have the same key, their parameters are different, thus should produce different + // hashes. + AssertExtensions.SequenceNotEqual( + nistP256Extension.SubjectKeyIdentifierBytes.Span, + brainpoolP256Extension.SubjectKeyIdentifierBytes.Span); } public static IEnumerable Rfc7093Examples() @@ -181,7 +257,7 @@ public static IEnumerable Rfc7093Examples() { example, X509SubjectKeyIdentifierHashAlgorithm.ShortSha256, - Convert.FromHexString("0414BF37B3E5808FD46D54B28E846311BCCE1CAD2E1A"), + "0414BF37B3E5808FD46D54B28E846311BCCE1CAD2E1A", "BF37B3E5808FD46D54B28E846311BCCE1CAD2E1A", }; @@ -190,11 +266,71 @@ public static IEnumerable Rfc7093Examples() { example, X509SubjectKeyIdentifierHashAlgorithm.Sha256, - Convert.FromHexString("04206D20896AB8BD833B6B66554BD59B20225D8A75A296088148399D7BF763D57405"), + "04206D20896AB8BD833B6B66554BD59B20225D8A75A296088148399D7BF763D57405", "6D20896AB8BD833B6B66554BD59B20225D8A75A296088148399D7BF763D57405", }; } + public static IEnumerable Rfc7093Vectors() + { + byte[] key = [1, 2, 3, 4]; + byte[] parameters = [4, 4, 5, 6, 7, 8]; + + yield return new object[] + { + key, + parameters, + X509SubjectKeyIdentifierHashAlgorithm.Sha256, + "04200B710654AEB48CE6A1FF80C0F3E83FAD8DB63B7E1004BE8F3EEC10E95CF3C620", + "0B710654AEB48CE6A1FF80C0F3E83FAD8DB63B7E1004BE8F3EEC10E95CF3C620", + }; + + yield return new object[] + { + key, + parameters, + X509SubjectKeyIdentifierHashAlgorithm.Sha384, + "0430150E43FE7ACE471CDB3910809145AD44B5B7E641A0364D608A1C106C9AD47963BAFE05E431B7782D791DE1B7E25F69DA", + "150E43FE7ACE471CDB3910809145AD44B5B7E641A0364D608A1C106C9AD47963BAFE05E431B7782D791DE1B7E25F69DA", + }; + + yield return new object[] + { + key, + parameters, + X509SubjectKeyIdentifierHashAlgorithm.Sha512, + "0440493CF4FC4B5CF15C17D5FCF4F85CFD1CFBCF29BEA538B8063733922A43693FEECAE70A11BEE932E23C32350C1F624DB16962A6AE6EF4B29BB3BFAD838048006F", + "493CF4FC4B5CF15C17D5FCF4F85CFD1CFBCF29BEA538B8063733922A43693FEECAE70A11BEE932E23C32350C1F624DB16962A6AE6EF4B29BB3BFAD838048006F", + }; + + yield return new object[] + { + key, + parameters, + X509SubjectKeyIdentifierHashAlgorithm.ShortSha256, + "04149F64A747E1B97F131FABB6B447296C9B6F0201E7", + "9F64A747E1B97F131FABB6B447296C9B6F0201E7", + }; + + yield return new object[] + { + key, + parameters, + X509SubjectKeyIdentifierHashAlgorithm.ShortSha384, + "04145A667D62430A8C253EBAE433333904DC6E1D41DC", + "5A667D62430A8C253EBAE433333904DC6E1D41DC", + }; + + yield return new object[] + { + key, + parameters, + X509SubjectKeyIdentifierHashAlgorithm.ShortSha512, + "0414A7C976DB1723ADB41274178DC82E9B777941AB20", + "A7C976DB1723ADB41274178DC82E9B777941AB20", + }; + } + private static void EncodeDecode( byte[] certBytes, X509SubjectKeyIdentifierHashAlgorithm algorithm, From 3963ca9889a9cc9da7d478361af1e4a7975fd543 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 16 Feb 2024 20:53:25 -0500 Subject: [PATCH 3/3] Remove extra blank line --- .../ExtensionsTests/SubjectKeyIdentifierTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs index 52e36fdf86ac92..583cac7b75c4a5 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExtensionsTests/SubjectKeyIdentifierTests.cs @@ -354,8 +354,6 @@ private static void EncodeDecodeSubjectPublicKeyInfo( PublicKey publicKey = PublicKey.CreateFromSubjectPublicKeyInfo(spkiBytes, out _); EncodeDecodePublicKey(publicKey, algorithm, critical, expectedDer, expectedIdentifier); } - - private static void EncodeDecodePublicKey( PublicKey publicKey, X509SubjectKeyIdentifierHashAlgorithm algorithm,