From f8e93162b4b3ce1a776604a71ea29278d68d14e8 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 5 Sep 2026 11:02:38 -0400 Subject: [PATCH 01/42] Add experimental HPKE cipher suite descriptors Add HPKE algorithm identifiers, suite validation and equality, shared tests, and Microsoft.Bcl.Cryptography support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- docs/project/list-of-diagnostics.md | 1 + .../Common/src/System/Experimentals.cs | 3 + .../System/Security/Cryptography/HpkeAead.cs | 30 ++++ .../System/Security/Cryptography/HpkeKdf.cs | 40 +++++ .../System/Security/Cryptography/HpkeKem.cs | 55 ++++++ .../System/Security/Cryptography/HpkeSuite.cs | 156 ++++++++++++++++++ .../Security/Cryptography/HpkeSuiteTests.cs | 149 +++++++++++++++++ .../Microsoft.Bcl.Cryptography.Forwards.cs | 4 + .../src/Microsoft.Bcl.Cryptography.csproj | 15 +- .../Microsoft.Bcl.Cryptography.Tests.csproj | 4 +- .../ref/System.Security.Cryptography.cs | 41 +++++ .../src/System.Security.Cryptography.csproj | 10 +- .../System.Security.Cryptography.Tests.csproj | 4 +- 13 files changed, 508 insertions(+), 4 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index 13e7e9c8aef8e0..8a45d572d09b19 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -333,3 +333,4 @@ Diagnostic id values for experimental APIs must not be recycled, as that could s | __`SYSLIB5006`__ | .NET 10 | TBD | Types for Post-Quantum Cryptography (PQC) are experimental. | | __`SYSLIB5007`__ | .NET 11 | TBD | Low-level TLS engine types (`TlsContext`, `TlsSession`) in `System.Net.Security` are experimental. | | __`SYSLIB5008`__ | .NET 11 | TBD | `SocketsHttpHandler` connection eviction control and `HttpRequestMessage.ConnectionId` APIs are experimental. | +| __`SYSLIB5009`__ | .NET 11 | TBD | Types for HPKE (Hybrid Public Key Encryption) are experimental. | diff --git a/src/libraries/Common/src/System/Experimentals.cs b/src/libraries/Common/src/System/Experimentals.cs index f196bfd2d50a29..dfc10357776001 100644 --- a/src/libraries/Common/src/System/Experimentals.cs +++ b/src/libraries/Common/src/System/Experimentals.cs @@ -39,6 +39,9 @@ internal static class Experimentals // SocketsHttpHandler connection eviction control and HttpRequestMessage.ConnectionId APIs are experimental. internal const string SocketsHttpHandlerExperimentalDiagId = "SYSLIB5008"; + // Types for HPKE (Hybrid Public Key Encryption) are experimental. + internal const string HpkeExperimentalDiagId = "SYSLIB5009"; + // When adding a new diagnostic ID, add it to the table in docs\project\list-of-diagnostics.md as well. // Keep new const identifiers above this comment. } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs new file mode 100644 index 00000000000000..f62de368cbb050 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + /// + /// Specifies an authenticated encryption with associated data (AEAD) algorithm for an HPKE cipher suite. + /// + /// + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public enum HpkeAead + { + /// + /// Indicates that authenticated encryption uses AES-GCM with a 128-bit key. + /// + AES_128_GCM = 1, + + /// + /// Indicates that authenticated encryption uses AES-GCM with a 256-bit key. + /// + AES_256_GCM = 2, + + /// + /// Indicates that authenticated encryption uses ChaCha20-Poly1305. + /// + ChaCha20Poly1305 = 3, + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs new file mode 100644 index 00000000000000..90c89b58db5739 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + /// + /// Specifies a key derivation function (KDF) for an HPKE cipher suite. + /// + /// + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public enum HpkeKdf + { + /// + /// Indicates that key derivation uses HKDF with SHA-256. + /// + HKDF_SHA256 = 1, + + /// + /// Indicates that key derivation uses HKDF with SHA-384. + /// + HKDF_SHA384 = 2, + + /// + /// Indicates that key derivation uses HKDF with SHA-512. + /// + HKDF_SHA512 = 3, + + /// + /// Indicates that key derivation uses SHAKE128. + /// + SHAKE128 = 16, + + /// + /// Indicates that key derivation uses SHAKE256. + /// + SHAKE256 = 17 + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs new file mode 100644 index 00000000000000..81b373d5e7bf8a --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + /// + /// Specifies a key encapsulation mechanism (KEM) for an HPKE cipher suite. + /// + /// + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public enum HpkeKem + { + /// + /// Indicates that key encapsulation uses DHKEM with the NIST P-256 curve and HKDF-SHA-256. + /// + DHKEM_P256_HKDF_SHA256 = 16, + + /// + /// Indicates that key encapsulation uses DHKEM with the NIST P-384 curve and HKDF-SHA-384. + /// + DHKEM_P384_HKDF_SHA384 = 17, + + /// + /// Indicates that key encapsulation uses DHKEM with X25519 and HKDF-SHA-256. + /// + DHKEM_X25519_HKDF_SHA256 = 32, + + /// + /// Indicates that key encapsulation uses ML-KEM-512. + /// + MLKEM_512 = 64, + + /// + /// Indicates that key encapsulation uses ML-KEM-768. + /// + MLKEM_768 = 65, + + /// + /// Indicates that key encapsulation uses ML-KEM-1024. + /// + MLKEM_1024 = 66, + + /// + /// Indicates that key encapsulation combines ML-KEM-768 with ECDH using the NIST P-256 curve. + /// + MLKEM768_P256 = 80, + + /// + /// Indicates that key encapsulation combines ML-KEM-1024 with ECDH using the NIST P-384 curve. + /// + MLKEM1024_P384 = 81, + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs new file mode 100644 index 00000000000000..fae5b291ac9d8c --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + /// + /// Represents a Hybrid Public Key Encryption (HPKE) cipher suite. + /// + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public sealed class HpkeSuite : IEquatable + { + /// + /// Initializes a new instance of the class with the specified algorithms. + /// + /// + /// One of the enumeration values that specifies the key encapsulation mechanism (KEM) for the cipher suite. + /// + /// + /// One of the enumeration values that specifies the key derivation function (KDF) for the cipher suite. + /// + /// + /// One of the enumeration values that specifies the authenticated encryption with associated data (AEAD) + /// algorithm for the cipher suite. + /// + /// + /// , , or is not a defined value + /// of its corresponding enumeration. + /// + public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + if (!IsValidHpkeKem(kem)) + throw new ArgumentOutOfRangeException(nameof(kem)); + + if (!IsValidHpkeKdf(kdf)) + throw new ArgumentOutOfRangeException(nameof(kdf)); + + if (!IsValidHpkeAead(aead)) + throw new ArgumentOutOfRangeException(nameof(aead)); + + AeadAlgorithm = aead; + KdfAlgorithm = kdf; + KemAlgorithm = kem; + } + + /// + /// Gets the authenticated encryption with associated data (AEAD) algorithm for the cipher suite. + /// + /// + /// The authenticated encryption with associated data (AEAD) algorithm for the cipher suite. + /// + public HpkeAead AeadAlgorithm { get; } + + /// + /// Gets the key derivation function (KDF) for the cipher suite. + /// + /// + /// The key derivation function (KDF) for the cipher suite. + /// + public HpkeKdf KdfAlgorithm { get; } + + /// + /// Gets the key encapsulation mechanism (KEM) for the cipher suite. + /// + /// + /// The key encapsulation mechanism (KEM) for the cipher suite. + /// + public HpkeKem KemAlgorithm { get; } + + /// + /// Compares two objects. + /// + /// + /// An object to be compared to the current object. + /// + /// + /// if is not and specifies + /// the same algorithms as the current object; otherwise, . + /// + public bool Equals([NotNullWhen(true)] HpkeSuite? other) + { + if (other is null) + { + return false; + } + + return AeadAlgorithm == other.AeadAlgorithm && + KdfAlgorithm == other.KdfAlgorithm && + KemAlgorithm == other.KemAlgorithm; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => obj is HpkeSuite suite && Equals(suite); + + /// + public override int GetHashCode() => HashCode.Combine(KemAlgorithm, KdfAlgorithm, AeadAlgorithm); + + /// + /// Determines whether two objects specify the same algorithms. + /// + /// + /// An object that specifies a cipher suite. + /// + /// + /// A second object, to be compared to the object that is identified by the parameter. + /// + /// + /// if the objects are considered equal; otherwise, . + /// + public static bool operator ==(HpkeSuite? left, HpkeSuite? right) + { + return left is null ? right is null : left.Equals(right); + } + + /// + /// Determines whether two objects do not specify the same algorithms. + /// + /// + /// An object that specifies a cipher suite. + /// + /// + /// A second object, to be compared to the object that is identified by the parameter. + /// + /// + /// if the objects are not considered equal; otherwise, . + /// + public static bool operator !=(HpkeSuite? left, HpkeSuite? right) => !(left == right); + + internal static bool IsValidHpkeAead(HpkeAead aead) + { + return aead is HpkeAead.AES_128_GCM or HpkeAead.AES_256_GCM or HpkeAead.ChaCha20Poly1305; + } + + internal static bool IsValidHpkeKdf(HpkeKdf kdf) + { + return kdf is HpkeKdf.HKDF_SHA256 or + HpkeKdf.HKDF_SHA384 or + HpkeKdf.HKDF_SHA512 or + HpkeKdf.SHAKE128 or + HpkeKdf.SHAKE256; + } + + internal static bool IsValidHpkeKem(HpkeKem kem) + { + return kem is HpkeKem.DHKEM_P256_HKDF_SHA256 or + HpkeKem.DHKEM_P384_HKDF_SHA384 or + HpkeKem.DHKEM_X25519_HKDF_SHA256 or + HpkeKem.MLKEM_512 or + HpkeKem.MLKEM_768 or + HpkeKem.MLKEM_1024 or + HpkeKem.MLKEM768_P256 or + HpkeKem.MLKEM1024_P384; + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs new file mode 100644 index 00000000000000..472cf0be6800c0 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs @@ -0,0 +1,149 @@ +// 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 Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static class HpkeSuiteTests + { + [Theory] + [MemberData(nameof(ValidAlgorithms))] + public static void Constructor_ValidAlgorithms(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + Assert.Equal(kem, suite.KemAlgorithm); + Assert.Equal(kdf, suite.KdfAlgorithm); + Assert.Equal(aead, suite.AeadAlgorithm); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-7)] + [InlineData(-1)] + [InlineData(0)] + [InlineData(15)] + [InlineData(18)] + [InlineData(31)] + [InlineData(33)] + [InlineData(63)] + [InlineData(67)] + [InlineData(79)] + [InlineData(82)] + [InlineData(ushort.MaxValue)] + [InlineData(ushort.MaxValue + 1)] + [InlineData(int.MaxValue)] + public static void Constructor_InvalidKem(int kem) + { + AssertExtensions.Throws( + nameof(kem), + () => new HpkeSuite((HpkeKem)kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-7)] + [InlineData(-1)] + [InlineData(0)] + [InlineData(4)] + [InlineData(15)] + [InlineData(18)] + [InlineData(ushort.MaxValue)] + [InlineData(ushort.MaxValue + 1)] + [InlineData(int.MaxValue)] + public static void Constructor_InvalidKdf(int kdf) + { + AssertExtensions.Throws( + nameof(kdf), + () => new HpkeSuite(HpkeKem.MLKEM_768, (HpkeKdf)kdf, HpkeAead.AES_128_GCM)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-7)] + [InlineData(-1)] + [InlineData(0)] + [InlineData(4)] + [InlineData(ushort.MaxValue)] + [InlineData(ushort.MaxValue + 1)] + [InlineData(int.MaxValue)] + public static void Constructor_InvalidAead(int aead) + { + AssertExtensions.Throws( + nameof(aead), + () => new HpkeSuite(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, (HpkeAead)aead)); + } + + [Theory] + [MemberData(nameof(ValidAlgorithms))] + public static void Equality_SameAlgorithms(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite left = new(kem, kdf, aead); + HpkeSuite right = new(kem, kdf, aead); + + AssertExtensions.TrueExpression(left.Equals(left)); + AssertExtensions.TrueExpression(left.Equals((object)left)); + AssertExtensions.TrueExpression(left.Equals(right)); + AssertExtensions.TrueExpression(right.Equals(left)); + AssertExtensions.TrueExpression(left.Equals((object)right)); + AssertExtensions.TrueExpression(right.Equals((object)left)); + AssertExtensions.TrueExpression(((IEquatable)left).Equals(right)); + AssertExtensions.TrueExpression(left == right); + AssertExtensions.TrueExpression(right == left); + AssertExtensions.FalseExpression(left != right); + AssertExtensions.FalseExpression(right != left); + Assert.Equal(left.GetHashCode(), right.GetHashCode()); + } + + [Theory] + [InlineData(HpkeKem.MLKEM_1024, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM)] + [InlineData(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA384, HpkeAead.AES_128_GCM)] + [InlineData(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, HpkeAead.AES_256_GCM)] + [InlineData(HpkeKem.MLKEM_1024, HpkeKdf.SHAKE256, HpkeAead.ChaCha20Poly1305)] + public static void Equality_DifferentAlgorithms(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite left = new(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + HpkeSuite right = new(kem, kdf, aead); + + AssertExtensions.FalseExpression(left.Equals(right)); + AssertExtensions.FalseExpression(right.Equals(left)); + AssertExtensions.FalseExpression(left.Equals((object)right)); + AssertExtensions.FalseExpression(right.Equals((object)left)); + AssertExtensions.FalseExpression(((IEquatable)left).Equals(right)); + AssertExtensions.FalseExpression(left == right); + AssertExtensions.FalseExpression(right == left); + AssertExtensions.TrueExpression(left != right); + AssertExtensions.TrueExpression(right != left); + } + + [Fact] + public static void Equality_NullAndUnrelatedObject() + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + HpkeSuite nullSuite = null; + + AssertExtensions.FalseExpression(suite.Equals(nullSuite)); + AssertExtensions.FalseExpression(suite.Equals((object)nullSuite)); + AssertExtensions.FalseExpression(suite.Equals(new object())); + AssertExtensions.FalseExpression(((IEquatable)suite).Equals(nullSuite)); + AssertExtensions.FalseExpression(suite == nullSuite); + AssertExtensions.FalseExpression(nullSuite == suite); + AssertExtensions.TrueExpression(suite != nullSuite); + AssertExtensions.TrueExpression(nullSuite != suite); + AssertExtensions.TrueExpression(nullSuite == (HpkeSuite)null); + AssertExtensions.FalseExpression(nullSuite != (HpkeSuite)null); + } + + public static IEnumerable ValidAlgorithms() + { + foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) + foreach (HpkeKdf kdf in Enum.GetValues(typeof(HpkeKdf))) + foreach (HpkeAead aead in Enum.GetValues(typeof(HpkeAead))) + { + yield return new object[] { kem, kdf, aead }; + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs index a8fd0720de91ac..18a1c6ccf231ea 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs @@ -23,6 +23,10 @@ #if NET11_0_OR_GREATER [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.CompositeMLKem))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.CompositeMLKemAlgorithm))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeAead))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeKdf))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeKem))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeSuite))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.X25519DiffieHellman))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.X25519DiffieHellmanCng))] #endif diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index 01d9007068cfe4..f5d8f32e99727a 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -5,7 +5,7 @@ true true Provides support for some cryptographic primitives for .NET Framework and .NET Standard. - $(NoWarn);SYSLIB5006 + $(NoWarn);SYSLIB5006;SYSLIB5009 true true @@ -35,6 +35,17 @@ + + + + + + + @@ -646,6 +657,8 @@ + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index 9092e17f128157..0a5c41db07d428 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -4,7 +4,7 @@ $(NetFrameworkCurrent);$(NetCoreAppCurrent) true true - $(NoWarn);SYSLIB5006 + $(NoWarn);SYSLIB5006;SYSLIB5009 ../src/Resources/Strings.resx true true @@ -127,6 +127,8 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLDsaAlgorithmTests.cs" /> + VerifyAsync(byte[] key, System.IO.Stream source, byte[] hash, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask VerifyAsync(System.ReadOnlyMemory key, System.IO.Stream source, System.ReadOnlyMemory hash, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public enum HpkeAead + { + AES_128_GCM = 1, + AES_256_GCM = 2, + ChaCha20Poly1305 = 3, + } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public enum HpkeKdf + { + HKDF_SHA256 = 1, + HKDF_SHA384 = 2, + HKDF_SHA512 = 3, + SHAKE128 = 16, + SHAKE256 = 17, + } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public enum HpkeKem + { + DHKEM_P256_HKDF_SHA256 = 16, + DHKEM_P384_HKDF_SHA384 = 17, + DHKEM_X25519_HKDF_SHA256 = 32, + MLKEM_512 = 64, + MLKEM_768 = 65, + MLKEM_1024 = 66, + MLKEM768_P256 = 80, + MLKEM1024_P384 = 81, + } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public sealed partial class HpkeSuite : System.IEquatable + { + public HpkeSuite(System.Security.Cryptography.HpkeKem kem, System.Security.Cryptography.HpkeKdf kdf, System.Security.Cryptography.HpkeAead aead) { } + public System.Security.Cryptography.HpkeAead AeadAlgorithm { get { throw null; } } + public System.Security.Cryptography.HpkeKdf KdfAlgorithm { get { throw null; } } + public System.Security.Cryptography.HpkeKem KemAlgorithm { get { throw null; } } + public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } + public bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] System.Security.Cryptography.HpkeSuite? other) { throw null; } + public override int GetHashCode() { throw null; } + public static bool operator ==(System.Security.Cryptography.HpkeSuite? left, System.Security.Cryptography.HpkeSuite? right) { throw null; } + public static bool operator !=(System.Security.Cryptography.HpkeSuite? left, System.Security.Cryptography.HpkeSuite? right) { throw null; } + } public partial interface ICryptoTransform : System.IDisposable { bool CanReuseTransform { get; } 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 e445d3b3765696..563ba4d272e26c 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -7,7 +7,7 @@ $(NoWarn);CA5350;CA5351;CA5379;CA5384;SYSLIB0026 $(NoWarn);CS0809 - $(NoWarn);SYSLIB5006 + $(NoWarn);SYSLIB5006;SYSLIB5009 false @@ -404,6 +404,14 @@ Link="Common\System\Security\Cryptography\DSAKeyFormatHelper.cs" /> + + + + true true $(NoWarn);SYSLIB0021;SYSLIB0026;SYSLIB0027;SYSLIB0028;SYSLIB0057 - $(NoWarn);SYSLIB5006 + $(NoWarn);SYSLIB5006;SYSLIB5009 true ../src/Resources/Strings.resx @@ -230,6 +230,8 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLDsaAlgorithmTests.cs" /> + Date: Sat, 5 Sep 2026 12:00:40 -0400 Subject: [PATCH 02/42] Complete HpkeSuite metadata and public API Add KEM, KDF, and AEAD metadata, expose suite sizes and names, implement ciphertext length calculation, and extend shared public API tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Security/Cryptography/HpkeAeadMetadata.cs | 39 ++++++ .../Security/Cryptography/HpkeKdfMetadata.cs | 44 ++++++ .../Security/Cryptography/HpkeKemMetadata.cs | 58 ++++++++ .../System/Security/Cryptography/HpkeSuite.cs | 125 ++++++++++++------ .../Security/Cryptography/HpkeSuiteTests.cs | 83 ++++++++++++ .../src/Microsoft.Bcl.Cryptography.csproj | 6 + .../ref/System.Security.Cryptography.cs | 7 + .../src/System.Security.Cryptography.csproj | 6 + 8 files changed, 327 insertions(+), 41 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs new file mode 100644 index 00000000000000..aa76255e9633e1 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ + internal sealed class HpkeAeadMetadata + { + internal HpkeAead Aead { get; } + internal int Nk { get; } + internal int Nn { get; } + internal int Nt { get; } + internal string Name { get; } + + private HpkeAeadMetadata(HpkeAead aead, int nk, int nn, int nt, string name) + { + Aead = aead; + Nk = nk; + Nn = nn; + Nt = nt; + Name = name; + } + + internal static HpkeAeadMetadata? Create(HpkeAead aead) + { + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.3 + switch (aead) + { + case HpkeAead.AES_128_GCM: + return new HpkeAeadMetadata(aead, nk: 16, nn: 12, nt: 16, name: "AES-128-GCM"); + case HpkeAead.AES_256_GCM: + return new HpkeAeadMetadata(aead, nk: 32, nn: 12, nt: 16, name: "AES-256-GCM"); + case HpkeAead.ChaCha20Poly1305: + return new HpkeAeadMetadata(aead, nk: 32, nn: 12, nt: 16, name: "ChaCha20Poly1305"); + default: + return null; + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs new file mode 100644 index 00000000000000..f3136a1b16f5a8 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ + internal sealed class HpkeKdfMetadata + { + internal HpkeKdf Kdf { get; } + internal int Nh { get; } + internal bool IsTwoStage { get; } + internal string Name { get; } + + private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name) + { + Kdf = kdf; + Nh = nh; + IsTwoStage = isTwoStage; + Name = name; + } + + internal static HpkeKdfMetadata? Create(HpkeKdf kdf) + { + switch (kdf) + { + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.2 + case HpkeKdf.HKDF_SHA256: + return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: true, name: "HKDF-SHA256"); + case HpkeKdf.HKDF_SHA384: + return new HpkeKdfMetadata(kdf, nh: 48, isTwoStage: true, name: "HKDF-SHA384"); + case HpkeKdf.HKDF_SHA512: + return new HpkeKdfMetadata(kdf, nh: 64, isTwoStage: true, name: "HKDF-SHA512"); + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-5 + case HpkeKdf.SHAKE128: + return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: false, name: "SHAKE128"); + case HpkeKdf.SHAKE256: + return new HpkeKdfMetadata(kdf, nh: 64, isTwoStage: false, name: "SHAKE256"); + + default: + return null; + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs new file mode 100644 index 00000000000000..eac79976ff47a0 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ + internal sealed class HpkeKemMetadata + { + internal HpkeKem Kem { get; } + internal int Nsk { get; } + internal int Npk { get; } + internal int Nenc { get; } + internal int Nsecret { get; } + internal string Name { get; } + + private HpkeKemMetadata(HpkeKem kem, int nsecret, int nenc, int npk, int nsk, string name) + { + Kem = kem; + Nsk = nsk; + Npk = npk; + Nenc = nenc; + Nsecret = nsecret; + Name = name; + } + + internal static HpkeKemMetadata? Create(HpkeKem kem) + { + switch (kem) + { + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1 + case HpkeKem.DHKEM_P256_HKDF_SHA256: + return new HpkeKemMetadata(kem, nsecret: 32, nenc: 65, npk: 65, nsk: 32, name: "DHKEM(P-256, HKDF-SHA256)"); + case HpkeKem.DHKEM_P384_HKDF_SHA384: + return new HpkeKemMetadata(kem, nsecret: 48, nenc: 97, npk: 97, nsk: 48, name: "DHKEM(P-384, HKDF-SHA384)"); + case HpkeKem.DHKEM_X25519_HKDF_SHA256: + return new HpkeKemMetadata(kem, nsecret: 32, nenc: 32, npk: 32, nsk: 32, name: "DHKEM(X25519, HKDF-SHA256)"); + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-8.1 + // Nsk is the 64-byte seed, not the expanded ML-KEM decapsulation key. + case HpkeKem.MLKEM_512: + return new HpkeKemMetadata(kem, nsecret: 32, nenc: 768, npk: 800, nsk: 64, name: "ML-KEM-512"); + case HpkeKem.MLKEM_768: + return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1088, npk: 1184, nsk: 64, name: "ML-KEM-768"); + case HpkeKem.MLKEM_1024: + return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1568, npk: 1568, nsk: 64, name: "ML-KEM-1024"); + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-8.2 + // Nsk is the 32-byte seed used to derive both component key pairs. + case HpkeKem.MLKEM768_P256: + return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1153, npk: 1249, nsk: 32, name: "MLKEM768-P256"); + case HpkeKem.MLKEM1024_P384: + return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1665, npk: 1665, nsk: 32, name: "MLKEM1024-P384"); + + default: + return null; + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs index fae5b291ac9d8c..504edc24979d28 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs @@ -11,6 +11,10 @@ namespace System.Security.Cryptography [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] public sealed class HpkeSuite : IEquatable { + private readonly HpkeAeadMetadata _aeadMetadata; + private readonly HpkeKdfMetadata _kdfMetadata; + private readonly HpkeKemMetadata _kemMetadata; + /// /// Initializes a new instance of the class with the specified algorithms. /// @@ -30,18 +34,9 @@ public sealed class HpkeSuite : IEquatable /// public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) { - if (!IsValidHpkeKem(kem)) - throw new ArgumentOutOfRangeException(nameof(kem)); - - if (!IsValidHpkeKdf(kdf)) - throw new ArgumentOutOfRangeException(nameof(kdf)); - - if (!IsValidHpkeAead(aead)) - throw new ArgumentOutOfRangeException(nameof(aead)); - - AeadAlgorithm = aead; - KdfAlgorithm = kdf; - KemAlgorithm = kem; + _kemMetadata = HpkeKemMetadata.Create(kem) ?? throw new ArgumentOutOfRangeException(nameof(kem)); + _kdfMetadata = HpkeKdfMetadata.Create(kdf) ?? throw new ArgumentOutOfRangeException(nameof(kdf)); + _aeadMetadata = HpkeAeadMetadata.Create(aead) ?? throw new ArgumentOutOfRangeException(nameof(aead)); } /// @@ -50,7 +45,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The authenticated encryption with associated data (AEAD) algorithm for the cipher suite. /// - public HpkeAead AeadAlgorithm { get; } + public HpkeAead AeadAlgorithm => _aeadMetadata.Aead; /// /// Gets the key derivation function (KDF) for the cipher suite. @@ -58,7 +53,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The key derivation function (KDF) for the cipher suite. /// - public HpkeKdf KdfAlgorithm { get; } + public HpkeKdf KdfAlgorithm => _kdfMetadata.Kdf; /// /// Gets the key encapsulation mechanism (KEM) for the cipher suite. @@ -66,7 +61,78 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The key encapsulation mechanism (KEM) for the cipher suite. /// - public HpkeKem KemAlgorithm { get; } + public HpkeKem KemAlgorithm => _kemMetadata.Kem; + + /// + /// Gets the size of the authentication tag for the cipher suite, in bytes. + /// + /// + /// The size of the authentication tag for the cipher suite, in bytes. + /// + public int AeadTagSizeInBytes => _aeadMetadata.Nt; + + /// + /// Gets the size of the decapsulation key for the cipher suite, in bytes. + /// + /// + /// The size of the decapsulation key for the cipher suite, in bytes. + /// + /// + /// For ML-KEM and hybrid ML-KEM cipher suites, this is the size of the private seed. + /// + public int DecapsulationKeySizeInBytes => _kemMetadata.Nsk; + + /// + /// Gets the size of an encapsulated secret for the cipher suite, in bytes. + /// + /// + /// The size of an encapsulated secret for the cipher suite, in bytes. + /// + public int EncapsulatedSecretSizeInBytes => _kemMetadata.Nenc; + + /// + /// Gets the size of the encapsulation key for the cipher suite, in bytes. + /// + /// + /// The size of the encapsulation key for the cipher suite, in bytes. + /// + public int EncapsulationKeySizeInBytes => _kemMetadata.Npk; + + /// + /// Gets the name of the cipher suite. + /// + /// + /// A string containing the KEM, KDF, and AEAD names, separated by spaces. + /// + public string Name => field ??= $"{_kemMetadata.Name} {_kdfMetadata.Name} {_aeadMetadata.Name}"; + + /// + /// Gets the length of the ciphertext produced by encrypting a plaintext of the specified length. + /// + /// + /// The length of the plaintext, in bytes. + /// + /// + /// The length of the ciphertext, in bytes. + /// + /// + /// is negative or the resulting ciphertext length cannot be + /// represented as a signed 32-bit integer. + /// + /// + /// The returned length includes the authentication tag, but does not include the encapsulated secret. + /// + public int GetCiphertextLength(int plaintextLength) + { + int tagSize = AeadTagSizeInBytes; + + if (plaintextLength < 0 || plaintextLength > int.MaxValue - tagSize) + { + throw new ArgumentOutOfRangeException(nameof(plaintextLength)); + } + + return plaintextLength + tagSize; + } /// /// Compares two objects. @@ -96,6 +162,9 @@ public bool Equals([NotNullWhen(true)] HpkeSuite? other) /// public override int GetHashCode() => HashCode.Combine(KemAlgorithm, KdfAlgorithm, AeadAlgorithm); + /// + public override string ToString() => Name; + /// /// Determines whether two objects specify the same algorithms. /// @@ -126,31 +195,5 @@ public bool Equals([NotNullWhen(true)] HpkeSuite? other) /// if the objects are not considered equal; otherwise, . /// public static bool operator !=(HpkeSuite? left, HpkeSuite? right) => !(left == right); - - internal static bool IsValidHpkeAead(HpkeAead aead) - { - return aead is HpkeAead.AES_128_GCM or HpkeAead.AES_256_GCM or HpkeAead.ChaCha20Poly1305; - } - - internal static bool IsValidHpkeKdf(HpkeKdf kdf) - { - return kdf is HpkeKdf.HKDF_SHA256 or - HpkeKdf.HKDF_SHA384 or - HpkeKdf.HKDF_SHA512 or - HpkeKdf.SHAKE128 or - HpkeKdf.SHAKE256; - } - - internal static bool IsValidHpkeKem(HpkeKem kem) - { - return kem is HpkeKem.DHKEM_P256_HKDF_SHA256 or - HpkeKem.DHKEM_P384_HKDF_SHA384 or - HpkeKem.DHKEM_X25519_HKDF_SHA256 or - HpkeKem.MLKEM_512 or - HpkeKem.MLKEM_768 or - HpkeKem.MLKEM_1024 or - HpkeKem.MLKEM768_P256 or - HpkeKem.MLKEM1024_P384; - } } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs index 472cf0be6800c0..6314b84b179e58 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs @@ -76,6 +76,89 @@ public static void Constructor_InvalidAead(int aead) () => new HpkeSuite(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, (HpkeAead)aead)); } + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, 32, 65, 65)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 48, 97, 97)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 32, 32, 32)] + [InlineData(HpkeKem.MLKEM_512, 64, 768, 800)] + [InlineData(HpkeKem.MLKEM_768, 64, 1088, 1184)] + [InlineData(HpkeKem.MLKEM_1024, 64, 1568, 1568)] + [InlineData(HpkeKem.MLKEM768_P256, 32, 1153, 1249)] + [InlineData(HpkeKem.MLKEM1024_P384, 32, 1665, 1665)] + public static void KemSizes( + HpkeKem kem, + int decapsulationKeySizeInBytes, + int encapsulatedSecretSizeInBytes, + int encapsulationKeySizeInBytes) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + Assert.Equal(decapsulationKeySizeInBytes, suite.DecapsulationKeySizeInBytes); + Assert.Equal(encapsulatedSecretSizeInBytes, suite.EncapsulatedSecretSizeInBytes); + Assert.Equal(encapsulationKeySizeInBytes, suite.EncapsulationKeySizeInBytes); + } + + [Theory] + [InlineData(HpkeAead.AES_128_GCM)] + [InlineData(HpkeAead.AES_256_GCM)] + [InlineData(HpkeAead.ChaCha20Poly1305)] + public static void AeadTagSizeInBytes(HpkeAead aead) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, aead); + + Assert.Equal(16, suite.AeadTagSizeInBytes); + } + + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM, + "DHKEM(P-256, HKDF-SHA256) HKDF-SHA256 AES-128-GCM")] + [InlineData(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, + "ML-KEM-768 HKDF-SHA512 AES-256-GCM")] + [InlineData(HpkeKem.MLKEM1024_P384, HpkeKdf.SHAKE256, HpkeAead.ChaCha20Poly1305, + "MLKEM1024-P384 SHAKE256 ChaCha20Poly1305")] + public static void NameAndToString(HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string expectedName) + { + HpkeSuite suite = new(kem, kdf, aead); + + Assert.Equal(expectedName, suite.ToString()); + Assert.Equal(expectedName, suite.Name); + } + + [Theory] + [InlineData(0, 16)] + [InlineData(1, 17)] + [InlineData(15, 31)] + [InlineData(16, 32)] + [InlineData(17, 33)] + [InlineData(1024, 1040)] + [InlineData(int.MaxValue - 16, int.MaxValue)] + public static void GetCiphertextLength(int plaintextLength, int expectedLength) + { + foreach (HpkeAead aead in Enum.GetValues(typeof(HpkeAead))) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, aead); + + Assert.Equal(expectedLength, suite.GetCiphertextLength(plaintextLength)); + } + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-1)] + [InlineData(int.MaxValue - 15)] + [InlineData(int.MaxValue)] + public static void GetCiphertextLength_InvalidLength(int plaintextLength) + { + foreach (HpkeAead aead in Enum.GetValues(typeof(HpkeAead))) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, aead); + + AssertExtensions.Throws( + nameof(plaintextLength), + () => suite.GetCiphertextLength(plaintextLength)); + } + } + [Theory] [MemberData(nameof(ValidAlgorithms))] public static void Equality_SameAlgorithms(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index f5d8f32e99727a..642c1e67efb557 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -38,10 +38,16 @@ + + + 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 06c825d1dad3c2..0d6fb2f20d91f2 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1921,13 +1921,20 @@ public sealed partial class HpkeSuite : System.IEquatable + + + Date: Sat, 5 Sep 2026 17:55:49 -0400 Subject: [PATCH 03/42] Add managed HPKE foundation Add the initial Hpke API, managed algorithm support metadata, deterministic DHKEM key derivation and adapters for NIST curves and X25519. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 25 ++ .../Security/Cryptography/HpkeAeadMetadata.cs | 2 +- .../Security/Cryptography/HpkeKdfMetadata.cs | 2 +- .../Security/Cryptography/HpkeKemMetadata.cs | 5 +- .../System/Security/Cryptography/HpkeSuite.cs | 28 +- .../src/Microsoft.Bcl.Cryptography.csproj | 2 + .../ref/System.Security.Cryptography.cs | 7 + .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 8 +- .../Cryptography/HpkeAeadMetadata.Managed.cs | 30 ++ .../HpkeImplementation.Managed.cs | 276 ++++++++++++++++++ .../Cryptography/HpkeKdfMetadata.Managed.cs | 41 +++ .../Cryptography/HpkeKemMetadata.Managed.cs | 95 ++++++ 13 files changed, 506 insertions(+), 18 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/Hpke.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKdfMetadata.Managed.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs new file mode 100644 index 00000000000000..4a84c4283be4a6 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public abstract class Hpke + { + public HpkeSuite Suite { get; } + + protected Hpke(HpkeSuite suite) + { + ArgumentNullException.ThrowIfNull(suite); + Suite = suite; + } + + public static bool IsSupported(HpkeSuite suite) + { + ArgumentNullException.ThrowIfNull(suite); + return HpkeImplementation.IsSupportedImpl(suite); + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs index aa76255e9633e1..cd68e18eb518d0 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs @@ -3,7 +3,7 @@ namespace System.Security.Cryptography { - internal sealed class HpkeAeadMetadata + internal sealed partial class HpkeAeadMetadata { internal HpkeAead Aead { get; } internal int Nk { get; } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs index f3136a1b16f5a8..b2292a4559d271 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs @@ -3,7 +3,7 @@ namespace System.Security.Cryptography { - internal sealed class HpkeKdfMetadata + internal sealed partial class HpkeKdfMetadata { internal HpkeKdf Kdf { get; } internal int Nh { get; } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs index eac79976ff47a0..7b27fc6c55096e 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs @@ -3,7 +3,7 @@ namespace System.Security.Cryptography { - internal sealed class HpkeKemMetadata + internal sealed partial class HpkeKemMetadata { internal HpkeKem Kem { get; } internal int Nsk { get; } @@ -20,8 +20,11 @@ private HpkeKemMetadata(HpkeKem kem, int nsecret, int nenc, int npk, int nsk, st Nenc = nenc; Nsecret = nsecret; Name = name; + Setup(); } + partial void Setup(); + internal static HpkeKemMetadata? Create(HpkeKem kem) { switch (kem) diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs index 504edc24979d28..62c0359346d6e3 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs @@ -11,9 +11,9 @@ namespace System.Security.Cryptography [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] public sealed class HpkeSuite : IEquatable { - private readonly HpkeAeadMetadata _aeadMetadata; - private readonly HpkeKdfMetadata _kdfMetadata; - private readonly HpkeKemMetadata _kemMetadata; + internal HpkeAeadMetadata AeadMetadata { get; } + internal HpkeKdfMetadata KdfMetadata { get; } + internal HpkeKemMetadata KemMetadata { get; } /// /// Initializes a new instance of the class with the specified algorithms. @@ -34,9 +34,9 @@ public sealed class HpkeSuite : IEquatable /// public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) { - _kemMetadata = HpkeKemMetadata.Create(kem) ?? throw new ArgumentOutOfRangeException(nameof(kem)); - _kdfMetadata = HpkeKdfMetadata.Create(kdf) ?? throw new ArgumentOutOfRangeException(nameof(kdf)); - _aeadMetadata = HpkeAeadMetadata.Create(aead) ?? throw new ArgumentOutOfRangeException(nameof(aead)); + KemMetadata = HpkeKemMetadata.Create(kem) ?? throw new ArgumentOutOfRangeException(nameof(kem)); + KdfMetadata = HpkeKdfMetadata.Create(kdf) ?? throw new ArgumentOutOfRangeException(nameof(kdf)); + AeadMetadata = HpkeAeadMetadata.Create(aead) ?? throw new ArgumentOutOfRangeException(nameof(aead)); } /// @@ -45,7 +45,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The authenticated encryption with associated data (AEAD) algorithm for the cipher suite. /// - public HpkeAead AeadAlgorithm => _aeadMetadata.Aead; + public HpkeAead AeadAlgorithm => AeadMetadata.Aead; /// /// Gets the key derivation function (KDF) for the cipher suite. @@ -53,7 +53,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The key derivation function (KDF) for the cipher suite. /// - public HpkeKdf KdfAlgorithm => _kdfMetadata.Kdf; + public HpkeKdf KdfAlgorithm => KdfMetadata.Kdf; /// /// Gets the key encapsulation mechanism (KEM) for the cipher suite. @@ -61,7 +61,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The key encapsulation mechanism (KEM) for the cipher suite. /// - public HpkeKem KemAlgorithm => _kemMetadata.Kem; + public HpkeKem KemAlgorithm => KemMetadata.Kem; /// /// Gets the size of the authentication tag for the cipher suite, in bytes. @@ -69,7 +69,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The size of the authentication tag for the cipher suite, in bytes. /// - public int AeadTagSizeInBytes => _aeadMetadata.Nt; + public int AeadTagSizeInBytes => AeadMetadata.Nt; /// /// Gets the size of the decapsulation key for the cipher suite, in bytes. @@ -80,7 +80,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// For ML-KEM and hybrid ML-KEM cipher suites, this is the size of the private seed. /// - public int DecapsulationKeySizeInBytes => _kemMetadata.Nsk; + public int DecapsulationKeySizeInBytes => KemMetadata.Nsk; /// /// Gets the size of an encapsulated secret for the cipher suite, in bytes. @@ -88,7 +88,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The size of an encapsulated secret for the cipher suite, in bytes. /// - public int EncapsulatedSecretSizeInBytes => _kemMetadata.Nenc; + public int EncapsulatedSecretSizeInBytes => KemMetadata.Nenc; /// /// Gets the size of the encapsulation key for the cipher suite, in bytes. @@ -96,7 +96,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The size of the encapsulation key for the cipher suite, in bytes. /// - public int EncapsulationKeySizeInBytes => _kemMetadata.Npk; + public int EncapsulationKeySizeInBytes => KemMetadata.Npk; /// /// Gets the name of the cipher suite. @@ -104,7 +104,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// A string containing the KEM, KDF, and AEAD names, separated by spaces. /// - public string Name => field ??= $"{_kemMetadata.Name} {_kdfMetadata.Name} {_aeadMetadata.Name}"; + public string Name => field ??= $"{KemMetadata.Name} {KdfMetadata.Name} {AeadMetadata.Name}"; /// /// Gets the length of the ciphertext produced by encrypting a plaintext of the specified length. diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index 642c1e67efb557..1e1b7e6c631dc1 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -36,6 +36,8 @@ + VerifyAsync(System.ReadOnlyMemory key, System.IO.Stream source, System.ReadOnlyMemory hash, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public abstract partial class Hpke + { + protected Hpke(System.Security.Cryptography.HpkeSuite suite) { } + public System.Security.Cryptography.HpkeSuite Suite { get { throw null; } } + public static bool IsSupported(System.Security.Cryptography.HpkeSuite suite) { throw null; } + } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public enum HpkeAead { AES_128_GCM = 1, diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index b7bf975e24e11d..9fae6073499f91 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -444,6 +444,9 @@ The HKDF pseudorandom key exceeds the maximum supported length of {0} bytes for this platform. + + An HPKE key pair could not be derived from the supplied input keying material. + The size of the specified tag does not match the expected size of {0}. 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 b14099a76fb577..7b40c7b13d5ebd 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -404,6 +404,8 @@ Link="Common\System\Security\Cryptography\DSAKeyFormatHelper.cs" /> + + @@ -629,6 +632,10 @@ + + + + @@ -1449,7 +1456,6 @@ - diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs new file mode 100644 index 00000000000000..187319b93ca3d0 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal sealed partial class HpkeAeadMetadata + { + internal bool IsSupported + { + get + { + switch (Aead) + { +#pragma warning disable CA1416 // Not supported on browser + case HpkeAead.AES_128_GCM: + case HpkeAead.AES_256_GCM: + return AesGcm.IsSupported; + case HpkeAead.ChaCha20Poly1305: + return ChaCha20Poly1305.IsSupported; + default: + Debug.Fail($"Aead {Aead}'s support is unknown."); + return false; +#pragma warning restore CA1416 // Not supported on browser + } + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs new file mode 100644 index 00000000000000..302d43d46e9573 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -0,0 +1,276 @@ +// 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.Binary; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + internal sealed class HpkeImplementation : Hpke + { + private HpkeImplementation(HpkeSuite suite) : base(suite) + { + } + + internal static bool IsSupportedImpl(HpkeSuite suite) => + suite.KemMetadata.IsSupported && + suite.KdfMetadata.IsSupported && + suite.AeadMetadata.IsSupported; + } + + internal abstract class ManagedHpkeKemAdapter : IDisposable + { + protected const int PrkStackBufferSize = SHA512.HashSizeInBytes; + + private static ReadOnlySpan VersionLabel => "HPKE-v1"u8; + + protected HpkeSuite Suite { get; } + protected HpkeKdfMetadata KeyDerivationKdf => Suite.KemMetadata.KemKdf; + + protected ManagedHpkeKemAdapter(HpkeSuite suite) + { + Suite = suite; + } + + internal static ManagedHpkeKemAdapter Create(HpkeSuite suite) + { + switch (suite.KemAlgorithm) + { + case HpkeKem.DHKEM_P256_HKDF_SHA256: + case HpkeKem.DHKEM_P384_HKDF_SHA384: + return new ECDiffieHellmanHpkeKemAdapter(suite); + case HpkeKem.DHKEM_X25519_HKDF_SHA256: + return new X25519DiffieHellmanHpkeKemAdapter(suite); + default: + throw new PlatformNotSupportedException(); + } + } + + internal void Generate() + { + const int MaxStackIkmSize = 64; + + using (CryptoPoolLease ikm = CryptoPoolLease.RentConditionally( + Suite.KemMetadata.Nsk, stackalloc byte[MaxStackIkmSize])) + { + RandomNumberGenerator.Fill(ikm.Span); + DeriveKeyPair(ikm.Span); + } + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 + protected void LabeledExtract( + ReadOnlySpan salt, + ReadOnlySpan label, + ReadOnlySpan ikm, + Span prk) + { + Debug.Assert(KeyDerivationKdf.IsTwoStage); + Debug.Assert(prk.Length == KeyDerivationKdf.Nh); + + using (IncrementalHash hmac = IncrementalHash.CreateHMAC(KeyDerivationKdf.HkdfHashAlgorithm, salt)) + { + hmac.AppendData(VersionLabel); + hmac.AppendData(Suite.KemMetadata.SuiteId); + hmac.AppendData(label); + hmac.AppendData(ikm); + int written = hmac.GetHashAndReset(prk); + Debug.Assert(written == prk.Length); + } + } + + protected void LabeledExpand( + ReadOnlySpan prk, + ReadOnlySpan label, + ReadOnlySpan info, + Span output) + { + Debug.Assert(KeyDerivationKdf.IsTwoStage); + Debug.Assert(prk.Length == KeyDerivationKdf.Nh); + Debug.Assert(output.Length <= ushort.MaxValue); + + ReadOnlySpan suiteId = Suite.KemMetadata.SuiteId; + int labeledInfoLength = checked(sizeof(ushort) + VersionLabel.Length + suiteId.Length + label.Length + info.Length); + const int MaxStackLabeledInfoLength = 64; + + using (CryptoPoolLease labeledInfo = CryptoPoolLease.RentConditionally( + labeledInfoLength, stackalloc byte[MaxStackLabeledInfoLength])) + { + Span destination = labeledInfo.Span; + BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)output.Length)); + int offset = sizeof(ushort); + VersionLabel.CopyTo(destination.Slice(offset)); + offset += VersionLabel.Length; + suiteId.CopyTo(destination.Slice(offset)); + offset += suiteId.Length; + label.CopyTo(destination.Slice(offset)); + offset += label.Length; + info.CopyTo(destination.Slice(offset)); + + HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, labeledInfo.Span); + } + } + + internal abstract void DeriveKeyPair(ReadOnlySpan ikm); + internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); + public abstract void Dispose(); + } + + internal sealed class ECDiffieHellmanHpkeKemAdapter : ManagedHpkeKemAdapter + { + private readonly ECCurve _curve; + private ECDiffieHellman? _ecdh; + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1.3 + private static ReadOnlySpan P256Order => + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, + 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51, + ]; + + private static ReadOnlySpan P384Order => + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC7, 0x63, 0x4D, 0x81, 0xF4, 0x37, 0x2D, 0xDF, + 0x58, 0x1A, 0x0D, 0xB2, 0x48, 0xB0, 0xA7, 0x7A, + 0xEC, 0xEC, 0x19, 0x6A, 0xCC, 0xC5, 0x29, 0x73, + ]; + + private ReadOnlySpan Order => Suite.KemAlgorithm switch + { + HpkeKem.DHKEM_P256_HKDF_SHA256 => P256Order, + HpkeKem.DHKEM_P384_HKDF_SHA384 => P384Order, + _ => throw new UnreachableException(), + }; + + internal ECDiffieHellmanHpkeKemAdapter(HpkeSuite suite) : base(suite) + { + _curve = suite.KemAlgorithm switch + { + HpkeKem.DHKEM_P256_HKDF_SHA256 => ECCurve.NamedCurves.nistP256, + HpkeKem.DHKEM_P384_HKDF_SHA384 => ECCurve.NamedCurves.nistP384, + _ => throw new UnreachableException(), + }; + } + + internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) + { + Debug.Assert(_ecdh is null); + + if (encapsulationKey.Length != Suite.EncapsulationKeySizeInBytes) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1.1 + AsymmetricAlgorithmHelpers.DecodeFromUncompressedAnsiX963Key( + encapsulationKey, hasPrivateKey: false, out ECParameters parameters); + parameters.Curve = _curve; +#pragma warning disable CA1416 // Not supported on browser + _ecdh = ECDiffieHellman.Create(parameters); +#pragma warning restore CA1416 // Not supported on browser + } + + internal override void DeriveKeyPair(ReadOnlySpan ikm) + { + Debug.Assert(_ecdh is null); + + ReadOnlySpan order = Order; + Debug.Assert(order.Length == Suite.KemMetadata.Nsk); + byte[] privateKey = new byte[Suite.KemMetadata.Nsk]; + + using (PinAndClear.Track(privateKey)) + using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( + KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) + { + LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); + Span counterBytes = stackalloc byte[1]; + + // The P-256 and P-384 masks are 0xFF, so no candidate bits need to be cleared. + for (int counter = 0; counter <= byte.MaxValue; counter++) + { + counterBytes[0] = (byte)counter; + LabeledExpand(prk.Span, "candidate"u8, counterBytes, privateKey); + + if (IsValidScalar(privateKey, order)) + { +#pragma warning disable CA1416 // Not supported on browser + _ecdh = ECDiffieHellman.Create(new ECParameters + { + Curve = _curve, + D = privateKey, + }); +#pragma warning restore CA1416 // Not supported on browser + return; + } + } + + throw new CryptographicException(SR.Cryptography_HpkeKeyDerivationFailed); + } + } + + public override void Dispose() => _ecdh?.Dispose(); + + private static bool IsValidScalar(ReadOnlySpan scalar, ReadOnlySpan order) + { + Debug.Assert(scalar.Length == order.Length); + + uint borrow = 0; + uint nonZero = 0; + + // Subtract the public order without branching on individual secret bytes. + for (int i = scalar.Length - 1; i >= 0; i--) + { + uint value = scalar[i]; + nonZero |= value; + borrow = unchecked(value - order[i] - borrow) >> 31; + } + + return (borrow != 0) & (nonZero != 0); + } + } + + internal sealed class X25519DiffieHellmanHpkeKemAdapter : ManagedHpkeKemAdapter + { + private X25519DiffieHellman? _x25519; + + internal X25519DiffieHellmanHpkeKemAdapter(HpkeSuite suite) : base(suite) + { + } + + internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) + { + Debug.Assert(_x25519 is null); + _x25519 = X25519DiffieHellman.ImportPublicKey(encapsulationKey); + } + + internal override void DeriveKeyPair(ReadOnlySpan ikm) + { + Debug.Assert(_x25519 is null); + + Span privateKey = stackalloc byte[X25519DiffieHellman.PrivateKeySizeInBytes]; + + using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( + KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) + { + try + { + LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); + LabeledExpand(prk.Span, "sk"u8, ReadOnlySpan.Empty, privateKey); + _x25519 = X25519DiffieHellman.ImportPrivateKey(privateKey); + } + finally + { + CryptographicOperations.ZeroMemory(privateKey); + } + } + } + + public override void Dispose() => _x25519?.Dispose(); + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKdfMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKdfMetadata.Managed.cs new file mode 100644 index 00000000000000..1798700db97954 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKdfMetadata.Managed.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal sealed partial class HpkeKdfMetadata + { + internal HashAlgorithmName HkdfHashAlgorithm => Kdf switch + { + HpkeKdf.HKDF_SHA256 => HashAlgorithmName.SHA256, + HpkeKdf.HKDF_SHA384 => HashAlgorithmName.SHA384, + HpkeKdf.HKDF_SHA512 => HashAlgorithmName.SHA512, + _ => throw new UnreachableException(), + }; + + internal bool IsSupported + { + get + { + switch (Kdf) + { + case HpkeKdf.HKDF_SHA256: + return HashProviderDispenser.MacSupported(HashAlgorithmNames.SHA256); + case HpkeKdf.HKDF_SHA384: + return HashProviderDispenser.MacSupported(HashAlgorithmNames.SHA384); + case HpkeKdf.HKDF_SHA512: + return HashProviderDispenser.MacSupported(HashAlgorithmNames.SHA512); + case HpkeKdf.SHAKE128: + return Shake128.IsSupported; + case HpkeKdf.SHAKE256: + return Shake256.IsSupported; + default: + Debug.Fail($"Kdf {Kdf}'s support is unknown."); + return false; + } + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs new file mode 100644 index 00000000000000..eb0e21ec019e52 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + internal sealed partial class HpkeKemMetadata + { + internal HpkeKdfMetadata KemKdf { get; private set; } + internal byte[] SuiteId { get; private set; } + + [MemberNotNull(nameof(KemKdf))] + [MemberNotNull(nameof(SuiteId))] + partial void Setup() + { + switch (Kem) + { + case HpkeKem.DHKEM_P256_HKDF_SHA256: + SuiteId = [.."KEM"u8, 0x00, 0x10]; + KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA256); + break; + case HpkeKem.DHKEM_P384_HKDF_SHA384: + SuiteId = [.."KEM"u8, 0x00, 0x11]; + KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA384); + break; + case HpkeKem.DHKEM_X25519_HKDF_SHA256: + SuiteId = [.."KEM"u8, 0x00, 0x20]; + KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA256); + break; + case HpkeKem.MLKEM_512: + SuiteId = [.."KEM"u8, 0x00, 0x40]; + KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM_768: + SuiteId = [.."KEM"u8, 0x00, 0x41]; + KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM_1024: + SuiteId = [.."KEM"u8, 0x00, 0x42]; + KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM768_P256: + SuiteId = [.."KEM"u8, 0x00, 0x50]; + KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM1024_P384: + SuiteId = [.."KEM"u8, 0x00, 0x51]; + KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + break; + default: + Debug.Fail($"Missing KEM KDF mapping for {Kem}."); + throw new CryptographicException(); + } + + static HpkeKdfMetadata CreateKemKdf(HpkeKdf kdf) + { + HpkeKdfMetadata? metadata = HpkeKdfMetadata.Create(kdf); + + if (metadata is null) + { + Debug.Fail("KEM depends on unmapped KDF."); + throw new CryptographicException(); + } + + return metadata; + } + } + + internal bool IsSupported + { + get + { + switch (Kem) + { + case HpkeKem.DHKEM_P256_HKDF_SHA256: + case HpkeKem.DHKEM_P384_HKDF_SHA384: + return !OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi(); + case HpkeKem.DHKEM_X25519_HKDF_SHA256: + return X25519DiffieHellman.IsSupported; + case HpkeKem.MLKEM_512: + case HpkeKem.MLKEM_768: + case HpkeKem.MLKEM_1024: + case HpkeKem.MLKEM768_P256: + case HpkeKem.MLKEM1024_P384: + return MLKem.IsSupported; + default: + Debug.Fail($"Kem ${Kem}'s support is unknown."); + return false; + } + } + } + } +} From e87f8724761f10854c7451c48809d948e13ebe41 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 5 Sep 2026 18:07:44 -0400 Subject: [PATCH 04/42] Split managed HPKE KEM adapters Move the base, ECDH, and X25519 KEM adapters into grouped source files and apply curve-specific candidate masking for deterministic scalar derivation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System.Security.Cryptography.csproj | 3 + .../HpkeECDiffieHellmanKemAdapter.cs | 137 +++++++++ .../HpkeImplementation.Managed.cs | 259 ------------------ .../Cryptography/HpkeManagedKemAdapter.cs | 107 ++++++++ .../HpkeX25519DiffieHellmanKemAdapter.cs | 46 ++++ 5 files changed, 293 insertions(+), 259 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.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 7b40c7b13d5ebd..3cf87644cbe6fc 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -633,9 +633,12 @@ + + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs new file mode 100644 index 00000000000000..caf6aaaf253b00 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Security.Cryptography +{ + internal sealed class HpkeECDiffieHellmanKemAdapter : HpkeManagedKemAdapter + { + private readonly byte _candidateBitmask; + private readonly ECCurve _curve; + private ECDiffieHellman? _ecdh; + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1.3 + private static ReadOnlySpan P256Order => + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, + 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51, + ]; + + private static ReadOnlySpan P384Order => + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC7, 0x63, 0x4D, 0x81, 0xF4, 0x37, 0x2D, 0xDF, + 0x58, 0x1A, 0x0D, 0xB2, 0x48, 0xB0, 0xA7, 0x7A, + 0xEC, 0xEC, 0x19, 0x6A, 0xCC, 0xC5, 0x29, 0x73, + ]; + + private ReadOnlySpan Order => Suite.KemAlgorithm switch + { + HpkeKem.DHKEM_P256_HKDF_SHA256 => P256Order, + HpkeKem.DHKEM_P384_HKDF_SHA384 => P384Order, + _ => throw new UnreachableException(), + }; + + internal HpkeECDiffieHellmanKemAdapter(HpkeSuite suite) : base(suite) + { + (_curve, _candidateBitmask) = suite.KemAlgorithm switch + { + HpkeKem.DHKEM_P256_HKDF_SHA256 => (ECCurve.NamedCurves.nistP256, byte.MaxValue), + HpkeKem.DHKEM_P384_HKDF_SHA384 => (ECCurve.NamedCurves.nistP384, byte.MaxValue), + _ => throw new UnreachableException(), + }; + } + + internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) + { + Debug.Assert(_ecdh is null); + + if (encapsulationKey.Length != Suite.EncapsulationKeySizeInBytes) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1.1 + AsymmetricAlgorithmHelpers.DecodeFromUncompressedAnsiX963Key( + encapsulationKey, + hasPrivateKey: false, + out ECParameters parameters); + +#pragma warning disable CA1416 // Not supported on browser + parameters.Curve = _curve; + _ecdh = ECDiffieHellman.Create(parameters); +#pragma warning restore CA1416 // Not supported on browser + } + + internal override void DeriveKeyPair(ReadOnlySpan ikm) + { + Debug.Assert(_ecdh is null); + + ReadOnlySpan order = Order; + Debug.Assert(order.Length == Suite.KemMetadata.Nsk); + byte[] privateKey = new byte[Suite.KemMetadata.Nsk]; + + using (PinAndClear.Track(privateKey)) + using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( + KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) + { + LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); + Span counterBytes = stackalloc byte[1]; + + for (int counter = 0; counter <= byte.MaxValue; counter++) + { + counterBytes[0] = (byte)counter; + LabeledExpand(prk.Span, "candidate"u8, counterBytes, privateKey); + // P-521 uses 0x01 here because Nsk is 66 bytes; P-256 and P-384 use 0xFF. + privateKey[0] &= _candidateBitmask; + + if (IsValidScalar(privateKey, order)) + { +#pragma warning disable CA1416 // Not supported on browser + _ecdh = ECDiffieHellman.Create(new ECParameters + { + Curve = _curve, + D = privateKey, + }); +#pragma warning restore CA1416 // Not supported on browser + return; + } + } + + throw new CryptographicException(SR.Cryptography_HpkeKeyDerivationFailed); + } + } + + public override void Dispose() => _ecdh?.Dispose(); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool IsValidScalar(ReadOnlySpan scalar, ReadOnlySpan order) + { + // NoOptimization because the comparison must remain non-short-circuiting. + // + // NoInlining because the NoOptimization would get lost if the method got inlined. + + Debug.Assert(scalar.Length == order.Length); + + uint borrow = 0; + uint nonZero = 0; + + // Subtract the public order without branching on individual secret bytes. + for (int i = scalar.Length - 1; i >= 0; i--) + { + uint value = scalar[i]; + nonZero |= value; + borrow = unchecked(value - order[i] - borrow) >> 31; + } + + return (borrow != 0) & (nonZero != 0); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 302d43d46e9573..fd8bb3b5da4e2f 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -1,10 +1,6 @@ // 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.Binary; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - namespace System.Security.Cryptography { internal sealed class HpkeImplementation : Hpke @@ -18,259 +14,4 @@ internal static bool IsSupportedImpl(HpkeSuite suite) => suite.KdfMetadata.IsSupported && suite.AeadMetadata.IsSupported; } - - internal abstract class ManagedHpkeKemAdapter : IDisposable - { - protected const int PrkStackBufferSize = SHA512.HashSizeInBytes; - - private static ReadOnlySpan VersionLabel => "HPKE-v1"u8; - - protected HpkeSuite Suite { get; } - protected HpkeKdfMetadata KeyDerivationKdf => Suite.KemMetadata.KemKdf; - - protected ManagedHpkeKemAdapter(HpkeSuite suite) - { - Suite = suite; - } - - internal static ManagedHpkeKemAdapter Create(HpkeSuite suite) - { - switch (suite.KemAlgorithm) - { - case HpkeKem.DHKEM_P256_HKDF_SHA256: - case HpkeKem.DHKEM_P384_HKDF_SHA384: - return new ECDiffieHellmanHpkeKemAdapter(suite); - case HpkeKem.DHKEM_X25519_HKDF_SHA256: - return new X25519DiffieHellmanHpkeKemAdapter(suite); - default: - throw new PlatformNotSupportedException(); - } - } - - internal void Generate() - { - const int MaxStackIkmSize = 64; - - using (CryptoPoolLease ikm = CryptoPoolLease.RentConditionally( - Suite.KemMetadata.Nsk, stackalloc byte[MaxStackIkmSize])) - { - RandomNumberGenerator.Fill(ikm.Span); - DeriveKeyPair(ikm.Span); - } - } - - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 - protected void LabeledExtract( - ReadOnlySpan salt, - ReadOnlySpan label, - ReadOnlySpan ikm, - Span prk) - { - Debug.Assert(KeyDerivationKdf.IsTwoStage); - Debug.Assert(prk.Length == KeyDerivationKdf.Nh); - - using (IncrementalHash hmac = IncrementalHash.CreateHMAC(KeyDerivationKdf.HkdfHashAlgorithm, salt)) - { - hmac.AppendData(VersionLabel); - hmac.AppendData(Suite.KemMetadata.SuiteId); - hmac.AppendData(label); - hmac.AppendData(ikm); - int written = hmac.GetHashAndReset(prk); - Debug.Assert(written == prk.Length); - } - } - - protected void LabeledExpand( - ReadOnlySpan prk, - ReadOnlySpan label, - ReadOnlySpan info, - Span output) - { - Debug.Assert(KeyDerivationKdf.IsTwoStage); - Debug.Assert(prk.Length == KeyDerivationKdf.Nh); - Debug.Assert(output.Length <= ushort.MaxValue); - - ReadOnlySpan suiteId = Suite.KemMetadata.SuiteId; - int labeledInfoLength = checked(sizeof(ushort) + VersionLabel.Length + suiteId.Length + label.Length + info.Length); - const int MaxStackLabeledInfoLength = 64; - - using (CryptoPoolLease labeledInfo = CryptoPoolLease.RentConditionally( - labeledInfoLength, stackalloc byte[MaxStackLabeledInfoLength])) - { - Span destination = labeledInfo.Span; - BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)output.Length)); - int offset = sizeof(ushort); - VersionLabel.CopyTo(destination.Slice(offset)); - offset += VersionLabel.Length; - suiteId.CopyTo(destination.Slice(offset)); - offset += suiteId.Length; - label.CopyTo(destination.Slice(offset)); - offset += label.Length; - info.CopyTo(destination.Slice(offset)); - - HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, labeledInfo.Span); - } - } - - internal abstract void DeriveKeyPair(ReadOnlySpan ikm); - internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); - public abstract void Dispose(); - } - - internal sealed class ECDiffieHellmanHpkeKemAdapter : ManagedHpkeKemAdapter - { - private readonly ECCurve _curve; - private ECDiffieHellman? _ecdh; - - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1.3 - private static ReadOnlySpan P256Order => - [ - 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, - 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51, - ]; - - private static ReadOnlySpan P384Order => - [ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xC7, 0x63, 0x4D, 0x81, 0xF4, 0x37, 0x2D, 0xDF, - 0x58, 0x1A, 0x0D, 0xB2, 0x48, 0xB0, 0xA7, 0x7A, - 0xEC, 0xEC, 0x19, 0x6A, 0xCC, 0xC5, 0x29, 0x73, - ]; - - private ReadOnlySpan Order => Suite.KemAlgorithm switch - { - HpkeKem.DHKEM_P256_HKDF_SHA256 => P256Order, - HpkeKem.DHKEM_P384_HKDF_SHA384 => P384Order, - _ => throw new UnreachableException(), - }; - - internal ECDiffieHellmanHpkeKemAdapter(HpkeSuite suite) : base(suite) - { - _curve = suite.KemAlgorithm switch - { - HpkeKem.DHKEM_P256_HKDF_SHA256 => ECCurve.NamedCurves.nistP256, - HpkeKem.DHKEM_P384_HKDF_SHA384 => ECCurve.NamedCurves.nistP384, - _ => throw new UnreachableException(), - }; - } - - internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) - { - Debug.Assert(_ecdh is null); - - if (encapsulationKey.Length != Suite.EncapsulationKeySizeInBytes) - { - throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); - } - - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1.1 - AsymmetricAlgorithmHelpers.DecodeFromUncompressedAnsiX963Key( - encapsulationKey, hasPrivateKey: false, out ECParameters parameters); - parameters.Curve = _curve; -#pragma warning disable CA1416 // Not supported on browser - _ecdh = ECDiffieHellman.Create(parameters); -#pragma warning restore CA1416 // Not supported on browser - } - - internal override void DeriveKeyPair(ReadOnlySpan ikm) - { - Debug.Assert(_ecdh is null); - - ReadOnlySpan order = Order; - Debug.Assert(order.Length == Suite.KemMetadata.Nsk); - byte[] privateKey = new byte[Suite.KemMetadata.Nsk]; - - using (PinAndClear.Track(privateKey)) - using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( - KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) - { - LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); - Span counterBytes = stackalloc byte[1]; - - // The P-256 and P-384 masks are 0xFF, so no candidate bits need to be cleared. - for (int counter = 0; counter <= byte.MaxValue; counter++) - { - counterBytes[0] = (byte)counter; - LabeledExpand(prk.Span, "candidate"u8, counterBytes, privateKey); - - if (IsValidScalar(privateKey, order)) - { -#pragma warning disable CA1416 // Not supported on browser - _ecdh = ECDiffieHellman.Create(new ECParameters - { - Curve = _curve, - D = privateKey, - }); -#pragma warning restore CA1416 // Not supported on browser - return; - } - } - - throw new CryptographicException(SR.Cryptography_HpkeKeyDerivationFailed); - } - } - - public override void Dispose() => _ecdh?.Dispose(); - - private static bool IsValidScalar(ReadOnlySpan scalar, ReadOnlySpan order) - { - Debug.Assert(scalar.Length == order.Length); - - uint borrow = 0; - uint nonZero = 0; - - // Subtract the public order without branching on individual secret bytes. - for (int i = scalar.Length - 1; i >= 0; i--) - { - uint value = scalar[i]; - nonZero |= value; - borrow = unchecked(value - order[i] - borrow) >> 31; - } - - return (borrow != 0) & (nonZero != 0); - } - } - - internal sealed class X25519DiffieHellmanHpkeKemAdapter : ManagedHpkeKemAdapter - { - private X25519DiffieHellman? _x25519; - - internal X25519DiffieHellmanHpkeKemAdapter(HpkeSuite suite) : base(suite) - { - } - - internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) - { - Debug.Assert(_x25519 is null); - _x25519 = X25519DiffieHellman.ImportPublicKey(encapsulationKey); - } - - internal override void DeriveKeyPair(ReadOnlySpan ikm) - { - Debug.Assert(_x25519 is null); - - Span privateKey = stackalloc byte[X25519DiffieHellman.PrivateKeySizeInBytes]; - - using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( - KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) - { - try - { - LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); - LabeledExpand(prk.Span, "sk"u8, ReadOnlySpan.Empty, privateKey); - _x25519 = X25519DiffieHellman.ImportPrivateKey(privateKey); - } - finally - { - CryptographicOperations.ZeroMemory(privateKey); - } - } - } - - public override void Dispose() => _x25519?.Dispose(); - } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs new file mode 100644 index 00000000000000..40d1405d8583b3 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -0,0 +1,107 @@ +// 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.Binary; +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal abstract class HpkeManagedKemAdapter : IDisposable + { + protected const int PrkStackBufferSize = SHA512.HashSizeInBytes; + + private static ReadOnlySpan VersionLabel => "HPKE-v1"u8; + + protected HpkeSuite Suite { get; } + protected HpkeKdfMetadata KeyDerivationKdf => Suite.KemMetadata.KemKdf; + + protected HpkeManagedKemAdapter(HpkeSuite suite) + { + Suite = suite; + } + + internal static HpkeManagedKemAdapter Create(HpkeSuite suite) + { + switch (suite.KemAlgorithm) + { + case HpkeKem.DHKEM_P256_HKDF_SHA256: + case HpkeKem.DHKEM_P384_HKDF_SHA384: + return new HpkeECDiffieHellmanKemAdapter(suite); + case HpkeKem.DHKEM_X25519_HKDF_SHA256: + return new HpkeX25519DiffieHellmanKemAdapter(suite); + default: + throw new PlatformNotSupportedException(); + } + } + + internal void Generate() + { + const int MaxStackIkmSize = 64; + + using (CryptoPoolLease ikm = CryptoPoolLease.RentConditionally( + Suite.KemMetadata.Nsk, stackalloc byte[MaxStackIkmSize])) + { + RandomNumberGenerator.Fill(ikm.Span); + DeriveKeyPair(ikm.Span); + } + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 + protected void LabeledExtract( + ReadOnlySpan salt, + ReadOnlySpan label, + ReadOnlySpan ikm, + Span prk) + { + Debug.Assert(KeyDerivationKdf.IsTwoStage); + Debug.Assert(prk.Length == KeyDerivationKdf.Nh); + + using (IncrementalHash hmac = IncrementalHash.CreateHMAC(KeyDerivationKdf.HkdfHashAlgorithm, salt)) + { + hmac.AppendData(VersionLabel); + hmac.AppendData(Suite.KemMetadata.SuiteId); + hmac.AppendData(label); + hmac.AppendData(ikm); + int written = hmac.GetHashAndReset(prk); + Debug.Assert(written == prk.Length); + } + } + + protected void LabeledExpand( + ReadOnlySpan prk, + ReadOnlySpan label, + ReadOnlySpan info, + Span output) + { + Debug.Assert(KeyDerivationKdf.IsTwoStage); + Debug.Assert(prk.Length == KeyDerivationKdf.Nh); + Debug.Assert(output.Length <= ushort.MaxValue); + + ReadOnlySpan suiteId = Suite.KemMetadata.SuiteId; + int labeledInfoLength = + checked(sizeof(ushort) + VersionLabel.Length + suiteId.Length + label.Length + info.Length); + const int MaxStackLabeledInfoLength = 64; + + using (CryptoPoolLease labeledInfo = CryptoPoolLease.RentConditionally( + labeledInfoLength, stackalloc byte[MaxStackLabeledInfoLength])) + { + Span destination = labeledInfo.Span; + BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)output.Length)); + int offset = sizeof(ushort); + VersionLabel.CopyTo(destination.Slice(offset)); + offset += VersionLabel.Length; + suiteId.CopyTo(destination.Slice(offset)); + offset += suiteId.Length; + label.CopyTo(destination.Slice(offset)); + offset += label.Length; + info.CopyTo(destination.Slice(offset)); + + HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, labeledInfo.Span); + } + } + + internal abstract void DeriveKeyPair(ReadOnlySpan ikm); + internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); + public abstract void Dispose(); + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs new file mode 100644 index 00000000000000..e323f171e3161e --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal sealed class HpkeX25519DiffieHellmanKemAdapter : HpkeManagedKemAdapter + { + private X25519DiffieHellman? _x25519; + + internal HpkeX25519DiffieHellmanKemAdapter(HpkeSuite suite) : base(suite) + { + } + + internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) + { + Debug.Assert(_x25519 is null); + _x25519 = X25519DiffieHellman.ImportPublicKey(encapsulationKey); + } + + internal override void DeriveKeyPair(ReadOnlySpan ikm) + { + Debug.Assert(_x25519 is null); + + Span privateKey = stackalloc byte[X25519DiffieHellman.PrivateKeySizeInBytes]; + + using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( + KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) + { + try + { + LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); + LabeledExpand(prk.Span, "sk"u8, ReadOnlySpan.Empty, privateKey); + _x25519 = X25519DiffieHellman.ImportPrivateKey(privateKey); + } + finally + { + CryptographicOperations.ZeroMemory(privateKey); + } + } + } + + public override void Dispose() => _x25519?.Dispose(); + } +} From f57271e25cb875b1a035cda60aa430b2e14a431f Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sun, 6 Sep 2026 11:34:58 -0400 Subject: [PATCH 05/42] Add HPKE key factory APIs Implement random and deterministic HPKE key creation, resource ownership, reference declarations, and known-answer tests for DHKEM P-256, P-384, and X25519. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 129 ++++++++++- .../ref/System.Security.Cryptography.cs | 7 +- .../HpkeImplementation.Managed.cs | 57 ++++- .../Cryptography/HpkeKemMetadata.Managed.cs | 2 +- .../Cryptography/HpkeManagedKemAdapter.cs | 2 +- .../tests/HpkeTests.cs | 210 ++++++++++++++++++ .../System.Security.Cryptography.Tests.csproj | 1 + 7 files changed, 403 insertions(+), 5 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/tests/HpkeTests.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 4a84c4283be4a6..7af31baeab4ae0 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -5,21 +5,148 @@ namespace System.Security.Cryptography { + /// + /// Represents a Hybrid Public Key Encryption (HPKE) key. + /// [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] - public abstract class Hpke + public abstract class Hpke : IDisposable { + private bool _disposed; + + /// + /// Gets the cipher suite associated with this key. + /// + /// + /// The cipher suite associated with this key. + /// public HpkeSuite Suite { get; } + /// + /// Initializes a new instance of the class with the specified cipher suite. + /// + /// + /// The cipher suite associated with this key. + /// + /// + /// is . + /// protected Hpke(HpkeSuite suite) { ArgumentNullException.ThrowIfNull(suite); Suite = suite; } + /// + /// Determines whether the specified cipher suite is supported on the current platform. + /// + /// + /// The cipher suite to check. + /// + /// + /// if the cipher suite is supported; otherwise, . + /// + /// + /// is . + /// public static bool IsSupported(HpkeSuite suite) { ArgumentNullException.ThrowIfNull(suite); return HpkeImplementation.IsSupportedImpl(suite); } + + /// + /// Derives an HPKE key for the specified cipher suite from input keying material. + /// + /// + /// The cipher suite for the derived key. + /// + /// + /// The input keying material from which to derive the key. + /// + /// + /// The derived HPKE key. + /// + /// + /// or is . + /// + /// + /// is not supported on the current platform. + /// + public static Hpke DeriveKey(HpkeSuite suite, byte[] ikm) + { + ArgumentNullException.ThrowIfNull(suite); + ArgumentNullException.ThrowIfNull(ikm); + return DeriveKey(suite, new ReadOnlySpan(ikm)); + } + + /// + /// Derives an HPKE key for the specified cipher suite from input keying material. + /// + /// + /// The cipher suite for the derived key. + /// + /// + /// The input keying material from which to derive the key. + /// + /// + /// The derived HPKE key. + /// + /// + /// is . + /// + /// + /// is not supported on the current platform. + /// + public static Hpke DeriveKey(HpkeSuite suite, ReadOnlySpan ikm) + { + ArgumentNullException.ThrowIfNull(suite); + return HpkeImplementation.DeriveKeyImpl(suite, ikm); + } + + /// + /// Generates a new HPKE key for the specified cipher suite. + /// + /// + /// The cipher suite for the new key. + /// + /// + /// A new HPKE key. + /// + /// + /// is . + /// + /// + /// is not supported on the current platform. + /// + public static Hpke GenerateKey(HpkeSuite suite) + { + ArgumentNullException.ThrowIfNull(suite); + return HpkeImplementation.GenerateKeyImpl(suite); + } + + /// + /// Releases all resources used by the class. + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + } + + /// + /// Called by the Dispose() and Finalize() methods to release the managed and unmanaged + /// resources used by the current instance of the class. + /// + /// + /// to release managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } } } 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 b2f25795745cb6..42c76e49b040f5 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1889,10 +1889,15 @@ public override void Initialize() { } public static System.Threading.Tasks.ValueTask VerifyAsync(System.ReadOnlyMemory key, System.IO.Stream source, System.ReadOnlyMemory hash, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] - public abstract partial class Hpke + public abstract partial class Hpke : System.IDisposable { protected Hpke(System.Security.Cryptography.HpkeSuite suite) { } public System.Security.Cryptography.HpkeSuite Suite { get { throw null; } } + public static System.Security.Cryptography.Hpke DeriveKey(System.Security.Cryptography.HpkeSuite suite, byte[] ikm) { throw null; } + public static System.Security.Cryptography.Hpke DeriveKey(System.Security.Cryptography.HpkeSuite suite, System.ReadOnlySpan ikm) { throw null; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public static System.Security.Cryptography.Hpke GenerateKey(System.Security.Cryptography.HpkeSuite suite) { throw null; } public static bool IsSupported(System.Security.Cryptography.HpkeSuite suite) { throw null; } } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index fd8bb3b5da4e2f..b5a956eab5a2dd 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -5,13 +5,68 @@ namespace System.Security.Cryptography { internal sealed class HpkeImplementation : Hpke { - private HpkeImplementation(HpkeSuite suite) : base(suite) + private readonly HpkeManagedKemAdapter _adapter; + + private HpkeImplementation(HpkeManagedKemAdapter adapter) : base(adapter.Suite) { + _adapter = adapter; } internal static bool IsSupportedImpl(HpkeSuite suite) => suite.KemMetadata.IsSupported && suite.KdfMetadata.IsSupported && suite.AeadMetadata.IsSupported; + + internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan ikm) + { + if (!IsSupportedImpl(suite)) + { + throw new PlatformNotSupportedException(); + } + + HpkeManagedKemAdapter adapter = HpkeManagedKemAdapter.Create(suite); + + try + { + adapter.DeriveKeyPair(ikm); + return new HpkeImplementation(adapter); + } + catch + { + adapter.Dispose(); + throw; + } + } + + internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite) + { + if (!IsSupportedImpl(suite)) + { + throw new PlatformNotSupportedException(); + } + + HpkeManagedKemAdapter adapter = HpkeManagedKemAdapter.Create(suite); + + try + { + adapter.Generate(); + return new HpkeImplementation(adapter); + } + catch + { + adapter.Dispose(); + throw; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _adapter.Dispose(); + } + + base.Dispose(disposing); + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs index eb0e21ec019e52..aa7dff5f76ffb9 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs @@ -84,7 +84,7 @@ internal bool IsSupported case HpkeKem.MLKEM_1024: case HpkeKem.MLKEM768_P256: case HpkeKem.MLKEM1024_P384: - return MLKem.IsSupported; + return false; default: Debug.Fail($"Kem ${Kem}'s support is unknown."); return false; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index 40d1405d8583b3..8746e6706468c3 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -12,7 +12,7 @@ internal abstract class HpkeManagedKemAdapter : IDisposable private static ReadOnlySpan VersionLabel => "HPKE-v1"u8; - protected HpkeSuite Suite { get; } + internal HpkeSuite Suite { get; } protected HpkeKdfMetadata KeyDerivationKdf => Suite.KemMetadata.KemKdf; protected HpkeManagedKemAdapter(HpkeSuite suite) diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs new file mode 100644 index 00000000000000..86acf196e656f7 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static class HpkeTests + { + [Fact] + public static void GenerateKey_NullSuite() + { + AssertExtensions.Throws("suite", () => Hpke.GenerateKey((HpkeSuite)null)); + } + + [Fact] + public static void DeriveKey_NullArguments() + { + HpkeSuite suite = new( + HpkeKem.DHKEM_P256_HKDF_SHA256, + HpkeKdf.HKDF_SHA256, + HpkeAead.AES_128_GCM); + + AssertExtensions.Throws( + "suite", + () => Hpke.DeriveKey((HpkeSuite)null, Array.Empty())); + AssertExtensions.Throws( + "ikm", + () => Hpke.DeriveKey(suite, (byte[])null)); + } + + [Theory] + [InlineData( + HpkeKem.DHKEM_P256_HKDF_SHA256, + "4270e54ffd08d79d5928020af4686d8f6b7d35dbe470265f1f5aa22816ce860e", + "4995788ef4b9d6132b249ce59a77281493eb39af373d236a1fe415cb0c2d7beb", + "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325a" + + "c98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4")] + [InlineData( + HpkeKem.DHKEM_P384_HKDF_SHA384, + "65fca3ea3b6db29a62bff28ec53c08710fab10b3798e59b678d3224296d5883f" + + "039123471784ce57b0d85a17cd521196", + "679172205e04663f40fda1018cd46c18ebaa876ede6998ba86b051614ca4d5e4" + + "bfbea34b720617a4b958cc80f6305244", + "04a5f53da8564364255bc36850df793672782a5c9e4a7fb5fb2e2146eb12e4d8" + + "477ab1f326a361dfd1e41212109510e813380547c68c0964c1908f16f67b902a" + + "061be27b2f8b43f1fab1bf0dbf89f5167ce80aca2c210b8fc0f040699db9ee1229")] + [InlineData( + HpkeKem.DHKEM_X25519_HKDF_SHA256, + "7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234", + "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736", + "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431")] + public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string privateKeyHex, string publicKeyHex) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws( + () => Hpke.DeriveKey(suite, Convert.FromHexString(ikmHex))); + return; + } + + byte[] ikm = Convert.FromHexString(ikmHex); + byte[] expectedPrivateKey = Convert.FromHexString(privateKeyHex); + + try + { + using (Hpke keyFromArray = Hpke.DeriveKey(suite, ikm)) + using (Hpke keyFromSpan = Hpke.DeriveKey(suite, ikm.AsSpan())) + { + byte[] arrayPrivateKey = ExportPrivateKey(keyFromArray, kem); + byte[] spanPrivateKey = ExportPrivateKey(keyFromSpan, kem); + + try + { + Assert.Equal(expectedPrivateKey, arrayPrivateKey); + Assert.Equal(Convert.FromHexString(publicKeyHex), ExportPublicKey(keyFromArray, kem)); + Assert.Equal(arrayPrivateKey, spanPrivateKey); + Assert.Equal(ExportPublicKey(keyFromArray, kem), ExportPublicKey(keyFromSpan, kem)); + } + finally + { + CryptographicOperations.ZeroMemory(arrayPrivateKey); + CryptographicOperations.ZeroMemory(spanPrivateKey); + } + } + } + finally + { + CryptographicOperations.ZeroMemory(ikm); + CryptographicOperations.ZeroMemory(expectedPrivateKey); + } + } + + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] + public static void GenerateKey(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + Hpke key = Hpke.GenerateKey(suite); + + try + { + Assert.Same(suite, key.Suite); + + FieldInfo adapterField = key.GetType().GetField( + "_adapter", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(adapterField); + + object adapter = adapterField.GetValue(key); + Assert.NotNull(adapter); + + string keyFieldName = kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 ? "_x25519" : "_ecdh"; + FieldInfo keyField = adapter.GetType().GetField( + keyFieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(keyField); + Assert.NotNull(keyField.GetValue(adapter)); + } + finally + { + key.Dispose(); + } + + key.Dispose(); + } + + private static object GetAdapter(Hpke key) + { + FieldInfo adapterField = key.GetType().GetField( + "_adapter", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(adapterField); + + object adapter = adapterField.GetValue(key); + Assert.NotNull(adapter); + return adapter; + } + + private static byte[] ExportPrivateKey(Hpke key, HpkeKem kem) + { + object adapter = GetAdapter(key); + string keyFieldName = kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 ? "_x25519" : "_ecdh"; + object implementation = GetImplementation(adapter, keyFieldName); + + if (kem == HpkeKem.DHKEM_X25519_HKDF_SHA256) + { + return (byte[])implementation.GetType().GetMethod( + nameof(X25519DiffieHellman.ExportPrivateKey), + Type.EmptyTypes).Invoke( + implementation, + parameters: null); + } + + ECParameters parameters = (ECParameters)implementation.GetType() + .GetMethod(nameof(ECDiffieHellman.ExportParameters)) + .Invoke(implementation, new object[] { true }); + return parameters.D; + } + + private static byte[] ExportPublicKey(Hpke key, HpkeKem kem) + { + object adapter = GetAdapter(key); + string keyFieldName = kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 ? "_x25519" : "_ecdh"; + object implementation = GetImplementation(adapter, keyFieldName); + + if (kem == HpkeKem.DHKEM_X25519_HKDF_SHA256) + { + return (byte[])implementation.GetType().GetMethod( + nameof(X25519DiffieHellman.ExportPublicKey), + Type.EmptyTypes).Invoke( + implementation, + parameters: null); + } + + ECParameters parameters = (ECParameters)implementation.GetType() + .GetMethod(nameof(ECDiffieHellman.ExportParameters)) + .Invoke(implementation, new object[] { false }); + byte[] publicKey = new byte[1 + parameters.Q.X.Length + parameters.Q.Y.Length]; + publicKey[0] = 0x04; + parameters.Q.X.CopyTo(publicKey, 1); + parameters.Q.Y.CopyTo(publicKey, 1 + parameters.Q.X.Length); + return publicKey; + } + + private static object GetImplementation(object adapter, string keyFieldName) + { + FieldInfo keyField = adapter.GetType().GetField( + keyFieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(keyField); + + object implementation = keyField.GetValue(adapter); + Assert.NotNull(implementation); + return implementation; + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index bade8976a59785..d0b80d0c11aece 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -607,6 +607,7 @@ + From fa21cda8eb1a1302098b185f7a01cd42425106c7 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Mon, 7 Sep 2026 10:36:24 -0400 Subject: [PATCH 06/42] Implement HPKE decapsulation key export Forward private-key export through the KEM adapters, clear temporary secret buffers, and add public export coverage. Centralize factory support guards in Hpke while retaining backend-specific support queries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 86 ++++++++++++++- .../ref/System.Security.Cryptography.cs | 3 + .../HpkeECDiffieHellmanKemAdapter.cs | 19 ++++ .../HpkeImplementation.Managed.cs | 13 +-- .../Cryptography/HpkeManagedKemAdapter.cs | 1 + .../HpkeX25519DiffieHellmanKemAdapter.cs | 8 ++ .../tests/HpkeTests.cs | 101 +++++++++++------- 7 files changed, 184 insertions(+), 47 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 7af31baeab4ae0..708541debc5045 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -74,7 +74,6 @@ public static bool IsSupported(HpkeSuite suite) /// public static Hpke DeriveKey(HpkeSuite suite, byte[] ikm) { - ArgumentNullException.ThrowIfNull(suite); ArgumentNullException.ThrowIfNull(ikm); return DeriveKey(suite, new ReadOnlySpan(ikm)); } @@ -100,6 +99,7 @@ public static Hpke DeriveKey(HpkeSuite suite, byte[] ikm) public static Hpke DeriveKey(HpkeSuite suite, ReadOnlySpan ikm) { ArgumentNullException.ThrowIfNull(suite); + ThrowIfNotSupported(suite); return HpkeImplementation.DeriveKeyImpl(suite, ikm); } @@ -121,9 +121,83 @@ public static Hpke DeriveKey(HpkeSuite suite, ReadOnlySpan ikm) public static Hpke GenerateKey(HpkeSuite suite) { ArgumentNullException.ThrowIfNull(suite); + ThrowIfNotSupported(suite); return HpkeImplementation.GenerateKeyImpl(suite); } + /// + /// Exports the decapsulation key. + /// + /// + /// The decapsulation key. + /// + /// + /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key. + /// + /// + /// The object has already been disposed. + /// + public byte[] ExportDecapsulationKey() + { + ThrowIfDisposed(); + byte[] key = new byte[Suite.DecapsulationKeySizeInBytes]; + + try + { + ExportDecapsulationKeyCore(key); + return key; + } + catch + { + CryptographicOperations.ZeroMemory(key); + throw; + } + } + + /// + /// Exports the decapsulation key into the provided buffer. + /// + /// + /// The buffer to receive the decapsulation key. + /// + /// + /// is not exactly + /// bytes long. + /// + /// + /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key. + /// + /// + /// The object has already been disposed. + /// + public void ExportDecapsulationKey(Span destination) + { + if (destination.Length != Suite.DecapsulationKeySizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, Suite.DecapsulationKeySizeInBytes), + nameof(destination)); + } + + ThrowIfDisposed(); + ExportDecapsulationKeyCore(destination); + } + + /// + /// When overridden in a derived class, exports the decapsulation key into the provided buffer. + /// + /// + /// The buffer to receive the decapsulation key. + /// + /// + /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key. + /// + /// + /// is exactly + /// bytes long. + /// + protected abstract void ExportDecapsulationKeyCore(Span destination); + /// /// Releases all resources used by the class. /// @@ -148,5 +222,15 @@ public void Dispose() protected virtual void Dispose(bool disposing) { } + + private static void ThrowIfNotSupported(HpkeSuite suite) + { + if (!IsSupported(suite)) + { + throw new PlatformNotSupportedException(); + } + } + + private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); } } 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 42c76e49b040f5..89c1b7ac054063 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1897,6 +1897,9 @@ protected Hpke(System.Security.Cryptography.HpkeSuite suite) { } public static System.Security.Cryptography.Hpke DeriveKey(System.Security.Cryptography.HpkeSuite suite, System.ReadOnlySpan ikm) { throw null; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } + public byte[] ExportDecapsulationKey() { throw null; } + public void ExportDecapsulationKey(System.Span destination) { } + protected abstract void ExportDecapsulationKeyCore(System.Span destination); public static System.Security.Cryptography.Hpke GenerateKey(System.Security.Cryptography.HpkeSuite suite) { throw null; } public static bool IsSupported(System.Security.Cryptography.HpkeSuite suite) { throw null; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index caf6aaaf253b00..5071e980b40d43 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -109,6 +109,25 @@ internal override void DeriveKeyPair(ReadOnlySpan ikm) } } + internal override void ExportDecapsulationKey(Span destination) + { + Debug.Assert(_ecdh is not null); + Debug.Assert(destination.Length == Suite.DecapsulationKeySizeInBytes); + + ECParameters parameters = _ecdh.ExportParameters(includePrivateParameters: true); + Debug.Assert(parameters.D is not null); + + using (PinAndClear.Track(parameters.D)) + { + if (parameters.D.Length != destination.Length) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + parameters.D.AsSpan().CopyTo(destination); + } + } + public override void Dispose() => _ecdh?.Dispose(); [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index b5a956eab5a2dd..b6cc8065d39a00 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -19,11 +19,6 @@ internal static bool IsSupportedImpl(HpkeSuite suite) => internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan ikm) { - if (!IsSupportedImpl(suite)) - { - throw new PlatformNotSupportedException(); - } - HpkeManagedKemAdapter adapter = HpkeManagedKemAdapter.Create(suite); try @@ -40,11 +35,6 @@ internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan destination) => + _adapter.ExportDecapsulationKey(destination); + protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index 8746e6706468c3..55d1ed34cc05c9 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -102,6 +102,7 @@ protected void LabeledExpand( internal abstract void DeriveKeyPair(ReadOnlySpan ikm); internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); + internal abstract void ExportDecapsulationKey(Span destination); public abstract void Dispose(); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs index e323f171e3161e..e92485ef7f20c4 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -41,6 +41,14 @@ internal override void DeriveKeyPair(ReadOnlySpan ikm) } } + internal override void ExportDecapsulationKey(Span destination) + { + Debug.Assert(_x25519 is not null); + Debug.Assert(destination.Length == Suite.DecapsulationKeySizeInBytes); + + _x25519.ExportPrivateKey(destination); + } + public override void Dispose() => _x25519?.Dispose(); } } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 86acf196e656f7..6e438a1998b717 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -70,11 +70,12 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv using (Hpke keyFromArray = Hpke.DeriveKey(suite, ikm)) using (Hpke keyFromSpan = Hpke.DeriveKey(suite, ikm.AsSpan())) { - byte[] arrayPrivateKey = ExportPrivateKey(keyFromArray, kem); - byte[] spanPrivateKey = ExportPrivateKey(keyFromSpan, kem); + byte[] arrayPrivateKey = keyFromArray.ExportDecapsulationKey(); + byte[] spanPrivateKey = new byte[suite.DecapsulationKeySizeInBytes]; try { + keyFromSpan.ExportDecapsulationKey(spanPrivateKey); Assert.Equal(expectedPrivateKey, arrayPrivateKey); Assert.Equal(Convert.FromHexString(publicKeyHex), ExportPublicKey(keyFromArray, kem)); Assert.Equal(arrayPrivateKey, spanPrivateKey); @@ -114,20 +115,16 @@ public static void GenerateKey(HpkeKem kem) { Assert.Same(suite, key.Suite); - FieldInfo adapterField = key.GetType().GetField( - "_adapter", - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(adapterField); + byte[] privateKey = key.ExportDecapsulationKey(); - object adapter = adapterField.GetValue(key); - Assert.NotNull(adapter); - - string keyFieldName = kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 ? "_x25519" : "_ecdh"; - FieldInfo keyField = adapter.GetType().GetField( - keyFieldName, - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(keyField); - Assert.NotNull(keyField.GetValue(adapter)); + try + { + Assert.Equal(suite.DecapsulationKeySizeInBytes, privateKey.Length); + } + finally + { + CryptographicOperations.ZeroMemory(privateKey); + } } finally { @@ -137,6 +134,59 @@ public static void GenerateKey(HpkeKem kem) key.Dispose(); } + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] + public static void ExportDecapsulationKey_BufferAndLifetime(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + using (Hpke key = Hpke.GenerateKey(suite)) + { + int keySize = suite.DecapsulationKeySizeInBytes; + byte[] buffer = new byte[keySize + 2]; + byte[] exported = key.ExportDecapsulationKey(); + + try + { + buffer.AsSpan().Fill(0xA5); + key.ExportDecapsulationKey(buffer.AsSpan(1, keySize)); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[^1]); + + exported.AsSpan().Clear(); + key.ExportDecapsulationKey(exported); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); + + AssertExtensions.Throws( + "destination", () => key.ExportDecapsulationKey(Span.Empty)); + AssertExtensions.Throws( + "destination", () => key.ExportDecapsulationKey(buffer.AsSpan(0, keySize - 1))); + AssertExtensions.Throws( + "destination", () => key.ExportDecapsulationKey(buffer.AsSpan(0, keySize + 1))); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); + + key.Dispose(); + Assert.Throws(() => key.ExportDecapsulationKey()); + Assert.Throws(() => key.ExportDecapsulationKey(exported)); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); + } + finally + { + CryptographicOperations.ZeroMemory(exported); + CryptographicOperations.ZeroMemory(buffer); + } + } + } + private static object GetAdapter(Hpke key) { FieldInfo adapterField = key.GetType().GetField( @@ -149,27 +199,6 @@ private static object GetAdapter(Hpke key) return adapter; } - private static byte[] ExportPrivateKey(Hpke key, HpkeKem kem) - { - object adapter = GetAdapter(key); - string keyFieldName = kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 ? "_x25519" : "_ecdh"; - object implementation = GetImplementation(adapter, keyFieldName); - - if (kem == HpkeKem.DHKEM_X25519_HKDF_SHA256) - { - return (byte[])implementation.GetType().GetMethod( - nameof(X25519DiffieHellman.ExportPrivateKey), - Type.EmptyTypes).Invoke( - implementation, - parameters: null); - } - - ECParameters parameters = (ECParameters)implementation.GetType() - .GetMethod(nameof(ECDiffieHellman.ExportParameters)) - .Invoke(implementation, new object[] { true }); - return parameters.D; - } - private static byte[] ExportPublicKey(Hpke key, HpkeKem kem) { object adapter = GetAdapter(key); From d035fc08ed55c55f1bb0b76b12485029ac8f69d9 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Mon, 7 Sep 2026 11:02:40 -0400 Subject: [PATCH 07/42] Implement HPKE encapsulation key export Expose public-key export through the KEM adapters, update reference declarations and tests, and clarify decapsulation-key export documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 91 ++++++++++++++++++- .../ref/System.Security.Cryptography.cs | 3 + .../HpkeECDiffieHellmanKemAdapter.cs | 23 +++++ .../HpkeImplementation.Managed.cs | 3 + .../Cryptography/HpkeManagedKemAdapter.cs | 1 + .../HpkeX25519DiffieHellmanKemAdapter.cs | 8 ++ .../tests/HpkeTests.cs | 88 +++++++++--------- 7 files changed, 171 insertions(+), 46 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 708541debc5045..9fc19cf646f04c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -129,7 +129,8 @@ public static Hpke GenerateKey(HpkeSuite suite) /// Exports the decapsulation key. /// /// - /// The decapsulation key. + /// A new byte array containing the serialized decapsulation key, with a length of + /// bytes. /// /// /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key. @@ -137,6 +138,19 @@ public static Hpke GenerateKey(HpkeSuite suite) /// /// The object has already been disposed. /// + /// + /// + /// The key is exported in the private-key format defined by the cipher suite's KEM, + /// without a PKCS#8 or other ASN.1 wrapper. For DHKEM with NIST curves, this is the fixed-width, + /// big-endian private scalar. For DHKEM with X25519, this is the raw 32-byte X25519 private key. + /// For ML-KEM and hybrid ML-KEM cipher suites, this is the private seed. + /// + /// + /// The returned key is not the original input keying material supplied to + /// . + /// The caller is responsible for protecting the returned secret bytes and clearing them when no longer needed. + /// + /// public byte[] ExportDecapsulationKey() { ThrowIfDisposed(); @@ -158,7 +172,7 @@ public byte[] ExportDecapsulationKey() /// Exports the decapsulation key into the provided buffer. /// /// - /// The buffer to receive the decapsulation key. + /// The buffer to receive the serialized decapsulation key. /// /// /// is not exactly @@ -170,6 +184,11 @@ public byte[] ExportDecapsulationKey() /// /// The object has already been disposed. /// + /// + /// The key format is the same as for . + /// On success, the entire destination is filled with the serialized key. + /// The caller is responsible for protecting the secret bytes and clearing the buffer when no longer needed. + /// public void ExportDecapsulationKey(Span destination) { if (destination.Length != Suite.DecapsulationKeySizeInBytes) @@ -193,11 +212,79 @@ public void ExportDecapsulationKey(Span destination) /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key. /// /// + /// The calling method has verified that this instance is not disposed and that /// is exactly /// bytes long. + /// Implementations must fill the entire destination using the key format described by + /// and throw + /// if the decapsulation key cannot be exported. /// protected abstract void ExportDecapsulationKeyCore(Span destination); + /// + /// Exports the encapsulation key. + /// + /// + /// The encapsulation key. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while exporting the key. + /// + /// + /// The object has already been disposed. + /// + public byte[] ExportEncapsulationKey() + { + ThrowIfDisposed(); + byte[] key = new byte[Suite.EncapsulationKeySizeInBytes]; + ExportEncapsulationKeyCore(key); + return key; + } + + /// + /// Exports the encapsulation key into the provided buffer. + /// + /// + /// The buffer to receive the encapsulation key. + /// + /// + /// is not exactly + /// bytes long. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while exporting the key. + /// + /// + /// The object has already been disposed. + /// + public void ExportEncapsulationKey(Span destination) + { + if (destination.Length != Suite.EncapsulationKeySizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulationKeySizeInBytes), + nameof(destination)); + } + + ThrowIfDisposed(); + ExportEncapsulationKeyCore(destination); + } + + /// + /// When overridden in a derived class, exports the encapsulation key into the provided buffer. + /// + /// + /// The buffer to receive the encapsulation key. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while exporting the key. + /// + /// + /// is exactly + /// bytes long. + /// + protected abstract void ExportEncapsulationKeyCore(Span destination); + /// /// Releases all resources used by the class. /// 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 89c1b7ac054063..42b87adf4bd165 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1900,6 +1900,9 @@ protected virtual void Dispose(bool disposing) { } public byte[] ExportDecapsulationKey() { throw null; } public void ExportDecapsulationKey(System.Span destination) { } protected abstract void ExportDecapsulationKeyCore(System.Span destination); + public byte[] ExportEncapsulationKey() { throw null; } + public void ExportEncapsulationKey(System.Span destination) { } + protected abstract void ExportEncapsulationKeyCore(System.Span destination); public static System.Security.Cryptography.Hpke GenerateKey(System.Security.Cryptography.HpkeSuite suite) { throw null; } public static bool IsSupported(System.Security.Cryptography.HpkeSuite suite) { throw null; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index 5071e980b40d43..edd8cb5067e981 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -128,6 +128,29 @@ internal override void ExportDecapsulationKey(Span destination) } } + internal override void ExportEncapsulationKey(Span destination) + { + Debug.Assert(_ecdh is not null); + Debug.Assert(destination.Length == Suite.EncapsulationKeySizeInBytes); + + ECParameters parameters = _ecdh.ExportParameters(includePrivateParameters: false); + byte[]? x = parameters.Q.X; + byte[]? y = parameters.Q.Y; + + Debug.Assert(x is not null); + Debug.Assert(y is not null); + + if (x is null || + y is null || + x.Length != destination.Length / 2 || + y.Length != destination.Length / 2) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + AsymmetricAlgorithmHelpers.EncodeToUncompressedAnsiX963Key(x, y, ReadOnlySpan.Empty, destination); + } + public override void Dispose() => _ecdh?.Dispose(); [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index b6cc8065d39a00..c1c7699d22bcaa 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -52,6 +52,9 @@ internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite) protected override void ExportDecapsulationKeyCore(Span destination) => _adapter.ExportDecapsulationKey(destination); + protected override void ExportEncapsulationKeyCore(Span destination) => + _adapter.ExportEncapsulationKey(destination); + protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index 55d1ed34cc05c9..a2ea2410feb544 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -103,6 +103,7 @@ protected void LabeledExpand( internal abstract void DeriveKeyPair(ReadOnlySpan ikm); internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); internal abstract void ExportDecapsulationKey(Span destination); + internal abstract void ExportEncapsulationKey(Span destination); public abstract void Dispose(); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs index e92485ef7f20c4..1ca3ccfdb71026 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -49,6 +49,14 @@ internal override void ExportDecapsulationKey(Span destination) _x25519.ExportPrivateKey(destination); } + internal override void ExportEncapsulationKey(Span destination) + { + Debug.Assert(_x25519 is not null); + Debug.Assert(destination.Length == Suite.EncapsulationKeySizeInBytes); + + _x25519.ExportPublicKey(destination); + } + public override void Dispose() => _x25519?.Dispose(); } } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 6e438a1998b717..1052478fa6c648 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; using Xunit; namespace System.Security.Cryptography.Tests @@ -64,6 +63,7 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv byte[] ikm = Convert.FromHexString(ikmHex); byte[] expectedPrivateKey = Convert.FromHexString(privateKeyHex); + byte[] expectedPublicKey = Convert.FromHexString(publicKeyHex); try { @@ -75,11 +75,15 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv try { + byte[] arrayPublicKey = keyFromArray.ExportEncapsulationKey(); + byte[] spanPublicKey = new byte[suite.EncapsulationKeySizeInBytes]; + keyFromSpan.ExportDecapsulationKey(spanPrivateKey); + keyFromSpan.ExportEncapsulationKey(spanPublicKey); Assert.Equal(expectedPrivateKey, arrayPrivateKey); - Assert.Equal(Convert.FromHexString(publicKeyHex), ExportPublicKey(keyFromArray, kem)); Assert.Equal(arrayPrivateKey, spanPrivateKey); - Assert.Equal(ExportPublicKey(keyFromArray, kem), ExportPublicKey(keyFromSpan, kem)); + Assert.Equal(expectedPublicKey, arrayPublicKey); + Assert.Equal(arrayPublicKey, spanPublicKey); } finally { @@ -187,53 +191,49 @@ public static void ExportDecapsulationKey_BufferAndLifetime(HpkeKem kem) } } - private static object GetAdapter(Hpke key) - { - FieldInfo adapterField = key.GetType().GetField( - "_adapter", - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(adapterField); - - object adapter = adapterField.GetValue(key); - Assert.NotNull(adapter); - return adapter; - } - - private static byte[] ExportPublicKey(Hpke key, HpkeKem kem) + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] + public static void ExportEncapsulationKey_BufferAndLifetime(HpkeKem kem) { - object adapter = GetAdapter(key); - string keyFieldName = kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 ? "_x25519" : "_ecdh"; - object implementation = GetImplementation(adapter, keyFieldName); + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - if (kem == HpkeKem.DHKEM_X25519_HKDF_SHA256) + if (!Hpke.IsSupported(suite)) { - return (byte[])implementation.GetType().GetMethod( - nameof(X25519DiffieHellman.ExportPublicKey), - Type.EmptyTypes).Invoke( - implementation, - parameters: null); + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; } - ECParameters parameters = (ECParameters)implementation.GetType() - .GetMethod(nameof(ECDiffieHellman.ExportParameters)) - .Invoke(implementation, new object[] { false }); - byte[] publicKey = new byte[1 + parameters.Q.X.Length + parameters.Q.Y.Length]; - publicKey[0] = 0x04; - parameters.Q.X.CopyTo(publicKey, 1); - parameters.Q.Y.CopyTo(publicKey, 1 + parameters.Q.X.Length); - return publicKey; - } + using (Hpke key = Hpke.GenerateKey(suite)) + { + int keySize = suite.EncapsulationKeySizeInBytes; + byte[] buffer = new byte[keySize + 2]; + byte[] exported = key.ExportEncapsulationKey(); + + buffer.AsSpan().Fill(0xA5); + key.ExportEncapsulationKey(buffer.AsSpan(1, keySize)); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[^1]); + + exported.AsSpan().Clear(); + key.ExportEncapsulationKey(exported); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); + + AssertExtensions.Throws( + "destination", () => key.ExportEncapsulationKey(Span.Empty)); + AssertExtensions.Throws( + "destination", () => key.ExportEncapsulationKey(buffer.AsSpan(0, keySize - 1))); + AssertExtensions.Throws( + "destination", () => key.ExportEncapsulationKey(buffer.AsSpan(0, keySize + 1))); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - private static object GetImplementation(object adapter, string keyFieldName) - { - FieldInfo keyField = adapter.GetType().GetField( - keyFieldName, - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(keyField); - - object implementation = keyField.GetValue(adapter); - Assert.NotNull(implementation); - return implementation; + key.Dispose(); + Assert.Throws(() => key.ExportEncapsulationKey()); + Assert.Throws(() => key.ExportEncapsulationKey(exported)); + AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); + } } } } From f589d5a8fe57b4dab7f58a23755561b35321c148 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Mon, 7 Sep 2026 12:50:10 -0400 Subject: [PATCH 08/42] Checkpoint HPKE Seal and managed AEAD adapters Add public Seal scaffolding, KDF info-length validation, an AES-GCM adapter, and unsupported-platform dispatch. This is an intentionally incomplete, non-building checkpoint; SealCore remains unimplemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 87 ++++++++++++++++ .../Security/Cryptography/HpkeKdfMetadata.cs | 19 +++- .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 6 +- .../HpkeImplementation.Managed.cs | 99 +++++++++++++++++-- .../HpkeImplementation.Unsupported.cs | 71 +++++++++++++ 6 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 9fc19cf646f04c..1c076a021c1266 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -285,6 +285,83 @@ public void ExportEncapsulationKey(Span destination) /// protected abstract void ExportEncapsulationKeyCore(Span destination); + public void Seal( + ReadOnlySpan plaintext, + out byte[] encapsulatedSecret, + out byte[] ciphertext, + ReadOnlySpan associatedData = default, + ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + byte[] ciphertextBuffer = new byte[Suite.GetCiphertextLength(plaintext.Length)]; + byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes]; + + SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info); + + encapsulatedSecret = encapsulatedSecretBuffer; + ciphertext = ciphertextBuffer; + } + + public void Seal( + byte[] plaintext, + out byte[] encapsulatedSecret, + out byte[] ciphertext, + byte[]? associatedData = null, + byte[]? info = null) + { + ArgumentNullException.ThrowIfNull(plaintext); + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + byte[] ciphertextBuffer = new byte[Suite.GetCiphertextLength(plaintext.Length)]; + byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes]; + + // associatedData and info null's implicity convert to empty span. + SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info); + + encapsulatedSecret = encapsulatedSecretBuffer; + ciphertext = ciphertextBuffer; + } + + public void Seal( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData = default, + ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes), + nameof(encapsulatedSecret)); + } + + int expectedCiphertextLength = Suite.GetCiphertextLength(plaintext.Length); + + if (ciphertext.Length != expectedCiphertextLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, expectedCiphertextLength), + nameof(ciphertext)); + } + + SealCore(plaintext, encapsulatedSecret, ciphertext, associatedData, info); + } + + protected abstract void SealCore( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData, + ReadOnlySpan info); + + /// /// Releases all resources used by the class. /// @@ -318,6 +395,16 @@ private static void ThrowIfNotSupported(HpkeSuite suite) } } + private void ThrowIfInfoExceedsLimit(ReadOnlySpan info) + { + if (info.Length > Suite.KdfMetadata.MaximumInfoLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeKdfInfoLength, Suite.KdfMetadata.MaximumInfoLength), + nameof(info)); + } + } + private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs index b2292a4559d271..9d6714d1d5c7eb 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs @@ -9,6 +9,7 @@ internal sealed partial class HpkeKdfMetadata internal int Nh { get; } internal bool IsTwoStage { get; } internal string Name { get; } + internal int? MaximumInfoLength { get; } private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name) { @@ -16,12 +17,28 @@ private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name) Nh = nh; IsTwoStage = isTwoStage; Name = name; + + if (!IsTwoStage) + { + // One stage (SHAKE) uses a 16-bit integer to encode the info length. Practically that means the info is limited + // to 65,535. See CombineSecrets_OneStage. info is described as lengthPrefixed(info). + // > lengthPrefixed(x): The two-byte length of the byte string x, concatenated with x itself. + // > (lengthPrefixed(x) = concat(I2OSP(len(x), 2), x)) It is an error to call this function with an x + // > value that is more than 65535 bytes long. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-5 + // We'll track that is the KDF having a maximum info length. + // Other KDFs have a maximum input length however they far exceed 32-bit integers which is limited by a + // Span's input limit. + MaximumInfoLength = ushort.MaxValue; + } } internal static HpkeKdfMetadata? Create(HpkeKdf kdf) { switch (kdf) { + // HKDF SHAs have limits on their info size, 2^61 - 91 and 2^125 - 155. Since this is well above + // A Span's possible length we'll treat it as unlimited. // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.2 case HpkeKdf.HKDF_SHA256: return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: true, name: "HKDF-SHA256"); @@ -29,8 +46,6 @@ private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name) return new HpkeKdfMetadata(kdf, nh: 48, isTwoStage: true, name: "HKDF-SHA384"); case HpkeKdf.HKDF_SHA512: return new HpkeKdfMetadata(kdf, nh: 64, isTwoStage: true, name: "HKDF-SHA512"); - - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-5 case HpkeKdf.SHAKE128: return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: false, name: "SHAKE128"); case HpkeKdf.SHAKE256: diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 9fae6073499f91..67afcbd9b2b789 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -147,6 +147,9 @@ The specified private seed is not the correct length for the ML-KEM algorithm. + + The specified info exceeds the maximum length of {0} bytes. + The specified mu value is not the correct length for the ML-DSA algorithm. 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 3cf87644cbe6fc..08794a7b9b73fc 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -634,7 +634,6 @@ - @@ -899,6 +898,7 @@ + @@ -2114,6 +2114,10 @@ + + + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index c1c7699d22bcaa..ce9d0a17b97bd4 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -1,15 +1,88 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; + +#pragma warning disable CA1416 // //TODO:HPKE Call is reachable on "unsupported platform" - deal with this messy daignostic later. + namespace System.Security.Cryptography { + internal abstract class HpkeManagedAeadAdapter : IDisposable + { + internal static HpkeManagedAeadAdapter Create(HpkeSuite suite, ReadOnlySpan key) + { + Debug.Assert(suite.AeadMetadata.Nt == 16); + + switch (suite.AeadAlgorithm) + { + case HpkeAead.AES_128_GCM: + case HpkeAead.AES_256_GCM: + return new HpkeManagedAesAeadAdapter(suite, key); + case HpkeAead.ChaCha20Poly1305: + throw new NotImplementedException(); + default: + Debug.Fail($"Unmapped AEAD adapter algorithm {suite.AeadAlgorithm}."); + throw new CryptographicException(); + } + } + + internal abstract void Encrypt( + ReadOnlySpan plaintext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + Span ciphertext, + Span tag); + + internal abstract void Decrypt( + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan tag, + Span plaintext); + + public abstract void Dispose(); + } + + internal sealed class HpkeManagedAesAeadAdapter : HpkeManagedAeadAdapter + { + private readonly AesGcm _aes; + + internal HpkeManagedAesAeadAdapter(HpkeSuite suite, ReadOnlySpan key) + { + _aes = new AesGcm(key, suite.AeadMetadata.Nt); + } + + internal override void Encrypt( + ReadOnlySpan plaintext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + Span ciphertext, + Span tag) + { + _aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); + } + + internal override void Decrypt( + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan tag, + Span plaintext) + { + _aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData); + } + + + public override void Dispose() => _aes.Dispose(); + } + internal sealed class HpkeImplementation : Hpke { - private readonly HpkeManagedKemAdapter _adapter; + private readonly HpkeManagedKemAdapter _kemAdapter; - private HpkeImplementation(HpkeManagedKemAdapter adapter) : base(adapter.Suite) + private HpkeImplementation(HpkeSuite suite, HpkeManagedKemAdapter kemAdapter) : base(suite) { - _adapter = adapter; + _kemAdapter = kemAdapter; } internal static bool IsSupportedImpl(HpkeSuite suite) => @@ -24,7 +97,7 @@ internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan destination) => - _adapter.ExportDecapsulationKey(destination); + _kemAdapter.ExportDecapsulationKey(destination); protected override void ExportEncapsulationKeyCore(Span destination) => - _adapter.ExportEncapsulationKey(destination); + _kemAdapter.ExportEncapsulationKey(destination); + + protected override void SealCore( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData, + ReadOnlySpan info) + { + throw new NotImplementedException(); + } protected override void Dispose(bool disposing) { if (disposing) { - _adapter.Dispose(); + _kemAdapter.Dispose(); } base.Dispose(disposing); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs new file mode 100644 index 00000000000000..2ce2fc5fe3defd --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal sealed class HpkeImplementation : Hpke + { + internal HpkeImplementation(HpkeSuite suite) : base(suite) + { + } + + internal static bool IsSupportedImpl(HpkeSuite suite) + { + _ = suite; + return false; + } + + internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan ikm) + { + _ = suite; + _ = ikm; + Debug.Fail("Platform validation should not permit this call."); + throw new CryptographicException(); + } + + internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite) + { + _ = suite; + Debug.Fail("Platform validation should not permit this call."); + throw new CryptographicException(); + } + + protected override void ExportDecapsulationKeyCore(Span destination) + { + _ = destination; + Debug.Fail("Platform validation should not permit this call."); + throw new CryptographicException(); + } + + protected override void ExportEncapsulationKeyCore(Span destination) + { + _ = destination; + Debug.Fail("Platform validation should not permit this call."); + throw new CryptographicException(); + } + + protected override void SealCore( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData, + ReadOnlySpan info) + { + _ = plaintext; + _ = encapsulatedSecret; + _ = ciphertext; + _ = associatedData; + _ = info; + Debug.Fail("Platform validation should not permit this call."); + throw new CryptographicException(); + } + + protected override void Dispose(bool disposing) + { + Debug.Fail("Platform validation should not permit this call."); + throw new CryptographicException(); + } + } +} From e621f1c6f5518f584c4571e1548e70dfb90518c1 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 10:06:33 -0400 Subject: [PATCH 09/42] Add HPKE KEM encapsulation and key schedule adapters Implement DHKEM encapsulation, HKDF and SHAKE key schedules, and the ChaCha20-Poly1305 adapter. Centralize suite IDs and KEM labels, correct PRK slicing, and update Seal reference declarations. SealCore remains unimplemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../System/Security/Cryptography/HpkeSuite.cs | 10 + .../ref/System.Security.Cryptography.cs | 4 + .../src/System.Security.Cryptography.csproj | 1 + .../HpkeECDiffieHellmanKemAdapter.cs | 20 ++ .../HpkeImplementation.Managed.cs | 35 +- .../Cryptography/HpkeManagedKdfAdapter.cs | 334 ++++++++++++++++++ .../Cryptography/HpkeManagedKemAdapter.cs | 49 ++- .../HpkeX25519DiffieHellmanKemAdapter.cs | 24 ++ 8 files changed, 474 insertions(+), 3 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs index 62c0359346d6e3..a028a5ed44f54c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.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.Binary; using System.Diagnostics.CodeAnalysis; namespace System.Security.Cryptography @@ -11,9 +12,12 @@ namespace System.Security.Cryptography [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] public sealed class HpkeSuite : IEquatable { + private readonly byte[] _suiteId; + internal HpkeAeadMetadata AeadMetadata { get; } internal HpkeKdfMetadata KdfMetadata { get; } internal HpkeKemMetadata KemMetadata { get; } + internal ReadOnlySpan SuiteId => _suiteId; /// /// Initializes a new instance of the class with the specified algorithms. @@ -37,6 +41,12 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) KemMetadata = HpkeKemMetadata.Create(kem) ?? throw new ArgumentOutOfRangeException(nameof(kem)); KdfMetadata = HpkeKdfMetadata.Create(kdf) ?? throw new ArgumentOutOfRangeException(nameof(kdf)); AeadMetadata = HpkeAeadMetadata.Create(aead) ?? throw new ArgumentOutOfRangeException(nameof(aead)); + + _suiteId = new byte[10]; + "HPKE"u8.CopyTo(_suiteId); + BinaryPrimitives.WriteUInt16BigEndian(_suiteId.AsSpan(4), checked((ushort)kem)); + BinaryPrimitives.WriteUInt16BigEndian(_suiteId.AsSpan(6), checked((ushort)kdf)); + BinaryPrimitives.WriteUInt16BigEndian(_suiteId.AsSpan(8), checked((ushort)aead)); } /// 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 42b87adf4bd165..47579a31670f75 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1905,6 +1905,10 @@ public void ExportEncapsulationKey(System.Span destination) { } protected abstract void ExportEncapsulationKeyCore(System.Span destination); public static System.Security.Cryptography.Hpke GenerateKey(System.Security.Cryptography.HpkeSuite suite) { throw null; } public static bool IsSupported(System.Security.Cryptography.HpkeSuite suite) { throw null; } + public void Seal(byte[] plaintext, out byte[] encapsulatedSecret, out byte[] ciphertext, byte[]? associatedData = null, byte[]? info = null) { throw null; } + public void Seal(System.ReadOnlySpan plaintext, out byte[] encapsulatedSecret, out byte[] ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan), System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + public void Seal(System.ReadOnlySpan plaintext, System.Span encapsulatedSecret, System.Span ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan), System.ReadOnlySpan info = default(System.ReadOnlySpan)) { } + protected abstract void SealCore(System.ReadOnlySpan plaintext, System.Span encapsulatedSecret, System.Span ciphertext, System.ReadOnlySpan associatedData, System.ReadOnlySpan info); } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public enum HpkeAead 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 08794a7b9b73fc..807700ab6e8531 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -2116,6 +2116,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index edd8cb5067e981..ddf9487e914e94 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -109,6 +109,26 @@ internal override void DeriveKeyPair(ReadOnlySpan ikm) } } + internal override void Encapsulate(Span encapsulatedSecret, Span sharedSecret) + { + Debug.Assert(_ecdh is not null); + + using (HpkeECDiffieHellmanKemAdapter ephemeral = new HpkeECDiffieHellmanKemAdapter(Suite)) + using (ECDiffieHellmanPublicKey recipientPublicKey = _ecdh.PublicKey) + { + ephemeral.Generate(); + Debug.Assert(ephemeral._ecdh is not null); + + byte[] secretAgreement = ephemeral._ecdh.DeriveRawSecretAgreement(recipientPublicKey); + + using (PinAndClear.Track(secretAgreement)) + { + ephemeral.ExportEncapsulationKey(encapsulatedSecret); + ExtractAndExpand(secretAgreement, encapsulatedSecret, sharedSecret); + } + } + } + internal override void ExportDecapsulationKey(Span destination) { Debug.Assert(_ecdh is not null); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index ce9d0a17b97bd4..8e1b4d0bcbf965 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -12,6 +12,7 @@ internal abstract class HpkeManagedAeadAdapter : IDisposable internal static HpkeManagedAeadAdapter Create(HpkeSuite suite, ReadOnlySpan key) { Debug.Assert(suite.AeadMetadata.Nt == 16); + Debug.Assert(key.Length == suite.AeadMetadata.Nk); switch (suite.AeadAlgorithm) { @@ -19,7 +20,7 @@ internal static HpkeManagedAeadAdapter Create(HpkeSuite suite, ReadOnlySpan _aes.Dispose(); } + internal sealed class HpkeManagedChaCha20Poly1305AeadAdapter : HpkeManagedAeadAdapter + { + private readonly ChaCha20Poly1305 _chacha; + + internal HpkeManagedChaCha20Poly1305AeadAdapter(ReadOnlySpan key) + { + _chacha = new ChaCha20Poly1305(key); + } + + internal override void Encrypt( + ReadOnlySpan plaintext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + Span ciphertext, + Span tag) + { + _chacha.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); + } + + internal override void Decrypt( + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan tag, + Span plaintext) + { + _chacha.Decrypt(nonce, ciphertext, tag, plaintext, associatedData); + } + + public override void Dispose() => _chacha.Dispose(); + } + internal sealed class HpkeImplementation : Hpke { private readonly HpkeManagedKemAdapter _kemAdapter; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs new file mode 100644 index 00000000000000..0513da5ae0f241 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -0,0 +1,334 @@ +// 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.Binary; +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal abstract class HpkeManagedKdfAdapter + { + protected static ReadOnlySpan VersionLabel => "HPKE-v1"u8; + protected ReadOnlySpan SuiteId => Suite.SuiteId; + protected HpkeSuite Suite { get; } + + protected HpkeManagedKdfAdapter(HpkeSuite suite) + { + Suite = suite; + } + + internal static HpkeManagedKdfAdapter Create(HpkeSuite suite) + { + switch (suite.KdfAlgorithm) + { + case HpkeKdf.HKDF_SHA256: + case HpkeKdf.HKDF_SHA384: + case HpkeKdf.HKDF_SHA512: + return new HpkeManagedHkdfAdapter(suite, suite.KdfMetadata.HkdfHashAlgorithm); + case HpkeKdf.SHAKE128: + return new HpkeManagedShake128KdfAdapter(suite); + case HpkeKdf.SHAKE256: + return new HpkeManagedShake256KdfAdapter(suite); + default: + Debug.Fail($"Unmapped KDF adapter algorithm {suite.KdfAlgorithm}."); + throw new CryptographicException(); + } + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1 + // Mode (0 for Base, 1 for PSK) and input validation is the caller's responsibility. + internal void DeriveSecrets( + byte mode, + ReadOnlySpan sharedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId, + Span key, + Span baseNonce, + Span exporterSecret) + { + int secretLength = checked(key.Length + baseNonce.Length + exporterSecret.Length); + const int MaxStackSecretLength = 128; + + using (CryptoPoolLease secret = CryptoPoolLease.RentConditionally( + secretLength, stackalloc byte[MaxStackSecretLength])) + { + Span derivedKey = secret.Span.Slice(0, key.Length); + Span derivedNonce = secret.Span.Slice(key.Length, baseNonce.Length); + Span derivedExporterSecret = secret.Span.Slice(key.Length + baseNonce.Length); + + DeriveSecretsCore(mode, sharedSecret, info, psk, pskId, derivedKey, derivedNonce, derivedExporterSecret); + + derivedKey.CopyTo(key); + derivedNonce.CopyTo(baseNonce); + derivedExporterSecret.CopyTo(exporterSecret); + } + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.3 + internal void ExportSecret( + ReadOnlySpan exporterSecret, + ReadOnlySpan exporterContext, + Span destination) + { + Debug.Assert(exporterSecret.Length == Suite.KdfMetadata.Nh); + + int maximumLength = Suite.KdfMetadata.IsTwoStage ? 255 * Suite.KdfMetadata.Nh : ushort.MaxValue; + + if (destination.Length > maximumLength) + { + throw new ArgumentException( + SR.Format(SR.Cryptography_Okm_TooLarge, maximumLength), + nameof(destination)); + } + + // HPKE allows a zero-length export; HKDF.Expand requires a nonempty output. + if (!destination.IsEmpty) + { + ExportSecretCore(exporterSecret, exporterContext, destination); + } + } + + protected abstract void DeriveSecretsCore( + byte mode, + ReadOnlySpan sharedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId, + Span key, + Span baseNonce, + Span exporterSecret); + + protected abstract void ExportSecretCore( + ReadOnlySpan exporterSecret, + ReadOnlySpan exporterContext, + Span destination); + } + + internal sealed class HpkeManagedHkdfAdapter : HpkeManagedKdfAdapter + { + private readonly HashAlgorithmName _hashAlgorithm; + + internal HpkeManagedHkdfAdapter(HpkeSuite suite, HashAlgorithmName hashAlgorithm) : base(suite) + { + Debug.Assert( + hashAlgorithm == HashAlgorithmName.SHA256 || + hashAlgorithm == HashAlgorithmName.SHA384 || + hashAlgorithm == HashAlgorithmName.SHA512); + + _hashAlgorithm = hashAlgorithm; + } + + protected override void DeriveSecretsCore( + byte mode, + ReadOnlySpan sharedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId, + Span key, + Span baseNonce, + Span exporterSecret) + { + int hashLength = Suite.KdfMetadata.Nh; + const int MaxStackContextLength = 1 + 2 * SHA512.HashSizeInBytes; + + using (CryptoPoolLease context = CryptoPoolLease.RentConditionally( + 1 + 2 * hashLength, stackalloc byte[MaxStackContextLength])) + using (CryptoPoolLease secret = CryptoPoolLease.RentConditionally( + hashLength, stackalloc byte[SHA512.HashSizeInBytes])) + { + context.Span[0] = mode; + LabeledExtract(ReadOnlySpan.Empty, "psk_id_hash"u8, pskId, context.Span.Slice(1, hashLength)); + LabeledExtract(ReadOnlySpan.Empty, "info_hash"u8, info, context.Span.Slice(1 + hashLength)); + LabeledExtract(sharedSecret, "secret"u8, psk, secret.Span); + + LabeledExpand(secret.Span, "key"u8, context.Span, key); + LabeledExpand(secret.Span, "base_nonce"u8, context.Span, baseNonce); + LabeledExpand(secret.Span, "exp"u8, context.Span, exporterSecret); + } + } + + protected override void ExportSecretCore( + ReadOnlySpan exporterSecret, + ReadOnlySpan exporterContext, + Span destination) => + LabeledExpand(exporterSecret, "sec"u8, exporterContext, destination); + + private void LabeledExtract( + ReadOnlySpan salt, + ReadOnlySpan label, + ReadOnlySpan ikm, + Span prk) + { + using (IncrementalHash hmac = IncrementalHash.CreateHMAC(_hashAlgorithm, salt)) + { + hmac.AppendData(VersionLabel); + hmac.AppendData(SuiteId); + hmac.AppendData(label); + hmac.AppendData(ikm); + int written = hmac.GetHashAndReset(prk); + Debug.Assert(written == prk.Length); + } + } + + private void LabeledExpand( + ReadOnlySpan prk, + ReadOnlySpan label, + ReadOnlySpan info, + Span output) + { + int length = checked(sizeof(ushort) + VersionLabel.Length + SuiteId.Length + label.Length + info.Length); + const int MaxStackInfoLength = 256; + + using (CryptoPoolLease labeledInfo = CryptoPoolLease.RentConditionally( + length, stackalloc byte[MaxStackInfoLength])) + { + Span buffer = labeledInfo.Span; + BinaryPrimitives.WriteUInt16BigEndian(buffer, checked((ushort)output.Length)); + int offset = sizeof(ushort); + VersionLabel.CopyTo(buffer.Slice(offset)); + offset += VersionLabel.Length; + SuiteId.CopyTo(buffer.Slice(offset)); + offset += SuiteId.Length; + label.CopyTo(buffer.Slice(offset)); + offset += label.Length; + info.CopyTo(buffer.Slice(offset)); + + HKDF.Expand(_hashAlgorithm, prk, output, buffer); + } + } + } + + internal abstract class HpkeManagedShakeKdfAdapter : HpkeManagedKdfAdapter + { + protected HpkeManagedShakeKdfAdapter(HpkeSuite suite) : base(suite) + { + } + + protected override void DeriveSecretsCore( + byte mode, + ReadOnlySpan sharedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId, + Span key, + Span baseNonce, + Span exporterSecret) + { + // The single-stage schedule length-prefixes both secrets and application context. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1 + int secretsLength = checked(2 * sizeof(ushort) + psk.Length + sharedSecret.Length); + int contextLength = checked(1 + 2 * sizeof(ushort) + pskId.Length + info.Length); + int outputLength = checked(key.Length + baseNonce.Length + exporterSecret.Length); + const int MaxStackInputLength = 256; + const int MaxStackOutputLength = 128; + + using (CryptoPoolLease secrets = CryptoPoolLease.RentConditionally( + secretsLength, stackalloc byte[MaxStackInputLength])) + using (CryptoPoolLease context = CryptoPoolLease.RentConditionally( + contextLength, stackalloc byte[MaxStackInputLength])) + using (CryptoPoolLease output = CryptoPoolLease.RentConditionally( + outputLength, stackalloc byte[MaxStackOutputLength])) + { + int offset = WriteLengthPrefixed(psk, secrets.Span); + WriteLengthPrefixed(sharedSecret, secrets.Span.Slice(offset)); + context.Span[0] = mode; + offset = 1 + WriteLengthPrefixed(pskId, context.Span.Slice(1)); + WriteLengthPrefixed(info, context.Span.Slice(offset)); + + LabeledDerive(secrets.Span, "secret"u8, context.Span, output.Span); + output.Span.Slice(0, key.Length).CopyTo(key); + output.Span.Slice(key.Length, baseNonce.Length).CopyTo(baseNonce); + output.Span.Slice(key.Length + baseNonce.Length).CopyTo(exporterSecret); + } + } + + protected override void ExportSecretCore( + ReadOnlySpan exporterSecret, + ReadOnlySpan exporterContext, + Span destination) => + LabeledDerive(exporterSecret, "sec"u8, exporterContext, destination); + + private void LabeledDerive( + ReadOnlySpan ikm, + ReadOnlySpan label, + ReadOnlySpan context, + Span output) + { + // ikm || "HPKE-v1" || suite_id || lengthPrefixed(label) || I2OSP(L, 2) || context + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 + int length = checked(VersionLabel.Length + SuiteId.Length + sizeof(ushort) + label.Length + sizeof(ushort)); + const int MaxStackPrefixLength = 64; + + using (CryptoPoolLease prefix = CryptoPoolLease.RentConditionally( + length, stackalloc byte[MaxStackPrefixLength])) + { + VersionLabel.CopyTo(prefix.Span); + int offset = VersionLabel.Length; + SuiteId.CopyTo(prefix.Span.Slice(offset)); + offset += SuiteId.Length; + offset += WriteLengthPrefixed(label, prefix.Span.Slice(offset)); + BinaryPrimitives.WriteUInt16BigEndian(prefix.Span.Slice(offset), checked((ushort)output.Length)); + + Derive(ikm, prefix.Span, context, output); + } + } + + private static int WriteLengthPrefixed(ReadOnlySpan value, Span destination) + { + BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)value.Length)); + value.CopyTo(destination.Slice(sizeof(ushort))); + return sizeof(ushort) + value.Length; + } + + protected abstract void Derive( + ReadOnlySpan ikm, + ReadOnlySpan prefix, + ReadOnlySpan context, + Span output); + } + + internal sealed class HpkeManagedShake128KdfAdapter : HpkeManagedShakeKdfAdapter + { + internal HpkeManagedShake128KdfAdapter(HpkeSuite suite) : base(suite) + { + } + + protected override void Derive( + ReadOnlySpan ikm, + ReadOnlySpan prefix, + ReadOnlySpan context, + Span output) + { + using (Shake128 shake = new Shake128()) + { + shake.AppendData(ikm); + shake.AppendData(prefix); + shake.AppendData(context); + shake.GetHashAndReset(output); + } + } + } + + internal sealed class HpkeManagedShake256KdfAdapter : HpkeManagedShakeKdfAdapter + { + internal HpkeManagedShake256KdfAdapter(HpkeSuite suite) : base(suite) + { + } + + protected override void Derive( + ReadOnlySpan ikm, + ReadOnlySpan prefix, + ReadOnlySpan context, + Span output) + { + using (Shake256 shake = new Shake256()) + { + shake.AppendData(ikm); + shake.AppendData(prefix); + shake.AppendData(context); + shake.GetHashAndReset(output); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index a2ea2410feb544..7f6b7b6cf9212c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -10,8 +10,15 @@ internal abstract class HpkeManagedKemAdapter : IDisposable { protected const int PrkStackBufferSize = SHA512.HashSizeInBytes; + // HPKE draft, Section 4.4: version prefix for LabeledExtract and LabeledExpand. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 private static ReadOnlySpan VersionLabel => "HPKE-v1"u8; + // HPKE draft, Section 4.5: ExtractAndExpand labels for the PRK and KEM shared secret. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.5 + private static ReadOnlySpan EaePrkLabel => "eae_prk"u8; + private static ReadOnlySpan SharedSecretLabel => "shared_secret"u8; + internal HpkeSuite Suite { get; } protected HpkeKdfMetadata KeyDerivationKdf => Suite.KemMetadata.KemKdf; @@ -37,15 +44,52 @@ internal static HpkeManagedKemAdapter Create(HpkeSuite suite) internal void Generate() { const int MaxStackIkmSize = 64; + Span ikmStack = stackalloc byte[MaxStackIkmSize]; - using (CryptoPoolLease ikm = CryptoPoolLease.RentConditionally( - Suite.KemMetadata.Nsk, stackalloc byte[MaxStackIkmSize])) + using (CryptoPoolLease ikm = CryptoPoolLease.RentConditionally(Suite.KemMetadata.Nsk, ikmStack)) { RandomNumberGenerator.Fill(ikm.Span); DeriveKeyPair(ikm.Span); } } + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.5 + protected void ExtractAndExpand( + ReadOnlySpan secretAgreement, + ReadOnlySpan encapsulatedSecret, + Span sharedSecret) + { + int contextLength = checked(encapsulatedSecret.Length + Suite.EncapsulationKeySizeInBytes); + + // We don't support any KDFs with a hash (Nh) > 64. Since our supported KDFs is a closed set assume 64 + // will work. + if (KeyDerivationKdf.Nh > PrkStackBufferSize) + { + Debug.Fail($"{KeyDerivationKdf.Nh} is unexpectedly bigger than {PrkStackBufferSize}."); + throw new CryptographicException(); + } + + Span prkBuffer = stackalloc byte[PrkStackBufferSize]; + Span prk = prkBuffer.Slice(0, KeyDerivationKdf.Nh); + + try + { + using (CryptoPoolLease context = CryptoPoolLease.Rent(contextLength, skipClear: true)) + { + // kem_context = enc || pkR. Both components are serialized public keys. + encapsulatedSecret.CopyTo(context.Span); + ExportEncapsulationKey(context.Span.Slice(encapsulatedSecret.Length)); + + LabeledExtract(ReadOnlySpan.Empty, EaePrkLabel, secretAgreement, prk); + LabeledExpand(prk, SharedSecretLabel, context.Span, sharedSecret); + } + } + finally + { + CryptographicOperations.ZeroMemory(prkBuffer); + } + } + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 protected void LabeledExtract( ReadOnlySpan salt, @@ -101,6 +145,7 @@ protected void LabeledExpand( } internal abstract void DeriveKeyPair(ReadOnlySpan ikm); + internal abstract void Encapsulate(Span encapsulatedSecret, Span sharedSecret); internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); internal abstract void ExportDecapsulationKey(Span destination); internal abstract void ExportEncapsulationKey(Span destination); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs index 1ca3ccfdb71026..c5742775b1d488 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -41,6 +41,30 @@ internal override void DeriveKeyPair(ReadOnlySpan ikm) } } + internal override void Encapsulate(Span encapsulatedSecret, Span sharedSecret) + { + Debug.Assert(_x25519 is not null); + + using (HpkeX25519DiffieHellmanKemAdapter ephemeral = new HpkeX25519DiffieHellmanKemAdapter(Suite)) + { + ephemeral.Generate(); + Debug.Assert(ephemeral._x25519 is not null); + + Span dh = stackalloc byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + + try + { + ephemeral._x25519.DeriveRawSecretAgreement(_x25519, dh); + ephemeral.ExportEncapsulationKey(encapsulatedSecret); + ExtractAndExpand(dh, encapsulatedSecret, sharedSecret); + } + finally + { + CryptographicOperations.ZeroMemory(dh); + } + } + } + internal override void ExportDecapsulationKey(Span destination) { Debug.Assert(_x25519 is not null); From ce437ba49aa46ffe7772083570b3cb225b2b8497 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 10:27:56 -0400 Subject: [PATCH 10/42] Implement HPKE single-shot sealing Connect KEM encapsulation, the Base-mode key schedule, and AEAD encryption, and split the AEAD adapters into their own files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System.Security.Cryptography.csproj | 3 + .../HpkeImplementation.Managed.cs | 143 +++++------------- .../Cryptography/HpkeManagedAeadAdapter.cs | 44 ++++++ .../Cryptography/HpkeManagedAesAeadAdapter.cs | 40 +++++ .../HpkeManagedChaCha20Poly1305AeadAdapter.cs | 39 +++++ 5 files changed, 162 insertions(+), 107 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAeadAdapter.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.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 807700ab6e8531..aab9bb170415a1 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -2116,6 +2116,9 @@ + + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 8e1b4d0bcbf965..2949a098bdb6bb 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -1,114 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; - -#pragma warning disable CA1416 // //TODO:HPKE Call is reachable on "unsupported platform" - deal with this messy daignostic later. - namespace System.Security.Cryptography { - internal abstract class HpkeManagedAeadAdapter : IDisposable - { - internal static HpkeManagedAeadAdapter Create(HpkeSuite suite, ReadOnlySpan key) - { - Debug.Assert(suite.AeadMetadata.Nt == 16); - Debug.Assert(key.Length == suite.AeadMetadata.Nk); - - switch (suite.AeadAlgorithm) - { - case HpkeAead.AES_128_GCM: - case HpkeAead.AES_256_GCM: - return new HpkeManagedAesAeadAdapter(suite, key); - case HpkeAead.ChaCha20Poly1305: - return new HpkeManagedChaCha20Poly1305AeadAdapter(key); - default: - Debug.Fail($"Unmapped AEAD adapter algorithm {suite.AeadAlgorithm}."); - throw new CryptographicException(); - } - } - - internal abstract void Encrypt( - ReadOnlySpan plaintext, - ReadOnlySpan nonce, - ReadOnlySpan associatedData, - Span ciphertext, - Span tag); - - internal abstract void Decrypt( - ReadOnlySpan ciphertext, - ReadOnlySpan nonce, - ReadOnlySpan associatedData, - ReadOnlySpan tag, - Span plaintext); - - public abstract void Dispose(); - } - - internal sealed class HpkeManagedAesAeadAdapter : HpkeManagedAeadAdapter - { - private readonly AesGcm _aes; - - internal HpkeManagedAesAeadAdapter(HpkeSuite suite, ReadOnlySpan key) - { - _aes = new AesGcm(key, suite.AeadMetadata.Nt); - } - - internal override void Encrypt( - ReadOnlySpan plaintext, - ReadOnlySpan nonce, - ReadOnlySpan associatedData, - Span ciphertext, - Span tag) - { - _aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); - } - - internal override void Decrypt( - ReadOnlySpan ciphertext, - ReadOnlySpan nonce, - ReadOnlySpan associatedData, - ReadOnlySpan tag, - Span plaintext) - { - _aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData); - } - - - public override void Dispose() => _aes.Dispose(); - } - - internal sealed class HpkeManagedChaCha20Poly1305AeadAdapter : HpkeManagedAeadAdapter - { - private readonly ChaCha20Poly1305 _chacha; - - internal HpkeManagedChaCha20Poly1305AeadAdapter(ReadOnlySpan key) - { - _chacha = new ChaCha20Poly1305(key); - } - - internal override void Encrypt( - ReadOnlySpan plaintext, - ReadOnlySpan nonce, - ReadOnlySpan associatedData, - Span ciphertext, - Span tag) - { - _chacha.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); - } - - internal override void Decrypt( - ReadOnlySpan ciphertext, - ReadOnlySpan nonce, - ReadOnlySpan associatedData, - ReadOnlySpan tag, - Span plaintext) - { - _chacha.Decrypt(nonce, ciphertext, tag, plaintext, associatedData); - } - - public override void Dispose() => _chacha.Dispose(); - } - internal sealed class HpkeImplementation : Hpke { private readonly HpkeManagedKemAdapter _kemAdapter; @@ -168,7 +62,42 @@ protected override void SealCore( ReadOnlySpan associatedData, ReadOnlySpan info) { - throw new NotImplementedException(); + const int MaxStackSecretLength = 64; + + using (CryptoPoolLease sharedSecret = CryptoPoolLease.RentConditionally( + Suite.KemMetadata.Nsecret, stackalloc byte[MaxStackSecretLength])) + using (CryptoPoolLease key = CryptoPoolLease.RentConditionally( + Suite.AeadMetadata.Nk, stackalloc byte[MaxStackSecretLength])) + using (CryptoPoolLease baseNonce = CryptoPoolLease.RentConditionally( + Suite.AeadMetadata.Nn, stackalloc byte[MaxStackSecretLength])) + using (CryptoPoolLease exporterSecret = CryptoPoolLease.RentConditionally( + Suite.KdfMetadata.Nh, stackalloc byte[MaxStackSecretLength])) + { + _kemAdapter.Encapsulate(encapsulatedSecret, sharedSecret.Span); + + HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); + kdf.DeriveSecrets( + mode: 0, + sharedSecret.Span, + info, + psk: default, + pskId: default, + key.Span, + baseNonce.Span, + exporterSecret.Span); + + using (HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key.Span)) + { + // Single-shot sealing uses sequence number zero, so the nonce is base_nonce. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 + aead.Encrypt( + plaintext, + baseNonce.Span, + associatedData, + ciphertext.Slice(0, plaintext.Length), + ciphertext.Slice(plaintext.Length)); + } + } } protected override void Dispose(bool disposing) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAeadAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAeadAdapter.cs new file mode 100644 index 00000000000000..360a4698606bae --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAeadAdapter.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal abstract class HpkeManagedAeadAdapter : IDisposable + { + internal static HpkeManagedAeadAdapter Create(HpkeSuite suite, ReadOnlySpan key) + { + Debug.Assert(suite.AeadMetadata.Nt == 16); + Debug.Assert(key.Length == suite.AeadMetadata.Nk); + + switch (suite.AeadAlgorithm) + { + case HpkeAead.AES_128_GCM: + case HpkeAead.AES_256_GCM: + return new HpkeManagedAesAeadAdapter(suite, key); + case HpkeAead.ChaCha20Poly1305: + return new HpkeManagedChaCha20Poly1305AeadAdapter(key); + default: + Debug.Fail($"Unmapped AEAD adapter algorithm {suite.AeadAlgorithm}."); + throw new CryptographicException(); + } + } + + internal abstract void Encrypt( + ReadOnlySpan plaintext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + Span ciphertext, + Span tag); + + internal abstract void Decrypt( + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan tag, + Span plaintext); + + public abstract void Dispose(); + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs new file mode 100644 index 00000000000000..9102a4150d572a --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1416 // //TODO:HPKE Call is reachable on "unsupported platform" - deal with this messy daignostic later. + +namespace System.Security.Cryptography +{ + internal sealed class HpkeManagedAesAeadAdapter : HpkeManagedAeadAdapter + { + private readonly AesGcm _aes; + + internal HpkeManagedAesAeadAdapter(HpkeSuite suite, ReadOnlySpan key) + { + _aes = new AesGcm(key, suite.AeadMetadata.Nt); + } + + internal override void Encrypt( + ReadOnlySpan plaintext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + Span ciphertext, + Span tag) + { + _aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); + } + + internal override void Decrypt( + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan tag, + Span plaintext) + { + _aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData); + } + + + public override void Dispose() => _aes.Dispose(); + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.cs new file mode 100644 index 00000000000000..102ef8b91f2b9e --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1416 // //TODO:HPKE Call is reachable on "unsupported platform" - deal with this messy daignostic later. + +namespace System.Security.Cryptography +{ + internal sealed class HpkeManagedChaCha20Poly1305AeadAdapter : HpkeManagedAeadAdapter + { + private readonly ChaCha20Poly1305 _chacha; + + internal HpkeManagedChaCha20Poly1305AeadAdapter(ReadOnlySpan key) + { + _chacha = new ChaCha20Poly1305(key); + } + + internal override void Encrypt( + ReadOnlySpan plaintext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + Span ciphertext, + Span tag) + { + _chacha.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); + } + + internal override void Decrypt( + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan tag, + Span plaintext) + { + _chacha.Decrypt(nonce, ciphertext, tag, plaintext, associatedData); + } + + public override void Dispose() => _chacha.Dispose(); + } +} From 7ea28a52466e0d04a203acea1e54008c3d0037a2 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 10:39:26 -0400 Subject: [PATCH 11/42] Use stack buffers for fixed-size HPKE intermediates Replace fixed-size crypto pool leases with bounded stack allocations and explicit secret clearing. Retain conditional pooling for variable-length KDF inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../HpkeECDiffieHellmanKemAdapter.cs | 45 +++++---- .../HpkeImplementation.Managed.cs | 38 +++++--- .../Cryptography/HpkeManagedKdfAdapter.cs | 96 ++++++++++++------- .../Cryptography/HpkeManagedKemAdapter.cs | 44 +++++---- .../HpkeX25519DiffieHellmanKemAdapter.cs | 23 +++-- 5 files changed, 147 insertions(+), 99 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index ddf9487e914e94..a6f8706b63d5de 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -77,35 +77,42 @@ internal override void DeriveKeyPair(ReadOnlySpan ikm) ReadOnlySpan order = Order; Debug.Assert(order.Length == Suite.KemMetadata.Nsk); byte[] privateKey = new byte[Suite.KemMetadata.Nsk]; + Span prkBuffer = stackalloc byte[PrkStackBufferSize]; using (PinAndClear.Track(privateKey)) - using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( - KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) { - LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); - Span counterBytes = stackalloc byte[1]; - - for (int counter = 0; counter <= byte.MaxValue; counter++) + try { - counterBytes[0] = (byte)counter; - LabeledExpand(prk.Span, "candidate"u8, counterBytes, privateKey); - // P-521 uses 0x01 here because Nsk is 66 bytes; P-256 and P-384 use 0xFF. - privateKey[0] &= _candidateBitmask; + Span prk = prkBuffer.Slice(0, KeyDerivationKdf.Nh); + LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk); + Span counterBytes = stackalloc byte[1]; - if (IsValidScalar(privateKey, order)) + for (int counter = 0; counter <= byte.MaxValue; counter++) { -#pragma warning disable CA1416 // Not supported on browser - _ecdh = ECDiffieHellman.Create(new ECParameters + counterBytes[0] = (byte)counter; + LabeledExpand(prk, "candidate"u8, counterBytes, privateKey); + // P-521 uses 0x01 here because Nsk is 66 bytes; P-256 and P-384 use 0xFF. + privateKey[0] &= _candidateBitmask; + + if (IsValidScalar(privateKey, order)) { - Curve = _curve, - D = privateKey, - }); +#pragma warning disable CA1416 // Not supported on browser + _ecdh = ECDiffieHellman.Create(new ECParameters + { + Curve = _curve, + D = privateKey, + }); #pragma warning restore CA1416 // Not supported on browser - return; + return; + } } - } - throw new CryptographicException(SR.Cryptography_HpkeKeyDerivationFailed); + throw new CryptographicException(SR.Cryptography_HpkeKeyDerivationFailed); + } + finally + { + CryptographicOperations.ZeroMemory(prkBuffer); + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 2949a098bdb6bb..77fc37a216293b 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -63,41 +63,49 @@ protected override void SealCore( ReadOnlySpan info) { const int MaxStackSecretLength = 64; + Span sharedSecretBuffer = stackalloc byte[MaxStackSecretLength]; + Span keyBuffer = stackalloc byte[MaxStackSecretLength]; + Span baseNonceBuffer = stackalloc byte[MaxStackSecretLength]; + Span exporterSecretBuffer = stackalloc byte[MaxStackSecretLength]; - using (CryptoPoolLease sharedSecret = CryptoPoolLease.RentConditionally( - Suite.KemMetadata.Nsecret, stackalloc byte[MaxStackSecretLength])) - using (CryptoPoolLease key = CryptoPoolLease.RentConditionally( - Suite.AeadMetadata.Nk, stackalloc byte[MaxStackSecretLength])) - using (CryptoPoolLease baseNonce = CryptoPoolLease.RentConditionally( - Suite.AeadMetadata.Nn, stackalloc byte[MaxStackSecretLength])) - using (CryptoPoolLease exporterSecret = CryptoPoolLease.RentConditionally( - Suite.KdfMetadata.Nh, stackalloc byte[MaxStackSecretLength])) + try { - _kemAdapter.Encapsulate(encapsulatedSecret, sharedSecret.Span); + Span sharedSecret = sharedSecretBuffer.Slice(0, Suite.KemMetadata.Nsecret); + Span key = keyBuffer.Slice(0, Suite.AeadMetadata.Nk); + Span baseNonce = baseNonceBuffer.Slice(0, Suite.AeadMetadata.Nn); + Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); + _kemAdapter.Encapsulate(encapsulatedSecret, sharedSecret); HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); kdf.DeriveSecrets( mode: 0, - sharedSecret.Span, + sharedSecret, info, psk: default, pskId: default, - key.Span, - baseNonce.Span, - exporterSecret.Span); + key, + baseNonce, + exporterSecret); - using (HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key.Span)) + using (HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key)) { // Single-shot sealing uses sequence number zero, so the nonce is base_nonce. // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 aead.Encrypt( plaintext, - baseNonce.Span, + baseNonce, associatedData, ciphertext.Slice(0, plaintext.Length), ciphertext.Slice(plaintext.Length)); } } + finally + { + CryptographicOperations.ZeroMemory(sharedSecretBuffer); + CryptographicOperations.ZeroMemory(keyBuffer); + CryptographicOperations.ZeroMemory(baseNonceBuffer); + CryptographicOperations.ZeroMemory(exporterSecretBuffer); + } } protected override void Dispose(bool disposing) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs index 0513da5ae0f241..1b181cf90931c8 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -49,13 +49,14 @@ internal void DeriveSecrets( { int secretLength = checked(key.Length + baseNonce.Length + exporterSecret.Length); const int MaxStackSecretLength = 128; + Span secretBuffer = stackalloc byte[MaxStackSecretLength]; - using (CryptoPoolLease secret = CryptoPoolLease.RentConditionally( - secretLength, stackalloc byte[MaxStackSecretLength])) + try { - Span derivedKey = secret.Span.Slice(0, key.Length); - Span derivedNonce = secret.Span.Slice(key.Length, baseNonce.Length); - Span derivedExporterSecret = secret.Span.Slice(key.Length + baseNonce.Length); + Span secret = secretBuffer.Slice(0, secretLength); + Span derivedKey = secret.Slice(0, key.Length); + Span derivedNonce = secret.Slice(key.Length, baseNonce.Length); + Span derivedExporterSecret = secret.Slice(key.Length + baseNonce.Length); DeriveSecretsCore(mode, sharedSecret, info, psk, pskId, derivedKey, derivedNonce, derivedExporterSecret); @@ -63,6 +64,10 @@ internal void DeriveSecrets( derivedNonce.CopyTo(baseNonce); derivedExporterSecret.CopyTo(exporterSecret); } + finally + { + CryptographicOperations.ZeroMemory(secretBuffer); + } } // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.3 @@ -131,20 +136,26 @@ protected override void DeriveSecretsCore( { int hashLength = Suite.KdfMetadata.Nh; const int MaxStackContextLength = 1 + 2 * SHA512.HashSizeInBytes; + Span contextBuffer = stackalloc byte[MaxStackContextLength]; + Span secretBuffer = stackalloc byte[SHA512.HashSizeInBytes]; - using (CryptoPoolLease context = CryptoPoolLease.RentConditionally( - 1 + 2 * hashLength, stackalloc byte[MaxStackContextLength])) - using (CryptoPoolLease secret = CryptoPoolLease.RentConditionally( - hashLength, stackalloc byte[SHA512.HashSizeInBytes])) + try { - context.Span[0] = mode; - LabeledExtract(ReadOnlySpan.Empty, "psk_id_hash"u8, pskId, context.Span.Slice(1, hashLength)); - LabeledExtract(ReadOnlySpan.Empty, "info_hash"u8, info, context.Span.Slice(1 + hashLength)); - LabeledExtract(sharedSecret, "secret"u8, psk, secret.Span); - - LabeledExpand(secret.Span, "key"u8, context.Span, key); - LabeledExpand(secret.Span, "base_nonce"u8, context.Span, baseNonce); - LabeledExpand(secret.Span, "exp"u8, context.Span, exporterSecret); + Span context = contextBuffer.Slice(0, 1 + 2 * hashLength); + Span secret = secretBuffer.Slice(0, hashLength); + context[0] = mode; + LabeledExtract(ReadOnlySpan.Empty, "psk_id_hash"u8, pskId, context.Slice(1, hashLength)); + LabeledExtract(ReadOnlySpan.Empty, "info_hash"u8, info, context.Slice(1 + hashLength)); + LabeledExtract(sharedSecret, "secret"u8, psk, secret); + + LabeledExpand(secret, "key"u8, context, key); + LabeledExpand(secret, "base_nonce"u8, context, baseNonce); + LabeledExpand(secret, "exp"u8, context, exporterSecret); + } + finally + { + CryptographicOperations.ZeroMemory(contextBuffer); + CryptographicOperations.ZeroMemory(secretBuffer); } } @@ -227,19 +238,27 @@ protected override void DeriveSecretsCore( secretsLength, stackalloc byte[MaxStackInputLength])) using (CryptoPoolLease context = CryptoPoolLease.RentConditionally( contextLength, stackalloc byte[MaxStackInputLength])) - using (CryptoPoolLease output = CryptoPoolLease.RentConditionally( - outputLength, stackalloc byte[MaxStackOutputLength])) { - int offset = WriteLengthPrefixed(psk, secrets.Span); - WriteLengthPrefixed(sharedSecret, secrets.Span.Slice(offset)); - context.Span[0] = mode; - offset = 1 + WriteLengthPrefixed(pskId, context.Span.Slice(1)); - WriteLengthPrefixed(info, context.Span.Slice(offset)); - - LabeledDerive(secrets.Span, "secret"u8, context.Span, output.Span); - output.Span.Slice(0, key.Length).CopyTo(key); - output.Span.Slice(key.Length, baseNonce.Length).CopyTo(baseNonce); - output.Span.Slice(key.Length + baseNonce.Length).CopyTo(exporterSecret); + Span outputBuffer = stackalloc byte[MaxStackOutputLength]; + + try + { + Span output = outputBuffer.Slice(0, outputLength); + int offset = WriteLengthPrefixed(psk, secrets.Span); + WriteLengthPrefixed(sharedSecret, secrets.Span.Slice(offset)); + context.Span[0] = mode; + offset = 1 + WriteLengthPrefixed(pskId, context.Span.Slice(1)); + WriteLengthPrefixed(info, context.Span.Slice(offset)); + + LabeledDerive(secrets.Span, "secret"u8, context.Span, output); + output.Slice(0, key.Length).CopyTo(key); + output.Slice(key.Length, baseNonce.Length).CopyTo(baseNonce); + output.Slice(key.Length + baseNonce.Length).CopyTo(exporterSecret); + } + finally + { + CryptographicOperations.ZeroMemory(outputBuffer); + } } } @@ -259,18 +278,23 @@ private void LabeledDerive( // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 int length = checked(VersionLabel.Length + SuiteId.Length + sizeof(ushort) + label.Length + sizeof(ushort)); const int MaxStackPrefixLength = 64; + Span prefixBuffer = stackalloc byte[MaxStackPrefixLength]; - using (CryptoPoolLease prefix = CryptoPoolLease.RentConditionally( - length, stackalloc byte[MaxStackPrefixLength])) + try { - VersionLabel.CopyTo(prefix.Span); + Span prefix = prefixBuffer.Slice(0, length); + VersionLabel.CopyTo(prefix); int offset = VersionLabel.Length; - SuiteId.CopyTo(prefix.Span.Slice(offset)); + SuiteId.CopyTo(prefix.Slice(offset)); offset += SuiteId.Length; - offset += WriteLengthPrefixed(label, prefix.Span.Slice(offset)); - BinaryPrimitives.WriteUInt16BigEndian(prefix.Span.Slice(offset), checked((ushort)output.Length)); + offset += WriteLengthPrefixed(label, prefix.Slice(offset)); + BinaryPrimitives.WriteUInt16BigEndian(prefix.Slice(offset), checked((ushort)output.Length)); - Derive(ikm, prefix.Span, context, output); + Derive(ikm, prefix, context, output); + } + finally + { + CryptographicOperations.ZeroMemory(prefixBuffer); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index 7f6b7b6cf9212c..5b97b2a478628c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -46,10 +46,15 @@ internal void Generate() const int MaxStackIkmSize = 64; Span ikmStack = stackalloc byte[MaxStackIkmSize]; - using (CryptoPoolLease ikm = CryptoPoolLease.RentConditionally(Suite.KemMetadata.Nsk, ikmStack)) + try { - RandomNumberGenerator.Fill(ikm.Span); - DeriveKeyPair(ikm.Span); + Span ikm = ikmStack.Slice(0, Suite.KemMetadata.Nsk); + RandomNumberGenerator.Fill(ikm); + DeriveKeyPair(ikm); + } + finally + { + CryptographicOperations.ZeroMemory(ikmStack); } } @@ -74,15 +79,16 @@ protected void ExtractAndExpand( try { - using (CryptoPoolLease context = CryptoPoolLease.Rent(contextLength, skipClear: true)) - { - // kem_context = enc || pkR. Both components are serialized public keys. - encapsulatedSecret.CopyTo(context.Span); - ExportEncapsulationKey(context.Span.Slice(encapsulatedSecret.Length)); - - LabeledExtract(ReadOnlySpan.Empty, EaePrkLabel, secretAgreement, prk); - LabeledExpand(prk, SharedSecretLabel, context.Span, sharedSecret); - } + const int MaxStackContextLength = 256; + Span contextBuffer = stackalloc byte[MaxStackContextLength]; + Span context = contextBuffer.Slice(0, contextLength); + + // kem_context = enc || pkR. Both components are serialized public keys. + encapsulatedSecret.CopyTo(context); + ExportEncapsulationKey(context.Slice(encapsulatedSecret.Length)); + + LabeledExtract(ReadOnlySpan.Empty, EaePrkLabel, secretAgreement, prk); + LabeledExpand(prk, SharedSecretLabel, context, sharedSecret); } finally { @@ -124,12 +130,12 @@ protected void LabeledExpand( ReadOnlySpan suiteId = Suite.KemMetadata.SuiteId; int labeledInfoLength = checked(sizeof(ushort) + VersionLabel.Length + suiteId.Length + label.Length + info.Length); - const int MaxStackLabeledInfoLength = 64; + const int MaxStackLabeledInfoLength = 256; + Span labeledInfoBuffer = stackalloc byte[MaxStackLabeledInfoLength]; - using (CryptoPoolLease labeledInfo = CryptoPoolLease.RentConditionally( - labeledInfoLength, stackalloc byte[MaxStackLabeledInfoLength])) + try { - Span destination = labeledInfo.Span; + Span destination = labeledInfoBuffer.Slice(0, labeledInfoLength); BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)output.Length)); int offset = sizeof(ushort); VersionLabel.CopyTo(destination.Slice(offset)); @@ -140,7 +146,11 @@ protected void LabeledExpand( offset += label.Length; info.CopyTo(destination.Slice(offset)); - HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, labeledInfo.Span); + HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, destination); + } + finally + { + CryptographicOperations.ZeroMemory(labeledInfoBuffer); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs index c5742775b1d488..6dc258159f9b31 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -24,20 +24,19 @@ internal override void DeriveKeyPair(ReadOnlySpan ikm) Debug.Assert(_x25519 is null); Span privateKey = stackalloc byte[X25519DiffieHellman.PrivateKeySizeInBytes]; + Span prkBuffer = stackalloc byte[PrkStackBufferSize]; - using (CryptoPoolLease prk = CryptoPoolLease.RentConditionally( - KeyDerivationKdf.Nh, stackalloc byte[PrkStackBufferSize])) + try { - try - { - LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk.Span); - LabeledExpand(prk.Span, "sk"u8, ReadOnlySpan.Empty, privateKey); - _x25519 = X25519DiffieHellman.ImportPrivateKey(privateKey); - } - finally - { - CryptographicOperations.ZeroMemory(privateKey); - } + Span prk = prkBuffer.Slice(0, KeyDerivationKdf.Nh); + LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk); + LabeledExpand(prk, "sk"u8, ReadOnlySpan.Empty, privateKey); + _x25519 = X25519DiffieHellman.ImportPrivateKey(privateKey); + } + finally + { + CryptographicOperations.ZeroMemory(privateKey); + CryptographicOperations.ZeroMemory(prkBuffer); } } From 576af50cda6fa66d2f064d36a08d2c026780e99d Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 10:52:28 -0400 Subject: [PATCH 12/42] Implement HPKE single-shot opening Add the approved Open APIs, DHKEM decapsulation, Base-mode authenticated decryption, and reference declarations. Cover known-answer vectors, round trips, input validation, and authentication failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 255 +++++++++++++ .../ref/System.Security.Cryptography.cs | 4 + .../src/Resources/Strings.resx | 6 + .../HpkeECDiffieHellmanKemAdapter.cs | 23 +- .../HpkeImplementation.Managed.cs | 53 +++ .../HpkeImplementation.Unsupported.cs | 16 + .../Cryptography/HpkeManagedKemAdapter.cs | 1 + .../HpkeX25519DiffieHellmanKemAdapter.cs | 17 + .../tests/HpkeTests.cs | 347 ++++++++++++++++++ 9 files changed, 721 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 1c076a021c1266..b1b7fd1ce25874 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -361,6 +361,234 @@ protected abstract void SealCore( ReadOnlySpan associatedData, ReadOnlySpan info); + /// + /// Decrypts and authenticates a single HPKE ciphertext using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The additional authenticated data, which must match the value used by the sender. + /// + /// + /// The application context, which must match the value used by the sender. + /// + /// + /// A new byte array containing the authenticated plaintext. + /// + /// + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// is shorter than bytes. + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred during decryption. + /// + /// + /// The object has already been disposed. + /// + public byte[] Open( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + ReadOnlySpan associatedData = default, + ReadOnlySpan info = default) + { + int plaintextLength = ValidateOpenInputs(encapsulatedSecret, ciphertext, info); + byte[] plaintext = new byte[plaintextLength]; + + try + { + OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info); + return plaintext; + } + catch + { + CryptographicOperations.ZeroMemory(plaintext); + throw; + } + } + + /// + /// Decrypts and authenticates a single HPKE ciphertext using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The additional authenticated data, which must match the value used by the sender, + /// or to use no additional authenticated data. + /// + /// + /// The application context, which must match the value used by the sender, + /// or to use an empty context. + /// + /// + /// A new byte array containing the authenticated plaintext. + /// + /// + /// or is . + /// + /// + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// is shorter than bytes. + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred during decryption. + /// + /// + /// The object has already been disposed. + /// + public byte[] Open( + byte[] encapsulatedSecret, + byte[] ciphertext, + byte[]? associatedData = null, + byte[]? info = null) + { + ArgumentNullException.ThrowIfNull(encapsulatedSecret); + ArgumentNullException.ThrowIfNull(ciphertext); + return Open( + new ReadOnlySpan(encapsulatedSecret), + ciphertext, + new ReadOnlySpan(associatedData), + info); + } + + /// + /// Decrypts and authenticates a single HPKE ciphertext into the provided buffer using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The buffer to receive the authenticated plaintext. + /// + /// + /// The additional authenticated data, which must match the value used by the sender. + /// + /// + /// The application context, which must match the value used by the sender. + /// + /// + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// is shorter than bytes. + /// + /// -or- + /// + /// The length of is not exactly the length of + /// minus . + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred during decryption. + /// + /// + /// The object has already been disposed. + /// + public void Open( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData = default, + ReadOnlySpan info = default) + { + int plaintextLength = ValidateOpenInputs(encapsulatedSecret, ciphertext, info); + + if (plaintext.Length != plaintextLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, plaintextLength), + nameof(plaintext)); + } + + OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info); + } + + /// + /// When overridden in a derived class, decrypts and authenticates a single HPKE ciphertext using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The buffer to receive the authenticated plaintext. + /// + /// + /// The additional authenticated data. + /// + /// + /// The application context. + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred during decryption. + /// + /// + /// The calling method has verified that this instance is not disposed, the input and output lengths + /// are valid for , and satisfies the KDF's length limit. + /// Implementations must fill the entire plaintext buffer on success and must not leave + /// unauthenticated plaintext in the buffer when authentication fails. + /// + protected abstract void OpenCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData, + ReadOnlySpan info); /// /// Releases all resources used by the class. @@ -405,6 +633,33 @@ private void ThrowIfInfoExceedsLimit(ReadOnlySpan info) } } + private int ValidateOpenInputs( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + ReadOnlySpan info) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeEncapsulatedSecretLength, Suite.EncapsulatedSecretSizeInBytes), + nameof(encapsulatedSecret)); + } + + int tagSize = Suite.AeadTagSizeInBytes; + + if (ciphertext.Length < tagSize) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeCiphertextTooShort, tagSize), + nameof(ciphertext)); + } + + return ciphertext.Length - tagSize; + } + private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); } } 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 47579a31670f75..d8f7af969f86de 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1905,6 +1905,10 @@ public void ExportEncapsulationKey(System.Span destination) { } protected abstract void ExportEncapsulationKeyCore(System.Span destination); public static System.Security.Cryptography.Hpke GenerateKey(System.Security.Cryptography.HpkeSuite suite) { throw null; } public static bool IsSupported(System.Security.Cryptography.HpkeSuite suite) { throw null; } + public byte[] Open(byte[] encapsulatedSecret, byte[] ciphertext, byte[]? associatedData = null, byte[]? info = null) { throw null; } + public byte[] Open(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan), System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + public void Open(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan ciphertext, System.Span plaintext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan), System.ReadOnlySpan info = default(System.ReadOnlySpan)) { } + protected abstract void OpenCore(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan ciphertext, System.Span plaintext, System.ReadOnlySpan associatedData, System.ReadOnlySpan info); public void Seal(byte[] plaintext, out byte[] encapsulatedSecret, out byte[] ciphertext, byte[]? associatedData = null, byte[]? info = null) { throw null; } public void Seal(System.ReadOnlySpan plaintext, out byte[] encapsulatedSecret, out byte[] ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan), System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } public void Seal(System.ReadOnlySpan plaintext, System.Span encapsulatedSecret, System.Span ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan), System.ReadOnlySpan info = default(System.ReadOnlySpan)) { } diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 67afcbd9b2b789..61d9f22ab67ec0 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -147,6 +147,12 @@ The specified private seed is not the correct length for the ML-KEM algorithm. + + The ciphertext must be at least {0} bytes long to contain the authentication tag. + + + The encapsulated secret must be exactly {0} bytes long. + The specified info exceeds the maximum length of {0} bytes. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index a6f8706b63d5de..04aabb0659a982 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -52,7 +52,11 @@ internal HpkeECDiffieHellmanKemAdapter(HpkeSuite suite) : base(suite) internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) { Debug.Assert(_ecdh is null); + _ecdh = CreateFromEncapsulationKey(encapsulationKey); + } + private ECDiffieHellman CreateFromEncapsulationKey(ReadOnlySpan encapsulationKey) + { if (encapsulationKey.Length != Suite.EncapsulationKeySizeInBytes) { throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); @@ -66,7 +70,7 @@ internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKe #pragma warning disable CA1416 // Not supported on browser parameters.Curve = _curve; - _ecdh = ECDiffieHellman.Create(parameters); + return ECDiffieHellman.Create(parameters); #pragma warning restore CA1416 // Not supported on browser } @@ -136,6 +140,23 @@ internal override void Encapsulate(Span encapsulatedSecret, Span sha } } + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.5 + internal override void Decapsulate(ReadOnlySpan encapsulatedSecret, Span sharedSecret) + { + Debug.Assert(_ecdh is not null); + + using (ECDiffieHellman ephemeral = CreateFromEncapsulationKey(encapsulatedSecret)) + using (ECDiffieHellmanPublicKey ephemeralPublicKey = ephemeral.PublicKey) + { + byte[] secretAgreement = _ecdh.DeriveRawSecretAgreement(ephemeralPublicKey); + + using (PinAndClear.Track(secretAgreement)) + { + ExtractAndExpand(secretAgreement, encapsulatedSecret, sharedSecret); + } + } + } + internal override void ExportDecapsulationKey(Span destination) { Debug.Assert(_ecdh is not null); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 77fc37a216293b..e1aba64e9304d7 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -108,6 +108,59 @@ protected override void SealCore( } } + protected override void OpenCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData, + ReadOnlySpan info) + { + const int MaxStackSecretLength = 64; + Span sharedSecretBuffer = stackalloc byte[MaxStackSecretLength]; + Span keyBuffer = stackalloc byte[MaxStackSecretLength]; + Span baseNonceBuffer = stackalloc byte[MaxStackSecretLength]; + Span exporterSecretBuffer = stackalloc byte[MaxStackSecretLength]; + + try + { + Span sharedSecret = sharedSecretBuffer.Slice(0, Suite.KemMetadata.Nsecret); + Span key = keyBuffer.Slice(0, Suite.AeadMetadata.Nk); + Span baseNonce = baseNonceBuffer.Slice(0, Suite.AeadMetadata.Nn); + Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); + _kemAdapter.Decapsulate(encapsulatedSecret, sharedSecret); + + HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); + kdf.DeriveSecrets( + mode: 0, + sharedSecret, + info, + psk: default, + pskId: default, + key, + baseNonce, + exporterSecret); + + using (HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key)) + { + // Single-shot opening uses sequence number zero, so the nonce is base_nonce. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 + aead.Decrypt( + ciphertext.Slice(0, plaintext.Length), + baseNonce, + associatedData, + ciphertext.Slice(plaintext.Length), + plaintext); + } + } + finally + { + CryptographicOperations.ZeroMemory(sharedSecretBuffer); + CryptographicOperations.ZeroMemory(keyBuffer); + CryptographicOperations.ZeroMemory(baseNonceBuffer); + CryptographicOperations.ZeroMemory(exporterSecretBuffer); + } + } + protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs index 2ce2fc5fe3defd..f902e25dd57dd5 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs @@ -62,6 +62,22 @@ protected override void SealCore( throw new CryptographicException(); } + protected override void OpenCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData, + ReadOnlySpan info) + { + _ = encapsulatedSecret; + _ = ciphertext; + _ = plaintext; + _ = associatedData; + _ = info; + Debug.Fail("Platform validation should not permit this call."); + throw new CryptographicException(); + } + protected override void Dispose(bool disposing) { Debug.Fail("Platform validation should not permit this call."); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index 5b97b2a478628c..be61b994241224 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -156,6 +156,7 @@ protected void LabeledExpand( internal abstract void DeriveKeyPair(ReadOnlySpan ikm); internal abstract void Encapsulate(Span encapsulatedSecret, Span sharedSecret); + internal abstract void Decapsulate(ReadOnlySpan encapsulatedSecret, Span sharedSecret); internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); internal abstract void ExportDecapsulationKey(Span destination); internal abstract void ExportEncapsulationKey(Span destination); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs index 6dc258159f9b31..9090ee05c24841 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -64,6 +64,23 @@ internal override void Encapsulate(Span encapsulatedSecret, Span sha } } + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.5 + internal override void Decapsulate(ReadOnlySpan encapsulatedSecret, Span sharedSecret) + { + Debug.Assert(_x25519 is not null); + Span secretAgreement = stackalloc byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + + try + { + _x25519.DeriveRawSecretAgreement(encapsulatedSecret, secretAgreement); + ExtractAndExpand(secretAgreement, encapsulatedSecret, sharedSecret); + } + finally + { + CryptographicOperations.ZeroMemory(secretAgreement); + } + } + internal override void ExportDecapsulationKey(Span destination) { Debug.Assert(_x25519 is not null); diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 1052478fa6c648..488c7fd2579b5b 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.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 Xunit; namespace System.Security.Cryptography.Tests @@ -99,6 +100,352 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv } } + // https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/b1f7cb0cdeab6906c61b3d6574e8bdfdbe1cd3fb/test-vectors.json + [Theory] + [InlineData( + HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM, + "668b37171f1072f3cf12ea8a236a45df23fc13b82af3609ad1e354f6ef817550", + "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325a" + + "c98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4", + "5ad590bb8baa577f8619db35a36311226a896e7342a6d836d8b7bcd2f20b6c7f9076ac232e3ab2523f39513434")] + [InlineData( + HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, + "a2f6e7c4d9e108e03be268a64fe73e11a320963c85375a30bfc9ec4a214c6a55", + "0404dc39344526dbfa728afba96986d575811b5af199c11f821a0e603a4d191b2554" + + "4a402f25364964b2c129cb417b3c1dab4dfc0854f3084e843f731654392726", + "949f58e87c39b3f55390b6a970de27dfac44aadc2fbc9d623dcde1a08b628c83ad07dbbee6aede7fcfbf955670")] + [InlineData( + HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, + "969bb169aa9c24a501ee9d962e96c310226d427fb6eb3fc579d9882dbc708315", + "1d38fc578d4209ea0ef3ee5f1128ac4876a9549d74dc2d2f46e75942a6188244", + "72da9627fd7eb3a8b7169c6d97419b80adefca751c6b52b39a2e084d35ce3eb4487aadaca5a9c590e0938c48b9")] + public static void Open_KnownAnswer( + HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, string ciphertextHex) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + byte[] ikm = Convert.FromHexString(ikmHex); + byte[] enc = Convert.FromHexString(encHex); + byte[] ciphertext = Convert.FromHexString(ciphertextHex); + byte[] plaintext = "Beauty is truth, truth beauty"u8.ToArray(); + byte[] associatedData = "Count-0"u8.ToArray(); + byte[] info = "Ode on a Grecian Urn"u8.ToArray(); + + try + { + using (Hpke key = Hpke.DeriveKey(suite, ikm)) + { + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); + Assert.Equal(plaintext, key.Open( + new ReadOnlySpan(enc), ciphertext, new ReadOnlySpan(associatedData), info)); + + byte[] destination = new byte[plaintext.Length]; + key.Open(enc, ciphertext, destination.AsSpan(), associatedData, info); + Assert.Equal(plaintext, destination); + } + } + finally + { + CryptographicOperations.ZeroMemory(ikm); + } + } + + public static IEnumerable OpenSuiteData() + { + HpkeKem[] kems = + [ + HpkeKem.DHKEM_P256_HKDF_SHA256, + HpkeKem.DHKEM_P384_HKDF_SHA384, + HpkeKem.DHKEM_X25519_HKDF_SHA256, + ]; + + foreach (HpkeKem kem in kems) + { + foreach (HpkeKdf kdf in Enum.GetValues()) + { + foreach (HpkeAead aead in Enum.GetValues()) + { + yield return new object[] { kem, kdf, aead }; + } + } + } + } + + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void Open_Roundtrip(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + using (Hpke key = Hpke.GenerateKey(suite)) + { + foreach (int length in new[] { 0, 1, 257 }) + { + byte[] plaintext = new byte[length]; + plaintext.AsSpan().Fill(0xA7); + byte[] associatedData = length == 0 ? [] : "associated data"u8.ToArray(); + byte[] info = new byte[length == 0 ? 0 : 1024]; + info.AsSpan().Fill(0x3C); + + key.Seal(plaintext, out byte[] enc, out byte[] ciphertext, associatedData, info); + byte[] originalEnc = (byte[])enc.Clone(); + byte[] originalCiphertext = (byte[])ciphertext.Clone(); + + Assert.Equal(plaintext, key.Open( + enc, ciphertext, length == 0 ? null : associatedData, length == 0 ? null : info)); + Assert.Equal(plaintext, key.Open( + new ReadOnlySpan(enc), ciphertext, new ReadOnlySpan(associatedData), info)); + + byte[] destination = new byte[length + 2]; + destination.AsSpan().Fill(0xA5); + key.Open(enc, ciphertext, destination.AsSpan(1, length), associatedData, info); + AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); + Assert.Equal(originalEnc, enc); + Assert.Equal(originalCiphertext, ciphertext); + } + } + } + + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void Open_AuthenticationFailure(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + using (Hpke key = Hpke.GenerateKey(suite)) + using (Hpke wrongKey = Hpke.GenerateKey(suite)) + { + byte[] plaintext = "plaintext"u8.ToArray(); + byte[] associatedData = "associated data"u8.ToArray(); + byte[] info = "application context"u8.ToArray(); + key.Seal(plaintext, out byte[] enc, out byte[] ciphertext, associatedData, info); + + for (int tamper = 0; tamper < 6; tamper++) + { + Hpke recipient = key; + byte[] modifiedEnc = (byte[])enc.Clone(); + byte[] modifiedCiphertext = (byte[])ciphertext.Clone(); + byte[] modifiedAssociatedData = (byte[])associatedData.Clone(); + byte[] modifiedInfo = (byte[])info.Clone(); + + switch (tamper) + { + case 0: + modifiedCiphertext[0] ^= 1; + break; + case 1: + modifiedCiphertext[^1] ^= 1; + break; + case 2: + modifiedAssociatedData[0] ^= 1; + break; + case 3: + modifiedInfo[0] ^= 1; + break; + case 4: + modifiedEnc = wrongKey.ExportEncapsulationKey(); + break; + case 5: + recipient = wrongKey; + break; + } + + Assert.Throws(() => recipient.Open( + modifiedEnc, modifiedCiphertext, modifiedAssociatedData, modifiedInfo)); + Assert.Throws(() => recipient.Open( + new ReadOnlySpan(modifiedEnc), modifiedCiphertext, + new ReadOnlySpan(modifiedAssociatedData), modifiedInfo)); + + byte[] destination = new byte[plaintext.Length + 2]; + destination.AsSpan().Fill(0xA5); + Assert.Throws(() => recipient.Open( + modifiedEnc, modifiedCiphertext, destination.AsSpan(1, plaintext.Length), + modifiedAssociatedData, modifiedInfo)); + AssertExtensions.SequenceEqual( + new byte[plaintext.Length].AsSpan(), destination.AsSpan(1, plaintext.Length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + } + + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); + } + } + + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, 0)] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, 4)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 0)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 4)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 0)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 1)] + public static void Open_InvalidEncapsulatedSecret(HpkeKem kem, byte firstByte) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + using (Hpke key = Hpke.GenerateKey(suite)) + { + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + enc[0] = firstByte; + byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; + + Assert.ThrowsAny(() => key.Open(enc, ciphertext)); + Assert.ThrowsAny(() => key.Open(enc.AsSpan(), ciphertext)); + Assert.ThrowsAny(() => key.Open(enc, ciphertext, Span.Empty)); + } + } + + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] + public static void Open_ArgumentValidation(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (OpenValidationHpke key = new(suite)) + { + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; + byte[] plaintext = new byte[1]; + + AssertExtensions.Throws("encapsulatedSecret", () => key.Open((byte[])null, ciphertext)); + AssertExtensions.Throws("ciphertext", () => key.Open(enc, (byte[])null)); + + foreach (int length in new[] { 0, enc.Length - 1, enc.Length + 1 }) + { + byte[] invalidEnc = new byte[length]; + AssertExtensions.Throws("encapsulatedSecret", () => key.Open(invalidEnc, ciphertext)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.Open(invalidEnc.AsSpan(), ciphertext)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.Open(invalidEnc, ciphertext, plaintext.AsSpan())); + } + + foreach (int length in new[] { 0, suite.AeadTagSizeInBytes - 1 }) + { + byte[] invalidCiphertext = new byte[length]; + AssertExtensions.Throws("ciphertext", () => key.Open(enc, invalidCiphertext)); + AssertExtensions.Throws( + "ciphertext", () => key.Open(enc.AsSpan(), invalidCiphertext)); + AssertExtensions.Throws( + "ciphertext", () => key.Open(enc, invalidCiphertext, plaintext.AsSpan())); + } + + foreach (int length in new[] { 0, 2 }) + { + byte[] invalidPlaintext = new byte[length]; + AssertExtensions.Throws( + "plaintext", () => key.Open(enc, ciphertext, invalidPlaintext.AsSpan())); + } + + Assert.False(key.OpenCoreCalled); + key.Dispose(); + Assert.Throws(() => key.Open(enc, ciphertext)); + Assert.Throws(() => key.Open(enc.AsSpan(), ciphertext)); + Assert.Throws(() => key.Open(enc, ciphertext, plaintext.AsSpan())); + Assert.False(key.OpenCoreCalled); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256, 65536, true)] + [InlineData(HpkeKdf.HKDF_SHA384, 65536, true)] + [InlineData(HpkeKdf.HKDF_SHA512, 65536, true)] + [InlineData(HpkeKdf.SHAKE128, 65535, true)] + [InlineData(HpkeKdf.SHAKE128, 65536, false)] + [InlineData(HpkeKdf.SHAKE256, 65535, true)] + [InlineData(HpkeKdf.SHAKE256, 65536, false)] + public static void Open_InfoLength(HpkeKdf kdf, int infoLength, bool valid) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + + using (OpenValidationHpke key = new(suite)) + { + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; + byte[] info = new byte[infoLength]; + + if (valid) + { + Assert.Empty(key.Open(enc, ciphertext, info: info)); + Assert.Empty(key.Open(enc.AsSpan(), ciphertext, info: info)); + key.Open(enc, ciphertext, Span.Empty, info: info); + } + else + { + AssertExtensions.Throws("info", () => key.Open(enc, ciphertext, info: info)); + AssertExtensions.Throws( + "info", () => key.Open(enc.AsSpan(), ciphertext, info: info)); + AssertExtensions.Throws( + "info", () => key.Open(enc, ciphertext, Span.Empty, info: info)); + } + + Assert.Equal(valid, key.OpenCoreCalled); + } + } + + private sealed class OpenValidationHpke : Hpke + { + internal bool OpenCoreCalled { get; private set; } + + internal OpenValidationHpke(HpkeSuite suite) : base(suite) + { + } + + protected override void OpenCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData, + ReadOnlySpan info) + { + OpenCoreCalled = true; + plaintext.Clear(); + } + + protected override void ExportDecapsulationKeyCore(Span destination) => + throw new InvalidOperationException("Unexpected key export."); + + protected override void ExportEncapsulationKeyCore(Span destination) => + throw new InvalidOperationException("Unexpected key export."); + + protected override void SealCore( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData, + ReadOnlySpan info) => + throw new InvalidOperationException("Unexpected encryption."); + } + [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] From 6e25e20d67eafd03e34456ba4102739c1f675170 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 12:16:04 -0400 Subject: [PATCH 13/42] Add abstract HPKE sender and recipient contexts Add the approved multi-shot base APIs, shared exporter limits, and CreateSender wrappers with managed and unsupported backend stubs. Include reference declarations and public wrapper coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 107 +++ .../Security/Cryptography/HpkeKdfMetadata.cs | 4 + .../Security/Cryptography/HpkeRecipient.cs | 380 ++++++++++ .../Security/Cryptography/HpkeSender.cs | 341 +++++++++ .../ref/System.Security.Cryptography.cs | 35 + .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 4 + .../HpkeImplementation.Managed.cs | 3 + .../HpkeImplementation.Unsupported.cs | 3 + .../Cryptography/HpkeManagedKdfAdapter.cs | 4 +- .../tests/HpkeTests.cs | 683 +++++++++++++++++- 11 files changed, 1561 insertions(+), 6 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index b1b7fd1ce25874..8cee84177ee11e 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -590,6 +590,113 @@ protected abstract void OpenCore( ReadOnlySpan associatedData, ReadOnlySpan info); + /// + /// Creates an HPKE sender context using Base mode. + /// + /// + /// When this method returns, contains the encapsulated secret to send to the recipient. + /// This parameter is treated as uninitialized. + /// + /// + /// The application context, which must match the value used by the recipient. + /// + /// + /// A new sender context for this key's cipher suite. + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// + /// Creating a sender is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + public HpkeSender CreateSender(out byte[] encapsulatedSecret, ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes]; + HpkeSender sender = CreateSenderCore(encapsulatedSecretBuffer, info); + encapsulatedSecret = encapsulatedSecretBuffer; + return sender; + } + + /// + /// Creates an HPKE sender context using Base mode and writes the encapsulated secret into the provided buffer. + /// + /// + /// The buffer to receive the encapsulated secret to send to the recipient. + /// + /// + /// The application context, which must match the value used by the recipient. + /// + /// + /// A new sender context for this key's cipher suite. + /// + /// + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// + /// Creating a sender is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes), + nameof(encapsulatedSecret)); + } + + return CreateSenderCore(encapsulatedSecret, info); + } + + /// + /// When overridden in a derived class, creates an HPKE sender context using Base mode. + /// + /// + /// The buffer to receive the encapsulated secret. + /// + /// + /// The application context. + /// + /// + /// A new sender context for this key's cipher suite. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// + /// Creating a sender is not supported on the current platform. + /// + /// + /// The calling method has verified that this instance is not disposed, the encapsulated secret buffer + /// has the exact required length, and satisfies the KDF's length limit. + /// Implementations must fill the entire buffer and return an initialized sender for . + /// + protected abstract HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info); + /// /// Releases all resources used by the class. /// diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs index 9d6714d1d5c7eb..ba96d95a3818cf 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs @@ -11,6 +11,10 @@ internal sealed partial class HpkeKdfMetadata internal string Name { get; } internal int? MaximumInfoLength { get; } + // HKDF is limited to 255 hash blocks; HPKE encodes SHAKE output lengths in two bytes. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 + internal int MaximumExportLength => IsTwoStage ? 255 * Nh : ushort.MaxValue; + private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name) { Kdf = kdf; diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs new file mode 100644 index 00000000000000..afdf0334b5675d --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs @@ -0,0 +1,380 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + /// + /// Represents an HPKE recipient context for decrypting multiple messages and exporting secrets. + /// + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public abstract class HpkeRecipient : IDisposable + { + private bool _disposed; + + /// + /// Gets the cipher suite associated with this recipient. + /// + /// + /// The cipher suite associated with this recipient. + /// + public HpkeSuite Suite { get; } + + /// + /// Initializes a new instance of the class with the specified cipher suite. + /// + /// + /// The cipher suite associated with this recipient. + /// + /// + /// is . + /// + protected HpkeRecipient(HpkeSuite suite) + { + ArgumentNullException.ThrowIfNull(suite); + Suite = suite; + } + + /// + /// Decrypts and authenticates a message using this recipient context. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The additional authenticated data, which must match the value used by the sender. + /// + /// + /// A new byte array containing the authenticated plaintext. + /// + /// + /// is shorter than bytes. + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The recipient's message limit has been reached, or an error occurred during decryption. + /// + /// + /// The object has already been disposed. + /// + /// + /// Messages must be supplied in the same order in which the corresponding sender context encrypted them. + /// + public byte[] Open(ReadOnlySpan ciphertext, ReadOnlySpan associatedData = default) + { + ThrowIfDisposed(); + byte[] plaintext = new byte[GetPlaintextLength(ciphertext)]; + + try + { + OpenCore(ciphertext, plaintext, associatedData); + return plaintext; + } + catch + { + CryptographicOperations.ZeroMemory(plaintext); + throw; + } + } + + /// + /// Decrypts and authenticates a message using this recipient context. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The additional authenticated data, which must match the value used by the sender, + /// or to use no additional authenticated data. + /// + /// + /// A new byte array containing the authenticated plaintext. + /// + /// + /// is . + /// + /// + /// is shorter than bytes. + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The recipient's message limit has been reached, or an error occurred during decryption. + /// + /// + /// The object has already been disposed. + /// + public byte[] Open(byte[] ciphertext, byte[]? associatedData = null) + { + ArgumentNullException.ThrowIfNull(ciphertext); + return Open(new ReadOnlySpan(ciphertext), new ReadOnlySpan(associatedData)); + } + + /// + /// Decrypts and authenticates a message into the provided buffer using this recipient context. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The buffer to receive the authenticated plaintext. + /// + /// + /// The additional authenticated data, which must match the value used by the sender. + /// + /// + /// + /// is shorter than bytes. + /// + /// -or- + /// + /// The length of is not exactly the length of + /// minus . + /// + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The recipient's message limit has been reached, or an error occurred during decryption. + /// + /// + /// The object has already been disposed. + /// + public void Open( + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData = default) + { + ThrowIfDisposed(); + int plaintextLength = GetPlaintextLength(ciphertext); + + if (plaintext.Length != plaintextLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, plaintextLength), + nameof(plaintext)); + } + + OpenCore(ciphertext, plaintext, associatedData); + } + + /// + /// When overridden in a derived class, decrypts and authenticates a message using this recipient context. + /// + /// + /// The ciphertext, including its trailing authentication tag. + /// + /// + /// The buffer to receive the authenticated plaintext. + /// + /// + /// The additional authenticated data. + /// + /// + /// The authentication tag could not be verified. + /// + /// + /// The recipient's message limit has been reached, or an error occurred during decryption. + /// + /// + /// The calling method has verified that this instance is not disposed, the ciphertext contains + /// enough bytes for the authentication tag, and the plaintext buffer has the exact required length. + /// Implementations must maintain the recipient's message sequence, reject decryption when the message + /// limit is reached, and fill the entire plaintext buffer on success. Authentication failures must not + /// advance the message sequence or leave unauthenticated plaintext in the buffer. + /// + protected abstract void OpenCore( + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData); + + /// + /// Derives an exported secret from this recipient context. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The length, in bytes, of the exported secret. + /// + /// + /// A new byte array containing the exported secret. + /// + /// + /// is negative or exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The object has already been disposed. + /// + /// + /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. + /// Exporting a secret does not advance the recipient's message sequence. + /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. + /// + public byte[] Export(ReadOnlySpan exporterContext, int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + ThrowIfDisposed(); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (length > maximumLength) + { + throw new ArgumentOutOfRangeException( + nameof(length), + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength)); + } + + byte[] secret = new byte[length]; + + try + { + ExportCore(exporterContext, secret); + return secret; + } + catch + { + CryptographicOperations.ZeroMemory(secret); + throw; + } + } + + /// + /// Derives an exported secret from this recipient context. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The length, in bytes, of the exported secret. + /// + /// + /// A new byte array containing the exported secret. + /// + /// + /// is . + /// + /// + /// is negative or exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The object has already been disposed. + /// + /// + /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. + /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. + /// + public byte[] Export(byte[] exporterContext, int length) + { + ArgumentNullException.ThrowIfNull(exporterContext); + return Export(new ReadOnlySpan(exporterContext), length); + } + + /// + /// Derives an exported secret from this recipient context into the provided buffer. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The buffer to receive the exported secret. + /// + /// + /// The length of exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The object has already been disposed. + /// + /// + /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. + /// Exporting a secret does not advance the recipient's message sequence. + /// The caller is responsible for protecting the secret and clearing the buffer when no longer needed. + /// + public void Export(ReadOnlySpan exporterContext, Span destination) + { + ThrowIfDisposed(); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (destination.Length > maximumLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength), + nameof(destination)); + } + + ExportCore(exporterContext, destination); + } + + /// + /// When overridden in a derived class, derives an exported secret from this recipient context. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The buffer to receive the exported secret. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The calling method has verified that this instance is not disposed and the destination length + /// does not exceed the KDF's maximum export length. The destination may be empty. + /// Implementations must fill the entire destination on success without advancing the recipient's message sequence. + /// + protected abstract void ExportCore(ReadOnlySpan exporterContext, Span destination); + + /// + /// Releases all resources used by the class. + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + } + + /// + /// Releases the unmanaged resources used by this recipient and optionally releases its managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } + + private int GetPlaintextLength(ReadOnlySpan ciphertext) + { + int tagSize = Suite.AeadTagSizeInBytes; + + if (ciphertext.Length < tagSize) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeCiphertextTooShort, tagSize), + nameof(ciphertext)); + } + + return ciphertext.Length - tagSize; + } + + private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs new file mode 100644 index 00000000000000..f4c349ff04888d --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs @@ -0,0 +1,341 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Security.Cryptography +{ + /// + /// Represents an HPKE sender context for encrypting multiple messages and exporting secrets. + /// + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public abstract class HpkeSender : IDisposable + { + private bool _disposed; + + /// + /// Gets the cipher suite associated with this sender. + /// + /// + /// The cipher suite associated with this sender. + /// + public HpkeSuite Suite { get; } + + /// + /// Initializes a new instance of the class with the specified cipher suite. + /// + /// + /// The cipher suite associated with this sender. + /// + /// + /// is . + /// + protected HpkeSender(HpkeSuite suite) + { + ArgumentNullException.ThrowIfNull(suite); + Suite = suite; + } + + /// + /// Encrypts and authenticates a message using this sender context. + /// + /// + /// The message to encrypt. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// A new byte array containing the ciphertext followed by its authentication tag. + /// + /// + /// The ciphertext length would exceed . + /// + /// + /// The sender's message limit has been reached, or an error occurred during encryption. + /// + /// + /// The object has already been disposed. + /// + /// + /// Messages must be decrypted by the corresponding recipient context in the same order + /// in which they were encrypted. + /// + public byte[] Seal(ReadOnlySpan plaintext, ReadOnlySpan associatedData = default) + { + ThrowIfDisposed(); + byte[] ciphertext = new byte[Suite.GetCiphertextLength(plaintext.Length)]; + SealCore(plaintext, ciphertext, associatedData); + return ciphertext; + } + + /// + /// Encrypts and authenticates a message using this sender context. + /// + /// + /// The message to encrypt. + /// + /// + /// The additional data to authenticate without encrypting, + /// or to use no additional authenticated data. + /// + /// + /// A new byte array containing the ciphertext followed by its authentication tag. + /// + /// + /// is . + /// + /// + /// The ciphertext length would exceed . + /// + /// + /// The sender's message limit has been reached, or an error occurred during encryption. + /// + /// + /// The object has already been disposed. + /// + public byte[] Seal(byte[] plaintext, byte[]? associatedData = null) + { + ArgumentNullException.ThrowIfNull(plaintext); + return Seal(new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData)); + } + + /// + /// Encrypts and authenticates a message into the provided buffer using this sender context. + /// + /// + /// The message to encrypt. + /// + /// + /// The buffer to receive the ciphertext followed by its authentication tag. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// is not exactly the length returned by + /// for . + /// + /// + /// The ciphertext length would exceed . + /// + /// + /// The sender's message limit has been reached, or an error occurred during encryption. + /// + /// + /// The object has already been disposed. + /// + public void Seal( + ReadOnlySpan plaintext, + Span ciphertext, + ReadOnlySpan associatedData = default) + { + ThrowIfDisposed(); + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); + + if (ciphertext.Length != ciphertextLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, ciphertextLength), + nameof(ciphertext)); + } + + SealCore(plaintext, ciphertext, associatedData); + } + + /// + /// When overridden in a derived class, encrypts and authenticates a message using this sender context. + /// + /// + /// The message to encrypt. + /// + /// + /// The buffer to receive the ciphertext followed by its authentication tag. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// The sender's message limit has been reached, or an error occurred during encryption. + /// + /// + /// The calling method has verified that this instance is not disposed and the ciphertext buffer + /// has the exact required length. Implementations must maintain the sender's message sequence, + /// reject encryption when the message limit is reached, and fill the entire ciphertext buffer on success. + /// + protected abstract void SealCore( + ReadOnlySpan plaintext, + Span ciphertext, + ReadOnlySpan associatedData); + + /// + /// Derives an exported secret from this sender context. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The length, in bytes, of the exported secret. + /// + /// + /// A new byte array containing the exported secret. + /// + /// + /// is negative or exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The object has already been disposed. + /// + /// + /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. + /// Exporting a secret does not advance the sender's message sequence. + /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. + /// + public byte[] Export(ReadOnlySpan exporterContext, int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + ThrowIfDisposed(); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (length > maximumLength) + { + throw new ArgumentOutOfRangeException( + nameof(length), + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength)); + } + + byte[] secret = new byte[length]; + + try + { + ExportCore(exporterContext, secret); + return secret; + } + catch + { + CryptographicOperations.ZeroMemory(secret); + throw; + } + } + + /// + /// Derives an exported secret from this sender context. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The length, in bytes, of the exported secret. + /// + /// + /// A new byte array containing the exported secret. + /// + /// + /// is . + /// + /// + /// is negative or exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The object has already been disposed. + /// + /// + /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. + /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. + /// + public byte[] Export(byte[] exporterContext, int length) + { + ArgumentNullException.ThrowIfNull(exporterContext); + return Export(new ReadOnlySpan(exporterContext), length); + } + + /// + /// Derives an exported secret from this sender context into the provided buffer. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The buffer to receive the exported secret. + /// + /// + /// The length of exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The object has already been disposed. + /// + /// + /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. + /// Exporting a secret does not advance the sender's message sequence. + /// The caller is responsible for protecting the secret and clearing the buffer when no longer needed. + /// + public void Export(ReadOnlySpan exporterContext, Span destination) + { + ThrowIfDisposed(); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (destination.Length > maximumLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength), + nameof(destination)); + } + + ExportCore(exporterContext, destination); + } + + /// + /// When overridden in a derived class, derives an exported secret from this sender context. + /// + /// + /// The application context used to derive the exported secret. + /// + /// + /// The buffer to receive the exported secret. + /// + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// The calling method has verified that this instance is not disposed and the destination length + /// does not exceed the KDF's maximum export length. The destination may be empty. + /// Implementations must fill the entire destination on success without advancing the sender's message sequence. + /// + protected abstract void ExportCore(ReadOnlySpan exporterContext, Span destination); + + /// + /// Releases all resources used by the class. + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + } + + /// + /// Releases the unmanaged resources used by this sender and optionally releases its managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } + + private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); + } +} 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 d8f7af969f86de..fbf67043deb10b 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1893,6 +1893,9 @@ public abstract partial class Hpke : System.IDisposable { protected Hpke(System.Security.Cryptography.HpkeSuite suite) { } public System.Security.Cryptography.HpkeSuite Suite { get { throw null; } } + public System.Security.Cryptography.HpkeSender CreateSender(out byte[] encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + public System.Security.Cryptography.HpkeSender CreateSender(System.Span encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + protected abstract System.Security.Cryptography.HpkeSender CreateSenderCore(System.Span encapsulatedSecret, System.ReadOnlySpan info); public static System.Security.Cryptography.Hpke DeriveKey(System.Security.Cryptography.HpkeSuite suite, byte[] ikm) { throw null; } public static System.Security.Cryptography.Hpke DeriveKey(System.Security.Cryptography.HpkeSuite suite, System.ReadOnlySpan ikm) { throw null; } public void Dispose() { } @@ -1943,6 +1946,38 @@ public enum HpkeKem MLKEM1024_P384 = 81, } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public abstract partial class HpkeRecipient : System.IDisposable + { + protected HpkeRecipient(System.Security.Cryptography.HpkeSuite suite) { } + public System.Security.Cryptography.HpkeSuite Suite { get { throw null; } } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public byte[] Export(byte[] exporterContext, int length) { throw null; } + public byte[] Export(System.ReadOnlySpan exporterContext, int length) { throw null; } + public void Export(System.ReadOnlySpan exporterContext, System.Span destination) { } + protected abstract void ExportCore(System.ReadOnlySpan exporterContext, System.Span destination); + public byte[] Open(byte[] ciphertext, byte[]? associatedData = null) { throw null; } + public byte[] Open(System.ReadOnlySpan ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan)) { throw null; } + public void Open(System.ReadOnlySpan ciphertext, System.Span plaintext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan)) { } + protected abstract void OpenCore(System.ReadOnlySpan ciphertext, System.Span plaintext, System.ReadOnlySpan associatedData); + } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public abstract partial class HpkeSender : System.IDisposable + { + protected HpkeSender(System.Security.Cryptography.HpkeSuite suite) { } + public System.Security.Cryptography.HpkeSuite Suite { get { throw null; } } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public byte[] Export(byte[] exporterContext, int length) { throw null; } + public byte[] Export(System.ReadOnlySpan exporterContext, int length) { throw null; } + public void Export(System.ReadOnlySpan exporterContext, System.Span destination) { } + protected abstract void ExportCore(System.ReadOnlySpan exporterContext, System.Span destination); + public byte[] Seal(byte[] plaintext, byte[]? associatedData = null) { throw null; } + public byte[] Seal(System.ReadOnlySpan plaintext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan)) { throw null; } + public void Seal(System.ReadOnlySpan plaintext, System.Span ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan)) { } + protected abstract void SealCore(System.ReadOnlySpan plaintext, System.Span ciphertext, System.ReadOnlySpan associatedData); + } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5009", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public sealed partial class HpkeSuite : System.IEquatable { public HpkeSuite(System.Security.Cryptography.HpkeKem kem, System.Security.Cryptography.HpkeKdf kdf, System.Security.Cryptography.HpkeAead aead) { } diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 61d9f22ab67ec0..166c5017919fd3 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -153,6 +153,9 @@ The encapsulated secret must be exactly {0} bytes long. + + The exported secret length can be at most {0} bytes. + The specified info exceeds the maximum length of {0} bytes. 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 aab9bb170415a1..9c38d120a2cd32 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -418,6 +418,10 @@ Link="Common\System\Security\Cryptography\HpkeKem.cs" /> + + encapsulatedSecret, ReadOnlySpan info) => + throw new NotImplementedException(); + protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs index f902e25dd57dd5..dfdc27f04b9f34 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs @@ -78,6 +78,9 @@ protected override void OpenCore( throw new CryptographicException(); } + protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) => + throw new PlatformNotSupportedException(); + protected override void Dispose(bool disposing) { Debug.Fail("Platform validation should not permit this call."); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs index 1b181cf90931c8..03338c706bfeeb 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -78,12 +78,12 @@ internal void ExportSecret( { Debug.Assert(exporterSecret.Length == Suite.KdfMetadata.Nh); - int maximumLength = Suite.KdfMetadata.IsTwoStage ? 255 * Suite.KdfMetadata.Nh : ushort.MaxValue; + int maximumLength = Suite.KdfMetadata.MaximumExportLength; if (destination.Length > maximumLength) { throw new ArgumentException( - SR.Format(SR.Cryptography_Okm_TooLarge, maximumLength), + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength), nameof(destination)); } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 488c7fd2579b5b..0163fd239c303a 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -330,7 +330,7 @@ public static void Open_ArgumentValidation(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - using (OpenValidationHpke key = new(suite)) + using (RecordingHpke key = new(suite)) { byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; @@ -387,7 +387,7 @@ public static void Open_InfoLength(HpkeKdf kdf, int infoLength, bool valid) { HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - using (OpenValidationHpke key = new(suite)) + using (RecordingHpke key = new(suite)) { byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; @@ -412,11 +412,172 @@ public static void Open_InfoLength(HpkeKdf kdf, int infoLength, bool valid) } } - private sealed class OpenValidationHpke : Hpke + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] + [InlineData(HpkeKem.MLKEM_512)] + [InlineData(HpkeKem.MLKEM_768)] + [InlineData(HpkeKem.MLKEM_1024)] + [InlineData(HpkeKem.MLKEM768_P256)] + [InlineData(HpkeKem.MLKEM1024_P384)] + public static void CreateSender_Overloads(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite)) + { + byte[] info = [1, 2, 3]; + byte[] expected = new byte[suite.EncapsulatedSecretSizeInBytes]; + expected.AsSpan().Fill(0xD7); + + using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) + { + Assert.IsType(sender); + Assert.Same(suite, sender.Suite); + Assert.Equal(expected, enc); + Assert.Equal(info, key.LastSenderInfo); + } + + byte[] destination = new byte[expected.Length + 2]; + destination.AsSpan().Fill(0xA5); + + using (HpkeSender sender = key.CreateSender(destination.AsSpan(1, expected.Length), info)) + { + Assert.Same(suite, sender.Suite); + AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + Assert.Equal(info, key.LastSenderInfo); + } + + using (HpkeSender sender = key.CreateSender(out _)) + { + Assert.Empty(key.LastSenderInfo); + } + + using (HpkeSender sender = key.CreateSender(destination.AsSpan(1, expected.Length))) + { + Assert.Empty(key.LastSenderInfo); + } + + Assert.Equal(4, key.CreateSenderCalls); + } + } + + [Fact] + public static void CreateSender_Validation() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite)) + { + foreach (int length in new[] { 0, suite.EncapsulatedSecretSizeInBytes - 1, suite.EncapsulatedSecretSizeInBytes + 1 }) + { + byte[] destination = new byte[length]; + destination.AsSpan().Fill(0xA5); + byte[] original = (byte[])destination.Clone(); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreateSender(destination)); + Assert.Equal(original, destination); + } + + key.Dispose(); + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + Assert.Throws(() => key.CreateSender(out _)); + Assert.Throws(() => key.CreateSender(enc)); + Assert.Equal(0, key.CreateSenderCalls); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256, 65536, true)] + [InlineData(HpkeKdf.HKDF_SHA384, 65536, true)] + [InlineData(HpkeKdf.HKDF_SHA512, 65536, true)] + [InlineData(HpkeKdf.SHAKE128, 65535, true)] + [InlineData(HpkeKdf.SHAKE128, 65536, false)] + [InlineData(HpkeKdf.SHAKE256, 65535, true)] + [InlineData(HpkeKdf.SHAKE256, 65536, false)] + public static void CreateSender_InfoLength(HpkeKdf kdf, int infoLength, bool valid) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite)) + { + byte[] info = new byte[infoLength]; + info.AsSpan().Fill(0x39); + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + + if (valid) + { + using (HpkeSender sender = key.CreateSender(out _, info)) + { + Assert.Equal(info, key.LastSenderInfo); + } + + using (HpkeSender sender = key.CreateSender(enc, info)) + { + Assert.Equal(info, key.LastSenderInfo); + } + } + else + { + AssertExtensions.Throws("info", () => key.CreateSender(out _, info)); + AssertExtensions.Throws("info", () => key.CreateSender(enc, info)); + } + + Assert.Equal(valid ? 2 : 0, key.CreateSenderCalls); + } + } + + [Fact] + public static void CreateSender_CoreFailureDoesNotPublishOutput() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite) { ThrowOnCreateSender = true }) + { + byte[] original = [0xA5]; + byte[] enc = original; + Assert.Throws(() => key.CreateSender(out enc)); + Assert.Same(original, enc); + Assert.Throws(() => key.CreateSender(new byte[suite.EncapsulatedSecretSizeInBytes])); + Assert.Equal(2, key.CreateSenderCalls); + } + } + + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] + public static void CreateSender_NotImplemented(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + using (Hpke key = Hpke.GenerateKey(suite)) + { + byte[] original = [0xA5]; + byte[] enc = original; + Assert.Throws(() => key.CreateSender(out enc)); + Assert.Same(original, enc); + Assert.Throws(() => key.CreateSender(new byte[suite.EncapsulatedSecretSizeInBytes])); + } + } + + private sealed class RecordingHpke : Hpke { internal bool OpenCoreCalled { get; private set; } + internal int CreateSenderCalls { get; private set; } + internal byte[] LastSenderInfo { get; private set; } = []; + internal bool ThrowOnCreateSender { get; set; } - internal OpenValidationHpke(HpkeSuite suite) : base(suite) + internal RecordingHpke(HpkeSuite suite) : base(suite) { } @@ -431,6 +592,20 @@ protected override void OpenCore( plaintext.Clear(); } + protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) + { + CreateSenderCalls++; + LastSenderInfo = info.ToArray(); + encapsulatedSecret.Fill(0xD7); + + if (ThrowOnCreateSender) + { + throw new CryptographicException("Sender creation test failure."); + } + + return new RecordingHpkeSender(Suite); + } + protected override void ExportDecapsulationKeyCore(Span destination) => throw new InvalidOperationException("Unexpected key export."); @@ -582,5 +757,505 @@ public static void ExportEncapsulationKey_BufferAndLifetime(HpkeKem kem) AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); } } + + [Fact] + public static void Sender_ConstructorAndDisposal() + { + AssertExtensions.Throws("suite", () => new RecordingHpkeSender(null)); + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpkeSender sender = new(suite)) + { + Assert.Same(suite, sender.Suite); + sender.Dispose(); + sender.Dispose(); + Assert.Equal(1, sender.DisposeCalls); + + byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; + Assert.Throws(() => sender.Seal(Array.Empty())); + Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty)); + Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty, ciphertext.AsSpan())); + Assert.Throws(() => sender.Export(Array.Empty(), 0)); + Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, 0)); + Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, Span.Empty)); + Assert.Equal(0, sender.SealCalls); + Assert.Equal(0, sender.ExportCalls); + } + } + + [Theory] + [InlineData(HpkeAead.AES_128_GCM, 0)] + [InlineData(HpkeAead.AES_128_GCM, 5)] + [InlineData(HpkeAead.AES_256_GCM, 5)] + [InlineData(HpkeAead.ChaCha20Poly1305, 5)] + public static void Sender_Seal(HpkeAead aead, int plaintextLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, aead); + + using (RecordingHpkeSender sender = new(suite)) + { + byte[] plaintext = new byte[plaintextLength]; + plaintext.AsSpan().Fill(0x3C); + byte[] associatedData = [1, 2, 3]; + byte[] expected = new byte[suite.GetCiphertextLength(plaintextLength)]; + expected.AsSpan().Fill(0xD3); + + Assert.Equal(expected, sender.Seal(plaintext, associatedData)); + Assert.Equal(plaintext, sender.LastPlaintext); + Assert.Equal(associatedData, sender.LastAssociatedData); + + Assert.Equal(expected, sender.Seal( + new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData))); + Assert.Equal(plaintext, sender.LastPlaintext); + Assert.Equal(associatedData, sender.LastAssociatedData); + + byte[] destination = new byte[expected.Length + 2]; + destination.AsSpan().Fill(0xA5); + sender.Seal(plaintext, destination.AsSpan(1, expected.Length), associatedData); + AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + Assert.Equal(plaintext, sender.LastPlaintext); + Assert.Equal(associatedData, sender.LastAssociatedData); + + Assert.Equal(expected, sender.Seal(plaintext)); + Assert.Empty(sender.LastAssociatedData); + Assert.Equal(expected, sender.Seal(plaintext.AsSpan())); + Assert.Empty(sender.LastAssociatedData); + Assert.Equal(5, sender.SealCalls); + Assert.Equal(0, sender.ExportCalls); + } + } + + [Fact] + public static void Sender_SealValidation() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpkeSender sender = new(suite)) + { + AssertExtensions.Throws("plaintext", () => sender.Seal((byte[])null)); + + foreach (int length in new[] { 0, suite.AeadTagSizeInBytes - 1, suite.AeadTagSizeInBytes + 1 }) + { + byte[] ciphertext = new byte[length]; + ciphertext.AsSpan().Fill(0xA5); + byte[] originalCiphertext = (byte[])ciphertext.Clone(); + AssertExtensions.Throws( + "ciphertext", () => sender.Seal(ReadOnlySpan.Empty, ciphertext.AsSpan())); + Assert.Equal(originalCiphertext, ciphertext); + } + + Assert.Equal(0, sender.SealCalls); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256, 8160)] + [InlineData(HpkeKdf.HKDF_SHA384, 12240)] + [InlineData(HpkeKdf.HKDF_SHA512, 16320)] + [InlineData(HpkeKdf.SHAKE128, 65535)] + [InlineData(HpkeKdf.SHAKE256, 65535)] + public static void Sender_Export(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + + using (RecordingHpkeSender sender = new(suite)) + { + // Unlike setup info, a SHAKE exporter context is not length-prefixed. + byte[] exporterContext = new byte[65536]; + exporterContext.AsSpan().Fill(0x39); + + foreach (int length in new[] { 0, 1, maximumLength }) + { + byte[] expected = new byte[length]; + expected.AsSpan().Fill(0xE7); + Assert.Equal(expected, sender.Export(exporterContext, length)); + Assert.Equal(exporterContext, sender.LastExporterContext); + Assert.Equal(expected, sender.Export(exporterContext.AsSpan(), length)); + Assert.Equal(exporterContext, sender.LastExporterContext); + + byte[] destination = new byte[length + 2]; + destination.AsSpan().Fill(0xA5); + sender.Export(exporterContext, destination.AsSpan(1, length)); + AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + Assert.Equal(exporterContext, sender.LastExporterContext); + } + + Assert.Equal(9, sender.ExportCalls); + Assert.Equal(0, sender.SealCalls); + Assert.Empty(sender.Export(Array.Empty(), 0)); + Assert.Empty(sender.LastExporterContext); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256, 8160)] + [InlineData(HpkeKdf.HKDF_SHA384, 12240)] + [InlineData(HpkeKdf.HKDF_SHA512, 16320)] + [InlineData(HpkeKdf.SHAKE128, 65535)] + [InlineData(HpkeKdf.SHAKE256, 65535)] + public static void Sender_ExportValidation(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + + using (RecordingHpkeSender sender = new(suite)) + { + AssertExtensions.Throws("exporterContext", () => sender.Export((byte[])null, 0)); + + foreach (int length in new[] { -1, int.MinValue, maximumLength + 1, int.MaxValue }) + { + AssertExtensions.Throws( + "length", () => sender.Export(Array.Empty(), length)); + AssertExtensions.Throws( + "length", () => sender.Export(ReadOnlySpan.Empty, length)); + } + + byte[] destination = new byte[maximumLength + 1]; + destination.AsSpan().Fill(0xA5); + byte[] originalDestination = (byte[])destination.Clone(); + AssertExtensions.Throws( + "destination", () => sender.Export(ReadOnlySpan.Empty, destination.AsSpan())); + Assert.Equal(originalDestination, destination); + Assert.Equal(0, sender.ExportCalls); + } + } + + [Fact] + public static void Sender_CoreFailuresPropagate() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpkeSender sender = new(suite) { ThrowOnCoreCall = true }) + { + byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; + Assert.Throws(() => sender.Seal(Array.Empty())); + Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty)); + Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty, ciphertext.AsSpan())); + Assert.Throws(() => sender.Export(Array.Empty(), 1)); + Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, 1)); + Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, new byte[1].AsSpan())); + Assert.Equal(3, sender.SealCalls); + Assert.Equal(3, sender.ExportCalls); + } + } + + [Fact] + public static void Recipient_ConstructorAndDisposal() + { + AssertExtensions.Throws("suite", () => new RecordingHpkeRecipient(null)); + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpkeRecipient recipient = new(suite)) + { + Assert.Same(suite, recipient.Suite); + recipient.Dispose(); + recipient.Dispose(); + Assert.Equal(1, recipient.DisposeCalls); + + byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; + Assert.Throws(() => recipient.Open(ciphertext)); + Assert.Throws(() => recipient.Open(ciphertext.AsSpan())); + Assert.Throws(() => recipient.Open(ciphertext, Span.Empty)); + Assert.Throws(() => recipient.Export(Array.Empty(), 0)); + Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, 0)); + Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, Span.Empty)); + Assert.Equal(0, recipient.OpenCalls); + Assert.Equal(0, recipient.ExportCalls); + } + } + + [Theory] + [InlineData(HpkeAead.AES_128_GCM, 0)] + [InlineData(HpkeAead.AES_128_GCM, 5)] + [InlineData(HpkeAead.AES_256_GCM, 5)] + [InlineData(HpkeAead.ChaCha20Poly1305, 5)] + public static void Recipient_Open(HpkeAead aead, int plaintextLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, aead); + + using (RecordingHpkeRecipient recipient = new(suite)) + { + byte[] ciphertext = new byte[suite.GetCiphertextLength(plaintextLength)]; + ciphertext.AsSpan().Fill(0x3C); + byte[] associatedData = [1, 2, 3]; + byte[] expected = new byte[plaintextLength]; + expected.AsSpan().Fill(0xD3); + + Assert.Equal(expected, recipient.Open(ciphertext, associatedData)); + Assert.Equal(ciphertext, recipient.LastCiphertext); + Assert.Equal(associatedData, recipient.LastAssociatedData); + + Assert.Equal(expected, recipient.Open( + new ReadOnlySpan(ciphertext), new ReadOnlySpan(associatedData))); + Assert.Equal(ciphertext, recipient.LastCiphertext); + Assert.Equal(associatedData, recipient.LastAssociatedData); + + byte[] destination = new byte[plaintextLength + 2]; + destination.AsSpan().Fill(0xA5); + recipient.Open(ciphertext, destination.AsSpan(1, plaintextLength), associatedData); + AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, plaintextLength)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + Assert.Equal(ciphertext, recipient.LastCiphertext); + Assert.Equal(associatedData, recipient.LastAssociatedData); + + Assert.Equal(expected, recipient.Open(ciphertext)); + Assert.Empty(recipient.LastAssociatedData); + Assert.Equal(expected, recipient.Open(ciphertext.AsSpan())); + Assert.Empty(recipient.LastAssociatedData); + Assert.Equal(5, recipient.OpenCalls); + Assert.Equal(0, recipient.ExportCalls); + } + } + + [Fact] + public static void Recipient_OpenValidation() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpkeRecipient recipient = new(suite)) + { + AssertExtensions.Throws("ciphertext", () => recipient.Open((byte[])null)); + byte[] plaintext = [0xA5]; + + foreach (int length in new[] { 0, suite.AeadTagSizeInBytes - 1 }) + { + byte[] ciphertext = new byte[length]; + AssertExtensions.Throws("ciphertext", () => recipient.Open(ciphertext)); + AssertExtensions.Throws("ciphertext", () => recipient.Open(ciphertext.AsSpan())); + AssertExtensions.Throws( + "ciphertext", () => recipient.Open(ciphertext, plaintext.AsSpan())); + Assert.Equal(0xA5, plaintext[0]); + } + + byte[] validLengthCiphertext = new byte[suite.GetCiphertextLength(1)]; + + foreach (int length in new[] { 0, 2 }) + { + byte[] destination = new byte[length]; + destination.AsSpan().Fill(0xA5); + byte[] originalDestination = (byte[])destination.Clone(); + AssertExtensions.Throws( + "plaintext", () => recipient.Open(validLengthCiphertext, destination.AsSpan())); + Assert.Equal(originalDestination, destination); + } + + Assert.Equal(0, recipient.OpenCalls); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256, 8160)] + [InlineData(HpkeKdf.HKDF_SHA384, 12240)] + [InlineData(HpkeKdf.HKDF_SHA512, 16320)] + [InlineData(HpkeKdf.SHAKE128, 65535)] + [InlineData(HpkeKdf.SHAKE256, 65535)] + public static void Recipient_Export(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + + using (RecordingHpkeRecipient recipient = new(suite)) + { + byte[] exporterContext = new byte[65536]; + exporterContext.AsSpan().Fill(0x39); + + foreach (int length in new[] { 0, 1, maximumLength }) + { + byte[] expected = new byte[length]; + expected.AsSpan().Fill(0xE7); + Assert.Equal(expected, recipient.Export(exporterContext, length)); + Assert.Equal(exporterContext, recipient.LastExporterContext); + Assert.Equal(expected, recipient.Export(exporterContext.AsSpan(), length)); + Assert.Equal(exporterContext, recipient.LastExporterContext); + + byte[] destination = new byte[length + 2]; + destination.AsSpan().Fill(0xA5); + recipient.Export(exporterContext, destination.AsSpan(1, length)); + AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + Assert.Equal(exporterContext, recipient.LastExporterContext); + } + + Assert.Equal(9, recipient.ExportCalls); + Assert.Equal(0, recipient.OpenCalls); + Assert.Empty(recipient.Export(Array.Empty(), 0)); + Assert.Empty(recipient.LastExporterContext); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256, 8160)] + [InlineData(HpkeKdf.HKDF_SHA384, 12240)] + [InlineData(HpkeKdf.HKDF_SHA512, 16320)] + [InlineData(HpkeKdf.SHAKE128, 65535)] + [InlineData(HpkeKdf.SHAKE256, 65535)] + public static void Recipient_ExportValidation(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + + using (RecordingHpkeRecipient recipient = new(suite)) + { + AssertExtensions.Throws("exporterContext", () => recipient.Export((byte[])null, 0)); + + foreach (int length in new[] { -1, int.MinValue, maximumLength + 1, int.MaxValue }) + { + AssertExtensions.Throws( + "length", () => recipient.Export(Array.Empty(), length)); + AssertExtensions.Throws( + "length", () => recipient.Export(ReadOnlySpan.Empty, length)); + } + + byte[] destination = new byte[maximumLength + 1]; + destination.AsSpan().Fill(0xA5); + byte[] originalDestination = (byte[])destination.Clone(); + AssertExtensions.Throws( + "destination", () => recipient.Export(ReadOnlySpan.Empty, destination.AsSpan())); + Assert.Equal(originalDestination, destination); + Assert.Equal(0, recipient.ExportCalls); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Recipient_CoreFailuresPropagate(bool authenticationFailure) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpkeRecipient recipient = new(suite) + { + ThrowOnCoreCall = true, + AuthenticationFailure = authenticationFailure, + }) + { + Type expectedException = authenticationFailure + ? typeof(AuthenticationTagMismatchException) + : typeof(CryptographicException); + byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; + byte[] plaintext = [0xA5, 0xA5, 0xA5]; + + Assert.Throws(expectedException, () => recipient.Open(ciphertext)); + Assert.Throws(expectedException, () => recipient.Open(ciphertext.AsSpan())); + Assert.Throws(expectedException, () => recipient.Open(ciphertext, plaintext.AsSpan(1, 1))); + Assert.Equal(new byte[] { 0xA5, 0, 0xA5 }, plaintext); + Assert.Throws(() => recipient.Export(Array.Empty(), 1)); + Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, 1)); + Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, new byte[1].AsSpan())); + Assert.Equal(3, recipient.OpenCalls); + Assert.Equal(3, recipient.ExportCalls); + } + } + + private sealed class RecordingHpkeRecipient : HpkeRecipient + { + internal int OpenCalls { get; private set; } + internal int ExportCalls { get; private set; } + internal int DisposeCalls { get; private set; } + internal byte[] LastCiphertext { get; private set; } = []; + internal byte[] LastAssociatedData { get; private set; } = []; + internal byte[] LastExporterContext { get; private set; } = []; + internal bool ThrowOnCoreCall { get; set; } + internal bool AuthenticationFailure { get; set; } + + internal RecordingHpkeRecipient(HpkeSuite suite) : base(suite) + { + } + + protected override void OpenCore( + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData) + { + OpenCalls++; + LastCiphertext = ciphertext.ToArray(); + LastAssociatedData = associatedData.ToArray(); + plaintext.Fill(0xD3); + + if (ThrowOnCoreCall) + { + plaintext.Clear(); + + if (AuthenticationFailure) + { + throw new AuthenticationTagMismatchException("Recipient test authentication failure."); + } + + throw new CryptographicException("Recipient test failure."); + } + } + + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) + { + ExportCalls++; + LastExporterContext = exporterContext.ToArray(); + destination.Fill(0xE7); + + if (ThrowOnCoreCall) + { + throw new CryptographicException("Recipient test failure."); + } + } + + protected override void Dispose(bool disposing) + { + Assert.True(disposing); + DisposeCalls++; + base.Dispose(disposing); + } + } + + private sealed class RecordingHpkeSender : HpkeSender + { + internal int SealCalls { get; private set; } + internal int ExportCalls { get; private set; } + internal int DisposeCalls { get; private set; } + internal byte[] LastPlaintext { get; private set; } = []; + internal byte[] LastAssociatedData { get; private set; } = []; + internal byte[] LastExporterContext { get; private set; } = []; + internal bool ThrowOnCoreCall { get; set; } + + internal RecordingHpkeSender(HpkeSuite suite) : base(suite) + { + } + + protected override void SealCore( + ReadOnlySpan plaintext, + Span ciphertext, + ReadOnlySpan associatedData) + { + SealCalls++; + LastPlaintext = plaintext.ToArray(); + LastAssociatedData = associatedData.ToArray(); + ciphertext.Fill(0xD3); + + if (ThrowOnCoreCall) + { + throw new CryptographicException("Sender test failure."); + } + } + + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) + { + ExportCalls++; + LastExporterContext = exporterContext.ToArray(); + destination.Fill(0xE7); + + if (ThrowOnCoreCall) + { + throw new CryptographicException("Sender test failure."); + } + } + + protected override void Dispose(bool disposing) + { + Assert.True(disposing); + DisposeCalls++; + base.Dispose(disposing); + } + } } } From 1b0907fb2034b63bce9ddb67b66b7ef6dbad9bfc Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 12:52:12 -0400 Subject: [PATCH 14/42] Implement stateful HPKE sender creation and sealing Initialize sender contexts with fresh encapsulation and Base-mode secrets. Add sequence-derived nonces, exhaustion checks, AEAD ownership and cleanup, and public factory and lifetime coverage. Leave secret export unimplemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/Resources/Strings.resx | 3 + .../HpkeImplementation.Managed.cs | 130 +++++++++++++++++- .../tests/HpkeTests.cs | 93 ++++++++++++- 3 files changed, 217 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 166c5017919fd3..a7dc5763ac001b 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 @@ An HPKE key pair could not be derived from the supplied input keying material. + + The HPKE message limit has been reached. + The size of the specified tag does not match the expected size of {0}. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 18bdcb171afeb7..e172f1ea945626 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.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.Buffers.Binary; +using System.Diagnostics; + namespace System.Security.Cryptography { internal sealed class HpkeImplementation : Hpke @@ -161,8 +164,53 @@ protected override void OpenCore( } } - protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) => - throw new NotImplementedException(); + protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) + { + const int MaxStackSecretLength = 64; + Span sharedSecretBuffer = stackalloc byte[MaxStackSecretLength]; + Span keyBuffer = stackalloc byte[MaxStackSecretLength]; + Span baseNonceBuffer = stackalloc byte[MaxStackSecretLength]; + Span exporterSecretBuffer = stackalloc byte[MaxStackSecretLength]; + + try + { + Span sharedSecret = sharedSecretBuffer.Slice(0, Suite.KemMetadata.Nsecret); + Span key = keyBuffer.Slice(0, Suite.AeadMetadata.Nk); + Span baseNonce = baseNonceBuffer.Slice(0, Suite.AeadMetadata.Nn); + Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); + _kemAdapter.Encapsulate(encapsulatedSecret, sharedSecret); + + HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); + kdf.DeriveSecrets( + mode: 0, + sharedSecret, + info, + psk: default, + pskId: default, + key, + baseNonce, + exporterSecret); + + HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key); + + try + { + return new HpkeSenderImplementation(Suite, aead, baseNonce); + } + catch + { + aead.Dispose(); + throw; + } + } + finally + { + CryptographicOperations.ZeroMemory(sharedSecretBuffer); + CryptographicOperations.ZeroMemory(keyBuffer); + CryptographicOperations.ZeroMemory(baseNonceBuffer); + CryptographicOperations.ZeroMemory(exporterSecretBuffer); + } + } protected override void Dispose(bool disposing) { @@ -174,4 +222,82 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } + + internal sealed class HpkeSenderImplementation : HpkeSender + { + private readonly HpkeManagedAeadAdapter _aeadAdapter; + private readonly byte[] _baseNonce; + private ulong _sequenceNumber; + + internal HpkeSenderImplementation( + HpkeSuite suite, + HpkeManagedAeadAdapter aeadAdapter, + ReadOnlySpan baseNonce) : base(suite) + { + Debug.Assert(baseNonce.Length == suite.AeadMetadata.Nn); + Debug.Assert(baseNonce.Length >= sizeof(ulong)); + + _baseNonce = baseNonce.ToArray(); + _aeadAdapter = aeadAdapter; + } + + protected override void SealCore( + ReadOnlySpan plaintext, + Span ciphertext, + ReadOnlySpan associatedData) + { + if (_sequenceNumber == ulong.MaxValue) + { + throw new CryptographicException(SR.Cryptography_HpkeMessageLimitReached); + } + + const int MaxStackNonceLength = 12; + Span nonceBuffer = stackalloc byte[MaxStackNonceLength]; + + try + { + Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); + _baseNonce.AsSpan().CopyTo(nonce); + + // The zero-padded sequence number only affects the final eight nonce bytes. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 + Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); + BinaryPrimitives.WriteUInt64BigEndian( + sequenceBytes, + BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); + + _aeadAdapter.Encrypt( + plaintext, + nonce, + associatedData, + ciphertext.Slice(0, plaintext.Length), + ciphertext.Slice(plaintext.Length)); + _sequenceNumber++; + } + finally + { + CryptographicOperations.ZeroMemory(nonceBuffer); + } + } + + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) => + throw new NotImplementedException(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + try + { + _aeadAdapter.Dispose(); + } + finally + { + CryptographicOperations.ZeroMemory(_baseNonce); + } + } + + base.Dispose(disposing); + } + } } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 0163fd239c303a..8f7c457f112e39 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -546,11 +546,75 @@ public static void CreateSender_CoreFailureDoesNotPublishOutput() } } + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void CreateSender_Seal(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + using (Hpke key = Hpke.GenerateKey(suite)) + { + byte[] info = new byte[1024]; + info.AsSpan().Fill(0x3C); + byte[] associatedData = "associated data"u8.ToArray(); + + foreach (int length in new[] { 0, 1, 257 }) + { + byte[] plaintext = new byte[length]; + plaintext.AsSpan().Fill(0xA7); + int ciphertextLength = suite.GetCiphertextLength(length); + + using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) + { + Assert.Same(suite, sender.Suite); + Assert.Equal(suite.EncapsulatedSecretSizeInBytes, enc.Length); + AssertExtensions.Throws( + "ciphertext", () => sender.Seal(plaintext, new byte[ciphertextLength - 1].AsSpan(), associatedData)); + + byte[] ciphertext = sender.Seal(plaintext, associatedData); + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); + + byte[] nextCiphertext = sender.Seal( + new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData)); + Assert.Equal(ciphertextLength, nextCiphertext.Length); + Assert.NotEqual(ciphertext, nextCiphertext); + Assert.Throws( + () => key.Open(enc, nextCiphertext, associatedData, info)); + } + + byte[] encBuffer = new byte[suite.EncapsulatedSecretSizeInBytes + 2]; + encBuffer.AsSpan().Fill(0xA5); + + using (HpkeSender sender = key.CreateSender( + encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes), info)) + { + Assert.Same(suite, sender.Suite); + Assert.Equal(0xA5, encBuffer[0]); + Assert.Equal(0xA5, encBuffer[^1]); + byte[] enc = encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes).ToArray(); + byte[] ciphertextBuffer = new byte[ciphertextLength + 2]; + ciphertextBuffer.AsSpan().Fill(0xA5); + sender.Seal(plaintext, ciphertextBuffer.AsSpan(1, ciphertextLength), associatedData); + Assert.Equal(0xA5, ciphertextBuffer[0]); + Assert.Equal(0xA5, ciphertextBuffer[^1]); + Assert.Equal(plaintext, key.Open( + enc, ciphertextBuffer.AsSpan(1, ciphertextLength), new ReadOnlySpan(associatedData), info)); + } + } + } + } + [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void CreateSender_NotImplemented(HpkeKem kem) + public static void CreateSender_IndependentLifetime(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); @@ -560,13 +624,28 @@ public static void CreateSender_NotImplemented(HpkeKem kem) return; } - using (Hpke key = Hpke.GenerateKey(suite)) + byte[] ikm = new byte[suite.DecapsulationKeySizeInBytes]; + + try { - byte[] original = [0xA5]; - byte[] enc = original; - Assert.Throws(() => key.CreateSender(out enc)); - Assert.Same(original, enc); - Assert.Throws(() => key.CreateSender(new byte[suite.EncapsulatedSecretSizeInBytes])); + using (Hpke key = Hpke.DeriveKey(suite, ikm)) + using (Hpke peer = Hpke.DeriveKey(suite, ikm)) + using (HpkeSender first = key.CreateSender(out byte[] firstEnc)) + using (HpkeSender second = key.CreateSender(out byte[] secondEnc)) + { + key.Dispose(); + Assert.NotEqual(firstEnc, secondEnc); + byte[] plaintext = "message"u8.ToArray(); + Assert.Equal(plaintext, peer.Open(firstEnc, first.Seal(plaintext))); + + first.Dispose(); + Assert.Throws(() => first.Seal(plaintext)); + Assert.Equal(plaintext, peer.Open(secondEnc, second.Seal(plaintext))); + } + } + finally + { + CryptographicOperations.ZeroMemory(ikm); } } From c2ff6cd480a42d911a07b8189934397008589019 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 15:53:36 -0400 Subject: [PATCH 15/42] Implement stateful HPKE recipient creation and opening Add approved CreateRecipient APIs and Base-mode recipient setup with sequence-derived nonces, authentication-failure recovery, and resource cleanup. Cover published multi-message vectors, validation, ordering and independent lifetimes. Leave secret export unimplemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 130 ++++++++- .../ref/System.Security.Cryptography.cs | 3 + .../HpkeImplementation.Managed.cs | 128 +++++++++ .../HpkeImplementation.Unsupported.cs | 5 + .../tests/HpkeTests.cs | 262 +++++++++++++++++- 5 files changed, 513 insertions(+), 15 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 8cee84177ee11e..3d53f1fe038948 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -697,6 +697,118 @@ public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan /// protected abstract HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info); + /// + /// Creates an HPKE recipient context using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The application context, which must match the value used by the sender. + /// + /// + /// A new recipient context for this key's cipher suite. + /// + /// + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred while creating the recipient. + /// + /// + /// Creating a recipient is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + public HpkeRecipient CreateRecipient( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); + return CreateRecipientCore(encapsulatedSecret, info); + } + + /// + /// Creates an HPKE recipient context using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The application context, which must match the value used by the sender, + /// or to use an empty context. + /// + /// + /// A new recipient context for this key's cipher suite. + /// + /// + /// is . + /// + /// + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred while creating the recipient. + /// + /// + /// Creating a recipient is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + public HpkeRecipient CreateRecipient(byte[] encapsulatedSecret, byte[]? info = null) + { + ArgumentNullException.ThrowIfNull(encapsulatedSecret); + return CreateRecipient(new ReadOnlySpan(encapsulatedSecret), info); + } + + /// + /// When overridden in a derived class, creates an HPKE recipient context using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The application context. + /// + /// + /// A new recipient context for this key's cipher suite. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred while creating the recipient. + /// + /// + /// Creating a recipient is not supported on the current platform. + /// + /// + /// The calling method has verified that this instance is not disposed, the encapsulated secret + /// has the exact required length, and satisfies the KDF's length limit. + /// Implementations must return an initialized recipient for . + /// + protected abstract HpkeRecipient CreateRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info); + /// /// Releases all resources used by the class. /// @@ -740,20 +852,24 @@ private void ThrowIfInfoExceedsLimit(ReadOnlySpan info) } } - private int ValidateOpenInputs( - ReadOnlySpan encapsulatedSecret, - ReadOnlySpan ciphertext, - ReadOnlySpan info) + private void ThrowIfInvalidEncapsulatedSecretLength(ReadOnlySpan encapsulatedSecret) { - ThrowIfInfoExceedsLimit(info); - ThrowIfDisposed(); - if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) { throw new ArgumentException( SR.Format(SR.Argument_HpkeEncapsulatedSecretLength, Suite.EncapsulatedSecretSizeInBytes), nameof(encapsulatedSecret)); } + } + + private int ValidateOpenInputs( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + ReadOnlySpan info) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); int tagSize = Suite.AeadTagSizeInBytes; 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 fbf67043deb10b..2b00b51d234b97 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1893,6 +1893,9 @@ public abstract partial class Hpke : System.IDisposable { protected Hpke(System.Security.Cryptography.HpkeSuite suite) { } public System.Security.Cryptography.HpkeSuite Suite { get { throw null; } } + public System.Security.Cryptography.HpkeRecipient CreateRecipient(byte[] encapsulatedSecret, byte[]? info = null) { throw null; } + public System.Security.Cryptography.HpkeRecipient CreateRecipient(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + protected abstract System.Security.Cryptography.HpkeRecipient CreateRecipientCore(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan info); public System.Security.Cryptography.HpkeSender CreateSender(out byte[] encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } public System.Security.Cryptography.HpkeSender CreateSender(System.Span encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } protected abstract System.Security.Cryptography.HpkeSender CreateSenderCore(System.Span encapsulatedSecret, System.ReadOnlySpan info); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index e172f1ea945626..bef0d052c5d5d1 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -212,6 +212,56 @@ protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, Re } } + protected override HpkeRecipient CreateRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info) + { + const int MaxStackSecretLength = 64; + Span sharedSecretBuffer = stackalloc byte[MaxStackSecretLength]; + Span keyBuffer = stackalloc byte[MaxStackSecretLength]; + Span baseNonceBuffer = stackalloc byte[MaxStackSecretLength]; + Span exporterSecretBuffer = stackalloc byte[MaxStackSecretLength]; + + try + { + Span sharedSecret = sharedSecretBuffer.Slice(0, Suite.KemMetadata.Nsecret); + Span key = keyBuffer.Slice(0, Suite.AeadMetadata.Nk); + Span baseNonce = baseNonceBuffer.Slice(0, Suite.AeadMetadata.Nn); + Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); + _kemAdapter.Decapsulate(encapsulatedSecret, sharedSecret); + + HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); + kdf.DeriveSecrets( + mode: 0, + sharedSecret, + info, + psk: default, + pskId: default, + key, + baseNonce, + exporterSecret); + + HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key); + + try + { + return new HpkeRecipientImplementation(Suite, aead, baseNonce); + } + catch + { + aead.Dispose(); + throw; + } + } + finally + { + CryptographicOperations.ZeroMemory(sharedSecretBuffer); + CryptographicOperations.ZeroMemory(keyBuffer); + CryptographicOperations.ZeroMemory(baseNonceBuffer); + CryptographicOperations.ZeroMemory(exporterSecretBuffer); + } + } + protected override void Dispose(bool disposing) { if (disposing) @@ -300,4 +350,82 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } + + internal sealed class HpkeRecipientImplementation : HpkeRecipient + { + private readonly HpkeManagedAeadAdapter _aeadAdapter; + private readonly byte[] _baseNonce; + private ulong _sequenceNumber; + + internal HpkeRecipientImplementation( + HpkeSuite suite, + HpkeManagedAeadAdapter aeadAdapter, + ReadOnlySpan baseNonce) : base(suite) + { + Debug.Assert(baseNonce.Length == suite.AeadMetadata.Nn); + Debug.Assert(baseNonce.Length >= sizeof(ulong)); + + _baseNonce = baseNonce.ToArray(); + _aeadAdapter = aeadAdapter; + } + + protected override void OpenCore( + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData) + { + if (_sequenceNumber == ulong.MaxValue) + { + throw new CryptographicException(SR.Cryptography_HpkeMessageLimitReached); + } + + const int MaxStackNonceLength = 12; + Span nonceBuffer = stackalloc byte[MaxStackNonceLength]; + + try + { + Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); + _baseNonce.AsSpan().CopyTo(nonce); + + // The zero-padded sequence number only affects the final eight nonce bytes. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 + Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); + BinaryPrimitives.WriteUInt64BigEndian( + sequenceBytes, + BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); + + _aeadAdapter.Decrypt( + ciphertext.Slice(0, plaintext.Length), + nonce, + associatedData, + ciphertext.Slice(plaintext.Length), + plaintext); + _sequenceNumber++; + } + finally + { + CryptographicOperations.ZeroMemory(nonceBuffer); + } + } + + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) => + throw new NotImplementedException(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + try + { + _aeadAdapter.Dispose(); + } + finally + { + CryptographicOperations.ZeroMemory(_baseNonce); + } + } + + base.Dispose(disposing); + } + } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs index dfdc27f04b9f34..c2cc79a7b14d01 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs @@ -81,6 +81,11 @@ protected override void OpenCore( protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) => throw new PlatformNotSupportedException(); + protected override HpkeRecipient CreateRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info) => + throw new PlatformNotSupportedException(); + protected override void Dispose(bool disposing) { Debug.Fail("Platform validation should not permit this call."); diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 8f7c457f112e39..67352b33c316db 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -107,20 +107,27 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv "668b37171f1072f3cf12ea8a236a45df23fc13b82af3609ad1e354f6ef817550", "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325a" + "c98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4", - "5ad590bb8baa577f8619db35a36311226a896e7342a6d836d8b7bcd2f20b6c7f9076ac232e3ab2523f39513434")] + "5ad590bb8baa577f8619db35a36311226a896e7342a6d836d8b7bcd2f20b6c7f9076ac232e3ab2523f39513434", + "fa6f037b47fc21826b610172ca9637e82d6e5801eb31cbd3748271affd4ecb06646e0329cbdf3c3cd655b28e82", + "895cabfac50ce6c6eb02ffe6c048bf53b7f7be9a91fc559402cbc5b8dcaeb52b2ccc93e466c28fb55fed7a7fec")] [InlineData( HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, "a2f6e7c4d9e108e03be268a64fe73e11a320963c85375a30bfc9ec4a214c6a55", "0404dc39344526dbfa728afba96986d575811b5af199c11f821a0e603a4d191b2554" + "4a402f25364964b2c129cb417b3c1dab4dfc0854f3084e843f731654392726", - "949f58e87c39b3f55390b6a970de27dfac44aadc2fbc9d623dcde1a08b628c83ad07dbbee6aede7fcfbf955670")] + "949f58e87c39b3f55390b6a970de27dfac44aadc2fbc9d623dcde1a08b628c83ad07dbbee6aede7fcfbf955670", + "2b122485c81e76277b6fb7d96d85e1e2f0d41c8b6659dbbd2fad77d4a2318ceb88a350b02f7fdb242af6ee6222", + "24612f7a27e9a8a0ddffcc18e769f5e03c9ebb658071b558058172d81336d151933f3d80846596d99f67994822")] [InlineData( HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, "969bb169aa9c24a501ee9d962e96c310226d427fb6eb3fc579d9882dbc708315", "1d38fc578d4209ea0ef3ee5f1128ac4876a9549d74dc2d2f46e75942a6188244", - "72da9627fd7eb3a8b7169c6d97419b80adefca751c6b52b39a2e084d35ce3eb4487aadaca5a9c590e0938c48b9")] + "72da9627fd7eb3a8b7169c6d97419b80adefca751c6b52b39a2e084d35ce3eb4487aadaca5a9c590e0938c48b9", + "bf59c5bfd8b31c3debc4a050388f7a047a24c18559902512d1146177a320616a6b527b194c92cf91d8832db1d5", + "a80cdfe1a370a2db7e664c4acc69948d3a095be78bbfb0160f1aa0313cf0ed440154e913e5f9bc6756d7693982")] public static void Open_KnownAnswer( - HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, string ciphertextHex) + HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, + string ciphertextHex, string secondCiphertextHex, string thirdCiphertextHex) { HpkeSuite suite = new(kem, kdf, aead); @@ -148,6 +155,15 @@ public static void Open_KnownAnswer( byte[] destination = new byte[plaintext.Length]; key.Open(enc, ciphertext, destination.AsSpan(), associatedData, info); Assert.Equal(plaintext, destination); + + using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) + { + Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); + Assert.Equal(plaintext, recipient.Open( + new ReadOnlySpan(Convert.FromHexString(secondCiphertextHex)), "Count-1"u8)); + recipient.Open(Convert.FromHexString(thirdCiphertextHex), destination.AsSpan(), "Count-2"u8); + Assert.Equal(plaintext, destination); + } } } finally @@ -319,6 +335,8 @@ public static void Open_InvalidEncapsulatedSecret(HpkeKem kem, byte firstByte) Assert.ThrowsAny(() => key.Open(enc, ciphertext)); Assert.ThrowsAny(() => key.Open(enc.AsSpan(), ciphertext)); Assert.ThrowsAny(() => key.Open(enc, ciphertext, Span.Empty)); + Assert.ThrowsAny(() => key.CreateRecipient(enc)); + Assert.ThrowsAny(() => key.CreateRecipient(enc.AsSpan())); } } @@ -548,7 +566,7 @@ public static void CreateSender_CoreFailureDoesNotPublishOutput() [Theory] [MemberData(nameof(OpenSuiteData))] - public static void CreateSender_Seal(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + public static void CreateContexts_SealAndOpen(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) { HpkeSuite suite = new(kem, kdf, aead); @@ -571,14 +589,17 @@ public static void CreateSender_Seal(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) int ciphertextLength = suite.GetCiphertextLength(length); using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) + using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) { Assert.Same(suite, sender.Suite); + Assert.Same(suite, recipient.Suite); Assert.Equal(suite.EncapsulatedSecretSizeInBytes, enc.Length); AssertExtensions.Throws( "ciphertext", () => sender.Seal(plaintext, new byte[ciphertextLength - 1].AsSpan(), associatedData)); byte[] ciphertext = sender.Seal(plaintext, associatedData); Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); + Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); byte[] nextCiphertext = sender.Seal( new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData)); @@ -586,6 +607,8 @@ public static void CreateSender_Seal(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) Assert.NotEqual(ciphertext, nextCiphertext); Assert.Throws( () => key.Open(enc, nextCiphertext, associatedData, info)); + Assert.Equal(plaintext, recipient.Open( + new ReadOnlySpan(nextCiphertext), new ReadOnlySpan(associatedData))); } byte[] encBuffer = new byte[suite.EncapsulatedSecretSizeInBytes + 2]; @@ -605,6 +628,17 @@ public static void CreateSender_Seal(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) Assert.Equal(0xA5, ciphertextBuffer[^1]); Assert.Equal(plaintext, key.Open( enc, ciphertextBuffer.AsSpan(1, ciphertextLength), new ReadOnlySpan(associatedData), info)); + + using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) + { + Assert.Same(suite, recipient.Suite); + byte[] destination = new byte[length + 2]; + destination.AsSpan().Fill(0xA5); + recipient.Open(ciphertextBuffer.AsSpan(1, ciphertextLength), destination.AsSpan(1, length), associatedData); + AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + } } } } @@ -614,7 +648,7 @@ public static void CreateSender_Seal(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void CreateSender_IndependentLifetime(HpkeKem kem) + public static void CreateContexts_IndependentLifetime(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); @@ -632,15 +666,23 @@ public static void CreateSender_IndependentLifetime(HpkeKem kem) using (Hpke peer = Hpke.DeriveKey(suite, ikm)) using (HpkeSender first = key.CreateSender(out byte[] firstEnc)) using (HpkeSender second = key.CreateSender(out byte[] secondEnc)) + using (HpkeRecipient firstRecipient = key.CreateRecipient(firstEnc)) + using (HpkeRecipient secondRecipient = key.CreateRecipient(secondEnc.AsSpan())) { key.Dispose(); Assert.NotEqual(firstEnc, secondEnc); byte[] plaintext = "message"u8.ToArray(); - Assert.Equal(plaintext, peer.Open(firstEnc, first.Seal(plaintext))); + byte[] firstCiphertext = first.Seal(plaintext); + Assert.Equal(plaintext, peer.Open(firstEnc, firstCiphertext)); + Assert.Equal(plaintext, firstRecipient.Open(firstCiphertext)); first.Dispose(); + firstRecipient.Dispose(); Assert.Throws(() => first.Seal(plaintext)); - Assert.Equal(plaintext, peer.Open(secondEnc, second.Seal(plaintext))); + Assert.Throws(() => firstRecipient.Open(firstCiphertext)); + byte[] secondCiphertext = second.Seal(plaintext); + Assert.Equal(plaintext, peer.Open(secondEnc, secondCiphertext)); + Assert.Equal(plaintext, secondRecipient.Open(secondCiphertext)); } } finally @@ -649,12 +691,206 @@ public static void CreateSender_IndependentLifetime(HpkeKem kem) } } + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] + [InlineData(HpkeKem.MLKEM_512)] + [InlineData(HpkeKem.MLKEM_768)] + [InlineData(HpkeKem.MLKEM_1024)] + [InlineData(HpkeKem.MLKEM768_P256)] + [InlineData(HpkeKem.MLKEM1024_P384)] + public static void CreateRecipient_Overloads(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite)) + { + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + enc.AsSpan().Fill(0xD7); + byte[] info = [1, 2, 3]; + + using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) + { + Assert.IsType(recipient); + Assert.Same(suite, recipient.Suite); + Assert.Equal(enc, key.LastRecipientEncapsulatedSecret); + Assert.Equal(info, key.LastRecipientInfo); + } + + using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) + { + Assert.Same(suite, recipient.Suite); + Assert.Equal(enc, key.LastRecipientEncapsulatedSecret); + Assert.Equal(info, key.LastRecipientInfo); + } + + using (HpkeRecipient recipient = key.CreateRecipient(enc, info: null)) + { + Assert.Empty(key.LastRecipientInfo); + } + + using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan())) + { + Assert.Empty(key.LastRecipientInfo); + } + + Assert.Equal(4, key.CreateRecipientCalls); + } + } + + [Fact] + public static void CreateRecipient_Validation() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite)) + { + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreateRecipient((byte[])null)); + + foreach (int length in new[] { 0, suite.EncapsulatedSecretSizeInBytes - 1, suite.EncapsulatedSecretSizeInBytes + 1 }) + { + byte[] enc = new byte[length]; + AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(enc)); + AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(enc.AsSpan())); + } + + key.Dispose(); + byte[] validLengthEnc = new byte[suite.EncapsulatedSecretSizeInBytes]; + Assert.Throws(() => key.CreateRecipient(validLengthEnc)); + Assert.Throws(() => key.CreateRecipient(validLengthEnc.AsSpan())); + Assert.Equal(0, key.CreateRecipientCalls); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256, 65536, true)] + [InlineData(HpkeKdf.HKDF_SHA384, 65536, true)] + [InlineData(HpkeKdf.HKDF_SHA512, 65536, true)] + [InlineData(HpkeKdf.SHAKE128, 65535, true)] + [InlineData(HpkeKdf.SHAKE128, 65536, false)] + [InlineData(HpkeKdf.SHAKE256, 65535, true)] + [InlineData(HpkeKdf.SHAKE256, 65536, false)] + public static void CreateRecipient_InfoLength(HpkeKdf kdf, int infoLength, bool valid) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite)) + { + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + byte[] info = new byte[infoLength]; + info.AsSpan().Fill(0x39); + + if (valid) + { + using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) + { + Assert.Equal(info, key.LastRecipientInfo); + } + + using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) + { + Assert.Equal(info, key.LastRecipientInfo); + } + } + else + { + AssertExtensions.Throws("info", () => key.CreateRecipient(enc, info)); + AssertExtensions.Throws("info", () => key.CreateRecipient(enc.AsSpan(), info)); + } + + Assert.Equal(valid ? 2 : 0, key.CreateRecipientCalls); + } + } + + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void Recipient_AuthenticationFailureAndOrdering(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + byte[] info = "application context"u8.ToArray(); + byte[] associatedData = "associated data"u8.ToArray(); + byte[] first = "first"u8.ToArray(); + byte[] second = "second"u8.ToArray(); + byte[] third = "third"u8.ToArray(); + + using (Hpke key = Hpke.GenerateKey(suite)) + using (Hpke wrongKey = Hpke.GenerateKey(suite)) + using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) + using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) + { + byte[] firstCiphertext = sender.Seal(first, associatedData); + byte[] secondCiphertext = sender.Seal(second, associatedData); + byte[] thirdCiphertext = sender.Seal(third, associatedData); + + using (HpkeRecipient wrongKeyRecipient = wrongKey.CreateRecipient(enc, info)) + using (HpkeRecipient wrongInfoRecipient = key.CreateRecipient(enc, "different context"u8.ToArray())) + { + Assert.Throws(() => wrongKeyRecipient.Open(firstCiphertext, associatedData)); + Assert.Throws(() => wrongInfoRecipient.Open(firstCiphertext, associatedData)); + } + + AssertExtensions.Throws( + "plaintext", () => recipient.Open(firstCiphertext, new byte[first.Length - 1].AsSpan(), associatedData)); + + for (int tamper = 0; tamper < 3; tamper++) + { + byte[] modifiedCiphertext = (byte[])firstCiphertext.Clone(); + byte[] modifiedAssociatedData = (byte[])associatedData.Clone(); + + switch (tamper) + { + case 0: + modifiedCiphertext[0] ^= 1; + break; + case 1: + modifiedCiphertext[^1] ^= 1; + break; + case 2: + modifiedAssociatedData[0] ^= 1; + break; + } + + Assert.Throws(() => recipient.Open(modifiedCiphertext, modifiedAssociatedData)); + Assert.Throws(() => recipient.Open( + new ReadOnlySpan(modifiedCiphertext), new ReadOnlySpan(modifiedAssociatedData))); + + byte[] destination = new byte[first.Length + 2]; + destination.AsSpan().Fill(0xA5); + Assert.Throws(() => recipient.Open( + modifiedCiphertext, destination.AsSpan(1, first.Length), modifiedAssociatedData)); + AssertExtensions.SequenceEqual(new byte[first.Length].AsSpan(), destination.AsSpan(1, first.Length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + } + + Assert.Equal(first, recipient.Open(firstCiphertext, associatedData)); + Assert.Throws(() => recipient.Open(firstCiphertext, associatedData)); + Assert.Throws(() => recipient.Open(thirdCiphertext, associatedData)); + Assert.Equal(second, recipient.Open(new ReadOnlySpan(secondCiphertext), new ReadOnlySpan(associatedData))); + byte[] thirdDestination = new byte[third.Length]; + recipient.Open(thirdCiphertext, thirdDestination.AsSpan(), associatedData); + Assert.Equal(third, thirdDestination); + } + } + private sealed class RecordingHpke : Hpke { internal bool OpenCoreCalled { get; private set; } internal int CreateSenderCalls { get; private set; } internal byte[] LastSenderInfo { get; private set; } = []; internal bool ThrowOnCreateSender { get; set; } + internal int CreateRecipientCalls { get; private set; } + internal byte[] LastRecipientEncapsulatedSecret { get; private set; } = []; + internal byte[] LastRecipientInfo { get; private set; } = []; internal RecordingHpke(HpkeSuite suite) : base(suite) { @@ -685,6 +921,16 @@ protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, Re return new RecordingHpkeSender(Suite); } + protected override HpkeRecipient CreateRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info) + { + CreateRecipientCalls++; + LastRecipientEncapsulatedSecret = encapsulatedSecret.ToArray(); + LastRecipientInfo = info.ToArray(); + return new RecordingHpkeRecipient(Suite); + } + protected override void ExportDecapsulationKeyCore(Span destination) => throw new InvalidOperationException("Unexpected key export."); From cfc2d3b39c7653ddb5269c158f225acd56d5d0cd Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 16:39:30 -0400 Subject: [PATCH 16/42] Implement HPKE PSK sender and recipient modes Add approved PSK factories with a 32-byte minimum key length, nonempty identifiers, and KDF-specific input limits. Share Base and PSK context setup and cover published vectors, mode separation, authentication failures, and independent lifetimes. Leave secret export unimplemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 371 +++++++++++++++++ .../Security/Cryptography/HpkeKdfMetadata.cs | 16 +- .../ref/System.Security.Cryptography.cs | 7 + .../src/Resources/Strings.resx | 12 + .../HpkeImplementation.Managed.cs | 48 ++- .../HpkeImplementation.Unsupported.cs | 14 + .../tests/HpkeTests.cs | 377 ++++++++++++++++++ 7 files changed, 829 insertions(+), 16 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 3d53f1fe038948..c98e4cdfb3a85d 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -809,6 +809,346 @@ protected abstract HpkeRecipient CreateRecipientCore( ReadOnlySpan encapsulatedSecret, ReadOnlySpan info); + /// + /// Creates an HPKE sender context using a pre-shared key. + /// + /// + /// The pre-shared key, which must be at least 32 bytes long. + /// + /// + /// The nonempty identifier for the pre-shared key. + /// + /// + /// When this method returns, contains the encapsulated secret to send to the recipient. + /// This parameter is treated as uninitialized. + /// + /// + /// The application context, which must match the value used by the recipient. + /// + /// + /// A new sender context for this key's cipher suite. + /// + /// + /// is shorter than 32 bytes, is empty, + /// or an input exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// + /// Creating a PSK sender is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + /// + /// The sender and recipient must use the same pre-shared key and identifier. + /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy; + /// length validation alone does not guarantee this. A low-entropy password is not a suitable pre-shared key. + /// + public HpkeSender CreatePskSender( + ReadOnlySpan psk, + ReadOnlySpan pskId, + out byte[] encapsulatedSecret, + ReadOnlySpan info = default) + { + ThrowIfInvalidPskInputs(psk, pskId); + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes]; + HpkeSender sender = CreatePskSenderCore(encapsulatedSecretBuffer, info, psk, pskId); + encapsulatedSecret = encapsulatedSecretBuffer; + return sender; + } + + /// + /// Creates an HPKE sender context using a pre-shared key. + /// + /// + /// The pre-shared key, which must be at least 32 bytes long. + /// + /// + /// The nonempty identifier for the pre-shared key. + /// + /// + /// When this method returns, contains the encapsulated secret to send to the recipient. + /// This parameter is treated as uninitialized. + /// + /// + /// The application context, which must match the value used by the recipient, + /// or to use an empty context. + /// + /// + /// A new sender context for this key's cipher suite. + /// + /// + /// or is . + /// + /// + /// is shorter than 32 bytes, is empty, + /// or an input exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// + /// Creating a PSK sender is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + /// + /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy. + /// The sender and recipient must use the same pre-shared key and identifier. + /// + public HpkeSender CreatePskSender( + byte[] psk, + byte[] pskId, + out byte[] encapsulatedSecret, + byte[]? info = null) + { + ArgumentNullException.ThrowIfNull(psk); + ArgumentNullException.ThrowIfNull(pskId); + return CreatePskSender(new ReadOnlySpan(psk), pskId, out encapsulatedSecret, info); + } + + /// + /// Creates an HPKE sender context using a pre-shared key and writes the encapsulated secret into the provided buffer. + /// + /// + /// The pre-shared key, which must be at least 32 bytes long. + /// + /// + /// The nonempty identifier for the pre-shared key. + /// + /// + /// The buffer to receive the encapsulated secret to send to the recipient. + /// + /// + /// The application context, which must match the value used by the recipient. + /// + /// + /// A new sender context for this key's cipher suite. + /// + /// + /// is shorter than 32 bytes, is empty, + /// an input exceeds the maximum length supported by the cipher suite's KDF, + /// or is not exactly + /// bytes long. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// + /// Creating a PSK sender is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + /// + /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy. + /// The sender and recipient must use the same pre-shared key and identifier. + /// + public HpkeSender CreatePskSender( + ReadOnlySpan psk, + ReadOnlySpan pskId, + Span encapsulatedSecret, + ReadOnlySpan info = default) + { + ThrowIfInvalidPskInputs(psk, pskId); + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + + if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes), + nameof(encapsulatedSecret)); + } + + return CreatePskSenderCore(encapsulatedSecret, info, psk, pskId); + } + + /// + /// When overridden in a derived class, creates an HPKE sender context using a pre-shared key. + /// + /// + /// The buffer to receive the encapsulated secret. + /// + /// + /// The application context. + /// + /// + /// The pre-shared key. + /// + /// + /// The identifier for the pre-shared key. + /// + /// + /// A new sender context for this key's cipher suite. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// + /// Creating a PSK sender is not supported on the current platform. + /// + /// + /// The calling method has verified that this instance is not disposed, the encapsulated secret buffer + /// has the exact required length, the pre-shared key is at least 32 bytes long, the identifier is nonempty, + /// and all inputs satisfy the KDF's length limits. Implementations must fill the entire buffer + /// and return an initialized sender for . + /// + protected abstract HpkeSender CreatePskSenderCore( + Span encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId); + + /// + /// Creates an HPKE recipient context using a pre-shared key. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The pre-shared key, which must be at least 32 bytes long. + /// + /// + /// The nonempty identifier for the pre-shared key. + /// + /// + /// The application context, which must match the value used by the sender. + /// + /// + /// A new recipient context for this key's cipher suite. + /// + /// + /// is shorter than 32 bytes, is empty, + /// an input exceeds the maximum length supported by the cipher suite's KDF, + /// or is not exactly + /// bytes long. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred while creating the recipient. + /// + /// + /// Creating a PSK recipient is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + /// + /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy. + /// The sender and recipient must use the same pre-shared key and identifier. + /// + public HpkeRecipient CreatePskRecipient( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan psk, + ReadOnlySpan pskId, + ReadOnlySpan info = default) + { + ThrowIfInvalidPskInputs(psk, pskId); + ThrowIfInfoExceedsLimit(info); + ThrowIfDisposed(); + ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); + return CreatePskRecipientCore(encapsulatedSecret, info, psk, pskId); + } + + /// + /// Creates an HPKE recipient context using a pre-shared key. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The pre-shared key, which must be at least 32 bytes long. + /// + /// + /// The nonempty identifier for the pre-shared key. + /// + /// + /// The application context, which must match the value used by the sender, + /// or to use an empty context. + /// + /// + /// A new recipient context for this key's cipher suite. + /// + /// + /// , , or + /// is . + /// + /// + /// is shorter than 32 bytes, is empty, + /// an input exceeds the maximum length supported by the cipher suite's KDF, + /// or is not exactly + /// bytes long. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred while creating the recipient. + /// + /// + /// Creating a PSK recipient is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + /// + /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy. + /// The sender and recipient must use the same pre-shared key and identifier. + /// + public HpkeRecipient CreatePskRecipient( + byte[] encapsulatedSecret, + byte[] psk, + byte[] pskId, + byte[]? info = null) + { + ArgumentNullException.ThrowIfNull(encapsulatedSecret); + ArgumentNullException.ThrowIfNull(psk); + ArgumentNullException.ThrowIfNull(pskId); + return CreatePskRecipient(new ReadOnlySpan(encapsulatedSecret), psk, pskId, info); + } + + /// + /// When overridden in a derived class, creates an HPKE recipient context using a pre-shared key. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The application context. + /// + /// + /// The pre-shared key. + /// + /// + /// The identifier for the pre-shared key. + /// + /// + /// A new recipient context for this key's cipher suite. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred while creating the recipient. + /// + /// + /// Creating a PSK recipient is not supported on the current platform. + /// + /// + /// The calling method has verified that this instance is not disposed, the encapsulated secret + /// has the exact required length, the pre-shared key is at least 32 bytes long, the identifier is nonempty, + /// and all inputs satisfy the KDF's length limits. Implementations must return an initialized recipient + /// for . + /// + protected abstract HpkeRecipient CreatePskRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId); + /// /// Releases all resources used by the class. /// @@ -852,6 +1192,37 @@ private void ThrowIfInfoExceedsLimit(ReadOnlySpan info) } } + private void ThrowIfInvalidPskInputs(ReadOnlySpan psk, ReadOnlySpan pskId) + { + // A shorter key cannot meet HPKE's requirement for 32 bytes of PSK entropy. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1.2 + const int MinimumPskLength = 32; + + if (psk.Length < MinimumPskLength) + { + throw new ArgumentException(SR.Format(SR.Argument_HpkePskTooShort, MinimumPskLength), nameof(psk)); + } + + if (pskId.IsEmpty) + { + throw new ArgumentException(SR.Argument_HpkePskIdEmpty, nameof(pskId)); + } + + if (psk.Length > Suite.KdfMetadata.MaximumPskLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkePskTooLong, Suite.KdfMetadata.MaximumPskLength), + nameof(psk)); + } + + if (pskId.Length > Suite.KdfMetadata.MaximumPskIdLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkePskIdTooLong, Suite.KdfMetadata.MaximumPskIdLength), + nameof(pskId)); + } + } + private void ThrowIfInvalidEncapsulatedSecretLength(ReadOnlySpan encapsulatedSecret) { if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs index ba96d95a3818cf..ccccba8c8ec1d1 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs @@ -10,6 +10,8 @@ internal sealed partial class HpkeKdfMetadata internal bool IsTwoStage { get; } internal string Name { get; } internal int? MaximumInfoLength { get; } + internal int? MaximumPskLength { get; } + internal int? MaximumPskIdLength { get; } // HKDF is limited to 255 hash blocks; HPKE encodes SHAKE output lengths in two bytes. // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 @@ -24,16 +26,12 @@ private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name) if (!IsTwoStage) { - // One stage (SHAKE) uses a 16-bit integer to encode the info length. Practically that means the info is limited - // to 65,535. See CombineSecrets_OneStage. info is described as lengthPrefixed(info). - // > lengthPrefixed(x): The two-byte length of the byte string x, concatenated with x itself. - // > (lengthPrefixed(x) = concat(I2OSP(len(x), 2), x)) It is an error to call this function with an x - // > value that is more than 65535 bytes long. - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-5 - // We'll track that is the KDF having a maximum info length. - // Other KDFs have a maximum input length however they far exceed 32-bit integers which is limited by a - // Span's input limit. + // One-stage KDFs length-prefix each of these inputs with a 16-bit length. + // HKDF input limits exceed the length representable by a span. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1 MaximumInfoLength = ushort.MaxValue; + MaximumPskLength = ushort.MaxValue; + MaximumPskIdLength = ushort.MaxValue; } } 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 2b00b51d234b97..3497ff81d5006a 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1893,6 +1893,13 @@ public abstract partial class Hpke : System.IDisposable { protected Hpke(System.Security.Cryptography.HpkeSuite suite) { } public System.Security.Cryptography.HpkeSuite Suite { get { throw null; } } + public System.Security.Cryptography.HpkeRecipient CreatePskRecipient(byte[] encapsulatedSecret, byte[] psk, byte[] pskId, byte[]? info = null) { throw null; } + public System.Security.Cryptography.HpkeRecipient CreatePskRecipient(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan psk, System.ReadOnlySpan pskId, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + protected abstract System.Security.Cryptography.HpkeRecipient CreatePskRecipientCore(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan info, System.ReadOnlySpan psk, System.ReadOnlySpan pskId); + public System.Security.Cryptography.HpkeSender CreatePskSender(byte[] psk, byte[] pskId, out byte[] encapsulatedSecret, byte[]? info = null) { throw null; } + public System.Security.Cryptography.HpkeSender CreatePskSender(System.ReadOnlySpan psk, System.ReadOnlySpan pskId, out byte[] encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + public System.Security.Cryptography.HpkeSender CreatePskSender(System.ReadOnlySpan psk, System.ReadOnlySpan pskId, System.Span encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } + protected abstract System.Security.Cryptography.HpkeSender CreatePskSenderCore(System.Span encapsulatedSecret, System.ReadOnlySpan info, System.ReadOnlySpan psk, System.ReadOnlySpan pskId); public System.Security.Cryptography.HpkeRecipient CreateRecipient(byte[] encapsulatedSecret, byte[]? info = null) { throw null; } public System.Security.Cryptography.HpkeRecipient CreateRecipient(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } protected abstract System.Security.Cryptography.HpkeRecipient CreateRecipientCore(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan info); diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index a7dc5763ac001b..715518ccb06684 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -159,6 +159,18 @@ The specified info exceeds the maximum length of {0} bytes. + + The pre-shared key identifier must not be empty. + + + The pre-shared key identifier exceeds the maximum length of {0} bytes. + + + The pre-shared key exceeds the maximum length of {0} bytes. + + + The pre-shared key must be at least {0} bytes long. + The specified mu value is not the correct length for the ML-DSA algorithm. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index bef0d052c5d5d1..b0e4f809b55007 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -165,6 +165,25 @@ protected override void OpenCore( } protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) + { + return CreateSenderContext(mode: 0, encapsulatedSecret, info, psk: default, pskId: default); + } + + protected override HpkeSender CreatePskSenderCore( + Span encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) + { + return CreateSenderContext(mode: 1, encapsulatedSecret, info, psk, pskId); + } + + private HpkeSenderImplementation CreateSenderContext( + byte mode, + Span encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) { const int MaxStackSecretLength = 64; Span sharedSecretBuffer = stackalloc byte[MaxStackSecretLength]; @@ -182,11 +201,11 @@ protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, Re HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); kdf.DeriveSecrets( - mode: 0, + mode, sharedSecret, info, - psk: default, - pskId: default, + psk, + pskId, key, baseNonce, exporterSecret); @@ -214,7 +233,22 @@ protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, Re protected override HpkeRecipient CreateRecipientCore( ReadOnlySpan encapsulatedSecret, - ReadOnlySpan info) + ReadOnlySpan info) => + CreateRecipientContext(mode: 0, encapsulatedSecret, info, psk: default, pskId: default); + + protected override HpkeRecipient CreatePskRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) => + CreateRecipientContext(mode: 1, encapsulatedSecret, info, psk, pskId); + + private HpkeRecipientImplementation CreateRecipientContext( + byte mode, + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) { const int MaxStackSecretLength = 64; Span sharedSecretBuffer = stackalloc byte[MaxStackSecretLength]; @@ -232,11 +266,11 @@ protected override HpkeRecipient CreateRecipientCore( HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); kdf.DeriveSecrets( - mode: 0, + mode, sharedSecret, info, - psk: default, - pskId: default, + psk, + pskId, key, baseNonce, exporterSecret); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs index c2cc79a7b14d01..3490845b436e54 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs @@ -86,6 +86,20 @@ protected override HpkeRecipient CreateRecipientCore( ReadOnlySpan info) => throw new PlatformNotSupportedException(); + protected override HpkeSender CreatePskSenderCore( + Span encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) => + throw new PlatformNotSupportedException(); + + protected override HpkeRecipient CreatePskRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) => + throw new PlatformNotSupportedException(); + protected override void Dispose(bool disposing) { Debug.Fail("Platform validation should not permit this call."); diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 67352b33c316db..0dbfd9aaa4a352 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -882,6 +882,353 @@ public static void Recipient_AuthenticationFailureAndOrdering(HpkeKem kem, HpkeK } } + // https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/b1f7cb0cdeab6906c61b3d6574e8bdfdbe1cd3fb/test-vectors.json + [Theory] + [InlineData( + HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM, + "d42ef874c1913d9568c9405407c805baddaffd0898a00f1e84e154fa787b2429", + "04305d35563527bce037773d79a13deabed0e8e7cde61eecee403496959e89e4d0" + + "ca701726696d1485137ccb5341b3c1c7aaee90a4a02449725e744b1193b53b5f", + "90c4deb5b75318530194e4bb62f890b019b1397bbf9d0d6eb918890e1fb2be1ac2603193b60a49c2126b75d0eb", + "9e223384a3620f4a75b5a52f546b7262d8826dea18db5a365feb8b997180b22d72dc1287f7089a1073a7102c27")] + [InlineData( + HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, + "509212d2ac43d399abd9050ae3c41c030b82623da0494c0d9f8f26ac56b7e188", + "048739ebbaea3156cbd5e39b4ef41ee7e3b52c8cb4958d087112b17b778897152c" + + "7e99307095b1cee54b807077f6f5092970a27fbb57ce2835263132c75e52e7e0", + "351d83aa6f2ba77c4b9b89aa22fcb18aff3f792bb04e999de9f76f03f99e92c8d9203605cc0dcbb5eb08a9db6b", + "e9deb7896d9414ea4d3e01763e425b5bce3b43874d9121f33441f601a8f7faafb0687512f8782f23ea7aa25b4d")] + [InlineData( + HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, + "92c0e581f1b0ad231dd7346d69071afa23eb4dacdf0b868b644a20bd5121dc07", + "bc441a64a700843a8efd5cd574c20e9909c3a2ff7d35e260f9328cbb8e555d56", + "65a46e483d921343f20cba85da69976b2e0e52f450db7919f7796604977d6708d884a40d5e4fd5b820211264aa", + "02019423af9256981bc0a8a7675494efee2244faa2be5b572d9470e451ea3f831e2c08cd47bfc78d6d1f11cfb1")] + public static void Psk_KnownAnswer( + HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, + string firstCiphertextHex, string secondCiphertextHex) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + byte[] ikm = Convert.FromHexString(ikmHex); + byte[] psk = Convert.FromHexString("0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"); + byte[] pskId = "Ennyn Durin aran Moria"u8.ToArray(); + byte[] info = "Ode on a Grecian Urn"u8.ToArray(); + byte[] enc = Convert.FromHexString(encHex); + byte[] plaintext = "Beauty is truth, truth beauty"u8.ToArray(); + + try + { + using (Hpke key = Hpke.DeriveKey(suite, ikm)) + using (HpkeRecipient fromArray = key.CreatePskRecipient(enc, psk, pskId, info)) + using (HpkeRecipient fromSpan = key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)) + { + byte[] firstCiphertext = Convert.FromHexString(firstCiphertextHex); + byte[] secondCiphertext = Convert.FromHexString(secondCiphertextHex); + Assert.Equal(plaintext, fromArray.Open(firstCiphertext, "Count-0"u8.ToArray())); + Assert.Equal(plaintext, fromArray.Open(new ReadOnlySpan(secondCiphertext), "Count-1"u8)); + byte[] destination = new byte[plaintext.Length]; + fromSpan.Open(firstCiphertext, destination.AsSpan(), "Count-0"u8); + Assert.Equal(plaintext, destination); + Assert.Equal(plaintext, fromSpan.Open(secondCiphertext, "Count-1"u8.ToArray())); + } + } + finally + { + CryptographicOperations.ZeroMemory(ikm); + CryptographicOperations.ZeroMemory(psk); + } + } + + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void Psk_RoundtripAndLifetime(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + for (int overload = 0; overload < 3; overload++) + { + byte[] psk = new byte[overload == 0 ? 32 : overload == 1 ? 33 : 64]; + psk.AsSpan().Fill(0x3C); + byte[] originalPsk = (byte[])psk.Clone(); + byte[] pskId = "psk identifier"u8.ToArray(); + byte[] originalPskId = (byte[])pskId.Clone(); + byte[] info = overload == 0 ? null : new byte[1024]; + + using (Hpke key = Hpke.GenerateKey(suite)) + { + HpkeSender sender; + byte[] enc; + + if (overload == 0) + { + sender = key.CreatePskSender(psk, pskId, out enc, info); + } + else if (overload == 1) + { + sender = key.CreatePskSender(psk.AsSpan(), pskId, out enc, info); + } + else + { + byte[] buffer = new byte[suite.EncapsulatedSecretSizeInBytes + 2]; + buffer.AsSpan().Fill(0xA5); + sender = key.CreatePskSender(psk, pskId, buffer.AsSpan(1, buffer.Length - 2), info); + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[^1]); + enc = buffer.AsSpan(1, buffer.Length - 2).ToArray(); + } + + using (sender) + using (HpkeRecipient recipient = overload == 1 + ? key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info) + : key.CreatePskRecipient(enc, psk, pskId, info)) + { + Assert.Same(suite, sender.Suite); + Assert.Same(suite, recipient.Suite); + Assert.Equal(originalPsk, psk); + Assert.Equal(originalPskId, pskId); + key.Dispose(); + psk.AsSpan().Clear(); + pskId.AsSpan().Clear(); + enc.AsSpan().Clear(); + info?.AsSpan().Clear(); + + foreach (int length in new[] { 0, 1, 257 }) + { + byte[] plaintext = new byte[length]; + plaintext.AsSpan().Fill(0xA7); + byte[] aad = length == 0 ? [] : "associated data"u8.ToArray(); + byte[] ciphertext = sender.Seal(plaintext, aad); + byte[] destination = new byte[length + 2]; + destination.AsSpan().Fill(0xA5); + recipient.Open(ciphertext, destination.AsSpan(1, length), aad); + AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + } + } + } + + CryptographicOperations.ZeroMemory(originalPsk); + } + } + + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void Psk_AuthenticationAndModeSeparation(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + byte[] psk = new byte[32]; + byte[] differentPsk = new byte[32]; + differentPsk[0] = 1; + byte[] pskId = "identifier"u8.ToArray(); + byte[] info = "info"u8.ToArray(); + byte[] plaintext = "plaintext"u8.ToArray(); + byte[] aad = "associated data"u8.ToArray(); + + using (Hpke key = Hpke.GenerateKey(suite)) + using (Hpke wrongKey = Hpke.GenerateKey(suite)) + using (HpkeSender sender = key.CreatePskSender(psk, pskId, out byte[] enc, info)) + using (HpkeRecipient recipient = key.CreatePskRecipient(enc, psk, pskId, info)) + using (HpkeRecipient badPsk = key.CreatePskRecipient(enc, differentPsk, pskId, info)) + using (HpkeRecipient badId = key.CreatePskRecipient(enc, psk, "different identifier"u8.ToArray(), info)) + using (HpkeRecipient badInfo = key.CreatePskRecipient(enc, psk, pskId, "different info"u8.ToArray())) + using (HpkeRecipient badKey = wrongKey.CreatePskRecipient(enc, psk, pskId, info)) + using (HpkeRecipient baseRecipient = key.CreateRecipient(enc, info)) + using (HpkeSender baseSender = key.CreateSender(out byte[] baseEnc, info)) + using (HpkeRecipient pskRecipientForBase = key.CreatePskRecipient(baseEnc, psk, pskId, info)) + { + byte[] ciphertext = sender.Seal(plaintext, aad); + foreach (HpkeRecipient incorrect in new[] { badPsk, badId, badInfo, badKey, baseRecipient }) + { + Assert.Throws(() => incorrect.Open(ciphertext, aad)); + } + + byte[] baseCiphertext = baseSender.Seal(plaintext, aad); + Assert.Throws(() => pskRecipientForBase.Open(baseCiphertext, aad)); + byte[] tamperedCiphertext = (byte[])ciphertext.Clone(); + tamperedCiphertext[^1] ^= 1; + byte[] destination = new byte[plaintext.Length]; + destination.AsSpan().Fill(0xA5); + Assert.Throws( + () => recipient.Open(tamperedCiphertext, destination.AsSpan(), aad)); + Assert.Equal(new byte[destination.Length], destination); + Assert.Equal(plaintext, recipient.Open(ciphertext, aad)); + Assert.Equal(plaintext, recipient.Open(sender.Seal(plaintext, aad), aad)); + } + } + + [Fact] + public static void Psk_ArgumentValidation() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite)) + { + byte[] psk = new byte[32]; + byte[] pskId = [1]; + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + AssertExtensions.Throws("psk", () => key.CreatePskSender((byte[])null, pskId, out _)); + AssertExtensions.Throws("pskId", () => key.CreatePskSender(psk, (byte[])null, out _)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreatePskRecipient((byte[])null, psk, pskId)); + AssertExtensions.Throws("psk", () => key.CreatePskRecipient(enc, (byte[])null, pskId)); + AssertExtensions.Throws("pskId", () => key.CreatePskRecipient(enc, psk, (byte[])null)); + + foreach (int length in new[] { 0, 1, 31 }) + { + AssertPskArgumentException(key, "psk", new byte[length], pskId, enc, []); + } + + AssertPskArgumentException(key, "pskId", psk, [], enc, []); + foreach (int length in new[] { 0, enc.Length - 1, enc.Length + 1 }) + { + byte[] invalidEnc = new byte[length]; + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreatePskSender(psk, pskId, invalidEnc.AsSpan())); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreatePskRecipient(invalidEnc, psk, pskId)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreatePskRecipient(invalidEnc.AsSpan(), psk, pskId)); + } + + key.Dispose(); + Assert.Throws(() => key.CreatePskSender(psk, pskId, out _)); + Assert.Throws(() => key.CreatePskSender(psk.AsSpan(), pskId, out _)); + Assert.Throws(() => key.CreatePskSender(psk, pskId, enc.AsSpan())); + Assert.Throws(() => key.CreatePskRecipient(enc, psk, pskId)); + Assert.Throws(() => key.CreatePskRecipient(enc.AsSpan(), psk, pskId)); + Assert.Equal(0, key.PskCalls); + } + } + + public static IEnumerable PskInputLengthData() + { + foreach (HpkeKdf kdf in Enum.GetValues()) + { + yield return new object[] { kdf, 32, 1, 0, null }; + yield return new object[] { kdf, 33, 1, 0, null }; + yield return new object[] { kdf, 65535, 65535, 65535, null }; + bool shake = kdf is HpkeKdf.SHAKE128 or HpkeKdf.SHAKE256; + yield return new object[] { kdf, 65536, 1, 0, shake ? "psk" : null }; + yield return new object[] { kdf, 32, 65536, 0, shake ? "pskId" : null }; + yield return new object[] { kdf, 32, 1, 65536, shake ? "info" : null }; + } + } + + [Theory] + [MemberData(nameof(PskInputLengthData))] + public static void Psk_InputLengths(HpkeKdf kdf, int pskLength, int idLength, int infoLength, string invalidParameter) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + byte[] psk = new byte[pskLength]; + psk.AsSpan().Fill(0x3C); + byte[] pskId = new byte[idLength]; + pskId.AsSpan().Fill(0x1D); + byte[] info = new byte[infoLength]; + info.AsSpan().Fill(0x39); + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + + using (RecordingHpke key = new(suite)) + { + if (invalidParameter is not null) + { + AssertPskArgumentException(key, invalidParameter, psk, pskId, enc, info); + Assert.Equal(0, key.PskCalls); + return; + } + + byte[] expectedEnc = new byte[enc.Length]; + expectedEnc.AsSpan().Fill(0xD7); + using (HpkeSender sender = key.CreatePskSender(psk, pskId, out byte[] arrayEnc, info)) + { + Assert.Equal(expectedEnc, arrayEnc); + Assert.Same(suite, sender.Suite); + } + + using (HpkeSender sender = key.CreatePskSender(psk.AsSpan(), pskId, out byte[] spanEnc, info)) + { + Assert.Equal(expectedEnc, spanEnc); + } + + using (HpkeSender sender = key.CreatePskSender(psk, pskId, enc.AsSpan(), info)) + { + Assert.Equal(expectedEnc, enc); + } + + Assert.Equal(info, key.LastSenderInfo); + using (HpkeRecipient recipient = key.CreatePskRecipient(enc, psk, pskId, info)) + using (HpkeRecipient spanRecipient = key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)) + { + Assert.Same(suite, recipient.Suite); + Assert.Same(suite, spanRecipient.Suite); + Assert.Equal(enc, key.LastRecipientEncapsulatedSecret); + Assert.Equal(info, key.LastRecipientInfo); + Assert.Equal(psk, key.LastPsk); + Assert.Equal(pskId, key.LastPskId); + } + + Assert.Equal(5, key.PskCalls); + } + } + + private static void AssertPskArgumentException( + Hpke key, string parameter, byte[] psk, byte[] pskId, byte[] enc, byte[] info) + { + byte[] original = [0xA5]; + byte[] result = original; + AssertExtensions.Throws(parameter, () => key.CreatePskSender(psk, pskId, out result, info)); + Assert.Same(original, result); + AssertExtensions.Throws( + parameter, () => key.CreatePskSender(psk.AsSpan(), pskId, out result, info)); + Assert.Same(original, result); + byte[] originalEnc = (byte[])enc.Clone(); + AssertExtensions.Throws(parameter, () => key.CreatePskSender(psk, pskId, enc.AsSpan(), info)); + Assert.Equal(originalEnc, enc); + AssertExtensions.Throws(parameter, () => key.CreatePskRecipient(enc, psk, pskId, info)); + AssertExtensions.Throws(parameter, () => key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)); + } + + [Fact] + public static void Psk_CoreFailureDoesNotPublishOutput() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + using (RecordingHpke key = new(suite) { ThrowOnCreateSender = true }) + { + byte[] psk = new byte[32]; + byte[] pskId = [1]; + byte[] original = [0xA5]; + byte[] enc = original; + Assert.Throws(() => key.CreatePskSender(psk, pskId, out enc)); + Assert.Same(original, enc); + Assert.Throws(() => key.CreatePskSender(psk.AsSpan(), pskId, out enc)); + Assert.Same(original, enc); + Assert.Throws( + () => key.CreatePskSender(psk, pskId, new byte[suite.EncapsulatedSecretSizeInBytes].AsSpan())); + Assert.Equal(3, key.PskCalls); + } + } + private sealed class RecordingHpke : Hpke { internal bool OpenCoreCalled { get; private set; } @@ -891,6 +1238,9 @@ private sealed class RecordingHpke : Hpke internal int CreateRecipientCalls { get; private set; } internal byte[] LastRecipientEncapsulatedSecret { get; private set; } = []; internal byte[] LastRecipientInfo { get; private set; } = []; + internal int PskCalls { get; private set; } + internal byte[] LastPsk { get; private set; } = []; + internal byte[] LastPskId { get; private set; } = []; internal RecordingHpke(HpkeSuite suite) : base(suite) { @@ -931,6 +1281,33 @@ protected override HpkeRecipient CreateRecipientCore( return new RecordingHpkeRecipient(Suite); } + protected override HpkeSender CreatePskSenderCore( + Span encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) + { + RecordPskInputs(psk, pskId); + return CreateSenderCore(encapsulatedSecret, info); + } + + protected override HpkeRecipient CreatePskRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) + { + RecordPskInputs(psk, pskId); + return CreateRecipientCore(encapsulatedSecret, info); + } + + private void RecordPskInputs(ReadOnlySpan psk, ReadOnlySpan pskId) + { + PskCalls++; + LastPsk = psk.ToArray(); + LastPskId = pskId.ToArray(); + } + protected override void ExportDecapsulationKeyCore(Span destination) => throw new InvalidOperationException("Unexpected key export."); From 8f4fd22df6565e49a0df155023fc5d1d5bc4793c Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 8 Sep 2026 16:50:50 -0400 Subject: [PATCH 17/42] Implement P-521 DHKEM support for HPKE Add the approved KEM identifier, metadata, HKDF-SHA512 mapping, and P-521 scalar derivation. Increase bounded stack buffers and extend public coverage with published key and Base/PSK ciphertext vectors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../System/Security/Cryptography/HpkeKem.cs | 5 ++ .../Security/Cryptography/HpkeKemMetadata.cs | 2 + .../Security/Cryptography/HpkeSuiteTests.cs | 5 +- .../ref/System.Security.Cryptography.cs | 1 + .../HpkeECDiffieHellmanKemAdapter.cs | 15 ++++++ .../Cryptography/HpkeKemMetadata.Managed.cs | 5 ++ .../Cryptography/HpkeManagedKemAdapter.cs | 7 +-- .../tests/HpkeTests.cs | 51 +++++++++++++++++++ 8 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs index 81b373d5e7bf8a..42e04ddbe0db9d 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs @@ -22,6 +22,11 @@ public enum HpkeKem /// DHKEM_P384_HKDF_SHA384 = 17, + /// + /// Indicates that key encapsulation uses DHKEM with the NIST P-521 curve and HKDF-SHA-512. + /// + DHKEM_P521_HKDF_SHA512 = 18, + /// /// Indicates that key encapsulation uses DHKEM with X25519 and HKDF-SHA-256. /// diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs index 7b27fc6c55096e..4d0eef979c7467 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs @@ -34,6 +34,8 @@ private HpkeKemMetadata(HpkeKem kem, int nsecret, int nenc, int npk, int nsk, st return new HpkeKemMetadata(kem, nsecret: 32, nenc: 65, npk: 65, nsk: 32, name: "DHKEM(P-256, HKDF-SHA256)"); case HpkeKem.DHKEM_P384_HKDF_SHA384: return new HpkeKemMetadata(kem, nsecret: 48, nenc: 97, npk: 97, nsk: 48, name: "DHKEM(P-384, HKDF-SHA384)"); + case HpkeKem.DHKEM_P521_HKDF_SHA512: + return new HpkeKemMetadata(kem, nsecret: 64, nenc: 133, npk: 133, nsk: 66, name: "DHKEM(P-521, HKDF-SHA512)"); case HpkeKem.DHKEM_X25519_HKDF_SHA256: return new HpkeKemMetadata(kem, nsecret: 32, nenc: 32, npk: 32, nsk: 32, name: "DHKEM(X25519, HKDF-SHA256)"); diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs index 6314b84b179e58..9aa471b8306f6b 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs @@ -25,7 +25,7 @@ public static void Constructor_ValidAlgorithms(HpkeKem kem, HpkeKdf kdf, HpkeAea [InlineData(-1)] [InlineData(0)] [InlineData(15)] - [InlineData(18)] + [InlineData(19)] [InlineData(31)] [InlineData(33)] [InlineData(63)] @@ -79,6 +79,7 @@ public static void Constructor_InvalidAead(int aead) [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, 32, 65, 65)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 48, 97, 97)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512, 66, 133, 133)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 32, 32, 32)] [InlineData(HpkeKem.MLKEM_512, 64, 768, 800)] [InlineData(HpkeKem.MLKEM_768, 64, 1088, 1184)] @@ -112,6 +113,8 @@ public static void AeadTagSizeInBytes(HpkeAead aead) [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM, "DHKEM(P-256, HKDF-SHA256) HKDF-SHA256 AES-128-GCM")] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, + "DHKEM(P-521, HKDF-SHA512) HKDF-SHA512 AES-256-GCM")] [InlineData(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, "ML-KEM-768 HKDF-SHA512 AES-256-GCM")] [InlineData(HpkeKem.MLKEM1024_P384, HpkeKdf.SHAKE256, HpkeAead.ChaCha20Poly1305, 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 3497ff81d5006a..418f480ed16872 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1948,6 +1948,7 @@ public enum HpkeKem { DHKEM_P256_HKDF_SHA256 = 16, DHKEM_P384_HKDF_SHA384 = 17, + DHKEM_P521_HKDF_SHA512 = 18, DHKEM_X25519_HKDF_SHA256 = 32, MLKEM_512 = 64, MLKEM_768 = 65, diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index 04aabb0659a982..5baedc2f82d8f1 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -32,10 +32,24 @@ internal sealed class HpkeECDiffieHellmanKemAdapter : HpkeManagedKemAdapter 0xEC, 0xEC, 0x19, 0x6A, 0xCC, 0xC5, 0x29, 0x73, ]; + private static ReadOnlySpan P521Order => + [ + 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFA, 0x51, 0x86, 0x87, 0x83, 0xBF, 0x2F, + 0x96, 0x6B, 0x7F, 0xCC, 0x01, 0x48, 0xF7, 0x09, + 0xA5, 0xD0, 0x3B, 0xB5, 0xC9, 0xB8, 0x89, 0x9C, + 0x47, 0xAE, 0xBB, 0x6F, 0xB7, 0x1E, 0x91, 0x38, + 0x64, 0x09, + ]; + private ReadOnlySpan Order => Suite.KemAlgorithm switch { HpkeKem.DHKEM_P256_HKDF_SHA256 => P256Order, HpkeKem.DHKEM_P384_HKDF_SHA384 => P384Order, + HpkeKem.DHKEM_P521_HKDF_SHA512 => P521Order, _ => throw new UnreachableException(), }; @@ -45,6 +59,7 @@ internal HpkeECDiffieHellmanKemAdapter(HpkeSuite suite) : base(suite) { HpkeKem.DHKEM_P256_HKDF_SHA256 => (ECCurve.NamedCurves.nistP256, byte.MaxValue), HpkeKem.DHKEM_P384_HKDF_SHA384 => (ECCurve.NamedCurves.nistP384, byte.MaxValue), + HpkeKem.DHKEM_P521_HKDF_SHA512 => (ECCurve.NamedCurves.nistP521, (byte)0x01), _ => throw new UnreachableException(), }; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs index aa7dff5f76ffb9..2e62229a9727c6 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs @@ -25,6 +25,10 @@ partial void Setup() SuiteId = [.."KEM"u8, 0x00, 0x11]; KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA384); break; + case HpkeKem.DHKEM_P521_HKDF_SHA512: + SuiteId = [.."KEM"u8, 0x00, 0x12]; + KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA512); + break; case HpkeKem.DHKEM_X25519_HKDF_SHA256: SuiteId = [.."KEM"u8, 0x00, 0x20]; KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA256); @@ -76,6 +80,7 @@ internal bool IsSupported { case HpkeKem.DHKEM_P256_HKDF_SHA256: case HpkeKem.DHKEM_P384_HKDF_SHA384: + case HpkeKem.DHKEM_P521_HKDF_SHA512: return !OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi(); case HpkeKem.DHKEM_X25519_HKDF_SHA256: return X25519DiffieHellman.IsSupported; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index be61b994241224..357579ccefef69 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -33,6 +33,7 @@ internal static HpkeManagedKemAdapter Create(HpkeSuite suite) { case HpkeKem.DHKEM_P256_HKDF_SHA256: case HpkeKem.DHKEM_P384_HKDF_SHA384: + case HpkeKem.DHKEM_P521_HKDF_SHA512: return new HpkeECDiffieHellmanKemAdapter(suite); case HpkeKem.DHKEM_X25519_HKDF_SHA256: return new HpkeX25519DiffieHellmanKemAdapter(suite); @@ -43,7 +44,7 @@ internal static HpkeManagedKemAdapter Create(HpkeSuite suite) internal void Generate() { - const int MaxStackIkmSize = 64; + const int MaxStackIkmSize = 128; Span ikmStack = stackalloc byte[MaxStackIkmSize]; try @@ -79,7 +80,7 @@ protected void ExtractAndExpand( try { - const int MaxStackContextLength = 256; + const int MaxStackContextLength = 512; Span contextBuffer = stackalloc byte[MaxStackContextLength]; Span context = contextBuffer.Slice(0, contextLength); @@ -130,7 +131,7 @@ protected void LabeledExpand( ReadOnlySpan suiteId = Suite.KemMetadata.SuiteId; int labeledInfoLength = checked(sizeof(ushort) + VersionLabel.Length + suiteId.Length + label.Length + info.Length); - const int MaxStackLabeledInfoLength = 256; + const int MaxStackLabeledInfoLength = 512; Span labeledInfoBuffer = stackalloc byte[MaxStackLabeledInfoLength]; try diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 0dbfd9aaa4a352..b3a0a6faaf3fe6 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -46,6 +46,26 @@ public static void DeriveKey_NullArguments() "04a5f53da8564364255bc36850df793672782a5c9e4a7fb5fb2e2146eb12e4d8" + "477ab1f326a361dfd1e41212109510e813380547c68c0964c1908f16f67b902a" + "061be27b2f8b43f1fab1bf0dbf89f5167ce80aca2c210b8fc0f040699db9ee1229")] + [InlineData( + HpkeKem.DHKEM_P521_HKDF_SHA512, + "2ad954bbe39b7122529f7dde780bff626cd97f850d0784a432784e69d86eccaa" + + "de43b6c10a8ffdb94bf943c6da479db137914ec835a7e715e36e45e29b587bab3bf1", + "01462680369ae375e4b3791070a7458ed527842f6a98a79ff5e0d4cbde83c2719" + + "6a3916956655523a6a2556a7af62c5cadabe2ef9da3760bb21e005202f7b2462847", + "0401b45498c1714e2dce167d3caf162e45e0642afc7ed435df7902ccae0e84ba0f7d" + + "373f646b7738bbbdca11ed91bdeae3cdcba3301f2457be452f271fa6837580e661" + + "012af49583a62e48d44bed350c7118c0d8dc861c238c72a2bda17f64704f464b573" + + "38e7f40b60959480c0e58e6559b190d81663ed816e523b6b6a418f66d2451ec64")] + [InlineData( + HpkeKem.DHKEM_P521_HKDF_SHA512, + "39a28dc317c3e48b908948f99d608059f882d3d09c0541824bc25f94e6dee7aa0" + + "df1c644296b06fbb76e84aef5008f8a908e08fbabadf70658538d74753a85f8856a", + "009227b4b91cf1eb6eecb6c0c0bae93a272d24e11c63bd4c34a581c49f9c3ca0" + + "1c16bbd32a0a1fac22784f2ae985c85f183baad103b2d02aee787179dfc1a94fea11", + "0400b81073b1612cf7fdb6db07b35cf4bc17bda5854f3d270ecd9ea99f6c07b46795" + + "b8014b66c523ceed6f4829c18bc3886c891b63fa902500ce3ddeb1fbec7e608ac7" + + "0050b76a0a7fc081dbf1cb30b005981113e635eb501a973aba662d7f16fcc12897d" + + "d752d657d37774bb16197c0d9724eecc1ed65349fb6ac1f280749e7669766f8cd")] [InlineData( HpkeKem.DHKEM_X25519_HKDF_SHA256, "7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234", @@ -118,6 +138,17 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv "949f58e87c39b3f55390b6a970de27dfac44aadc2fbc9d623dcde1a08b628c83ad07dbbee6aede7fcfbf955670", "2b122485c81e76277b6fb7d96d85e1e2f0d41c8b6659dbbd2fad77d4a2318ceb88a350b02f7fdb242af6ee6222", "24612f7a27e9a8a0ddffcc18e769f5e03c9ebb658071b558058172d81336d151933f3d80846596d99f67994822")] + [InlineData( + HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, + "2ad954bbe39b7122529f7dde780bff626cd97f850d0784a432784e69d86eccaa" + + "de43b6c10a8ffdb94bf943c6da479db137914ec835a7e715e36e45e29b587bab3bf1", + "040138b385ca16bb0d5fa0c0665fbbd7e69e3ee29f63991d3e9b5fa740aab8900aa" + + "eed46ed73a49055758425a0ce36507c54b29cc5b85a5cee6bae0cf1c21f2731ece2" + + "013dc3fb7c8d21654bb161b463962ca19e8c654ff24c94dd2898de12051f1ed0692" + + "237fb02b2f8d1dc1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0", + "170f8beddfe949b75ef9c387e201baf4132fa7374593dfafa90768788b7b2b200aafcc6d80ea4c795a7c5b841a", + "d9ee248e220ca24ac00bbbe7e221a832e4f7fa64c4fbab3945b6f3af0c5ecd5e16815b328be4954a05fd352256", + "142cf1e02d1f58d9285f2af7dcfa44f7c3f2d15c73d460c48c6e0e506a3144bae35284e7e221105b61d24e1c7a")] [InlineData( HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, "969bb169aa9c24a501ee9d962e96c310226d427fb6eb3fc579d9882dbc708315", @@ -178,6 +209,7 @@ public static IEnumerable OpenSuiteData() [ HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKem.DHKEM_P384_HKDF_SHA384, + HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKem.DHKEM_X25519_HKDF_SHA256, ]; @@ -314,6 +346,8 @@ public static void Open_AuthenticationFailure(HpkeKem kem, HpkeKdf kdf, HpkeAead [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, 4)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 0)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 4)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512, 0)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512, 4)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 0)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 1)] public static void Open_InvalidEncapsulatedSecret(HpkeKem kem, byte firstByte) @@ -343,6 +377,7 @@ public static void Open_InvalidEncapsulatedSecret(HpkeKem kem, byte firstByte) [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] public static void Open_ArgumentValidation(HpkeKem kem) { @@ -433,6 +468,7 @@ public static void Open_InfoLength(HpkeKdf kdf, int infoLength, bool valid) [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] [InlineData(HpkeKem.MLKEM_512)] [InlineData(HpkeKem.MLKEM_768)] @@ -647,6 +683,7 @@ public static void CreateContexts_SealAndOpen(HpkeKem kem, HpkeKdf kdf, HpkeAead [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] public static void CreateContexts_IndependentLifetime(HpkeKem kem) { @@ -694,6 +731,7 @@ public static void CreateContexts_IndependentLifetime(HpkeKem kem) [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] [InlineData(HpkeKem.MLKEM_512)] [InlineData(HpkeKem.MLKEM_768)] @@ -898,6 +936,16 @@ public static void Recipient_AuthenticationFailureAndOrdering(HpkeKem kem, HpkeK "7e99307095b1cee54b807077f6f5092970a27fbb57ce2835263132c75e52e7e0", "351d83aa6f2ba77c4b9b89aa22fcb18aff3f792bb04e999de9f76f03f99e92c8d9203605cc0dcbb5eb08a9db6b", "e9deb7896d9414ea4d3e01763e425b5bce3b43874d9121f33441f601a8f7faafb0687512f8782f23ea7aa25b4d")] + [InlineData( + HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, + "a2a2458705e278e574f835effecd18232f8a4c459e7550a09d44348ae5d3b1ea" + + "9d95c51995e657ad6f7cae659f5e186126a471c017f8f5e41da9eba74d4e0473e179", + "040085eff0835cc84351f32471d32aa453cdc1f6418eaaecf1c2824210eb1d48d076" + + "8b368110fab21407c324b8bb4bec63f042cfa4d0868d19b760eb4beba1bff793b3" + + "0036d2c614d55730bd2a40c718f9466faf4d5f8170d22b6df98dfe0c067d02b349" + + "ae4a142e0c03418f0a1479ff78a3db07ae2c2e89e5840f712c174ba2118e90fdcb", + "de69e9d943a5d0b70be3359a19f317bd9aca4a2ebb4332a39bcdfc97d5fe62f3a77702f4822c3be531aa7843a1", + "77a16162831f90de350fea9152cfc685ecfa10acb4f7994f41aed43fa5431f2382d078ec88baec53943984553e")] [InlineData( HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, "92c0e581f1b0ad231dd7346d69071afa23eb4dacdf0b868b644a20bd5121dc07", @@ -1326,6 +1374,7 @@ protected override void SealCore( [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] public static void GenerateKey(HpkeKem kem) { @@ -1365,6 +1414,7 @@ public static void GenerateKey(HpkeKem kem) [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] public static void ExportDecapsulationKey_BufferAndLifetime(HpkeKem kem) { @@ -1418,6 +1468,7 @@ public static void ExportDecapsulationKey_BufferAndLifetime(HpkeKem kem) [Theory] [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] public static void ExportEncapsulationKey_BufferAndLifetime(HpkeKem kem) { From a798ae5c2cbae0388d127b04f1f858641e10896d Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 10:39:39 -0400 Subject: [PATCH 18/42] Implement HPKE context secret export Retain exporter secrets in fixed native memory and use the new span-capable UseKey overload for reference-protected exports. Remove nonce-only zeroing while preserving secret cleanup, and cover Base/PSK exporter vectors, length limits, sequencing and lifetimes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/FixedMemoryKeyBox.cs | 25 +++ .../HpkeImplementation.Managed.cs | 134 +++++++------ .../tests/HpkeTests.cs | 184 +++++++++++++++++- 3 files changed, 269 insertions(+), 74 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/FixedMemoryKeyBox.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/FixedMemoryKeyBox.cs index 36f5d034194c5b..0a120a0c9aa4b9 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/FixedMemoryKeyBox.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/FixedMemoryKeyBox.cs @@ -68,5 +68,30 @@ internal TRet UseKey( } } } + + internal void UseKey( + TState1 state1, + TState2 state2, + TState3 state3, + Action> func) + where TState1 : allows ref struct + where TState2 : allows ref struct + where TState3 : allows ref struct + { + bool addedRef = false; + + try + { + DangerousAddRef(ref addedRef); + func(state1, state2, state3, DangerousKeySpan); + } + finally + { + if (addedRef) + { + DangerousRelease(); + } + } + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index b0e4f809b55007..25bf1cb0faed1a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -106,7 +106,6 @@ protected override void SealCore( { CryptographicOperations.ZeroMemory(sharedSecretBuffer); CryptographicOperations.ZeroMemory(keyBuffer); - CryptographicOperations.ZeroMemory(baseNonceBuffer); CryptographicOperations.ZeroMemory(exporterSecretBuffer); } } @@ -159,7 +158,6 @@ protected override void OpenCore( { CryptographicOperations.ZeroMemory(sharedSecretBuffer); CryptographicOperations.ZeroMemory(keyBuffer); - CryptographicOperations.ZeroMemory(baseNonceBuffer); CryptographicOperations.ZeroMemory(exporterSecretBuffer); } } @@ -214,7 +212,7 @@ private HpkeSenderImplementation CreateSenderContext( try { - return new HpkeSenderImplementation(Suite, aead, baseNonce); + return new HpkeSenderImplementation(Suite, aead, kdf, baseNonce, exporterSecret); } catch { @@ -226,7 +224,6 @@ private HpkeSenderImplementation CreateSenderContext( { CryptographicOperations.ZeroMemory(sharedSecretBuffer); CryptographicOperations.ZeroMemory(keyBuffer); - CryptographicOperations.ZeroMemory(baseNonceBuffer); CryptographicOperations.ZeroMemory(exporterSecretBuffer); } } @@ -279,7 +276,7 @@ private HpkeRecipientImplementation CreateRecipientContext( try { - return new HpkeRecipientImplementation(Suite, aead, baseNonce); + return new HpkeRecipientImplementation(Suite, aead, kdf, baseNonce, exporterSecret); } catch { @@ -291,7 +288,6 @@ private HpkeRecipientImplementation CreateRecipientContext( { CryptographicOperations.ZeroMemory(sharedSecretBuffer); CryptographicOperations.ZeroMemory(keyBuffer); - CryptographicOperations.ZeroMemory(baseNonceBuffer); CryptographicOperations.ZeroMemory(exporterSecretBuffer); } } @@ -310,19 +306,26 @@ protected override void Dispose(bool disposing) internal sealed class HpkeSenderImplementation : HpkeSender { private readonly HpkeManagedAeadAdapter _aeadAdapter; + private readonly HpkeManagedKdfAdapter _kdfAdapter; private readonly byte[] _baseNonce; + private readonly FixedMemoryKeyBox _exporterSecret; private ulong _sequenceNumber; internal HpkeSenderImplementation( HpkeSuite suite, HpkeManagedAeadAdapter aeadAdapter, - ReadOnlySpan baseNonce) : base(suite) + HpkeManagedKdfAdapter kdfAdapter, + ReadOnlySpan baseNonce, + ReadOnlySpan exporterSecret) : base(suite) { Debug.Assert(baseNonce.Length == suite.AeadMetadata.Nn); Debug.Assert(baseNonce.Length >= sizeof(ulong)); + Debug.Assert(exporterSecret.Length == suite.KdfMetadata.Nh); _baseNonce = baseNonce.ToArray(); + _exporterSecret = new FixedMemoryKeyBox(exporterSecret); _aeadAdapter = aeadAdapter; + _kdfAdapter = kdfAdapter; } protected override void SealCore( @@ -337,35 +340,33 @@ protected override void SealCore( const int MaxStackNonceLength = 12; Span nonceBuffer = stackalloc byte[MaxStackNonceLength]; - - try - { - Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); - _baseNonce.AsSpan().CopyTo(nonce); - - // The zero-padded sequence number only affects the final eight nonce bytes. - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 - Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); - BinaryPrimitives.WriteUInt64BigEndian( - sequenceBytes, - BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); - - _aeadAdapter.Encrypt( - plaintext, - nonce, - associatedData, - ciphertext.Slice(0, plaintext.Length), - ciphertext.Slice(plaintext.Length)); - _sequenceNumber++; - } - finally - { - CryptographicOperations.ZeroMemory(nonceBuffer); - } + Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); + _baseNonce.AsSpan().CopyTo(nonce); + + // The zero-padded sequence number only affects the final eight nonce bytes. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 + Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); + BinaryPrimitives.WriteUInt64BigEndian( + sequenceBytes, + BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); + + _aeadAdapter.Encrypt( + plaintext, + nonce, + associatedData, + ciphertext.Slice(0, plaintext.Length), + ciphertext.Slice(plaintext.Length)); + _sequenceNumber++; } - protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) => - throw new NotImplementedException(); + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) + { + _exporterSecret.UseKey( + _kdfAdapter, + exporterContext, + destination, + static (kdf, context, output, key) => kdf.ExportSecret(key, context, output)); + } protected override void Dispose(bool disposing) { @@ -377,7 +378,7 @@ protected override void Dispose(bool disposing) } finally { - CryptographicOperations.ZeroMemory(_baseNonce); + _exporterSecret.Dispose(); } } @@ -388,19 +389,26 @@ protected override void Dispose(bool disposing) internal sealed class HpkeRecipientImplementation : HpkeRecipient { private readonly HpkeManagedAeadAdapter _aeadAdapter; + private readonly HpkeManagedKdfAdapter _kdfAdapter; private readonly byte[] _baseNonce; + private readonly FixedMemoryKeyBox _exporterSecret; private ulong _sequenceNumber; internal HpkeRecipientImplementation( HpkeSuite suite, HpkeManagedAeadAdapter aeadAdapter, - ReadOnlySpan baseNonce) : base(suite) + HpkeManagedKdfAdapter kdfAdapter, + ReadOnlySpan baseNonce, + ReadOnlySpan exporterSecret) : base(suite) { Debug.Assert(baseNonce.Length == suite.AeadMetadata.Nn); Debug.Assert(baseNonce.Length >= sizeof(ulong)); + Debug.Assert(exporterSecret.Length == suite.KdfMetadata.Nh); _baseNonce = baseNonce.ToArray(); + _exporterSecret = new FixedMemoryKeyBox(exporterSecret); _aeadAdapter = aeadAdapter; + _kdfAdapter = kdfAdapter; } protected override void OpenCore( @@ -415,35 +423,33 @@ protected override void OpenCore( const int MaxStackNonceLength = 12; Span nonceBuffer = stackalloc byte[MaxStackNonceLength]; - - try - { - Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); - _baseNonce.AsSpan().CopyTo(nonce); - - // The zero-padded sequence number only affects the final eight nonce bytes. - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 - Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); - BinaryPrimitives.WriteUInt64BigEndian( - sequenceBytes, - BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); - - _aeadAdapter.Decrypt( - ciphertext.Slice(0, plaintext.Length), - nonce, - associatedData, - ciphertext.Slice(plaintext.Length), - plaintext); - _sequenceNumber++; - } - finally - { - CryptographicOperations.ZeroMemory(nonceBuffer); - } + Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); + _baseNonce.AsSpan().CopyTo(nonce); + + // The zero-padded sequence number only affects the final eight nonce bytes. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 + Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); + BinaryPrimitives.WriteUInt64BigEndian( + sequenceBytes, + BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); + + _aeadAdapter.Decrypt( + ciphertext.Slice(0, plaintext.Length), + nonce, + associatedData, + ciphertext.Slice(plaintext.Length), + plaintext); + _sequenceNumber++; } - protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) => - throw new NotImplementedException(); + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) + { + _exporterSecret.UseKey( + _kdfAdapter, + exporterContext, + destination, + static (kdf, context, output, key) => kdf.ExportSecret(key, context, output)); + } protected override void Dispose(bool disposing) { @@ -455,7 +461,7 @@ protected override void Dispose(bool disposing) } finally { - CryptographicOperations.ZeroMemory(_baseNonce); + _exporterSecret.Dispose(); } } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index b3a0a6faaf3fe6..2bd8633f9794c5 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -129,7 +129,10 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv "c98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4", "5ad590bb8baa577f8619db35a36311226a896e7342a6d836d8b7bcd2f20b6c7f9076ac232e3ab2523f39513434", "fa6f037b47fc21826b610172ca9637e82d6e5801eb31cbd3748271affd4ecb06646e0329cbdf3c3cd655b28e82", - "895cabfac50ce6c6eb02ffe6c048bf53b7f7be9a91fc559402cbc5b8dcaeb52b2ccc93e466c28fb55fed7a7fec")] + "895cabfac50ce6c6eb02ffe6c048bf53b7f7be9a91fc559402cbc5b8dcaeb52b2ccc93e466c28fb55fed7a7fec", + "5e9bc3d236e1911d95e65b576a8a86d478fb827e8bdfe77b741b289890490d4d", + "6cff87658931bda83dc857e6353efe4987a201b849658d9b047aab4cf216e796", + "d8f1ea7942adbba7412c6d431c62d01371ea476b823eb697e1f6e6cae1dab85a")] [InlineData( HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, "a2f6e7c4d9e108e03be268a64fe73e11a320963c85375a30bfc9ec4a214c6a55", @@ -137,7 +140,10 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv "4a402f25364964b2c129cb417b3c1dab4dfc0854f3084e843f731654392726", "949f58e87c39b3f55390b6a970de27dfac44aadc2fbc9d623dcde1a08b628c83ad07dbbee6aede7fcfbf955670", "2b122485c81e76277b6fb7d96d85e1e2f0d41c8b6659dbbd2fad77d4a2318ceb88a350b02f7fdb242af6ee6222", - "24612f7a27e9a8a0ddffcc18e769f5e03c9ebb658071b558058172d81336d151933f3d80846596d99f67994822")] + "24612f7a27e9a8a0ddffcc18e769f5e03c9ebb658071b558058172d81336d151933f3d80846596d99f67994822", + "c9d634be6e873105fc38fae1f86e195a0aa025c5cf1672acd2a358e7e2a84244", + "d51a7dee4bb7da5e8d6271c5d6755967bbade71c4ceddab1acded3e6e5f642d0", + "1a677fc144ec3f0df86cfebd6578a0a1a402beeb6f6c36235006369f1211edfa")] [InlineData( HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, "2ad954bbe39b7122529f7dde780bff626cd97f850d0784a432784e69d86eccaa" + @@ -148,17 +154,24 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv "237fb02b2f8d1dc1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0", "170f8beddfe949b75ef9c387e201baf4132fa7374593dfafa90768788b7b2b200aafcc6d80ea4c795a7c5b841a", "d9ee248e220ca24ac00bbbe7e221a832e4f7fa64c4fbab3945b6f3af0c5ecd5e16815b328be4954a05fd352256", - "142cf1e02d1f58d9285f2af7dcfa44f7c3f2d15c73d460c48c6e0e506a3144bae35284e7e221105b61d24e1c7a")] + "142cf1e02d1f58d9285f2af7dcfa44f7c3f2d15c73d460c48c6e0e506a3144bae35284e7e221105b61d24e1c7a", + "05e2e5bd9f0c30832b80a279ff211cc65eceb0d97001524085d609ead60d0412", + "fca69744bb537f5b7a1596dbf34eaa8d84bf2e3ee7f1a155d41bd3624aa92b63", + "f389beaac6fcf6c0d9376e20f97e364f0609a88f1bc76d7328e9104df8477013")] [InlineData( HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, "969bb169aa9c24a501ee9d962e96c310226d427fb6eb3fc579d9882dbc708315", "1d38fc578d4209ea0ef3ee5f1128ac4876a9549d74dc2d2f46e75942a6188244", "72da9627fd7eb3a8b7169c6d97419b80adefca751c6b52b39a2e084d35ce3eb4487aadaca5a9c590e0938c48b9", "bf59c5bfd8b31c3debc4a050388f7a047a24c18559902512d1146177a320616a6b527b194c92cf91d8832db1d5", - "a80cdfe1a370a2db7e664c4acc69948d3a095be78bbfb0160f1aa0313cf0ed440154e913e5f9bc6756d7693982")] + "a80cdfe1a370a2db7e664c4acc69948d3a095be78bbfb0160f1aa0313cf0ed440154e913e5f9bc6756d7693982", + "5b6120165c82456080db3c730b886b07129e0aec9b5f7beae9e5bbd103c67f2d", + "30890b81a37b14b818c462ae5b680b4273cdc7a1ce5ca86d30d482fbe4323e7a", + "b0b5c19ae0daf8d005593f5755d6e8cab29bd3c5c8245823586d009d15aa5237")] public static void Open_KnownAnswer( HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, - string ciphertextHex, string secondCiphertextHex, string thirdCiphertextHex) + string ciphertextHex, string secondCiphertextHex, string thirdCiphertextHex, + string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) { HpkeSuite suite = new(kem, kdf, aead); @@ -189,11 +202,13 @@ public static void Open_KnownAnswer( using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) { + AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); Assert.Equal(plaintext, recipient.Open( new ReadOnlySpan(Convert.FromHexString(secondCiphertextHex)), "Count-1"u8)); recipient.Open(Convert.FromHexString(thirdCiphertextHex), destination.AsSpan(), "Count-2"u8); Assert.Equal(plaintext, destination); + AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); } } } @@ -928,14 +943,20 @@ public static void Recipient_AuthenticationFailureAndOrdering(HpkeKem kem, HpkeK "04305d35563527bce037773d79a13deabed0e8e7cde61eecee403496959e89e4d0" + "ca701726696d1485137ccb5341b3c1c7aaee90a4a02449725e744b1193b53b5f", "90c4deb5b75318530194e4bb62f890b019b1397bbf9d0d6eb918890e1fb2be1ac2603193b60a49c2126b75d0eb", - "9e223384a3620f4a75b5a52f546b7262d8826dea18db5a365feb8b997180b22d72dc1287f7089a1073a7102c27")] + "9e223384a3620f4a75b5a52f546b7262d8826dea18db5a365feb8b997180b22d72dc1287f7089a1073a7102c27", + "a115a59bf4dd8dc49332d6a0093af8efca1bcbfd3627d850173f5c4a55d0c185", + "4517eaede0669b16aac7c92d5762dd459c301fa10e02237cd5aeb9be969430c4", + "164e02144d44b607a7722e58b0f4156e67c0c2874d74cf71da6ca48a4cbdc5e0")] [InlineData( HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, "509212d2ac43d399abd9050ae3c41c030b82623da0494c0d9f8f26ac56b7e188", "048739ebbaea3156cbd5e39b4ef41ee7e3b52c8cb4958d087112b17b778897152c" + "7e99307095b1cee54b807077f6f5092970a27fbb57ce2835263132c75e52e7e0", "351d83aa6f2ba77c4b9b89aa22fcb18aff3f792bb04e999de9f76f03f99e92c8d9203605cc0dcbb5eb08a9db6b", - "e9deb7896d9414ea4d3e01763e425b5bce3b43874d9121f33441f601a8f7faafb0687512f8782f23ea7aa25b4d")] + "e9deb7896d9414ea4d3e01763e425b5bce3b43874d9121f33441f601a8f7faafb0687512f8782f23ea7aa25b4d", + "850caf7336dd83d41fdee7cb133c7c12b62bf7111d3c5d3d60b20128484adada", + "50121f10b5674e3dc46eed39616ff502ef0d6d7f356783808887a867f6a717c6", + "32b9b0b8315cfc2415852b21e9353e79c233233f400def9623404e21657bdab5")] [InlineData( HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, "a2a2458705e278e574f835effecd18232f8a4c459e7550a09d44348ae5d3b1ea" + @@ -945,16 +966,23 @@ public static void Recipient_AuthenticationFailureAndOrdering(HpkeKem kem, HpkeK "0036d2c614d55730bd2a40c718f9466faf4d5f8170d22b6df98dfe0c067d02b349" + "ae4a142e0c03418f0a1479ff78a3db07ae2c2e89e5840f712c174ba2118e90fdcb", "de69e9d943a5d0b70be3359a19f317bd9aca4a2ebb4332a39bcdfc97d5fe62f3a77702f4822c3be531aa7843a1", - "77a16162831f90de350fea9152cfc685ecfa10acb4f7994f41aed43fa5431f2382d078ec88baec53943984553e")] + "77a16162831f90de350fea9152cfc685ecfa10acb4f7994f41aed43fa5431f2382d078ec88baec53943984553e", + "62691f0f971e34de38370bff24deb5a7d40ab628093d304be60946afcdb3a936", + "76083c6d1b6809da088584674327b39488eaf665f0731151128452e04ce81bff", + "0c7cfc0976e25ae7680cf909ae2de1859cd9b679610a14bec40d69b91785b2f6")] [InlineData( HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, "92c0e581f1b0ad231dd7346d69071afa23eb4dacdf0b868b644a20bd5121dc07", "bc441a64a700843a8efd5cd574c20e9909c3a2ff7d35e260f9328cbb8e555d56", "65a46e483d921343f20cba85da69976b2e0e52f450db7919f7796604977d6708d884a40d5e4fd5b820211264aa", - "02019423af9256981bc0a8a7675494efee2244faa2be5b572d9470e451ea3f831e2c08cd47bfc78d6d1f11cfb1")] + "02019423af9256981bc0a8a7675494efee2244faa2be5b572d9470e451ea3f831e2c08cd47bfc78d6d1f11cfb1", + "722aa34bd26f69aa1763f46d7eae6cf461ce74b6952483f3ea7d490c88882982", + "ea0c03bea28f6a22f5c93c52a999fdbd386572920a2838304e987d6f930d5fa4", + "3a3980d8a63287c12db540669ded019a0643e236e25896f2f3197edda044b3ce")] public static void Psk_KnownAnswer( HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, - string firstCiphertextHex, string secondCiphertextHex) + string firstCiphertextHex, string secondCiphertextHex, + string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) { HpkeSuite suite = new(kem, kdf, aead); @@ -977,6 +1005,7 @@ public static void Psk_KnownAnswer( using (HpkeRecipient fromArray = key.CreatePskRecipient(enc, psk, pskId, info)) using (HpkeRecipient fromSpan = key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)) { + AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); byte[] firstCiphertext = Convert.FromHexString(firstCiphertextHex); byte[] secondCiphertext = Convert.FromHexString(secondCiphertextHex); Assert.Equal(plaintext, fromArray.Open(firstCiphertext, "Count-0"u8.ToArray())); @@ -985,6 +1014,8 @@ public static void Psk_KnownAnswer( fromSpan.Open(firstCiphertext, destination.AsSpan(), "Count-0"u8); Assert.Equal(plaintext, destination); Assert.Equal(plaintext, fromSpan.Open(secondCiphertext, "Count-1"u8.ToArray())); + AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); + AssertRecipientExports(fromSpan, emptyContextExportHex, zeroContextExportHex, testContextExportHex); } } finally @@ -1277,6 +1308,139 @@ public static void Psk_CoreFailureDoesNotPublishOutput() } } + private static void AssertRecipientExports( + HpkeRecipient recipient, string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) + { + byte[][] contexts = [[], [0], "TestContext"u8.ToArray()]; + string[] expectedHex = [emptyContextExportHex, zeroContextExportHex, testContextExportHex]; + + for (int i = 0; i < contexts.Length; i++) + { + byte[] expected = Convert.FromHexString(expectedHex[i]); + Assert.Equal(expected, recipient.Export(contexts[i], expected.Length)); + Assert.Equal(expected, recipient.Export(contexts[i].AsSpan(), expected.Length)); + byte[] destination = new byte[expected.Length + 2]; + destination.AsSpan().Fill(0xA5); + recipient.Export(contexts[i], destination.AsSpan(1, expected.Length)); + AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + } + } + + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + int maximumLength = kdf switch + { + HpkeKdf.HKDF_SHA256 => 8160, + HpkeKdf.HKDF_SHA384 => 12240, + HpkeKdf.HKDF_SHA512 => 16320, + HpkeKdf.SHAKE128 or HpkeKdf.SHAKE256 => 65535, + _ => throw new InvalidOperationException(), + }; + + foreach (bool usePsk in new[] { false, true }) + { + using (Hpke key = Hpke.GenerateKey(suite)) + { + byte[] psk = new byte[32]; + byte[] pskId = "identifier"u8.ToArray(); + byte[] info = "application context"u8.ToArray(); + byte[] enc; + using (HpkeSender sender = usePsk + ? key.CreatePskSender(psk, pskId, out enc, info) + : key.CreateSender(out enc, info)) + using (HpkeRecipient recipient = usePsk + ? key.CreatePskRecipient(enc, psk, pskId, info) + : key.CreateRecipient(enc, info)) + { + byte[] context = "exporter context"u8.ToArray(); + byte[] originalContext = (byte[])context.Clone(); + byte[] referenceExport = sender.Export(context, 32); + Assert.Equal(referenceExport, recipient.Export(context, 32)); + + key.Dispose(); + psk.AsSpan().Clear(); + pskId.AsSpan().Clear(); + info.AsSpan().Clear(); + + foreach (int length in new[] { 0, 1, 31, 32, 33, 65, maximumLength }) + { + byte[] expected = sender.Export(context, length); + Assert.Equal(length, expected.Length); + Assert.Equal(expected, sender.Export(context.AsSpan(), length)); + Assert.Equal(expected, recipient.Export(context, length)); + Assert.Equal(expected, recipient.Export(context.AsSpan(), length)); + + byte[] senderBuffer = new byte[length + 2]; + byte[] recipientBuffer = new byte[length + 2]; + senderBuffer.AsSpan().Fill(0xA5); + recipientBuffer.AsSpan().Fill(0xA5); + sender.Export(context, senderBuffer.AsSpan(1, length)); + recipient.Export(context, recipientBuffer.AsSpan(1, length)); + AssertExtensions.SequenceEqual(expected.AsSpan(), senderBuffer.AsSpan(1, length)); + Assert.Equal(senderBuffer, recipientBuffer); + Assert.Equal(0xA5, senderBuffer[0]); + Assert.Equal(0xA5, senderBuffer[^1]); + expected.AsSpan().Clear(); + } + + Assert.Equal(originalContext, context); + Assert.Equal(referenceExport, sender.Export(context, 32)); + Assert.NotEqual(referenceExport, sender.Export(Array.Empty(), 32)); + Assert.NotEqual(referenceExport, sender.Export(new byte[] { 0 }, 32)); + Assert.False(referenceExport.AsSpan().SequenceEqual(sender.Export(context, 33).AsSpan(0, 32))); + byte[] longContext = new byte[65536]; + longContext.AsSpan().Fill(0x39); + Assert.Equal(sender.Export(longContext, 32), recipient.Export(longContext, 32)); + + AssertExtensions.Throws( + "length", () => sender.Export(context, maximumLength + 1)); + AssertExtensions.Throws( + "length", () => recipient.Export(context, maximumLength + 1)); + byte[] invalidDestination = new byte[maximumLength + 1]; + invalidDestination.AsSpan().Fill(0xA5); + byte[] originalDestination = (byte[])invalidDestination.Clone(); + AssertExtensions.Throws( + "destination", () => sender.Export(context, invalidDestination.AsSpan())); + AssertExtensions.Throws( + "destination", () => recipient.Export(context, invalidDestination.AsSpan())); + Assert.Equal(originalDestination, invalidDestination); + + byte[] message = "message"u8.ToArray(); + for (int i = 0; i < 3; i++) + { + byte[] ciphertext = sender.Seal(message); + Assert.Equal(referenceExport, sender.Export(context, 32)); + Assert.Equal(referenceExport, recipient.Export(context, 32)); + Assert.Equal(message, recipient.Open(ciphertext)); + Assert.Equal(referenceExport, recipient.Export(context, 32)); + } + + sender.Dispose(); + Assert.Throws(() => sender.Export(context, 0)); + Assert.Throws(() => sender.Export(context.AsSpan(), 32)); + Assert.Throws(() => sender.Export(context, new byte[32].AsSpan())); + Assert.Equal(referenceExport, recipient.Export(context, 32)); + recipient.Dispose(); + Assert.Throws(() => recipient.Export(context, 0)); + Assert.Throws(() => recipient.Export(context.AsSpan(), 32)); + Assert.Throws(() => recipient.Export(context, new byte[32].AsSpan())); + } + } + } + } + private sealed class RecordingHpke : Hpke { internal bool OpenCoreCalled { get; private set; } From 4a10e1d5fca5e4a8cb60ed16e594c1436e95dedb Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 10:55:29 -0400 Subject: [PATCH 19/42] Implement HPKE key import APIs Add approved encapsulation and decapsulation key import overloads with NIST scalar validation, raw X25519 import, and exception-safe adapter ownership. Cover known-answer keys, malformed input, public-only operations, and imported key lifetimes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 158 ++++++++++ .../ref/System.Security.Cryptography.cs | 4 + .../HpkeECDiffieHellmanKemAdapter.cs | 24 ++ .../HpkeImplementation.Managed.cs | 32 ++ .../HpkeImplementation.Unsupported.cs | 6 + .../Cryptography/HpkeManagedKemAdapter.cs | 1 + .../HpkeX25519DiffieHellmanKemAdapter.cs | 6 + .../tests/HpkeTests.cs | 290 ++++++++++++++++++ 8 files changed, 521 insertions(+) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index c98e4cdfb3a85d..fedb191f6c107b 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -125,6 +125,164 @@ public static Hpke GenerateKey(HpkeSuite suite) return HpkeImplementation.GenerateKeyImpl(suite); } + /// + /// Imports an HPKE key pair from a serialized decapsulation key. + /// + /// + /// The cipher suite associated with the key. + /// + /// + /// The serialized decapsulation key. + /// + /// + /// A new HPKE key containing the decapsulation key and its corresponding encapsulation key. + /// + /// + /// or is . + /// + /// + /// is not exactly bytes long. + /// + /// + /// The decapsulation key is invalid, or an error occurred while importing the key. + /// + /// + /// is not supported on the current platform. + /// + /// + /// The key must use the format returned by , + /// not the input keying material accepted by . + /// The imported key does not retain a reference to . + /// The caller remains responsible for protecting and clearing the source key bytes. + /// + public static Hpke ImportDecapsulationKey(HpkeSuite suite, byte[] source) + { + ArgumentNullException.ThrowIfNull(source); + return ImportDecapsulationKey(suite, new ReadOnlySpan(source)); + } + + /// + /// Imports an HPKE key pair from a serialized decapsulation key. + /// + /// + /// The cipher suite associated with the key. + /// + /// + /// The serialized decapsulation key. + /// + /// + /// A new HPKE key containing the decapsulation key and its corresponding encapsulation key. + /// + /// + /// is . + /// + /// + /// is not exactly bytes long. + /// + /// + /// The decapsulation key is invalid, or an error occurred while importing the key. + /// + /// + /// is not supported on the current platform. + /// + /// + /// The key must use the format returned by , + /// not the input keying material accepted by . + /// The imported key does not retain a reference to . + /// The caller remains responsible for protecting and clearing the source key bytes. + /// + public static Hpke ImportDecapsulationKey(HpkeSuite suite, ReadOnlySpan source) + { + ArgumentNullException.ThrowIfNull(suite); + + if (source.Length != suite.DecapsulationKeySizeInBytes) + { + throw new ArgumentException(SR.Argument_PrivateKeyWrongSizeForAlgorithm, nameof(source)); + } + + ThrowIfNotSupported(suite); + return HpkeImplementation.ImportDecapsulationKeyImpl(suite, source); + } + + /// + /// Imports an HPKE key from a serialized encapsulation key. + /// + /// + /// The cipher suite associated with the key. + /// + /// + /// The serialized encapsulation key. + /// + /// + /// A new HPKE key containing only the encapsulation key. + /// + /// + /// or is . + /// + /// + /// is not exactly bytes long. + /// + /// + /// The encapsulation key is invalid, or an error occurred while importing the key. + /// + /// + /// is not supported on the current platform. + /// + /// + /// The key must use the format returned by . + /// The imported key can encrypt messages and create sender contexts, but cannot decrypt messages, + /// create recipient contexts, or export a decapsulation key. + /// The imported key does not retain a reference to . + /// + public static Hpke ImportEncapsulationKey(HpkeSuite suite, byte[] source) + { + ArgumentNullException.ThrowIfNull(source); + return ImportEncapsulationKey(suite, new ReadOnlySpan(source)); + } + + /// + /// Imports an HPKE key from a serialized encapsulation key. + /// + /// + /// The cipher suite associated with the key. + /// + /// + /// The serialized encapsulation key. + /// + /// + /// A new HPKE key containing only the encapsulation key. + /// + /// + /// is . + /// + /// + /// is not exactly bytes long. + /// + /// + /// The encapsulation key is invalid, or an error occurred while importing the key. + /// + /// + /// is not supported on the current platform. + /// + /// + /// The key must use the format returned by . + /// The imported key can encrypt messages and create sender contexts, but cannot decrypt messages, + /// create recipient contexts, or export a decapsulation key. + /// The imported key does not retain a reference to . + /// + public static Hpke ImportEncapsulationKey(HpkeSuite suite, ReadOnlySpan source) + { + ArgumentNullException.ThrowIfNull(suite); + + if (source.Length != suite.EncapsulationKeySizeInBytes) + { + throw new ArgumentException(SR.Argument_PublicKeyWrongSizeForAlgorithm, nameof(source)); + } + + ThrowIfNotSupported(suite); + return HpkeImplementation.ImportEncapsulationKeyImpl(suite, source); + } + /// /// Exports the decapsulation key. /// 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 418f480ed16872..49f5cf057d84cf 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1917,6 +1917,10 @@ public void ExportDecapsulationKey(System.Span destination) { } public void ExportEncapsulationKey(System.Span destination) { } protected abstract void ExportEncapsulationKeyCore(System.Span destination); public static System.Security.Cryptography.Hpke GenerateKey(System.Security.Cryptography.HpkeSuite suite) { throw null; } + public static System.Security.Cryptography.Hpke ImportDecapsulationKey(System.Security.Cryptography.HpkeSuite suite, byte[] source) { throw null; } + public static System.Security.Cryptography.Hpke ImportDecapsulationKey(System.Security.Cryptography.HpkeSuite suite, System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.Hpke ImportEncapsulationKey(System.Security.Cryptography.HpkeSuite suite, byte[] source) { throw null; } + public static System.Security.Cryptography.Hpke ImportEncapsulationKey(System.Security.Cryptography.HpkeSuite suite, System.ReadOnlySpan source) { throw null; } public static bool IsSupported(System.Security.Cryptography.HpkeSuite suite) { throw null; } public byte[] Open(byte[] encapsulatedSecret, byte[] ciphertext, byte[]? associatedData = null, byte[]? info = null) { throw null; } public byte[] Open(System.ReadOnlySpan encapsulatedSecret, System.ReadOnlySpan ciphertext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan), System.ReadOnlySpan info = default(System.ReadOnlySpan)) { throw null; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index 5baedc2f82d8f1..6557503a26c869 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -64,6 +64,30 @@ internal HpkeECDiffieHellmanKemAdapter(HpkeSuite suite) : base(suite) }; } + internal override void ImportDecapsulationKey(ReadOnlySpan decapsulationKey) + { + Debug.Assert(_ecdh is null); + + if (decapsulationKey.Length != Suite.DecapsulationKeySizeInBytes || + !IsValidScalar(decapsulationKey, Order)) + { + throw new CryptographicException(SR.Cryptography_NotValidPrivateKey); + } + + byte[] privateKey = decapsulationKey.ToArray(); + + using (PinAndClear.Track(privateKey)) + { +#pragma warning disable CA1416 // Not supported on browser + _ecdh = ECDiffieHellman.Create(new ECParameters + { + Curve = _curve, + D = privateKey, + }); +#pragma warning restore CA1416 // Not supported on browser + } + } + internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) { Debug.Assert(_ecdh is null); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 25bf1cb0faed1a..144353749a3ef5 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -52,6 +52,38 @@ internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite) } } + internal static HpkeImplementation ImportDecapsulationKeyImpl(HpkeSuite suite, ReadOnlySpan source) + { + HpkeManagedKemAdapter adapter = HpkeManagedKemAdapter.Create(suite); + + try + { + adapter.ImportDecapsulationKey(source); + return new HpkeImplementation(suite, adapter); + } + catch + { + adapter.Dispose(); + throw; + } + } + + internal static HpkeImplementation ImportEncapsulationKeyImpl(HpkeSuite suite, ReadOnlySpan source) + { + HpkeManagedKemAdapter adapter = HpkeManagedKemAdapter.Create(suite); + + try + { + adapter.ImportEncapsulationKey(source); + return new HpkeImplementation(suite, adapter); + } + catch + { + adapter.Dispose(); + throw; + } + } + protected override void ExportDecapsulationKeyCore(Span destination) => _kemAdapter.ExportDecapsulationKey(destination); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs index 3490845b436e54..286dcfc3a4b3f3 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Unsupported.cs @@ -32,6 +32,12 @@ internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite) throw new CryptographicException(); } + internal static HpkeImplementation ImportDecapsulationKeyImpl(HpkeSuite suite, ReadOnlySpan source) => + throw new PlatformNotSupportedException(); + + internal static HpkeImplementation ImportEncapsulationKeyImpl(HpkeSuite suite, ReadOnlySpan source) => + throw new PlatformNotSupportedException(); + protected override void ExportDecapsulationKeyCore(Span destination) { _ = destination; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index 357579ccefef69..a2e148c1194b5a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -158,6 +158,7 @@ protected void LabeledExpand( internal abstract void DeriveKeyPair(ReadOnlySpan ikm); internal abstract void Encapsulate(Span encapsulatedSecret, Span sharedSecret); internal abstract void Decapsulate(ReadOnlySpan encapsulatedSecret, Span sharedSecret); + internal abstract void ImportDecapsulationKey(ReadOnlySpan decapsulationKey); internal abstract void ImportEncapsulationKey(ReadOnlySpan encapsulationKey); internal abstract void ExportDecapsulationKey(Span destination); internal abstract void ExportEncapsulationKey(Span destination); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs index 9090ee05c24841..fc1528e359a01a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -13,6 +13,12 @@ internal HpkeX25519DiffieHellmanKemAdapter(HpkeSuite suite) : base(suite) { } + internal override void ImportDecapsulationKey(ReadOnlySpan decapsulationKey) + { + Debug.Assert(_x25519 is null); + _x25519 = X25519DiffieHellman.ImportPrivateKey(decapsulationKey); + } + internal override void ImportEncapsulationKey(ReadOnlySpan encapsulationKey) { Debug.Assert(_x25519 is null); diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 2bd8633f9794c5..7516fd96d4cfd3 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -105,6 +105,21 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv Assert.Equal(arrayPrivateKey, spanPrivateKey); Assert.Equal(expectedPublicKey, arrayPublicKey); Assert.Equal(arrayPublicKey, spanPublicKey); + + using (Hpke privateFromArray = Hpke.ImportDecapsulationKey(suite, expectedPrivateKey)) + using (Hpke privateFromSpan = Hpke.ImportDecapsulationKey(suite, expectedPrivateKey.AsSpan())) + using (Hpke publicFromArray = Hpke.ImportEncapsulationKey(suite, expectedPublicKey)) + using (Hpke publicFromSpan = Hpke.ImportEncapsulationKey(suite, expectedPublicKey.AsSpan())) + { + privateFromArray.ExportDecapsulationKey(spanPrivateKey); + Assert.Equal(expectedPrivateKey, spanPrivateKey); + privateFromSpan.ExportDecapsulationKey(spanPrivateKey); + Assert.Equal(expectedPrivateKey, spanPrivateKey); + Assert.Equal(expectedPublicKey, privateFromArray.ExportEncapsulationKey()); + Assert.Equal(expectedPublicKey, privateFromSpan.ExportEncapsulationKey()); + Assert.Equal(expectedPublicKey, publicFromArray.ExportEncapsulationKey()); + Assert.Equal(expectedPublicKey, publicFromSpan.ExportEncapsulationKey()); + } } finally { @@ -120,6 +135,281 @@ public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string priv } } + [Fact] + public static void ImportKey_ArgumentValidation() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, (byte[])null)); + AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, (byte[])null)); + AssertExtensions.Throws("suite", () => Hpke.ImportDecapsulationKey(null, Array.Empty())); + AssertExtensions.Throws("suite", () => Hpke.ImportDecapsulationKey(null, ReadOnlySpan.Empty)); + AssertExtensions.Throws("suite", () => Hpke.ImportEncapsulationKey(null, Array.Empty())); + AssertExtensions.Throws("suite", () => Hpke.ImportEncapsulationKey(null, ReadOnlySpan.Empty)); + + foreach (HpkeKem kem in Enum.GetValues()) + { + suite = new HpkeSuite(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + foreach (int length in new[] { 0, suite.DecapsulationKeySizeInBytes - 1, suite.DecapsulationKeySizeInBytes + 1 }) + { + byte[] source = new byte[length]; + AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, source)); + AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, source.AsSpan())); + } + + foreach (int length in new[] { 0, suite.EncapsulationKeySizeInBytes - 1, suite.EncapsulationKeySizeInBytes + 1 }) + { + byte[] source = new byte[length]; + AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, source)); + AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, source.AsSpan())); + } + + if (!Hpke.IsSupported(suite)) + { + byte[] privateKey = new byte[suite.DecapsulationKeySizeInBytes]; + byte[] publicKey = new byte[suite.EncapsulationKeySizeInBytes]; + Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, privateKey)); + Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, privateKey.AsSpan())); + Assert.Throws(() => Hpke.ImportEncapsulationKey(suite, publicKey)); + Assert.Throws(() => Hpke.ImportEncapsulationKey(suite, publicKey.AsSpan())); + } + } + } + + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] + public static void ImportDecapsulationKey_ScalarBoundaries(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + byte[] order = kem switch + { + HpkeKem.DHKEM_P256_HKDF_SHA256 => EccTestData.GetNistP256ExplicitCurve().Order, + HpkeKem.DHKEM_P384_HKDF_SHA384 => EccTestData.GetNistP384ExplicitCurve().Order, + HpkeKem.DHKEM_P521_HKDF_SHA512 => EccTestData.GetNistP521ExplicitCurve().Order, + _ => throw new InvalidOperationException(), + }; + byte[] allBitsSet = new byte[order.Length]; + allBitsSet.AsSpan().Fill(0xFF); + byte[] orderPlusOne = (byte[])order.Clone(); + orderPlusOne[^1]++; + + foreach (byte[] invalid in new[] { new byte[order.Length], order, orderPlusOne, allBitsSet }) + { + Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, invalid)); + Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, invalid.AsSpan())); + } + + byte[] one = new byte[order.Length]; + one[^1] = 1; + byte[] orderMinusOne = (byte[])order.Clone(); + orderMinusOne[^1]--; + + foreach (byte[] valid in new[] { one, orderMinusOne }) + { + byte[] exported = new byte[valid.Length]; + + using (Hpke fromArray = Hpke.ImportDecapsulationKey(suite, valid)) + using (Hpke fromSpan = Hpke.ImportDecapsulationKey(suite, valid.AsSpan())) + { + fromArray.ExportDecapsulationKey(exported); + Assert.Equal(valid, exported); + fromSpan.ExportDecapsulationKey(exported); + Assert.Equal(valid, exported); + fromArray.Seal("message"u8, out byte[] enc, out byte[] ciphertext); + AssertExtensions.SequenceEqual("message"u8, fromSpan.Open(enc, ciphertext)); + } + + CryptographicOperations.ZeroMemory(exported); + } + } + + [Theory] + [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] + [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] + [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] + public static void ImportEncapsulationKey_InvalidNistPoint(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + foreach (byte prefix in new byte[] { 0, 2, 3, 4, 6, 7, 0xFF }) + { + byte[] source = new byte[suite.EncapsulationKeySizeInBytes]; + source[0] = prefix; + byte[] original = (byte[])source.Clone(); + Assert.ThrowsAny(() => Hpke.ImportEncapsulationKey(suite, source)); + Assert.ThrowsAny(() => Hpke.ImportEncapsulationKey(suite, source.AsSpan())); + Assert.Equal(original, source); + } + } + + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void ImportKey_OperationsAndOwnership(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws( + () => Hpke.ImportDecapsulationKey(suite, new byte[suite.DecapsulationKeySizeInBytes])); + Assert.Throws( + () => Hpke.ImportEncapsulationKey(suite, new byte[suite.EncapsulationKeySizeInBytes])); + return; + } + + using (Hpke original = Hpke.GenerateKey(suite)) + { + byte[] privateKey = original.ExportDecapsulationKey(); + byte[] publicKey = original.ExportEncapsulationKey(); + + try + { + foreach (bool useSpan in new[] { false, true }) + { + int padding = useSpan ? 2 : 0; + int offset = useSpan ? 1 : 0; + byte[] privateSource = new byte[privateKey.Length + padding]; + byte[] publicSource = new byte[publicKey.Length + padding]; + privateSource.AsSpan().Fill(0xA5); + publicSource.AsSpan().Fill(0xA5); + privateKey.CopyTo(privateSource, offset); + publicKey.CopyTo(publicSource, offset); + + try + { + using (Hpke importedPrivate = useSpan + ? Hpke.ImportDecapsulationKey(suite, privateSource.AsSpan(1, privateKey.Length)) + : Hpke.ImportDecapsulationKey(suite, privateSource)) + using (Hpke importedPublic = useSpan + ? Hpke.ImportEncapsulationKey(suite, publicSource.AsSpan(1, publicKey.Length)) + : Hpke.ImportEncapsulationKey(suite, publicSource)) + { + Assert.Same(suite, importedPrivate.Suite); + Assert.Same(suite, importedPublic.Suite); + Assert.Equal(publicKey, importedPrivate.ExportEncapsulationKey()); + Assert.Equal(publicKey, importedPublic.ExportEncapsulationKey()); + AssertExtensions.SequenceEqual(privateKey.AsSpan(), privateSource.AsSpan(offset, privateKey.Length)); + AssertExtensions.SequenceEqual(publicKey.AsSpan(), publicSource.AsSpan(offset, publicKey.Length)); + + if (useSpan) + { + Assert.Equal(0xA5, privateSource[0]); + Assert.Equal(0xA5, privateSource[^1]); + Assert.Equal(0xA5, publicSource[0]); + Assert.Equal(0xA5, publicSource[^1]); + } + + privateSource.AsSpan().Clear(); + publicSource.AsSpan().Clear(); + original.Dispose(); + byte[] plaintext = "message"u8.ToArray(); + byte[] aad = "associated data"u8.ToArray(); + byte[] info = "application context"u8.ToArray(); + importedPublic.Seal(plaintext, out byte[] enc, out byte[] ciphertext, aad, info); + Assert.Equal(plaintext, importedPrivate.Open(enc, ciphertext, aad, info)); + byte[] opened = new byte[plaintext.Length]; + importedPrivate.Open(enc, ciphertext, opened.AsSpan(), aad, info); + Assert.Equal(plaintext, opened); + + byte[] psk = new byte[32]; + byte[] pskId = [1]; + using (HpkeSender sender = importedPublic.CreateSender(out byte[] contextEnc, info)) + using (HpkeRecipient recipient = importedPrivate.CreateRecipient(contextEnc, info)) + using (HpkeSender pskSender = importedPublic.CreatePskSender(psk, pskId, out byte[] pskEnc, info)) + using (HpkeRecipient pskRecipient = importedPrivate.CreatePskRecipient(pskEnc, psk, pskId, info)) + { + Assert.ThrowsAny(() => importedPublic.ExportDecapsulationKey()); + Assert.ThrowsAny( + () => importedPublic.ExportDecapsulationKey(new byte[privateKey.Length])); + Assert.ThrowsAny(() => importedPublic.Open(enc, ciphertext, aad, info)); + Assert.ThrowsAny( + () => importedPublic.Open(enc, ciphertext, opened.AsSpan(), aad, info)); + Assert.ThrowsAny(() => importedPublic.CreateRecipient(contextEnc, info)); + Assert.ThrowsAny( + () => importedPublic.CreatePskRecipient(pskEnc, psk, pskId, info)); + + for (int i = 0; i < 2; i++) + { + Assert.Equal(plaintext, recipient.Open(sender.Seal(plaintext, aad), aad)); + Assert.Equal(plaintext, pskRecipient.Open(pskSender.Seal(plaintext, aad), aad)); + } + + Assert.Equal(sender.Export([], 32), recipient.Export([], 32)); + Assert.Equal(pskSender.Export([], 32), pskRecipient.Export([], 32)); + importedPublic.Dispose(); + importedPrivate.Dispose(); + Assert.Throws(() => importedPublic.ExportEncapsulationKey()); + Assert.Throws(() => importedPrivate.ExportDecapsulationKey()); + Assert.Equal(plaintext, recipient.Open(sender.Seal(plaintext, aad), aad)); + Assert.Equal(plaintext, pskRecipient.Open(pskSender.Seal(plaintext, aad), aad)); + } + } + } + finally + { + CryptographicOperations.ZeroMemory(privateSource); + } + } + } + finally + { + CryptographicOperations.ZeroMemory(privateKey); + } + } + } + + [Theory] + [InlineData(0)] + [InlineData(255)] + public static void ImportDecapsulationKey_X25519RawKey(byte value) + { + HpkeSuite suite = new(HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + if (!Hpke.IsSupported(suite)) + { + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } + + byte[] source = new byte[suite.DecapsulationKeySizeInBytes]; + source.AsSpan().Fill(value); + byte[] exported = new byte[source.Length]; + + try + { + using (Hpke fromArray = Hpke.ImportDecapsulationKey(suite, source)) + using (Hpke fromSpan = Hpke.ImportDecapsulationKey(suite, source.AsSpan())) + { + fromArray.ExportDecapsulationKey(exported); + Assert.Equal(source, exported); + fromSpan.ExportDecapsulationKey(exported); + Assert.Equal(source, exported); + fromArray.Seal("message"u8, out byte[] enc, out byte[] ciphertext); + AssertExtensions.SequenceEqual("message"u8, fromSpan.Open(enc, ciphertext)); + } + } + finally + { + CryptographicOperations.ZeroMemory(source); + CryptographicOperations.ZeroMemory(exported); + } + } + // https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/b1f7cb0cdeab6906c61b3d6574e8bdfdbe1cd3fb/test-vectors.json [Theory] [InlineData( From 6f8aa6359cb918ff95a55d3d88560b24b9a954d0 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 11:56:14 -0400 Subject: [PATCH 20/42] Reject concurrent HPKE sender sealing Guard the entire stateful SealCore operation with ConcurrencyBlock to prevent silent nonce reuse while preserving success-only sequence advancement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../HpkeImplementation.Managed.cs | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 144353749a3ef5..49dbfbaa41e7ea 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -342,6 +342,7 @@ internal sealed class HpkeSenderImplementation : HpkeSender private readonly byte[] _baseNonce; private readonly FixedMemoryKeyBox _exporterSecret; private ulong _sequenceNumber; + private ConcurrencyBlock _block; internal HpkeSenderImplementation( HpkeSuite suite, @@ -365,30 +366,34 @@ protected override void SealCore( Span ciphertext, ReadOnlySpan associatedData) { - if (_sequenceNumber == ulong.MaxValue) + // While this API is not documented as thread-safe, we block concurrent calls to prevent silent nonce reuse. + using (ConcurrencyBlock.Enter(ref _block)) { - throw new CryptographicException(SR.Cryptography_HpkeMessageLimitReached); - } - - const int MaxStackNonceLength = 12; - Span nonceBuffer = stackalloc byte[MaxStackNonceLength]; - Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); - _baseNonce.AsSpan().CopyTo(nonce); - - // The zero-padded sequence number only affects the final eight nonce bytes. - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 - Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); - BinaryPrimitives.WriteUInt64BigEndian( - sequenceBytes, - BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); + if (_sequenceNumber == ulong.MaxValue) + { + throw new CryptographicException(SR.Cryptography_HpkeMessageLimitReached); + } - _aeadAdapter.Encrypt( - plaintext, - nonce, - associatedData, - ciphertext.Slice(0, plaintext.Length), - ciphertext.Slice(plaintext.Length)); - _sequenceNumber++; + const int MaxStackNonceLength = 12; + Span nonceBuffer = stackalloc byte[MaxStackNonceLength]; + Span nonce = nonceBuffer.Slice(0, _baseNonce.Length); + _baseNonce.AsSpan().CopyTo(nonce); + + // The zero-padded sequence number only affects the final eight nonce bytes. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.2 + Span sequenceBytes = nonce.Slice(nonce.Length - sizeof(ulong)); + BinaryPrimitives.WriteUInt64BigEndian( + sequenceBytes, + BinaryPrimitives.ReadUInt64BigEndian(sequenceBytes) ^ _sequenceNumber); + + _aeadAdapter.Encrypt( + plaintext, + nonce, + associatedData, + ciphertext.Slice(0, plaintext.Length), + ciphertext.Slice(plaintext.Length)); + _sequenceNumber++; + } } protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) From 4152869012e710c22bfa6cfbf6ffea9bb2aa5a74 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 12:12:06 -0400 Subject: [PATCH 21/42] Validate HPKE sender buffer overlaps Reject input/output and output/output overlap before sender core operations while permitting read-only input aliasing. Document overlapping-buffer exceptions and cover rejection before mutation and adjacent buffer handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 78 +++++++- .../Security/Cryptography/HpkeSender.cs | 13 +- .../tests/HpkeTests.cs | 179 +++++++++++++++++- 3 files changed, 265 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index fedb191f6c107b..8f6530d10066a8 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -483,6 +483,45 @@ public void Seal( ciphertext = ciphertextBuffer; } + /// + /// Encrypts and authenticates a single message into the provided buffers using Base mode. + /// + /// + /// The message to encrypt. + /// + /// + /// The buffer to receive the encapsulated secret to send to the recipient. + /// + /// + /// The buffer to receive the ciphertext followed by its authentication tag. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// The application context, which must match the value used by the recipient. + /// + /// + /// is not exactly + /// bytes long, is not exactly the length returned by + /// for , + /// or exceeds the cipher suite's KDF length limit. + /// + /// + /// The ciphertext length would exceed . + /// + /// + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// + /// + /// + /// The object has already been disposed. + /// public void Seal( ReadOnlySpan plaintext, Span encapsulatedSecret, @@ -509,6 +548,17 @@ public void Seal( nameof(ciphertext)); } + if (encapsulatedSecret.Overlaps(ciphertext) || + plaintext.Overlaps(encapsulatedSecret) || + associatedData.Overlaps(encapsulatedSecret) || + info.Overlaps(encapsulatedSecret) || + plaintext.Overlaps(ciphertext) || + associatedData.Overlaps(ciphertext) || + info.Overlaps(ciphertext)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + SealCore(plaintext, encapsulatedSecret, ciphertext, associatedData, info); } @@ -807,7 +857,13 @@ public HpkeSender CreateSender(out byte[] encapsulatedSecret, ReadOnlySpan /// /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// /// /// /// Creating a sender is not supported on the current platform. @@ -827,6 +883,11 @@ public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan nameof(encapsulatedSecret)); } + if (info.Overlaps(encapsulatedSecret)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + return CreateSenderCore(encapsulatedSecret, info); } @@ -1096,7 +1157,13 @@ public HpkeSender CreatePskSender( /// bytes long. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// /// /// /// Creating a PSK sender is not supported on the current platform. @@ -1125,6 +1192,13 @@ public HpkeSender CreatePskSender( nameof(encapsulatedSecret)); } + if (psk.Overlaps(encapsulatedSecret) || + pskId.Overlaps(encapsulatedSecret) || + info.Overlaps(encapsulatedSecret)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + return CreatePskSenderCore(encapsulatedSecret, info, psk, pskId); } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs index f4c349ff04888d..5bdc9fab53849e 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs @@ -120,7 +120,13 @@ public byte[] Seal(byte[] plaintext, byte[]? associatedData = null) /// The ciphertext length would exceed . /// /// - /// The sender's message limit has been reached, or an error occurred during encryption. + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// The sender's message limit has been reached, or an error occurred during encryption. + /// /// /// /// The object has already been disposed. @@ -140,6 +146,11 @@ public void Seal( nameof(ciphertext)); } + if (plaintext.Overlaps(ciphertext) || associatedData.Overlaps(ciphertext)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + SealCore(plaintext, ciphertext, associatedData); } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 7516fd96d4cfd3..42b5364ae5d9e0 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -1731,9 +1731,180 @@ public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) } } + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public static void Seal_RejectsOverlappingBuffers(int offset) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + // Indices: plaintext, associatedData, info, encapsulatedSecret, ciphertext. + (int First, int Second)[] pairs = [(0, 3), (1, 3), (2, 3), (0, 4), (1, 4), (2, 4), (3, 4)]; + + foreach ((int first, int second) in pairs) + { + byte[][] buffers = [new byte[128], new byte[128], new byte[128], new byte[128], new byte[128]]; + buffers[second] = buffers[first]; + buffers[first].AsSpan().Fill(0xA5); + byte[] original = (byte[])buffers[first].Clone(); + int[] starts = [16, 16, 16, 16, 16]; + starts[second] += offset; + + using (RecordingHpke key = new(suite)) + { + Assert.Throws(() => key.Seal( + buffers[0].AsSpan(starts[0], 32), + buffers[3].AsSpan(starts[3], suite.EncapsulatedSecretSizeInBytes), + buffers[4].AsSpan(starts[4], suite.GetCiphertextLength(32)), + buffers[1].AsSpan(starts[1], 32), + buffers[2].AsSpan(starts[2], 32))); + Assert.Equal(0, key.SealCalls); + Assert.Equal(original, buffers[first]); + } + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public static void CreateSender_RejectsOverlappingBuffers(int offset) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + byte[] buffer = new byte[128]; + buffer.AsSpan().Fill(0xA5); + byte[] original = (byte[])buffer.Clone(); + + using (RecordingHpke key = new(suite)) + { + Assert.Throws(() => key.CreateSender( + buffer.AsSpan(16 + offset, suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(16, 32))); + Assert.Equal(0, key.CreateSenderCalls); + Assert.Equal(original, buffer); + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public static void CreatePskSender_RejectsOverlappingBuffers(int offset) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + for (int input = 0; input < 3; input++) + { + byte[][] inputs = [new byte[128], new byte[128], new byte[128]]; + byte[] output = inputs[input]; + output.AsSpan().Fill(0xA5); + byte[] original = (byte[])output.Clone(); + + using (RecordingHpke key = new(suite)) + { + Assert.Throws(() => key.CreatePskSender( + inputs[0].AsSpan(16, 32), + inputs[1].AsSpan(16, 32), + output.AsSpan(16 + offset, suite.EncapsulatedSecretSizeInBytes), + inputs[2].AsSpan(16, 32))); + Assert.Equal(0, key.PskCalls); + Assert.Equal(0, key.CreateSenderCalls); + Assert.Equal(original, output); + } + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public static void Sender_SealRejectsOverlappingBuffers(int offset) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + for (int input = 0; input < 2; input++) + { + byte[][] inputs = [new byte[128], new byte[128]]; + byte[] output = inputs[input]; + output.AsSpan().Fill(0xA5); + byte[] original = (byte[])output.Clone(); + + using (RecordingHpkeSender sender = new(suite)) + { + Assert.Throws(() => sender.Seal( + inputs[0].AsSpan(16, 32), + output.AsSpan(16 + offset, suite.GetCiphertextLength(32)), + inputs[1].AsSpan(16, 32))); + Assert.Equal(0, sender.SealCalls); + Assert.Equal(original, output); + } + } + } + + [Theory] + [InlineData(0)] + [InlineData(32)] + public static void Seal_AllowsReadOnlyOverlapAndAdjacentOutputs(int inputLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + int encLength = suite.EncapsulatedSecretSizeInBytes; + int ciphertextLength = suite.GetCiphertextLength(inputLength); + byte[] buffer = new byte[inputLength + encLength + ciphertextLength]; + buffer.AsSpan().Fill(0xA5); + byte[] originalInput = buffer.AsSpan(0, inputLength).ToArray(); + + using (RecordingHpke key = new(suite)) + { + key.Seal( + buffer.AsSpan(0, inputLength), + buffer.AsSpan(inputLength, encLength), + buffer.AsSpan(inputLength + encLength, ciphertextLength), + buffer.AsSpan(0, inputLength), + buffer.AsSpan(0, inputLength)); + Assert.Equal(1, key.SealCalls); + AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); + Assert.Equal(0xD7, buffer[inputLength]); + Assert.Equal(0xC8, buffer[^1]); + } + + using (RecordingHpkeSender sender = new(suite)) + { + sender.Seal( + buffer.AsSpan(0, inputLength), + buffer.AsSpan(inputLength, ciphertextLength), + buffer.AsSpan(0, inputLength)); + Assert.Equal(1, sender.SealCalls); + AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); + } + } + + [Fact] + public static void CreateSender_AllowsReadOnlyOverlapAndAdjacentOutput() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + byte[] buffer = new byte[32 + suite.EncapsulatedSecretSizeInBytes]; + buffer.AsSpan().Fill(0xA5); + byte[] originalInput = buffer.AsSpan(0, 32).ToArray(); + + using (RecordingHpke key = new(suite)) + using (HpkeSender sender = key.CreateSender(buffer.AsSpan(32), buffer.AsSpan(0, 32))) + using (HpkeSender pskSender = key.CreatePskSender( + buffer.AsSpan(0, 32), buffer.AsSpan(0, 32), buffer.AsSpan(32), buffer.AsSpan(0, 32))) + { + Assert.Equal(2, key.CreateSenderCalls); + Assert.Equal(1, key.PskCalls); + Assert.Equal(originalInput, key.LastPsk); + Assert.Equal(originalInput, key.LastPskId); + Assert.Equal(originalInput, key.LastSenderInfo); + AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, 32)); + Assert.Equal(0xD7, buffer[^1]); + } + } + private sealed class RecordingHpke : Hpke { internal bool OpenCoreCalled { get; private set; } + internal int SealCalls { get; private set; } internal int CreateSenderCalls { get; private set; } internal byte[] LastSenderInfo { get; private set; } = []; internal bool ThrowOnCreateSender { get; set; } @@ -1821,8 +1992,12 @@ protected override void SealCore( Span encapsulatedSecret, Span ciphertext, ReadOnlySpan associatedData, - ReadOnlySpan info) => - throw new InvalidOperationException("Unexpected encryption."); + ReadOnlySpan info) + { + SealCalls++; + encapsulatedSecret.Fill(0xD7); + ciphertext.Fill(0xC8); + } } [Theory] From 9d8e8b65993e90aa9fbf1dbba8f80c3209a8af1a Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 12:27:45 -0400 Subject: [PATCH 22/42] Complete HPKE single-shot API documentation Document allocating Seal overloads and SealCore, and align buffer-length exception documentation with Open. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 112 +++++++++++++++++- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 8f6530d10066a8..157c35b4a903fe 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -443,6 +443,38 @@ public void ExportEncapsulationKey(Span destination) /// protected abstract void ExportEncapsulationKeyCore(Span destination); + /// + /// Encrypts and authenticates a single message using Base mode. + /// + /// + /// The message to encrypt. + /// + /// + /// When this method returns, contains a new byte array containing the encapsulated secret to send + /// to the recipient. This parameter is treated as uninitialized. + /// + /// + /// When this method returns, contains a new byte array containing the ciphertext followed by its + /// authentication tag. This parameter is treated as uninitialized. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// The application context, which must match the value used by the recipient. + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// The ciphertext length would exceed . + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// + /// + /// The object has already been disposed. + /// public void Seal( ReadOnlySpan plaintext, out byte[] encapsulatedSecret, @@ -462,6 +494,43 @@ public void Seal( ciphertext = ciphertextBuffer; } + /// + /// Encrypts and authenticates a single message using Base mode. + /// + /// + /// The message to encrypt. + /// + /// + /// When this method returns, contains a new byte array containing the encapsulated secret to send + /// to the recipient. This parameter is treated as uninitialized. + /// + /// + /// When this method returns, contains a new byte array containing the ciphertext followed by its + /// authentication tag. This parameter is treated as uninitialized. + /// + /// + /// The additional data to authenticate without encrypting, + /// or to use no additional authenticated data. + /// + /// + /// The application context, which must match the value used by the recipient, + /// or to use an empty context. + /// + /// + /// is . + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// The ciphertext length would exceed . + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// + /// + /// The object has already been disposed. + /// public void Seal( byte[] plaintext, out byte[] encapsulatedSecret, @@ -502,10 +571,19 @@ public void Seal( /// The application context, which must match the value used by the recipient. /// /// - /// is not exactly - /// bytes long, is not exactly the length returned by - /// for , - /// or exceeds the cipher suite's KDF length limit. + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// is not exactly the length returned by + /// for . + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// /// /// /// The ciphertext length would exceed . @@ -562,6 +640,32 @@ public void Seal( SealCore(plaintext, encapsulatedSecret, ciphertext, associatedData, info); } + /// + /// When overridden in a derived class, encrypts and authenticates a single message using Base mode. + /// + /// + /// The message to encrypt. + /// + /// + /// The buffer to receive the encapsulated secret. + /// + /// + /// The buffer to receive the ciphertext followed by its authentication tag. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// The application context. + /// + /// + /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// + /// + /// The calling method has verified that this instance is not disposed, the output buffers + /// have the exact required lengths for , and + /// satisfies the KDF's length limit. Implementations must fill both output buffers on success. + /// protected abstract void SealCore( ReadOnlySpan plaintext, Span encapsulatedSecret, From 0961d98d0b909eaeb1b6869f777949912b0668f5 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 16:40:11 -0400 Subject: [PATCH 23/42] Fix HPKE target wiring in Microsoft.Bcl.Cryptography Forward all HPKE types on .NET 11, use the unsupported implementation on .NET Framework, and omit HPKE from .NET 10 and .NET Standard. Add required resources and restrict the HashCode dependency to the Framework HPKE build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Microsoft.Bcl.Cryptography.Forwards.cs | 3 +++ .../src/Microsoft.Bcl.Cryptography.csproj | 14 ++++++++--- .../src/Resources/Strings.resx | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs index 18a1c6ccf231ea..1dea6beea4acbe 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs @@ -23,9 +23,12 @@ #if NET11_0_OR_GREATER [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.CompositeMLKem))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.CompositeMLKemAlgorithm))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.Hpke))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeAead))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeKdf))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeKem))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeRecipient))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeSender))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.HpkeSuite))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.X25519DiffieHellman))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.X25519DiffieHellmanCng))] diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index 1e1b7e6c631dc1..561026fb4cb782 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -21,7 +21,9 @@ true true - true + + true + true @@ -35,7 +37,7 @@ - + + + + @@ -666,7 +674,7 @@ + Condition="'$(BuildHpke)' == 'true'" /> diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx index da27f6e171843b..2768faf6ae3dd8 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -69,6 +69,30 @@ Hash must be exactly {0} bytes. + + The ciphertext must be at least {0} bytes long to contain the authentication tag. + + + The encapsulated secret must be exactly {0} bytes long. + + + The exported secret length can be at most {0} bytes. + + + The specified info exceeds the maximum length of {0} bytes. + + + The pre-shared key identifier must not be empty. + + + The pre-shared key identifier exceeds the maximum length of {0} bytes. + + + The pre-shared key exceeds the maximum length of {0} bytes. + + + The pre-shared key must be at least {0} bytes long. + Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection. From 6a8047410263493e4cdf7e18200dee87e2212f91 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 17:02:11 -0400 Subject: [PATCH 24/42] Validate HPKE Open and Export buffer overlaps Reject plaintext/input and exporter-context/destination overlap in public span overloads before invoking core operations. Document overlapping-buffer exceptions and cover rejection without mutation, empty spans and adjacent buffers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 18 +- .../Security/Cryptography/HpkeRecipient.cs | 26 ++- .../Security/Cryptography/HpkeSender.cs | 13 +- .../tests/HpkeTests.cs | 166 ++++++++++++++++++ 4 files changed, 218 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index 157c35b4a903fe..abb6b8472eeb6a 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -839,8 +839,14 @@ public byte[] Open( /// The authentication tag could not be verified. /// /// - /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, - /// or an error occurred during decryption. + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred during decryption. + /// /// /// /// The object has already been disposed. @@ -861,6 +867,14 @@ public void Open( nameof(plaintext)); } + if (encapsulatedSecret.Overlaps(plaintext) || + ciphertext.Overlaps(plaintext) || + associatedData.Overlaps(plaintext) || + info.Overlaps(plaintext)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info); } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs index afdf0334b5675d..ba391592d1307c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs @@ -140,7 +140,13 @@ public byte[] Open(byte[] ciphertext, byte[]? associatedData = null) /// The authentication tag could not be verified. /// /// - /// The recipient's message limit has been reached, or an error occurred during decryption. + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// The recipient's message limit has been reached, or an error occurred during decryption. + /// /// /// /// The object has already been disposed. @@ -160,6 +166,11 @@ public void Open( nameof(plaintext)); } + if (ciphertext.Overlaps(plaintext) || associatedData.Overlaps(plaintext)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + OpenCore(ciphertext, plaintext, associatedData); } @@ -293,7 +304,13 @@ public byte[] Export(byte[] exporterContext, int length) /// The length of exceeds the maximum export length supported by the cipher suite's KDF. /// /// - /// An error occurred while deriving the exported secret. + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// An error occurred while deriving the exported secret. + /// /// /// /// The object has already been disposed. @@ -315,6 +332,11 @@ public void Export(ReadOnlySpan exporterContext, Span destination) nameof(destination)); } + if (exporterContext.Overlaps(destination)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + ExportCore(exporterContext, destination); } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs index 5bdc9fab53849e..6a844768d25448 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs @@ -279,7 +279,13 @@ public byte[] Export(byte[] exporterContext, int length) /// The length of exceeds the maximum export length supported by the cipher suite's KDF. /// /// - /// An error occurred while deriving the exported secret. + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// An error occurred while deriving the exported secret. + /// /// /// /// The object has already been disposed. @@ -301,6 +307,11 @@ public void Export(ReadOnlySpan exporterContext, Span destination) nameof(destination)); } + if (exporterContext.Overlaps(destination)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + ExportCore(exporterContext, destination); } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 42b5364ae5d9e0..3abfc4595c429f 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -1901,6 +1901,172 @@ public static void CreateSender_AllowsReadOnlyOverlapAndAdjacentOutput() } } + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + [InlineData(null)] + public static void Open_RejectsOverlappingBuffers(int? offset) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + int[] lengths = [suite.EncapsulatedSecretSizeInBytes, suite.GetCiphertextLength(32), 32, 32]; + + for (int input = 0; input < lengths.Length; input++) + { + byte[][] inputs = [new byte[128], new byte[128], new byte[128], new byte[128]]; + byte[] output = inputs[input]; + output.AsSpan().Fill(0xA5); + byte[] original = (byte[])output.Clone(); + int outputStart = 16 + (offset ?? lengths[input] - 1); + + using (RecordingHpke key = new(suite)) + { + Assert.Throws(() => key.Open( + inputs[0].AsSpan(16, lengths[0]), + inputs[1].AsSpan(16, lengths[1]), + output.AsSpan(outputStart, 32), + inputs[2].AsSpan(16, lengths[2]), + inputs[3].AsSpan(16, lengths[3]))); + Assert.False(key.OpenCoreCalled); + Assert.Equal(original, output); + } + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + [InlineData(null)] + public static void Recipient_OpenRejectsOverlappingBuffers(int? offset) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + int[] lengths = [suite.GetCiphertextLength(32), 32]; + + for (int input = 0; input < lengths.Length; input++) + { + byte[][] inputs = [new byte[128], new byte[128]]; + byte[] output = inputs[input]; + output.AsSpan().Fill(0xA5); + byte[] original = (byte[])output.Clone(); + int outputStart = 16 + (offset ?? lengths[input] - 1); + + using (RecordingHpkeRecipient recipient = new(suite)) + { + Assert.Throws(() => recipient.Open( + inputs[0].AsSpan(16, lengths[0]), + output.AsSpan(outputStart, 32), + inputs[1].AsSpan(16, lengths[1]))); + Assert.Equal(0, recipient.OpenCalls); + Assert.Equal(original, output); + } + } + } + + [Theory] + [InlineData(0)] + [InlineData(32)] + public static void Open_AllowsReadOnlyOverlapAndAdjacentOutput(int plaintextLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + int encLength = suite.EncapsulatedSecretSizeInBytes; + int ciphertextLength = suite.GetCiphertextLength(plaintextLength); + int inputLength = Math.Max(encLength, ciphertextLength); + byte[] buffer = new byte[inputLength + plaintextLength + 1]; + buffer.AsSpan().Fill(0xA5); + byte[] originalInput = buffer.AsSpan(0, inputLength).ToArray(); + + using (RecordingHpke key = new(suite)) + { + key.Open( + buffer.AsSpan(0, encLength), + buffer.AsSpan(0, ciphertextLength), + buffer.AsSpan(inputLength, plaintextLength), + buffer.AsSpan(0, inputLength), + buffer.AsSpan(0, inputLength)); + Assert.True(key.OpenCoreCalled); + AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); + AssertExtensions.SequenceEqual(new byte[plaintextLength].AsSpan(), buffer.AsSpan(inputLength, plaintextLength)); + Assert.Equal(0xA5, buffer[^1]); + } + + using (RecordingHpkeRecipient recipient = new(suite)) + { + recipient.Open( + buffer.AsSpan(0, ciphertextLength), + buffer.AsSpan(inputLength, plaintextLength), + buffer.AsSpan(0, inputLength)); + Assert.Equal(1, recipient.OpenCalls); + AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); + Assert.Equal(0xA5, buffer[^1]); + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + [InlineData(31)] + public static void Export_RejectsOverlappingBuffers(int offset) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + byte[] buffer = new byte[96]; + buffer.AsSpan().Fill(0xA5); + byte[] original = (byte[])buffer.Clone(); + + using (RecordingHpkeSender sender = new(suite)) + { + Assert.Throws(() => sender.Export( + buffer.AsSpan(16, 32), buffer.AsSpan(16 + offset, 32))); + Assert.Equal(0, sender.ExportCalls); + Assert.Equal(original, buffer); + } + + using (RecordingHpkeRecipient recipient = new(suite)) + { + Assert.Throws(() => recipient.Export( + buffer.AsSpan(16, 32), buffer.AsSpan(16 + offset, 32))); + Assert.Equal(0, recipient.ExportCalls); + Assert.Equal(original, buffer); + } + } + + [Theory] + [InlineData(0, 32)] + [InlineData(32, 0)] + [InlineData(32, 32)] + public static void Export_AllowsEmptyAndAdjacentBuffers(int contextLength, int outputLength) + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + byte[] buffer = new byte[contextLength + outputLength + 1]; + buffer.AsSpan().Fill(0xA5); + byte[] originalContext = buffer.AsSpan(0, contextLength).ToArray(); + byte[] expected = new byte[outputLength]; + expected.AsSpan().Fill(0xE7); + + using (RecordingHpkeSender sender = new(suite)) + { + sender.Export(buffer.AsSpan(0, contextLength), buffer.AsSpan(contextLength, outputLength)); + Assert.Equal(1, sender.ExportCalls); + Assert.Equal(originalContext, sender.LastExporterContext); + AssertExtensions.SequenceEqual(originalContext.AsSpan(), buffer.AsSpan(0, contextLength)); + AssertExtensions.SequenceEqual(expected.AsSpan(), buffer.AsSpan(contextLength, outputLength)); + Assert.Equal(0xA5, buffer[^1]); + } + + buffer.AsSpan(contextLength).Fill(0xA5); + + using (RecordingHpkeRecipient recipient = new(suite)) + { + recipient.Export(buffer.AsSpan(0, contextLength), buffer.AsSpan(contextLength, outputLength)); + Assert.Equal(1, recipient.ExportCalls); + Assert.Equal(originalContext, recipient.LastExporterContext); + AssertExtensions.SequenceEqual(originalContext.AsSpan(), buffer.AsSpan(0, contextLength)); + AssertExtensions.SequenceEqual(expected.AsSpan(), buffer.AsSpan(contextLength, outputLength)); + Assert.Equal(0xA5, buffer[^1]); + } + } + private sealed class RecordingHpke : Hpke { internal bool OpenCoreCalled { get; private set; } From 32f473ca4c5ed933074d4686c88cd2f7b1d3a070 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 18:11:03 -0400 Subject: [PATCH 25/42] Refine HPKE validation order and temporary buffers Validate arguments before disposal checks. Use bounded stack storage with allocation fallbacks for nonsecret KDF context buffers, retaining pooled handling for secret material. Cover disposed-input validation and the labeled-info stack threshold. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 19 ++- .../Security/Cryptography/HpkeRecipient.cs | 9 +- .../Security/Cryptography/HpkeSender.cs | 9 +- .../Cryptography/HpkeManagedKdfAdapter.cs | 45 ++--- .../tests/HpkeTests.cs | 158 +++++++++++++++++- 5 files changed, 199 insertions(+), 41 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index abb6b8472eeb6a..e33cb5ce678db8 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -483,9 +483,10 @@ public void Seal( ReadOnlySpan info = default) { ThrowIfInfoExceedsLimit(info); + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); ThrowIfDisposed(); - byte[] ciphertextBuffer = new byte[Suite.GetCiphertextLength(plaintext.Length)]; + byte[] ciphertextBuffer = new byte[ciphertextLength]; byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes]; SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info); @@ -540,9 +541,10 @@ public void Seal( { ArgumentNullException.ThrowIfNull(plaintext); ThrowIfInfoExceedsLimit(info); + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); ThrowIfDisposed(); - byte[] ciphertextBuffer = new byte[Suite.GetCiphertextLength(plaintext.Length)]; + byte[] ciphertextBuffer = new byte[ciphertextLength]; byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes]; // associatedData and info null's implicity convert to empty span. @@ -608,7 +610,6 @@ public void Seal( ReadOnlySpan info = default) { ThrowIfInfoExceedsLimit(info); - ThrowIfDisposed(); if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) { @@ -637,6 +638,7 @@ public void Seal( throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); SealCore(plaintext, encapsulatedSecret, ciphertext, associatedData, info); } @@ -722,6 +724,7 @@ public byte[] Open( ReadOnlySpan info = default) { int plaintextLength = ValidateOpenInputs(encapsulatedSecret, ciphertext, info); + ThrowIfDisposed(); byte[] plaintext = new byte[plaintextLength]; try @@ -875,6 +878,7 @@ public void Open( throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info); } @@ -992,7 +996,6 @@ public HpkeSender CreateSender(out byte[] encapsulatedSecret, ReadOnlySpan public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan info = default) { ThrowIfInfoExceedsLimit(info); - ThrowIfDisposed(); if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) { @@ -1006,6 +1009,7 @@ public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); return CreateSenderCore(encapsulatedSecret, info); } @@ -1071,8 +1075,8 @@ public HpkeRecipient CreateRecipient( ReadOnlySpan info = default) { ThrowIfInfoExceedsLimit(info); - ThrowIfDisposed(); ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); + ThrowIfDisposed(); return CreateRecipientCore(encapsulatedSecret, info); } @@ -1301,7 +1305,6 @@ public HpkeSender CreatePskSender( { ThrowIfInvalidPskInputs(psk, pskId); ThrowIfInfoExceedsLimit(info); - ThrowIfDisposed(); if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) { @@ -1317,6 +1320,7 @@ public HpkeSender CreatePskSender( throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); return CreatePskSenderCore(encapsulatedSecret, info, psk, pskId); } @@ -1402,8 +1406,8 @@ public HpkeRecipient CreatePskRecipient( { ThrowIfInvalidPskInputs(psk, pskId); ThrowIfInfoExceedsLimit(info); - ThrowIfDisposed(); ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); + ThrowIfDisposed(); return CreatePskRecipientCore(encapsulatedSecret, info, psk, pskId); } @@ -1589,7 +1593,6 @@ private int ValidateOpenInputs( ReadOnlySpan info) { ThrowIfInfoExceedsLimit(info); - ThrowIfDisposed(); ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); int tagSize = Suite.AeadTagSizeInBytes; diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs index ba391592d1307c..90ead47bf7b179 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs @@ -65,8 +65,9 @@ protected HpkeRecipient(HpkeSuite suite) /// public byte[] Open(ReadOnlySpan ciphertext, ReadOnlySpan associatedData = default) { + int plaintextLength = GetPlaintextLength(ciphertext); ThrowIfDisposed(); - byte[] plaintext = new byte[GetPlaintextLength(ciphertext)]; + byte[] plaintext = new byte[plaintextLength]; try { @@ -156,7 +157,6 @@ public void Open( Span plaintext, ReadOnlySpan associatedData = default) { - ThrowIfDisposed(); int plaintextLength = GetPlaintextLength(ciphertext); if (plaintext.Length != plaintextLength) @@ -171,6 +171,7 @@ public void Open( throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); OpenCore(ciphertext, plaintext, associatedData); } @@ -233,7 +234,6 @@ protected abstract void OpenCore( public byte[] Export(ReadOnlySpan exporterContext, int length) { ArgumentOutOfRangeException.ThrowIfNegative(length); - ThrowIfDisposed(); int maximumLength = Suite.KdfMetadata.MaximumExportLength; if (length > maximumLength) @@ -243,6 +243,7 @@ public byte[] Export(ReadOnlySpan exporterContext, int length) SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength)); } + ThrowIfDisposed(); byte[] secret = new byte[length]; try @@ -322,7 +323,6 @@ public byte[] Export(byte[] exporterContext, int length) /// public void Export(ReadOnlySpan exporterContext, Span destination) { - ThrowIfDisposed(); int maximumLength = Suite.KdfMetadata.MaximumExportLength; if (destination.Length > maximumLength) @@ -337,6 +337,7 @@ public void Export(ReadOnlySpan exporterContext, Span destination) throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); ExportCore(exporterContext, destination); } diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs index 6a844768d25448..282a600f1c5053 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs @@ -63,8 +63,9 @@ protected HpkeSender(HpkeSuite suite) /// public byte[] Seal(ReadOnlySpan plaintext, ReadOnlySpan associatedData = default) { + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); ThrowIfDisposed(); - byte[] ciphertext = new byte[Suite.GetCiphertextLength(plaintext.Length)]; + byte[] ciphertext = new byte[ciphertextLength]; SealCore(plaintext, ciphertext, associatedData); return ciphertext; } @@ -136,7 +137,6 @@ public void Seal( Span ciphertext, ReadOnlySpan associatedData = default) { - ThrowIfDisposed(); int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); if (ciphertext.Length != ciphertextLength) @@ -151,6 +151,7 @@ public void Seal( throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); SealCore(plaintext, ciphertext, associatedData); } @@ -208,7 +209,6 @@ protected abstract void SealCore( public byte[] Export(ReadOnlySpan exporterContext, int length) { ArgumentOutOfRangeException.ThrowIfNegative(length); - ThrowIfDisposed(); int maximumLength = Suite.KdfMetadata.MaximumExportLength; if (length > maximumLength) @@ -218,6 +218,7 @@ public byte[] Export(ReadOnlySpan exporterContext, int length) SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength)); } + ThrowIfDisposed(); byte[] secret = new byte[length]; try @@ -297,7 +298,6 @@ public byte[] Export(byte[] exporterContext, int length) /// public void Export(ReadOnlySpan exporterContext, Span destination) { - ThrowIfDisposed(); int maximumLength = Suite.KdfMetadata.MaximumExportLength; if (destination.Length > maximumLength) @@ -312,6 +312,7 @@ public void Export(ReadOnlySpan exporterContext, Span destination) throw new CryptographicException(SR.Cryptography_OverlappingBuffers); } + ThrowIfDisposed(); ExportCore(exporterContext, destination); } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs index 03338c706bfeeb..0e07a8b86b14fa 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -191,22 +191,21 @@ private void LabeledExpand( int length = checked(sizeof(ushort) + VersionLabel.Length + SuiteId.Length + label.Length + info.Length); const int MaxStackInfoLength = 256; - using (CryptoPoolLease labeledInfo = CryptoPoolLease.RentConditionally( - length, stackalloc byte[MaxStackInfoLength])) - { - Span buffer = labeledInfo.Span; - BinaryPrimitives.WriteUInt16BigEndian(buffer, checked((ushort)output.Length)); - int offset = sizeof(ushort); - VersionLabel.CopyTo(buffer.Slice(offset)); - offset += VersionLabel.Length; - SuiteId.CopyTo(buffer.Slice(offset)); - offset += SuiteId.Length; - label.CopyTo(buffer.Slice(offset)); - offset += label.Length; - info.CopyTo(buffer.Slice(offset)); - - HKDF.Expand(_hashAlgorithm, prk, output, buffer); - } + Span buffer = length <= MaxStackInfoLength + ? stackalloc byte[MaxStackInfoLength] + : new byte[length]; + buffer = buffer.Slice(0, length); + BinaryPrimitives.WriteUInt16BigEndian(buffer, checked((ushort)output.Length)); + int offset = sizeof(ushort); + VersionLabel.CopyTo(buffer.Slice(offset)); + offset += VersionLabel.Length; + SuiteId.CopyTo(buffer.Slice(offset)); + offset += SuiteId.Length; + label.CopyTo(buffer.Slice(offset)); + offset += label.Length; + info.CopyTo(buffer.Slice(offset)); + + HKDF.Expand(_hashAlgorithm, prk, output, buffer); } } @@ -236,9 +235,11 @@ protected override void DeriveSecretsCore( using (CryptoPoolLease secrets = CryptoPoolLease.RentConditionally( secretsLength, stackalloc byte[MaxStackInputLength])) - using (CryptoPoolLease context = CryptoPoolLease.RentConditionally( - contextLength, stackalloc byte[MaxStackInputLength])) { + Span context = contextLength <= MaxStackInputLength + ? stackalloc byte[MaxStackInputLength] + : new byte[contextLength]; + context = context.Slice(0, contextLength); Span outputBuffer = stackalloc byte[MaxStackOutputLength]; try @@ -246,11 +247,11 @@ protected override void DeriveSecretsCore( Span output = outputBuffer.Slice(0, outputLength); int offset = WriteLengthPrefixed(psk, secrets.Span); WriteLengthPrefixed(sharedSecret, secrets.Span.Slice(offset)); - context.Span[0] = mode; - offset = 1 + WriteLengthPrefixed(pskId, context.Span.Slice(1)); - WriteLengthPrefixed(info, context.Span.Slice(offset)); + context[0] = mode; + offset = 1 + WriteLengthPrefixed(pskId, context.Slice(1)); + WriteLengthPrefixed(info, context.Slice(offset)); - LabeledDerive(secrets.Span, "secret"u8, context.Span, output); + LabeledDerive(secrets.Span, "secret"u8, context, output); output.Slice(0, key.Length).CopyTo(key); output.Slice(key.Length, baseNonce.Length).CopyTo(baseNonce); output.Slice(key.Length + baseNonce.Length).CopyTo(exporterSecret); diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index 3abfc4595c429f..a801ccc6027801 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -1690,9 +1690,13 @@ public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) Assert.NotEqual(referenceExport, sender.Export(Array.Empty(), 32)); Assert.NotEqual(referenceExport, sender.Export(new byte[] { 0 }, 32)); Assert.False(referenceExport.AsSpan().SequenceEqual(sender.Export(context, 33).AsSpan(0, 32))); - byte[] longContext = new byte[65536]; - longContext.AsSpan().Fill(0x39); - Assert.Equal(sender.Export(longContext, 32), recipient.Export(longContext, 32)); + // HKDF export framing adds 22 bytes; the first two cases straddle the 256-byte stack limit. + foreach (int contextLength in new[] { 234, 235, 65536 }) + { + byte[] longContext = new byte[contextLength]; + longContext.AsSpan().Fill(0x39); + Assert.Equal(sender.Export(longContext, 32), recipient.Export(longContext, 32)); + } AssertExtensions.Throws( "length", () => sender.Export(context, maximumLength + 1)); @@ -2067,6 +2071,154 @@ public static void Export_AllowsEmptyAndAdjacentBuffers(int contextLength, int o } } + [Fact] + public static void DisposedKey_ValidatesArgumentsFirst() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.SHAKE128, HpkeAead.AES_128_GCM); + using (RecordingHpke key = new(suite)) + { + key.Dispose(); + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; + byte[] plaintext = new byte[1]; + byte[] psk = new byte[32]; + byte[] pskId = [1]; + byte[] invalidInfo = new byte[65536]; + + AssertExtensions.Throws("destination", () => key.ExportDecapsulationKey(Span.Empty)); + AssertExtensions.Throws("destination", () => key.ExportEncapsulationKey(Span.Empty)); + AssertExtensions.Throws("plaintext", () => key.Seal((byte[])null, out _, out _)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.Seal(plaintext, Span.Empty, ciphertext)); + AssertExtensions.Throws( + "ciphertext", () => key.Seal(plaintext, enc, Span.Empty)); + Assert.Throws(() => key.Seal(enc.AsSpan(0, 1), enc, ciphertext)); + + AssertExtensions.Throws("encapsulatedSecret", () => key.Open((byte[])null, ciphertext)); + AssertExtensions.Throws("ciphertext", () => key.Open(enc, (byte[])null)); + AssertExtensions.Throws("encapsulatedSecret", () => key.Open(Array.Empty(), ciphertext)); + AssertExtensions.Throws("encapsulatedSecret", () => key.Open(ReadOnlySpan.Empty, ciphertext)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.Open(ReadOnlySpan.Empty, ciphertext, plaintext.AsSpan())); + AssertExtensions.Throws("ciphertext", () => key.Open(enc, Array.Empty())); + AssertExtensions.Throws("ciphertext", () => key.Open(enc.AsSpan(), ReadOnlySpan.Empty)); + AssertExtensions.Throws( + "ciphertext", () => key.Open(enc, ReadOnlySpan.Empty, plaintext.AsSpan())); + AssertExtensions.Throws("plaintext", () => key.Open(enc, ciphertext, Span.Empty)); + Assert.Throws(() => key.Open(enc, ciphertext, ciphertext.AsSpan(0, 1))); + + AssertExtensions.Throws("encapsulatedSecret", () => key.CreateSender(Span.Empty)); + Assert.Throws(() => key.CreateSender(enc, enc.AsSpan(0, 1))); + AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient((byte[])null)); + AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(Array.Empty())); + AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(ReadOnlySpan.Empty)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreatePskSender(psk, pskId, Span.Empty)); + Assert.Throws(() => key.CreatePskSender(enc.AsSpan(0, 32), pskId, enc)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreatePskRecipient(Array.Empty(), psk, pskId)); + AssertExtensions.Throws( + "encapsulatedSecret", () => key.CreatePskRecipient(ReadOnlySpan.Empty, psk, pskId)); + + AssertExtensions.Throws("info", () => key.Seal(plaintext, out _, out _, info: invalidInfo)); + AssertExtensions.Throws("info", () => key.Seal(plaintext.AsSpan(), out _, out _, info: invalidInfo)); + AssertExtensions.Throws("info", () => key.CreateSender(out _, invalidInfo)); + AssertExtensions.Throws("info", () => key.CreateSender(enc, invalidInfo)); + AssertExtensions.Throws("info", () => key.Open(enc, ciphertext, info: invalidInfo)); + AssertExtensions.Throws("info", () => key.CreateRecipient(enc, invalidInfo)); + AssertPskArgumentException(key, "psk", [], pskId, enc, []); + AssertPskArgumentException(key, "pskId", psk, [], enc, []); + AssertPskArgumentException(key, "info", psk, pskId, enc, invalidInfo); + + Assert.Throws(() => key.Seal(plaintext, out _, out _)); + Assert.Throws(() => key.Seal(plaintext.AsSpan(), out _, out _)); + Assert.Throws(() => key.Seal(plaintext, enc, ciphertext)); + Assert.Throws(() => key.Open(enc, ciphertext)); + Assert.Throws(() => key.Open(enc.AsSpan(), ciphertext)); + Assert.Throws(() => key.Open(enc, ciphertext, plaintext.AsSpan())); + Assert.Throws(() => key.CreateSender(out _)); + Assert.Throws(() => key.CreateSender(enc)); + Assert.Throws(() => key.CreateRecipient(enc)); + Assert.Throws(() => key.CreatePskSender(psk, pskId, out _)); + Assert.Throws(() => key.CreatePskSender(psk, pskId, enc)); + Assert.Throws(() => key.CreatePskRecipient(enc, psk, pskId)); + Assert.Equal(0, key.SealCalls); + Assert.False(key.OpenCoreCalled); + Assert.Equal(0, key.CreateSenderCalls); + Assert.Equal(0, key.CreateRecipientCalls); + Assert.Equal(0, key.PskCalls); + } + } + + [Fact] + public static void DisposedSender_ValidatesArgumentsFirst() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + using (RecordingHpkeSender sender = new(suite)) + { + sender.Dispose(); + byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; + AssertExtensions.Throws("plaintext", () => sender.Seal((byte[])null)); + AssertExtensions.Throws( + "ciphertext", () => sender.Seal(ReadOnlySpan.Empty, Span.Empty)); + Assert.Throws(() => sender.Seal(ciphertext.AsSpan(0, 1), ciphertext.AsSpan())); + AssertExtensions.Throws("exporterContext", () => sender.Export((byte[])null, 1)); + + foreach (int length in new[] { -1, 8161 }) + { + AssertExtensions.Throws("length", () => sender.Export(Array.Empty(), length)); + AssertExtensions.Throws("length", () => sender.Export(ReadOnlySpan.Empty, length)); + } + + AssertExtensions.Throws( + "destination", () => sender.Export(ReadOnlySpan.Empty, new byte[8161].AsSpan())); + Assert.Throws(() => sender.Export(ciphertext.AsSpan(), ciphertext.AsSpan())); + Assert.Throws(() => sender.Seal(ciphertext)); + Assert.Throws(() => sender.Seal(ciphertext.AsSpan())); + Assert.Throws(() => sender.Seal(new byte[1], ciphertext.AsSpan())); + Assert.Throws(() => sender.Export(Array.Empty(), 0)); + Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, Span.Empty)); + Assert.Equal(0, sender.SealCalls); + Assert.Equal(0, sender.ExportCalls); + } + } + + [Fact] + public static void DisposedRecipient_ValidatesArgumentsFirst() + { + HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + using (RecordingHpkeRecipient recipient = new(suite)) + { + recipient.Dispose(); + byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; + AssertExtensions.Throws("ciphertext", () => recipient.Open((byte[])null)); + AssertExtensions.Throws("ciphertext", () => recipient.Open(Array.Empty())); + AssertExtensions.Throws("ciphertext", () => recipient.Open(ReadOnlySpan.Empty)); + AssertExtensions.Throws( + "ciphertext", () => recipient.Open(ReadOnlySpan.Empty, Span.Empty)); + AssertExtensions.Throws("plaintext", () => recipient.Open(ciphertext, Span.Empty)); + Assert.Throws(() => recipient.Open(ciphertext, ciphertext.AsSpan(0, 1))); + AssertExtensions.Throws("exporterContext", () => recipient.Export((byte[])null, 1)); + + foreach (int length in new[] { -1, 8161 }) + { + AssertExtensions.Throws("length", () => recipient.Export(Array.Empty(), length)); + AssertExtensions.Throws("length", () => recipient.Export(ReadOnlySpan.Empty, length)); + } + + AssertExtensions.Throws( + "destination", () => recipient.Export(ReadOnlySpan.Empty, new byte[8161].AsSpan())); + Assert.Throws(() => recipient.Export(ciphertext.AsSpan(), ciphertext.AsSpan())); + Assert.Throws(() => recipient.Open(ciphertext)); + Assert.Throws(() => recipient.Open(ciphertext.AsSpan())); + Assert.Throws(() => recipient.Open(ciphertext, new byte[1].AsSpan())); + Assert.Throws(() => recipient.Export(Array.Empty(), 0)); + Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, Span.Empty)); + Assert.Equal(0, recipient.OpenCalls); + Assert.Equal(0, recipient.ExportCalls); + } + } + private sealed class RecordingHpke : Hpke { internal bool OpenCoreCalled { get; private set; } From 850ea5f56a688764d82a2c3776dfde366a2bb1bd Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 9 Sep 2026 20:14:08 -0400 Subject: [PATCH 26/42] Remove redundant HPKE key-schedule output staging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeManagedKdfAdapter.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs index 0e07a8b86b14fa..5532a34fb97b3d 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -47,26 +47,15 @@ internal void DeriveSecrets( Span baseNonce, Span exporterSecret) { - int secretLength = checked(key.Length + baseNonce.Length + exporterSecret.Length); - const int MaxStackSecretLength = 128; - Span secretBuffer = stackalloc byte[MaxStackSecretLength]; - try { - Span secret = secretBuffer.Slice(0, secretLength); - Span derivedKey = secret.Slice(0, key.Length); - Span derivedNonce = secret.Slice(key.Length, baseNonce.Length); - Span derivedExporterSecret = secret.Slice(key.Length + baseNonce.Length); - - DeriveSecretsCore(mode, sharedSecret, info, psk, pskId, derivedKey, derivedNonce, derivedExporterSecret); - - derivedKey.CopyTo(key); - derivedNonce.CopyTo(baseNonce); - derivedExporterSecret.CopyTo(exporterSecret); + DeriveSecretsCore(mode, sharedSecret, info, psk, pskId, key, baseNonce, exporterSecret); } - finally + catch { - CryptographicOperations.ZeroMemory(secretBuffer); + CryptographicOperations.ZeroMemory(key); + CryptographicOperations.ZeroMemory(exporterSecret); + throw; } } From b26bfddc093f403bbfe169e76423576740eccda9 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 10 Sep 2026 10:54:58 -0400 Subject: [PATCH 27/42] Stop clearing non-secret HPKE buffers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeManagedKdfAdapter.cs | 25 +++++---------- .../Cryptography/HpkeManagedKemAdapter.cs | 32 +++++++------------ 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs index 5532a34fb97b3d..b0711435e1216b 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -143,7 +143,6 @@ protected override void DeriveSecretsCore( } finally { - CryptographicOperations.ZeroMemory(contextBuffer); CryptographicOperations.ZeroMemory(secretBuffer); } } @@ -269,23 +268,15 @@ private void LabeledDerive( int length = checked(VersionLabel.Length + SuiteId.Length + sizeof(ushort) + label.Length + sizeof(ushort)); const int MaxStackPrefixLength = 64; Span prefixBuffer = stackalloc byte[MaxStackPrefixLength]; + Span prefix = prefixBuffer.Slice(0, length); + VersionLabel.CopyTo(prefix); + int offset = VersionLabel.Length; + SuiteId.CopyTo(prefix.Slice(offset)); + offset += SuiteId.Length; + offset += WriteLengthPrefixed(label, prefix.Slice(offset)); + BinaryPrimitives.WriteUInt16BigEndian(prefix.Slice(offset), checked((ushort)output.Length)); - try - { - Span prefix = prefixBuffer.Slice(0, length); - VersionLabel.CopyTo(prefix); - int offset = VersionLabel.Length; - SuiteId.CopyTo(prefix.Slice(offset)); - offset += SuiteId.Length; - offset += WriteLengthPrefixed(label, prefix.Slice(offset)); - BinaryPrimitives.WriteUInt16BigEndian(prefix.Slice(offset), checked((ushort)output.Length)); - - Derive(ikm, prefix, context, output); - } - finally - { - CryptographicOperations.ZeroMemory(prefixBuffer); - } + Derive(ikm, prefix, context, output); } private static int WriteLengthPrefixed(ReadOnlySpan value, Span destination) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs index a2e148c1194b5a..f108f9895c89aa 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -133,26 +133,18 @@ protected void LabeledExpand( checked(sizeof(ushort) + VersionLabel.Length + suiteId.Length + label.Length + info.Length); const int MaxStackLabeledInfoLength = 512; Span labeledInfoBuffer = stackalloc byte[MaxStackLabeledInfoLength]; - - try - { - Span destination = labeledInfoBuffer.Slice(0, labeledInfoLength); - BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)output.Length)); - int offset = sizeof(ushort); - VersionLabel.CopyTo(destination.Slice(offset)); - offset += VersionLabel.Length; - suiteId.CopyTo(destination.Slice(offset)); - offset += suiteId.Length; - label.CopyTo(destination.Slice(offset)); - offset += label.Length; - info.CopyTo(destination.Slice(offset)); - - HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, destination); - } - finally - { - CryptographicOperations.ZeroMemory(labeledInfoBuffer); - } + Span destination = labeledInfoBuffer.Slice(0, labeledInfoLength); + BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)output.Length)); + int offset = sizeof(ushort); + VersionLabel.CopyTo(destination.Slice(offset)); + offset += VersionLabel.Length; + suiteId.CopyTo(destination.Slice(offset)); + offset += suiteId.Length; + label.CopyTo(destination.Slice(offset)); + offset += label.Length; + info.CopyTo(destination.Slice(offset)); + + HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, destination); } internal abstract void DeriveKeyPair(ReadOnlySpan ikm); From f0afdece353ac47e46d68ca05b6c0e0092783181 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 10 Sep 2026 11:16:22 -0400 Subject: [PATCH 28/42] Derive HPKE KEM suite IDs from enum values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeKemMetadata.Managed.cs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs index 2e62229a9727c6..7caa891de7ecfb 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.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.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -18,47 +19,38 @@ partial void Setup() switch (Kem) { case HpkeKem.DHKEM_P256_HKDF_SHA256: - SuiteId = [.."KEM"u8, 0x00, 0x10]; - KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA256); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA256); break; case HpkeKem.DHKEM_P384_HKDF_SHA384: - SuiteId = [.."KEM"u8, 0x00, 0x11]; - KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA384); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA384); break; case HpkeKem.DHKEM_P521_HKDF_SHA512: - SuiteId = [.."KEM"u8, 0x00, 0x12]; - KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA512); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA512); break; case HpkeKem.DHKEM_X25519_HKDF_SHA256: - SuiteId = [.."KEM"u8, 0x00, 0x20]; - KemKdf = CreateKemKdf(HpkeKdf.HKDF_SHA256); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA256); break; case HpkeKem.MLKEM_512: - SuiteId = [.."KEM"u8, 0x00, 0x40]; - KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); break; case HpkeKem.MLKEM_768: - SuiteId = [.."KEM"u8, 0x00, 0x41]; - KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); break; case HpkeKem.MLKEM_1024: - SuiteId = [.."KEM"u8, 0x00, 0x42]; - KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); break; case HpkeKem.MLKEM768_P256: - SuiteId = [.."KEM"u8, 0x00, 0x50]; - KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); break; case HpkeKem.MLKEM1024_P384: - SuiteId = [.."KEM"u8, 0x00, 0x51]; - KemKdf = CreateKemKdf(HpkeKdf.SHAKE256); + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); break; default: Debug.Fail($"Missing KEM KDF mapping for {Kem}."); throw new CryptographicException(); } - static HpkeKdfMetadata CreateKemKdf(HpkeKdf kdf) + static (HpkeKdfMetadata Metadata, byte[] SuiteId) CreateMetadata(HpkeKem kem, HpkeKdf kdf) { HpkeKdfMetadata? metadata = HpkeKdfMetadata.Create(kdf); @@ -68,7 +60,9 @@ static HpkeKdfMetadata CreateKemKdf(HpkeKdf kdf) throw new CryptographicException(); } - return metadata; + byte[] suiteId = [.."KEM"u8, 0x00, 0x00]; + BinaryPrimitives.WriteUInt16BigEndian(suiteId.AsSpan(^2), checked((ushort)kem)); + return (metadata, suiteId); } } From 9badba1f0c2c38b193fea4c7fd3c0eea48bd2ac0 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 10 Sep 2026 11:28:18 -0400 Subject: [PATCH 29/42] Assert the internal HPKE export-length invariant Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Security/Cryptography/HpkeManagedKdfAdapter.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs index b0711435e1216b..08529facc6a949 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -66,15 +66,7 @@ internal void ExportSecret( Span destination) { Debug.Assert(exporterSecret.Length == Suite.KdfMetadata.Nh); - - int maximumLength = Suite.KdfMetadata.MaximumExportLength; - - if (destination.Length > maximumLength) - { - throw new ArgumentException( - SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength), - nameof(destination)); - } + Debug.Assert(destination.Length <= Suite.KdfMetadata.MaximumExportLength); // HPKE allows a zero-length export; HKDF.Expand requires a nonempty output. if (!destination.IsEmpty) From 68535fa75032e92789e4cafd27fc2805dbd1fd09 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 10 Sep 2026 11:43:35 -0400 Subject: [PATCH 30/42] Reuse the KDF adapter across HPKE key operations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeImplementation.Managed.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 49dbfbaa41e7ea..7542046c9a1291 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -9,10 +9,12 @@ namespace System.Security.Cryptography internal sealed class HpkeImplementation : Hpke { private readonly HpkeManagedKemAdapter _kemAdapter; + private readonly HpkeManagedKdfAdapter _kdfAdapter; private HpkeImplementation(HpkeSuite suite, HpkeManagedKemAdapter kemAdapter) : base(suite) { _kemAdapter = kemAdapter; + _kdfAdapter = HpkeManagedKdfAdapter.Create(suite); } internal static bool IsSupportedImpl(HpkeSuite suite) => @@ -111,8 +113,7 @@ protected override void SealCore( Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); _kemAdapter.Encapsulate(encapsulatedSecret, sharedSecret); - HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); - kdf.DeriveSecrets( + _kdfAdapter.DeriveSecrets( mode: 0, sharedSecret, info, @@ -163,8 +164,7 @@ protected override void OpenCore( Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); _kemAdapter.Decapsulate(encapsulatedSecret, sharedSecret); - HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); - kdf.DeriveSecrets( + _kdfAdapter.DeriveSecrets( mode: 0, sharedSecret, info, @@ -229,8 +229,7 @@ private HpkeSenderImplementation CreateSenderContext( Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); _kemAdapter.Encapsulate(encapsulatedSecret, sharedSecret); - HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); - kdf.DeriveSecrets( + _kdfAdapter.DeriveSecrets( mode, sharedSecret, info, @@ -244,7 +243,7 @@ private HpkeSenderImplementation CreateSenderContext( try { - return new HpkeSenderImplementation(Suite, aead, kdf, baseNonce, exporterSecret); + return new HpkeSenderImplementation(Suite, aead, _kdfAdapter, baseNonce, exporterSecret); } catch { @@ -293,8 +292,7 @@ private HpkeRecipientImplementation CreateRecipientContext( Span exporterSecret = exporterSecretBuffer.Slice(0, Suite.KdfMetadata.Nh); _kemAdapter.Decapsulate(encapsulatedSecret, sharedSecret); - HpkeManagedKdfAdapter kdf = HpkeManagedKdfAdapter.Create(Suite); - kdf.DeriveSecrets( + _kdfAdapter.DeriveSecrets( mode, sharedSecret, info, @@ -308,7 +306,7 @@ private HpkeRecipientImplementation CreateRecipientContext( try { - return new HpkeRecipientImplementation(Suite, aead, kdf, baseNonce, exporterSecret); + return new HpkeRecipientImplementation(Suite, aead, _kdfAdapter, baseNonce, exporterSecret); } catch { From 60e3a09b5dadbf874a62e0a09f00171df79b42ca Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 10 Sep 2026 14:55:30 -0400 Subject: [PATCH 31/42] Stream HPKE SHAKE inputs through public APIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeManagedKdfAdapter.cs | 141 ++++++++---------- 1 file changed, 65 insertions(+), 76 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs index 08529facc6a949..55f03b7df800c0 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -49,6 +49,11 @@ internal void DeriveSecrets( { try { + // Callers slice these buffers to the suite's exact output sizes. + Debug.Assert(key.Length == Suite.AeadMetadata.Nk); + Debug.Assert(baseNonce.Length == Suite.AeadMetadata.Nn); + Debug.Assert(exporterSecret.Length == Suite.KdfMetadata.Nh); + DeriveSecretsCore(mode, sharedSecret, info, psk, pskId, key, baseNonce, exporterSecret); } catch @@ -116,6 +121,7 @@ protected override void DeriveSecretsCore( Span exporterSecret) { int hashLength = Suite.KdfMetadata.Nh; + // One mode byte plus psk_id_hash and info_hash; SHA-512 has the largest supported hash size. const int MaxStackContextLength = 1 + 2 * SHA512.HashSizeInBytes; Span contextBuffer = stackalloc byte[MaxStackContextLength]; Span secretBuffer = stackalloc byte[SHA512.HashSizeInBytes]; @@ -151,6 +157,8 @@ private void LabeledExtract( ReadOnlySpan ikm, Span prk) { + // This is HPKE-Extract, but using HMAC directly so we don't need a contiguous buffer of all of + // these components. HKDF-Extract is defined as `PRK = HMAC-Hash(salt, IKM)`. using (IncrementalHash hmac = IncrementalHash.CreateHMAC(_hashAlgorithm, salt)) { hmac.AppendData(VersionLabel); @@ -189,7 +197,8 @@ private void LabeledExpand( } } - internal abstract class HpkeManagedShakeKdfAdapter : HpkeManagedKdfAdapter + internal abstract class HpkeManagedShakeKdfAdapter : HpkeManagedKdfAdapter + where TShake : class, IDisposable { protected HpkeManagedShakeKdfAdapter(HpkeSuite suite) : base(suite) { @@ -207,31 +216,28 @@ protected override void DeriveSecretsCore( { // The single-stage schedule length-prefixes both secrets and application context. // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1 - int secretsLength = checked(2 * sizeof(ushort) + psk.Length + sharedSecret.Length); - int contextLength = checked(1 + 2 * sizeof(ushort) + pskId.Length + info.Length); int outputLength = checked(key.Length + baseNonce.Length + exporterSecret.Length); - const int MaxStackInputLength = 256; const int MaxStackOutputLength = 128; - using (CryptoPoolLease secrets = CryptoPoolLease.RentConditionally( - secretsLength, stackalloc byte[MaxStackInputLength])) + // Current suites require at most 32 + 12 + 64 bytes for these outputs. + Debug.Assert(outputLength <= MaxStackOutputLength); + + using (TShake shake = CreateShake()) { - Span context = contextLength <= MaxStackInputLength - ? stackalloc byte[MaxStackInputLength] - : new byte[contextLength]; - context = context.Slice(0, contextLength); Span outputBuffer = stackalloc byte[MaxStackOutputLength]; try { Span output = outputBuffer.Slice(0, outputLength); - int offset = WriteLengthPrefixed(psk, secrets.Span); - WriteLengthPrefixed(sharedSecret, secrets.Span.Slice(offset)); - context[0] = mode; - offset = 1 + WriteLengthPrefixed(pskId, context.Slice(1)); - WriteLengthPrefixed(info, context.Slice(offset)); - LabeledDerive(secrets.Span, "secret"u8, context, output); + AppendLengthPrefixed(shake, psk); + AppendLengthPrefixed(shake, sharedSecret); + AppendLabeledDerivePrefix(shake, "secret"u8, outputLength); + Append(shake, new ReadOnlySpan(in mode)); + AppendLengthPrefixed(shake, pskId); + AppendLengthPrefixed(shake, info); + + GetHashAndReset(shake, output); output.Slice(0, key.Length).CopyTo(key); output.Slice(key.Length, baseNonce.Length).CopyTo(baseNonce); output.Slice(key.Length + baseNonce.Length).CopyTo(exporterSecret); @@ -246,86 +252,69 @@ protected override void DeriveSecretsCore( protected override void ExportSecretCore( ReadOnlySpan exporterSecret, ReadOnlySpan exporterContext, - Span destination) => - LabeledDerive(exporterSecret, "sec"u8, exporterContext, destination); - - private void LabeledDerive( - ReadOnlySpan ikm, - ReadOnlySpan label, - ReadOnlySpan context, - Span output) + Span destination) { - // ikm || "HPKE-v1" || suite_id || lengthPrefixed(label) || I2OSP(L, 2) || context - // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 - int length = checked(VersionLabel.Length + SuiteId.Length + sizeof(ushort) + label.Length + sizeof(ushort)); - const int MaxStackPrefixLength = 64; - Span prefixBuffer = stackalloc byte[MaxStackPrefixLength]; - Span prefix = prefixBuffer.Slice(0, length); - VersionLabel.CopyTo(prefix); - int offset = VersionLabel.Length; - SuiteId.CopyTo(prefix.Slice(offset)); - offset += SuiteId.Length; - offset += WriteLengthPrefixed(label, prefix.Slice(offset)); - BinaryPrimitives.WriteUInt16BigEndian(prefix.Slice(offset), checked((ushort)output.Length)); + using (TShake shake = CreateShake()) + { + Append(shake, exporterSecret); + AppendLabeledDerivePrefix(shake, "sec"u8, destination.Length); + Append(shake, exporterContext); + GetHashAndReset(shake, destination); + } + } - Derive(ikm, prefix, context, output); + // "HPKE-v1" || suite_id || lengthPrefixed(label) || I2OSP(L, 2) + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4 + private void AppendLabeledDerivePrefix(TShake shake, ReadOnlySpan label, int outputLength) + { + Append(shake, VersionLabel); + Append(shake, SuiteId); + AppendLengthPrefixed(shake, label); + Span lengthBytes = stackalloc byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16BigEndian(lengthBytes, checked((ushort)outputLength)); + Append(shake, lengthBytes); } - private static int WriteLengthPrefixed(ReadOnlySpan value, Span destination) + private void AppendLengthPrefixed(TShake shake, ReadOnlySpan value) { - BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)value.Length)); - value.CopyTo(destination.Slice(sizeof(ushort))); - return sizeof(ushort) + value.Length; + Span lengthBytes = stackalloc byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16BigEndian(lengthBytes, checked((ushort)value.Length)); + Append(shake, lengthBytes); + Append(shake, value); } - protected abstract void Derive( - ReadOnlySpan ikm, - ReadOnlySpan prefix, - ReadOnlySpan context, - Span output); + protected abstract TShake CreateShake(); + protected abstract void Append(TShake shake, ReadOnlySpan data); + protected abstract void GetHashAndReset(TShake shake, Span destination); } - internal sealed class HpkeManagedShake128KdfAdapter : HpkeManagedShakeKdfAdapter + internal sealed class HpkeManagedShake128KdfAdapter : HpkeManagedShakeKdfAdapter { internal HpkeManagedShake128KdfAdapter(HpkeSuite suite) : base(suite) { } - protected override void Derive( - ReadOnlySpan ikm, - ReadOnlySpan prefix, - ReadOnlySpan context, - Span output) - { - using (Shake128 shake = new Shake128()) - { - shake.AppendData(ikm); - shake.AppendData(prefix); - shake.AppendData(context); - shake.GetHashAndReset(output); - } - } + protected override Shake128 CreateShake() => new Shake128(); + + protected override void Append(Shake128 shake, ReadOnlySpan data) => + shake.AppendData(data); + + protected override void GetHashAndReset(Shake128 shake, Span destination) => + shake.GetHashAndReset(destination); } - internal sealed class HpkeManagedShake256KdfAdapter : HpkeManagedShakeKdfAdapter + internal sealed class HpkeManagedShake256KdfAdapter : HpkeManagedShakeKdfAdapter { internal HpkeManagedShake256KdfAdapter(HpkeSuite suite) : base(suite) { } - protected override void Derive( - ReadOnlySpan ikm, - ReadOnlySpan prefix, - ReadOnlySpan context, - Span output) - { - using (Shake256 shake = new Shake256()) - { - shake.AppendData(ikm); - shake.AppendData(prefix); - shake.AppendData(context); - shake.GetHashAndReset(output); - } - } + protected override Shake256 CreateShake() => new Shake256(); + + protected override void Append(Shake256 shake, ReadOnlySpan data) => + shake.AppendData(data); + + protected override void GetHashAndReset(Shake256 shake, Span destination) => + shake.GetHashAndReset(destination); } } From 66242302e30c07941c0c7e2fd9debb17aee15932 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 10 Sep 2026 20:43:24 -0400 Subject: [PATCH 32/42] Add shared HPKE contract tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeContractTests.cs | 1661 +++++++++++++++++ .../Microsoft.Bcl.Cryptography.Tests.csproj | 2 + .../System.Security.Cryptography.Tests.csproj | 2 + 3 files changed, 1665 insertions(+) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs new file mode 100644 index 00000000000000..bfd0316e78197b --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs @@ -0,0 +1,1661 @@ +// 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.Runtime.CompilerServices; +using Xunit; +using Xunit.Sdk; + +namespace System.Security.Cryptography.Tests +{ + public static class HpkeContractTests + { + private static readonly HpkeSuite s_suite = new(HpkeKem.MLKEM_768, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + public static IEnumerable Suites() + { + foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) + foreach (HpkeKdf kdf in Enum.GetValues(typeof(HpkeKdf))) + foreach (HpkeAead aead in Enum.GetValues(typeof(HpkeAead))) + { + yield return new object[] { kem, kdf, aead }; + } + } + + public static IEnumerable KemAlgorithms() + { + foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) + { + yield return new object[] { kem }; + } + } + + [Fact] + public static void Constructor_NullSuite() + { + AssertExtensions.Throws("suite", () => new HpkeContract(null)); + } + + [Theory] + [MemberData(nameof(Suites))] + public static void Constructor_SetsSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + using (HpkeContract hpke = new(suite)) + { + Assert.Same(suite, hpke.Suite); + } + } + + [Fact] + public static void StaticMethods_NullArguments() + { + AssertExtensions.Throws("suite", () => Hpke.IsSupported(null)); + AssertExtensions.Throws("suite", () => Hpke.GenerateKey(null)); + AssertExtensions.Throws("suite", () => Hpke.DeriveKey(null, Array.Empty())); + AssertExtensions.Throws("suite", + () => Hpke.DeriveKey(null, ReadOnlySpan.Empty)); + AssertExtensions.Throws("ikm", () => Hpke.DeriveKey(s_suite, (byte[])null)); + AssertExtensions.Throws("suite", + () => Hpke.ImportDecapsulationKey(null, Array.Empty())); + AssertExtensions.Throws("suite", + () => Hpke.ImportDecapsulationKey(null, ReadOnlySpan.Empty)); + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(s_suite, (byte[])null)); + AssertExtensions.Throws("suite", + () => Hpke.ImportEncapsulationKey(null, Array.Empty())); + AssertExtensions.Throws("suite", + () => Hpke.ImportEncapsulationKey(null, ReadOnlySpan.Empty)); + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(s_suite, (byte[])null)); + } + + [Theory] + [MemberData(nameof(KemAlgorithms))] + public static void ImportKeys_InvalidSize(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + foreach (int length in new[] + { + 0, + suite.DecapsulationKeySizeInBytes - 1, + suite.DecapsulationKeySizeInBytes + 1 + }) + { + byte[] source = new byte[length]; + AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, source)); + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(suite, source.AsSpan())); + } + + foreach (int length in new[] + { + 0, + suite.EncapsulationKeySizeInBytes - 1, + suite.EncapsulationKeySizeInBytes + 1 + }) + { + byte[] source = new byte[length]; + AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, source)); + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(suite, source.AsSpan())); + } + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + public static void Dispose_CallsCoreOnce(int disposeCalls) + { + int calls = 0; + HpkeContract hpke = new(s_suite) + { + OnDispose = disposing => + { + Assert.True(disposing); + calls++; + }, + }; + + for (int i = 0; i < disposeCalls; i++) + { + hpke.Dispose(); + } + + Assert.Equal(1, calls); + } + + [Fact] + public static void Dispose_FailurePropagatesAndDoesNotRepeat() + { + InvalidOperationException exception = new(); + int calls = 0; + HpkeContract hpke = new(s_suite) + { + OnDispose = disposing => + { + Assert.True(disposing); + calls++; + throw exception; + }, + }; + + Assert.Same(exception, Assert.Throws(() => hpke.Dispose())); + hpke.Dispose(); + Assert.Equal(1, calls); + + foreach (Action operation in InstanceOperations(hpke)) + { + Assert.Throws(operation); + } + } + + [Fact] + public static void Disposed_InstanceOperationsDoNotCallCore() + { + using (HpkeContract hpke = new(s_suite)) + { + hpke.Dispose(); + + foreach (Action operation in InstanceOperations(hpke)) + { + Assert.Throws(operation); + } + } + } + + [Theory] + [MemberData(nameof(KemAlgorithms))] + public static void ExportKeys_Allocated(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = new(suite) + { + OnExportDecapsulationKeyCore = destination => destination.Fill(0x42), + OnExportEncapsulationKeyCore = destination => destination.Fill(0xE7), + }) + { + byte[] privateKey = hpke.ExportDecapsulationKey(); + byte[] publicKey = hpke.ExportEncapsulationKey(); + Assert.Equal(suite.DecapsulationKeySizeInBytes, privateKey.Length); + Assert.Equal(suite.EncapsulationKeySizeInBytes, publicKey.Length); + AssertExtensions.FilledWith(0x42, privateKey); + AssertExtensions.FilledWith(0xE7, publicKey); + Assert.Equal(1, hpke.ExportDecapsulationKeyCoreCount); + Assert.Equal(1, hpke.ExportEncapsulationKeyCoreCount); + } + } + + [Theory] + [MemberData(nameof(KemAlgorithms))] + public static void ExportKeys_Exact(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + byte[] privateBuffer = Filled(suite.DecapsulationKeySizeInBytes + 2, 0xA5); + byte[] publicBuffer = Filled(suite.EncapsulationKeySizeInBytes + 2, 0xA5); + Memory privateKey = privateBuffer.AsMemory(1, suite.DecapsulationKeySizeInBytes); + Memory publicKey = publicBuffer.AsMemory(1, suite.EncapsulationKeySizeInBytes); + + using (HpkeContract hpke = new(suite) + { + OnExportDecapsulationKeyCore = destination => + { + AssertExtensions.Same(privateKey.Span, destination); + destination.Fill(0x42); + }, + OnExportEncapsulationKeyCore = destination => + { + AssertExtensions.Same(publicKey.Span, destination); + destination.Fill(0xE7); + }, + }) + { + hpke.ExportDecapsulationKey(privateKey.Span); + hpke.ExportEncapsulationKey(publicKey.Span); + AssertGuardedOutput(privateBuffer, 0x42); + AssertGuardedOutput(publicBuffer, 0xE7); + Assert.Equal(1, hpke.ExportDecapsulationKeyCoreCount); + Assert.Equal(1, hpke.ExportEncapsulationKeyCoreCount); + } + } + + [Theory] + [MemberData(nameof(KemAlgorithms))] + public static void ExportKeys_InvalidSizeBeforeDisposal(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + foreach (bool disposed in new[] { false, true }) + { + using (HpkeContract hpke = new(suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + foreach (int length in new[] + { + 0, + suite.DecapsulationKeySizeInBytes - 1, + suite.DecapsulationKeySizeInBytes + 1 + }) + { + AssertExtensions.Throws("destination", + () => hpke.ExportDecapsulationKey(new byte[length])); + } + + foreach (int length in new[] + { + 0, + suite.EncapsulationKeySizeInBytes - 1, + suite.EncapsulationKeySizeInBytes + 1 + }) + { + AssertExtensions.Throws("destination", + () => hpke.ExportEncapsulationKey(new byte[length])); + } + } + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void Seal_Allocated(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 32 }) + foreach (bool useSpan in new[] { false, true }) + { + byte[] plaintext = Filled(length, 0x31); + byte[] associatedData = [0x51, 0x52, 0x53]; + byte[] info = [0x71, 0x72]; + + using (HpkeContract hpke = new(suite) + { + OnSealCore = (p, enc, ct, aad, context) => + { + AssertSameBuffer(plaintext, p); + AssertSameBuffer(associatedData, aad); + AssertSameBuffer(info, context); + enc.Fill(0x42); + ct.Fill(0xE7); + }, + }) + { + byte[] encapsulatedSecret; + byte[] ciphertext; + + if (useSpan) + { + hpke.Seal( + plaintext.AsSpan(), + out encapsulatedSecret, + out ciphertext, + associatedData.AsSpan(), + info.AsSpan()); + } + else + { + hpke.Seal(plaintext, out encapsulatedSecret, out ciphertext, associatedData, info); + } + + Assert.Equal(suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + Assert.Equal(suite.GetCiphertextLength(length), ciphertext.Length); + AssertExtensions.FilledWith(0x42, encapsulatedSecret); + AssertExtensions.FilledWith(0xE7, ciphertext); + AssertExtensions.FilledWith(0x31, plaintext); + Assert.Equal(new byte[] { 0x51, 0x52, 0x53 }, associatedData); + Assert.Equal(new byte[] { 0x71, 0x72 }, info); + Assert.Equal(1, hpke.SealCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void Seal_Exact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 32 }) + { + byte[] plaintext = Filled(length, 0x31); + byte[] associatedData = [0x51, 0x52, 0x53]; + byte[] info = [0x71, 0x72]; + byte[] encBuffer = Filled(suite.EncapsulatedSecretSizeInBytes + 2, 0xA5); + byte[] ctBuffer = Filled(suite.GetCiphertextLength(length) + 2, 0xA5); + Memory encapsulatedSecret = encBuffer.AsMemory(1, encBuffer.Length - 2); + Memory ciphertext = ctBuffer.AsMemory(1, ctBuffer.Length - 2); + + using (HpkeContract hpke = new(suite) + { + OnSealCore = (p, enc, ct, aad, context) => + { + AssertSameBuffer(plaintext, p); + AssertSameBuffer(associatedData, aad); + AssertSameBuffer(info, context); + AssertExtensions.Same(encapsulatedSecret.Span, enc); + AssertExtensions.Same(ciphertext.Span, ct); + enc.Fill(0x42); + ct.Fill(0xE7); + }, + }) + { + hpke.Seal(plaintext, encapsulatedSecret.Span, ciphertext.Span, associatedData, info); + AssertGuardedOutput(encBuffer, 0x42); + AssertGuardedOutput(ctBuffer, 0xE7); + AssertExtensions.FilledWith(0x31, plaintext); + Assert.Equal(1, hpke.SealCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(KemAlgorithms))] + public static void Seal_InvalidOutputSizesBeforeDisposal(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + byte[] plaintext = new byte[32]; + byte[] ciphertext = new byte[suite.GetCiphertextLength(plaintext.Length)]; + byte[] encapsulatedSecret = new byte[suite.EncapsulatedSecretSizeInBytes]; + + foreach (bool disposed in new[] { false, true }) + { + using (HpkeContract hpke = new(suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + foreach (int length in new[] { 0, encapsulatedSecret.Length - 1, encapsulatedSecret.Length + 1 }) + { + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.Seal(plaintext, new byte[length], ciphertext)); + } + + foreach (int length in new[] { 0, ciphertext.Length - 1, ciphertext.Length + 1 }) + { + AssertExtensions.Throws("ciphertext", + () => hpke.Seal(plaintext, encapsulatedSecret, new byte[length])); + } + } + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void Open_Allocated(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 32 }) + foreach (bool useSpan in new[] { false, true }) + { + byte[] encapsulatedSecret = Filled(suite.EncapsulatedSecretSizeInBytes, 0x31); + byte[] ciphertext = Filled(suite.GetCiphertextLength(length), 0x41); + byte[] associatedData = [0x51, 0x52, 0x53]; + byte[] info = [0x71, 0x72]; + + using (HpkeContract hpke = new(suite) + { + OnOpenCore = (enc, ct, p, aad, context) => + { + AssertSameBuffer(encapsulatedSecret, enc); + AssertSameBuffer(ciphertext, ct); + AssertSameBuffer(associatedData, aad); + AssertSameBuffer(info, context); + p.Fill(0xE7); + }, + }) + { + byte[] plaintext = useSpan + ? hpke.Open( + encapsulatedSecret.AsSpan(), + ciphertext.AsSpan(), + associatedData: associatedData.AsSpan(), + info: info.AsSpan()) + : hpke.Open(encapsulatedSecret, ciphertext, associatedData: associatedData, info: info); + Assert.Equal(length, plaintext.Length); + AssertExtensions.FilledWith(0xE7, plaintext); + AssertExtensions.FilledWith(0x31, encapsulatedSecret); + AssertExtensions.FilledWith(0x41, ciphertext); + Assert.Equal(new byte[] { 0x51, 0x52, 0x53 }, associatedData); + Assert.Equal(new byte[] { 0x71, 0x72 }, info); + Assert.Equal(1, hpke.OpenCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void Open_Exact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 32 }) + { + byte[] encapsulatedSecret = Filled(suite.EncapsulatedSecretSizeInBytes, 0x31); + byte[] ciphertext = Filled(suite.GetCiphertextLength(length), 0x41); + byte[] associatedData = [0x51, 0x52, 0x53]; + byte[] info = [0x71, 0x72]; + byte[] buffer = Filled(length + 2, 0xA5); + Memory plaintext = buffer.AsMemory(1, length); + + using (HpkeContract hpke = new(suite) + { + OnOpenCore = (enc, ct, p, aad, context) => + { + AssertSameBuffer(encapsulatedSecret, enc); + AssertSameBuffer(ciphertext, ct); + AssertSameBuffer(associatedData, aad); + AssertSameBuffer(info, context); + AssertSameBuffer(plaintext.Span, p); + p.Fill(0xE7); + }, + }) + { + hpke.Open(encapsulatedSecret, ciphertext, plaintext.Span, associatedData, info); + AssertGuardedOutput(buffer, 0xE7); + Assert.Equal(1, hpke.OpenCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(KemAlgorithms))] + public static void Open_InvalidSizesBeforeDisposal(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + byte[] encapsulatedSecret = new byte[suite.EncapsulatedSecretSizeInBytes]; + byte[] ciphertext = new byte[suite.GetCiphertextLength(32)]; + byte[] plaintext = new byte[32]; + + foreach (bool disposed in new[] { false, true }) + { + using (HpkeContract hpke = new(suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + foreach (int length in new[] { 0, encapsulatedSecret.Length - 1, encapsulatedSecret.Length + 1 }) + { + byte[] invalid = new byte[length]; + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.Open(invalid, ciphertext)); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.Open(invalid.AsSpan(), ciphertext.AsSpan())); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.Open(invalid, ciphertext, plaintext.AsSpan())); + } + + foreach (int length in new[] { 0, suite.AeadTagSizeInBytes - 1 }) + { + byte[] invalid = new byte[length]; + AssertExtensions.Throws("ciphertext", + () => hpke.Open(encapsulatedSecret, invalid)); + AssertExtensions.Throws("ciphertext", + () => hpke.Open(encapsulatedSecret.AsSpan(), invalid.AsSpan())); + AssertExtensions.Throws("ciphertext", + () => hpke.Open(encapsulatedSecret, invalid, plaintext.AsSpan())); + } + + foreach (int length in new[] { 0, plaintext.Length - 1, plaintext.Length + 1 }) + { + AssertExtensions.Throws("plaintext", + () => hpke.Open(encapsulatedSecret, ciphertext, new byte[length].AsSpan())); + } + } + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void CreateSender_AllocatedAndExact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + byte[] info = [0x71, 0x72]; + byte[] buffer = Filled(suite.EncapsulatedSecretSizeInBytes + 2, 0xA5); + Memory destination = buffer.AsMemory(1, buffer.Length - 2); + + using (ReturnedSender expected = new(suite)) + using (HpkeContract hpke = new(suite)) + { + hpke.OnCreateSenderCore = (enc, context) => + { + AssertSameBuffer(info, context); + + if (hpke.CreateSenderCoreCount == 2) + { + AssertExtensions.Same(destination.Span, enc); + } + + enc.Fill(0x42); + return expected; + }; + + Assert.Same(expected, hpke.CreateSender(out byte[] encapsulatedSecret, info)); + Assert.Equal(suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + AssertExtensions.FilledWith(0x42, encapsulatedSecret); + Assert.Same(expected, hpke.CreateSender(destination.Span, info)); + AssertGuardedOutput(buffer, 0x42); + Assert.Equal(2, hpke.CreateSenderCoreCount); + hpke.Dispose(); + Assert.False(expected.Disposed); + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void CreateRecipient_ArrayAndSpan(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + byte[] encapsulatedSecret = Filled(suite.EncapsulatedSecretSizeInBytes, 0x31); + byte[] info = [0x71, 0x72]; + + using (ReturnedRecipient expected = new(suite)) + using (HpkeContract hpke = new(suite) + { + OnCreateRecipientCore = (enc, context) => + { + AssertSameBuffer(encapsulatedSecret, enc); + AssertSameBuffer(info, context); + return expected; + }, + }) + { + Assert.Same(expected, hpke.CreateRecipient(encapsulatedSecret, info)); + Assert.Same(expected, hpke.CreateRecipient(encapsulatedSecret.AsSpan(), info.AsSpan())); + AssertExtensions.FilledWith(0x31, encapsulatedSecret); + Assert.Equal(2, hpke.CreateRecipientCoreCount); + hpke.Dispose(); + Assert.False(expected.Disposed); + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void CreatePskSender_AllocatedAndExact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + byte[] psk = Filled(32, 0x31); + byte[] pskId = [0x51, 0x52, 0x53]; + byte[] info = [0x71, 0x72]; + byte[] buffer = Filled(suite.EncapsulatedSecretSizeInBytes + 2, 0xA5); + Memory destination = buffer.AsMemory(1, buffer.Length - 2); + + using (ReturnedSender expected = new(suite)) + using (HpkeContract hpke = new(suite)) + { + hpke.OnCreatePskSenderCore = (enc, context, key, id) => + { + AssertSameBuffer(psk, key); + AssertSameBuffer(pskId, id); + AssertSameBuffer(info, context); + + if (hpke.CreatePskSenderCoreCount == 3) + { + AssertExtensions.Same(destination.Span, enc); + } + + enc.Fill(0x42); + return expected; + }; + + Assert.Same(expected, hpke.CreatePskSender(psk, pskId, out byte[] arrayEnc, info)); + Assert.Same(expected, hpke.CreatePskSender( + psk.AsSpan(), + pskId.AsSpan(), + out byte[] spanEnc, + info.AsSpan())); + Assert.Equal(suite.EncapsulatedSecretSizeInBytes, arrayEnc.Length); + Assert.Equal(suite.EncapsulatedSecretSizeInBytes, spanEnc.Length); + AssertExtensions.FilledWith(0x42, arrayEnc); + AssertExtensions.FilledWith(0x42, spanEnc); + Assert.Same(expected, hpke.CreatePskSender(psk, pskId, destination.Span, info)); + AssertGuardedOutput(buffer, 0x42); + AssertExtensions.FilledWith(0x31, psk); + Assert.Equal(3, hpke.CreatePskSenderCoreCount); + hpke.Dispose(); + Assert.False(expected.Disposed); + } + } + + [Theory] + [MemberData(nameof(Suites))] + public static void CreatePskRecipient_ArrayAndSpan(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + byte[] encapsulatedSecret = Filled(suite.EncapsulatedSecretSizeInBytes, 0x21); + byte[] psk = Filled(32, 0x31); + byte[] pskId = [0x51, 0x52, 0x53]; + byte[] info = [0x71, 0x72]; + + using (ReturnedRecipient expected = new(suite)) + using (HpkeContract hpke = new(suite) + { + OnCreatePskRecipientCore = (enc, context, key, id) => + { + AssertSameBuffer(encapsulatedSecret, enc); + AssertSameBuffer(psk, key); + AssertSameBuffer(pskId, id); + AssertSameBuffer(info, context); + return expected; + }, + }) + { + Assert.Same(expected, hpke.CreatePskRecipient(encapsulatedSecret, psk, pskId, info)); + Assert.Same(expected, hpke.CreatePskRecipient( + encapsulatedSecret.AsSpan(), + psk.AsSpan(), + pskId.AsSpan(), + info.AsSpan())); + AssertExtensions.FilledWith(0x21, encapsulatedSecret); + AssertExtensions.FilledWith(0x31, psk); + Assert.Equal(2, hpke.CreatePskRecipientCoreCount); + hpke.Dispose(); + Assert.False(expected.Disposed); + } + } + + [Theory] + [MemberData(nameof(KemAlgorithms))] + public static void ContextFactories_InvalidEncapsulationSizeBeforeDisposal(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + byte[] psk = new byte[32]; + byte[] pskId = [1]; + + foreach (bool disposed in new[] { false, true }) + { + using (HpkeContract hpke = new(suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + foreach (int length in new[] + { + 0, + suite.EncapsulatedSecretSizeInBytes - 1, + suite.EncapsulatedSecretSizeInBytes + 1 + }) + { + byte[] invalid = new byte[length]; + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreateSender(invalid.AsSpan())); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreateRecipient(invalid)); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreateRecipient(invalid.AsSpan())); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreatePskSender(psk, pskId, invalid.AsSpan())); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreatePskRecipient(invalid, psk, pskId)); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreatePskRecipient(invalid.AsSpan(), psk.AsSpan(), pskId.AsSpan())); + } + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void NullArgumentsBeforeDisposal(bool disposed) + { + byte[] encapsulatedSecret = new byte[s_suite.EncapsulatedSecretSizeInBytes]; + byte[] ciphertext = new byte[s_suite.AeadTagSizeInBytes]; + byte[] psk = new byte[32]; + byte[] pskId = [1]; + + using (HpkeContract hpke = new(s_suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + AssertExtensions.Throws("plaintext", + () => hpke.Seal((byte[])null, out _, out _)); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.Open((byte[])null, ciphertext)); + AssertExtensions.Throws("ciphertext", + () => hpke.Open(encapsulatedSecret, (byte[])null)); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreateRecipient((byte[])null)); + AssertExtensions.Throws("psk", + () => hpke.CreatePskSender((byte[])null, pskId, out _)); + AssertExtensions.Throws("pskId", + () => hpke.CreatePskSender(psk, (byte[])null, out _)); + AssertExtensions.Throws("encapsulatedSecret", + () => hpke.CreatePskRecipient((byte[])null, psk, pskId)); + AssertExtensions.Throws("psk", + () => hpke.CreatePskRecipient(encapsulatedSecret, (byte[])null, pskId)); + AssertExtensions.Throws("pskId", + () => hpke.CreatePskRecipient(encapsulatedSecret, psk, (byte[])null)); + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256)] + [InlineData(HpkeKdf.HKDF_SHA384)] + [InlineData(HpkeKdf.HKDF_SHA512)] + [InlineData(HpkeKdf.SHAKE128)] + [InlineData(HpkeKdf.SHAKE256)] + public static void InfoLength_Boundaries(HpkeKdf kdf) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = CreateContextContract(suite)) + { + foreach (int length in new[] { 0, 64, ushort.MaxValue }) + { + foreach (Action operation in ContextOperations(hpke, new byte[length])) + { + operation(); + } + } + + if (!HpkeContract.HasInputLengthLimit(kdf)) + { + foreach (Action operation in ContextOperations(hpke, new byte[ushort.MaxValue + 1])) + { + operation(); + } + } + } + } + + [Theory] + [InlineData(HpkeKdf.SHAKE128, false)] + [InlineData(HpkeKdf.SHAKE128, true)] + [InlineData(HpkeKdf.SHAKE256, false)] + [InlineData(HpkeKdf.SHAKE256, true)] + public static void InfoLength_InvalidBeforeDisposal(HpkeKdf kdf, bool disposed) + { + using (HpkeContract hpke = new(new HpkeSuite(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM))) + { + if (disposed) + { + hpke.Dispose(); + } + + foreach (Action operation in ContextOperations(hpke, new byte[ushort.MaxValue + 1])) + { + AssertExtensions.Throws("info", operation); + } + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256)] + [InlineData(HpkeKdf.HKDF_SHA384)] + [InlineData(HpkeKdf.HKDF_SHA512)] + [InlineData(HpkeKdf.SHAKE128)] + [InlineData(HpkeKdf.SHAKE256)] + public static void PskInputs_Boundaries(HpkeKdf kdf) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = new(suite) + { + OnCreatePskSenderCore = (enc, info, psk, id) => new ReturnedSender(suite), + OnCreatePskRecipientCore = (enc, info, psk, id) => new ReturnedRecipient(suite), + }) + { + foreach ((int keyLength, int idLength) in new[] + { + (32, 1), + (33, 2), + (ushort.MaxValue, ushort.MaxValue) + }) + { + foreach (Action operation in PskOperations( + hpke, new byte[keyLength], new byte[idLength], Array.Empty())) + { + operation(); + } + } + + if (!HpkeContract.HasInputLengthLimit(kdf)) + { + foreach (Action operation in PskOperations( + hpke, + new byte[ushort.MaxValue + 1], + new byte[ushort.MaxValue + 1], + Array.Empty())) + { + operation(); + } + } + } + } + + public static IEnumerable InvalidPskInputs() + { + foreach (HpkeKdf kdf in Enum.GetValues(typeof(HpkeKdf))) + { + yield return new object[] { kdf, 0, 1, "psk" }; + yield return new object[] { kdf, 31, 1, "psk" }; + yield return new object[] { kdf, 32, 0, "pskId" }; + + if (HpkeContract.HasInputLengthLimit(kdf)) + { + yield return new object[] { kdf, 65536, 1, "psk" }; + yield return new object[] { kdf, 32, 65536, "pskId" }; + } + } + } + + [Theory] + [MemberData(nameof(InvalidPskInputs))] + public static void PskInputs_InvalidBeforeDisposal( + HpkeKdf kdf, + int keyLength, + int idLength, + string parameterName) + { + foreach (bool disposed in new[] { false, true }) + { + using (HpkeContract hpke = new(new HpkeSuite(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM))) + { + if (disposed) + { + hpke.Dispose(); + } + + foreach (Action operation in PskOperations( + hpke, new byte[keyLength], new byte[idLength], Array.Empty())) + { + AssertExtensions.Throws(parameterName, operation); + } + } + } + } + + [Fact] + public static void OptionalArguments_AreEmpty() + { + byte[] enc = new byte[s_suite.EncapsulatedSecretSizeInBytes]; + byte[] ct = new byte[s_suite.AeadTagSizeInBytes]; + byte[] psk = new byte[32]; + byte[] pskId = [1]; + + using (HpkeContract hpke = new(s_suite) + { + OnSealCore = (p, e, c, aad, info) => + { + Assert.True(p.IsEmpty); + Assert.True(aad.IsEmpty); + Assert.True(info.IsEmpty); + }, + OnOpenCore = (e, c, p, aad, info) => + { + Assert.True(p.IsEmpty); + Assert.True(aad.IsEmpty); + Assert.True(info.IsEmpty); + }, + OnCreateSenderCore = (e, info) => + { + Assert.True(info.IsEmpty); + return new ReturnedSender(s_suite); + }, + OnCreateRecipientCore = (e, info) => + { + Assert.True(info.IsEmpty); + return new ReturnedRecipient(s_suite); + }, + OnCreatePskSenderCore = (e, info, key, id) => + { + Assert.True(info.IsEmpty); + return new ReturnedSender(s_suite); + }, + OnCreatePskRecipientCore = (e, info, key, id) => + { + Assert.True(info.IsEmpty); + return new ReturnedRecipient(s_suite); + }, + }) + { + hpke.Seal(Array.Empty(), out _, out _); + hpke.Seal(ReadOnlySpan.Empty, out _, out _); + hpke.Seal(ReadOnlySpan.Empty, enc, ct); + hpke.Open(enc, ct); + hpke.Open(enc.AsSpan(), ct.AsSpan()); + hpke.Open(enc, ct, Span.Empty); + hpke.CreateSender(out _).Dispose(); + hpke.CreateSender(enc.AsSpan()).Dispose(); + hpke.CreateRecipient(enc).Dispose(); + hpke.CreateRecipient(enc.AsSpan()).Dispose(); + hpke.CreatePskSender(psk, pskId, out _).Dispose(); + hpke.CreatePskSender(psk.AsSpan(), pskId.AsSpan(), out _).Dispose(); + hpke.CreatePskSender(psk, pskId, enc.AsSpan()).Dispose(); + hpke.CreatePskRecipient(enc, psk, pskId).Dispose(); + hpke.CreatePskRecipient(enc.AsSpan(), psk.AsSpan(), pskId.AsSpan()).Dispose(); + Assert.Equal(3, hpke.SealCoreCount); + Assert.Equal(3, hpke.OpenCoreCount); + Assert.Equal(2, hpke.CreateSenderCoreCount); + Assert.Equal(2, hpke.CreateRecipientCoreCount); + Assert.Equal(3, hpke.CreatePskSenderCoreCount); + Assert.Equal(2, hpke.CreatePskRecipientCoreCount); + } + } + + public static IEnumerable SealOverlaps() + { + // Slots: plaintext, encapsulatedSecret, ciphertext, associatedData, info. + int[] lengths = [32, s_suite.EncapsulatedSecretSizeInBytes, s_suite.GetCiphertextLength(32), 16, 16]; + + // Include one-byte overlaps at both ends of each pair. + foreach ((int first, int second) in new[] { (0, 1), (0, 2), (1, 2), (3, 1), (3, 2), (4, 1), (4, 2) }) + foreach (int offset in new[] { -1, 0, 1, lengths[first] - 1, 1 - lengths[second] }) + { + yield return new object[] { first, second, offset }; + } + } + + [Theory] + [MemberData(nameof(SealOverlaps))] + public static void Seal_OverlapsRejectedBeforeDisposal(int first, int second, int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[][] buffers = new byte[5][]; + + for (int i = 0; i < buffers.Length; i++) + { + buffers[i] = Filled(2 * s_suite.EncapsulatedSecretSizeInBytes + 96, 0xA5); + } + + buffers[second] = buffers[first]; + int start = s_suite.EncapsulatedSecretSizeInBytes + 8; + int[] starts = [start, start, start, start, start]; + starts[second] += offset; + + using (HpkeContract hpke = new(s_suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + Assert.Throws(() => hpke.Seal( + buffers[0].AsSpan(starts[0], 32), + buffers[1].AsSpan(starts[1], s_suite.EncapsulatedSecretSizeInBytes), + buffers[2].AsSpan(starts[2], s_suite.GetCiphertextLength(32)), + buffers[3].AsSpan(starts[3], 16), + buffers[4].AsSpan(starts[4], 16))); + + foreach (byte[] buffer in buffers) + { + AssertExtensions.FilledWith(0xA5, buffer); + } + } + } + } + + public static IEnumerable OpenOverlaps() + { + // Input slots: encapsulatedSecret, ciphertext, associatedData, info. + int[] lengths = [s_suite.EncapsulatedSecretSizeInBytes, s_suite.GetCiphertextLength(32), 16, 16]; + + for (int input = 0; input < 4; input++) + foreach (int offset in new[] { -1, 0, 1, lengths[input] - 1, 1 - 32 }) + { + yield return new object[] { input, offset }; + } + } + + [Theory] + [MemberData(nameof(OpenOverlaps))] + public static void Open_OverlapsRejectedBeforeDisposal(int input, int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[][] buffers = new byte[5][]; + + for (int i = 0; i < buffers.Length; i++) + { + buffers[i] = Filled(2 * s_suite.EncapsulatedSecretSizeInBytes + 96, 0xA5); + } + + buffers[4] = buffers[input]; + int start = s_suite.EncapsulatedSecretSizeInBytes + 8; + + using (HpkeContract hpke = new(s_suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + Assert.Throws(() => hpke.Open( + buffers[0].AsSpan(start, s_suite.EncapsulatedSecretSizeInBytes), + buffers[1].AsSpan(start, s_suite.GetCiphertextLength(32)), + buffers[4].AsSpan(start + offset, 32), + buffers[2].AsSpan(start, 16), + buffers[3].AsSpan(start, 16))); + AssertExtensions.FilledWith(0xA5, buffers[4]); + } + } + } + + public static IEnumerable SenderOverlaps() + { + foreach (int offset in new[] { -1, 0, 1, 1 - 16, s_suite.EncapsulatedSecretSizeInBytes - 1 }) + { + yield return new object[] { offset }; + } + } + + [Theory] + [MemberData(nameof(SenderOverlaps))] + public static void CreateSender_OverlapsRejectedBeforeDisposal(int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[] buffer = Filled(2 * s_suite.EncapsulatedSecretSizeInBytes + 32, 0xA5); + int start = s_suite.EncapsulatedSecretSizeInBytes + 8; + + using (HpkeContract hpke = new(s_suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + Assert.Throws(() => + hpke.CreateSender( + buffer.AsSpan(start, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(start + offset, 16))); + AssertExtensions.FilledWith(0xA5, buffer); + } + } + } + + public static IEnumerable PskSenderOverlaps() + { + // Input slots: psk, pskId, info. + int[] lengths = [32, 16, 16]; + + for (int input = 0; input < 3; input++) + foreach (int offset in new[] { -1, 0, 1, lengths[input] - 1, 1 - s_suite.EncapsulatedSecretSizeInBytes }) + { + yield return new object[] { input, offset }; + } + } + + [Theory] + [MemberData(nameof(PskSenderOverlaps))] + public static void CreatePskSender_OverlapsRejectedBeforeDisposal(int input, int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[][] buffers = new byte[4][]; + + for (int i = 0; i < buffers.Length; i++) + { + buffers[i] = Filled(2 * s_suite.EncapsulatedSecretSizeInBytes + 96, 0xA5); + } + + buffers[3] = buffers[input]; + int start = s_suite.EncapsulatedSecretSizeInBytes + 8; + + using (HpkeContract hpke = new(s_suite)) + { + if (disposed) + { + hpke.Dispose(); + } + + Assert.Throws(() => hpke.CreatePskSender( + buffers[0].AsSpan(start, 32), + buffers[1].AsSpan(start, 16), + buffers[3].AsSpan(start + offset, s_suite.EncapsulatedSecretSizeInBytes), + buffers[2].AsSpan(start, 16))); + AssertExtensions.FilledWith(0xA5, buffers[3]); + } + } + } + + [Fact] + public static void Seal_ReadOnlyOverlapAndAdjacentOutputs() + { + byte[] buffer = Filled(32 + s_suite.EncapsulatedSecretSizeInBytes + s_suite.GetCiphertextLength(32), 0xA5); + + using (HpkeContract hpke = new(s_suite) + { + OnSealCore = (p, enc, ct, aad, info) => + { + AssertSameBuffer(buffer.AsSpan(0, 32), p); + AssertSameBuffer(buffer.AsSpan(0, 16), aad); + AssertSameBuffer(buffer.AsSpan(0, 16), info); + enc.Fill(0x42); + ct.Fill(0xE7); + }, + }) + { + hpke.Seal( + buffer.AsSpan(0, 32), + buffer.AsSpan(32, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(32 + s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(0, 16), + buffer.AsSpan(0, 16)); + AssertExtensions.FilledWith(0xA5, buffer.AsSpan(0, 32)); + AssertExtensions.FilledWith(0x42, buffer.AsSpan(32, s_suite.EncapsulatedSecretSizeInBytes)); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(32 + s_suite.EncapsulatedSecretSizeInBytes)); + Assert.Equal(1, hpke.SealCoreCount); + } + } + + [Fact] + public static void Open_ReadOnlyOverlapAndAdjacentOutput() + { + byte[] buffer = Filled(s_suite.EncapsulatedSecretSizeInBytes + 32, 0xA5); + + using (HpkeContract hpke = new(s_suite) + { + OnOpenCore = (enc, ct, p, aad, info) => p.Fill(0xE7), + }) + { + hpke.Open( + buffer.AsSpan(0, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(0, s_suite.GetCiphertextLength(32)), + buffer.AsSpan(s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(0, 16), + buffer.AsSpan(0, 16)); + AssertExtensions.FilledWith(0xA5, buffer.AsSpan(0, s_suite.EncapsulatedSecretSizeInBytes)); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(s_suite.EncapsulatedSecretSizeInBytes)); + Assert.Equal(1, hpke.OpenCoreCount); + } + } + + [Fact] + public static void ContextFactories_ReadOnlyOverlapAndAdjacentOutput() + { + byte[] buffer = Filled(s_suite.EncapsulatedSecretSizeInBytes + 32, 0xA5); + + using (HpkeContract hpke = new(s_suite) + { + OnCreateSenderCore = (enc, info) => new ReturnedSender(s_suite), + OnCreateRecipientCore = (enc, info) => new ReturnedRecipient(s_suite), + OnCreatePskSenderCore = (enc, info, psk, id) => new ReturnedSender(s_suite), + OnCreatePskRecipientCore = (enc, info, psk, id) => new ReturnedRecipient(s_suite), + }) + { + hpke.CreateSender(buffer.AsSpan(32), buffer.AsSpan(0, 32)).Dispose(); + hpke.CreateRecipient( + buffer.AsSpan(0, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(0, 32)).Dispose(); + hpke.CreatePskSender( + buffer.AsSpan(0, 32), + buffer.AsSpan(0, 32), + buffer.AsSpan(32), + buffer.AsSpan(0, 32)).Dispose(); + hpke.CreatePskRecipient(buffer.AsSpan(0, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(0, 32), buffer.AsSpan(0, 32), buffer.AsSpan(0, 32)).Dispose(); + Assert.Equal(1, hpke.CreateSenderCoreCount); + Assert.Equal(1, hpke.CreateRecipientCoreCount); + Assert.Equal(1, hpke.CreatePskSenderCoreCount); + Assert.Equal(1, hpke.CreatePskRecipientCoreCount); + } + } + + [Fact] + public static void EmptySpans_DoNotOverlap() + { + byte[] buffer = new byte[s_suite.EncapsulatedSecretSizeInBytes + s_suite.AeadTagSizeInBytes]; + + using (HpkeContract hpke = new(s_suite) + { + OnSealCore = (p, enc, ct, aad, info) => { }, + OnOpenCore = (enc, ct, p, aad, info) => { }, + OnCreateSenderCore = (enc, info) => new ReturnedSender(s_suite), + }) + { + hpke.Seal(buffer.AsSpan(0, 0), buffer.AsSpan(0, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(s_suite.EncapsulatedSecretSizeInBytes), buffer.AsSpan(1, 0), buffer.AsSpan(2, 0)); + hpke.Open(buffer.AsSpan(0, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(0, s_suite.AeadTagSizeInBytes), buffer.AsSpan(1, 0), + buffer.AsSpan(2, 0), buffer.AsSpan(3, 0)); + hpke.CreateSender( + buffer.AsSpan(0, s_suite.EncapsulatedSecretSizeInBytes), + buffer.AsSpan(1, 0)).Dispose(); + Assert.Equal(1, hpke.SealCoreCount); + Assert.Equal(1, hpke.OpenCoreCount); + Assert.Equal(1, hpke.CreateSenderCoreCount); + } + } + + [Fact] + public static void CoreFailures_PropagateUnchanged() + { + CryptographicException exception = new(); + + using (HpkeContract hpke = new(s_suite) + { + OnExportDecapsulationKeyCore = destination => throw exception, + OnExportEncapsulationKeyCore = destination => throw exception, + OnSealCore = (p, enc, ct, aad, info) => throw exception, + OnOpenCore = (enc, ct, p, aad, info) => throw exception, + OnCreateSenderCore = (enc, info) => throw exception, + OnCreateRecipientCore = (enc, info) => throw exception, + OnCreatePskSenderCore = (enc, info, psk, id) => throw exception, + OnCreatePskRecipientCore = (enc, info, psk, id) => throw exception, + }) + { + foreach (Action operation in InstanceOperations(hpke)) + { + Assert.Same(exception, Assert.Throws(operation)); + } + + Assert.Equal(2, hpke.ExportDecapsulationKeyCoreCount); + Assert.Equal(2, hpke.ExportEncapsulationKeyCoreCount); + Assert.Equal(3, hpke.SealCoreCount); + Assert.Equal(3, hpke.OpenCoreCount); + Assert.Equal(2, hpke.CreateSenderCoreCount); + Assert.Equal(2, hpke.CreateRecipientCoreCount); + Assert.Equal(3, hpke.CreatePskSenderCoreCount); + Assert.Equal(2, hpke.CreatePskRecipientCoreCount); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Seal_CoreFailureDoesNotPublishOutputs(bool useSpan) + { + CryptographicException exception = new(); + byte[] originalEnc = [0x31]; + byte[] originalCiphertext = [0x41]; + byte[] enc = originalEnc; + byte[] ciphertext = originalCiphertext; + + using (HpkeContract hpke = new(s_suite) + { + OnSealCore = (p, e, ct, aad, info) => + { + e.Fill(0x42); + ct.Fill(0xE7); + throw exception; + }, + }) + { + if (useSpan) + { + Assert.Same(exception, Assert.Throws(() => + hpke.Seal(ReadOnlySpan.Empty, out enc, out ciphertext))); + } + else + { + Assert.Same(exception, Assert.Throws(() => + hpke.Seal(Array.Empty(), out enc, out ciphertext))); + } + + Assert.Same(originalEnc, enc); + Assert.Same(originalCiphertext, ciphertext); + Assert.Equal(1, hpke.SealCoreCount); + } + } + + [Theory] + [InlineData(SenderFactory.Base)] + [InlineData(SenderFactory.PskArray)] + [InlineData(SenderFactory.PskSpan)] + public static void CreateSender_CoreFailureDoesNotPublishOutput(SenderFactory factory) + { + CryptographicException exception = new(); + byte[] original = [0x31]; + byte[] encapsulatedSecret = original; + + using (HpkeContract hpke = new(s_suite)) + { + if (factory == SenderFactory.Base) + { + hpke.OnCreateSenderCore = (enc, info) => throw exception; + Assert.Same(exception, Assert.Throws( + () => hpke.CreateSender(out encapsulatedSecret))); + } + else + { + byte[] psk = new byte[32]; + byte[] pskId = [1]; + hpke.OnCreatePskSenderCore = (enc, info, key, id) => throw exception; + + if (factory == SenderFactory.PskArray) + { + Assert.Same(exception, Assert.Throws(() => + hpke.CreatePskSender(psk, pskId, out encapsulatedSecret))); + } + else + { + Assert.Same(exception, Assert.Throws(() => + hpke.CreatePskSender(psk.AsSpan(), pskId.AsSpan(), out encapsulatedSecret))); + } + } + + Assert.Same(original, encapsulatedSecret); + Assert.Equal(1, hpke.CreateSenderCoreCount + hpke.CreatePskSenderCoreCount); + } + } + + public enum SenderFactory + { + Base, + PskArray, + PskSpan, + } + + private static HpkeContract CreateContextContract(HpkeSuite suite) => new(suite) + { + OnSealCore = (p, enc, ct, aad, info) => { }, + OnOpenCore = (enc, ct, p, aad, info) => { }, + OnCreateSenderCore = (enc, info) => new ReturnedSender(suite), + OnCreateRecipientCore = (enc, info) => new ReturnedRecipient(suite), + OnCreatePskSenderCore = (enc, info, psk, id) => new ReturnedSender(suite), + OnCreatePskRecipientCore = (enc, info, psk, id) => new ReturnedRecipient(suite), + }; + + private static IEnumerable InstanceOperations(HpkeContract hpke) + { + yield return () => hpke.ExportDecapsulationKey(); + yield return () => hpke.ExportDecapsulationKey(new byte[hpke.Suite.DecapsulationKeySizeInBytes]); + yield return () => hpke.ExportEncapsulationKey(); + yield return () => hpke.ExportEncapsulationKey(new byte[hpke.Suite.EncapsulationKeySizeInBytes]); + + foreach (Action operation in ContextOperations(hpke, Array.Empty())) + { + yield return operation; + } + } + + private static IEnumerable ContextOperations(HpkeContract hpke, byte[] info) + { + byte[] plaintext = new byte[32]; + byte[] ciphertext = new byte[hpke.Suite.GetCiphertextLength(plaintext.Length)]; + byte[] encapsulatedSecret = new byte[hpke.Suite.EncapsulatedSecretSizeInBytes]; + byte[] associatedData = [1, 2, 3]; + yield return () => hpke.Seal(plaintext, out _, out _, associatedData, info); + yield return () => hpke.Seal(plaintext.AsSpan(), out _, out _, associatedData.AsSpan(), info.AsSpan()); + yield return () => hpke.Seal(plaintext, encapsulatedSecret, ciphertext, associatedData, info); + yield return () => hpke.Open(encapsulatedSecret, ciphertext, associatedData: associatedData, info: info); + yield return () => hpke.Open( + encapsulatedSecret.AsSpan(), + ciphertext.AsSpan(), + associatedData: associatedData.AsSpan(), + info: info.AsSpan()); + yield return () => hpke.Open(encapsulatedSecret, ciphertext, plaintext.AsSpan(), associatedData, info); + yield return () => hpke.CreateSender(out _, info).Dispose(); + yield return () => hpke.CreateSender(encapsulatedSecret.AsSpan(), info).Dispose(); + yield return () => hpke.CreateRecipient(encapsulatedSecret, info).Dispose(); + yield return () => hpke.CreateRecipient(encapsulatedSecret.AsSpan(), info.AsSpan()).Dispose(); + + foreach (Action operation in PskOperations(hpke, new byte[32], new byte[] { 1 }, info)) + { + yield return operation; + } + } + + private static IEnumerable PskOperations(HpkeContract hpke, byte[] psk, byte[] pskId, byte[] info) + { + byte[] encapsulatedSecret = new byte[hpke.Suite.EncapsulatedSecretSizeInBytes]; + yield return () => hpke.CreatePskSender(psk, pskId, out _, info).Dispose(); + yield return () => hpke.CreatePskSender(psk.AsSpan(), pskId.AsSpan(), out _, info.AsSpan()).Dispose(); + yield return () => hpke.CreatePskSender(psk, pskId, encapsulatedSecret.AsSpan(), info).Dispose(); + yield return () => hpke.CreatePskRecipient(encapsulatedSecret, psk, pskId, info).Dispose(); + yield return () => hpke.CreatePskRecipient( + encapsulatedSecret.AsSpan(), + psk.AsSpan(), + pskId.AsSpan(), + info.AsSpan()).Dispose(); + } + + private static byte[] Filled(int length, byte value) + { + byte[] buffer = new byte[length]; + buffer.AsSpan().Fill(value); + return buffer; + } + + private static void AssertGuardedOutput(byte[] buffer, byte value) + { + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[buffer.Length - 1]); + AssertExtensions.FilledWith(value, buffer.AsSpan(1, buffer.Length - 2)); + } + + private static void AssertSameBuffer(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + + if (!expected.IsEmpty) + { + AssertExtensions.Same(expected, actual); + } + } + + private sealed class ReturnedSender : HpkeSender + { + internal bool Disposed { get; private set; } + internal ReturnedSender(HpkeSuite suite) : base(suite) { } + protected override void SealCore( + ReadOnlySpan plaintext, + Span ciphertext, + ReadOnlySpan associatedData) => + throw new XunitException("Unexpected sender operation."); + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) => + throw new XunitException("Unexpected sender export."); + protected override void Dispose(bool disposing) => Disposed = true; + } + + private sealed class ReturnedRecipient : HpkeRecipient + { + internal bool Disposed { get; private set; } + internal ReturnedRecipient(HpkeSuite suite) : base(suite) { } + protected override void OpenCore( + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData) => + throw new XunitException("Unexpected recipient operation."); + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) => + throw new XunitException("Unexpected recipient export."); + protected override void Dispose(bool disposing) => Disposed = true; + } + } + + internal sealed class HpkeContract : Hpke + { + private bool _disposed; + + internal ExportKeyCoreCallback OnExportDecapsulationKeyCore { get; set; } + internal ExportKeyCoreCallback OnExportEncapsulationKeyCore { get; set; } + internal SealCoreCallback OnSealCore { get; set; } + internal OpenCoreCallback OnOpenCore { get; set; } + internal CreateSenderCoreCallback OnCreateSenderCore { get; set; } + internal CreateRecipientCoreCallback OnCreateRecipientCore { get; set; } + internal CreatePskSenderCoreCallback OnCreatePskSenderCore { get; set; } + internal CreatePskRecipientCoreCallback OnCreatePskRecipientCore { get; set; } + internal Action OnDispose { get; set; } = static disposing => { }; + + internal int ExportDecapsulationKeyCoreCount { get; private set; } + internal int ExportEncapsulationKeyCoreCount { get; private set; } + internal int SealCoreCount { get; private set; } + internal int OpenCoreCount { get; private set; } + internal int CreateSenderCoreCount { get; private set; } + internal int CreateRecipientCoreCount { get; private set; } + internal int CreatePskSenderCoreCount { get; private set; } + internal int CreatePskRecipientCoreCount { get; private set; } + + internal HpkeContract(HpkeSuite suite) : base(suite) + { + } + + protected override void ExportDecapsulationKeyCore(Span destination) + { + ExportDecapsulationKeyCoreCount++; + Assert.Equal(Suite.DecapsulationKeySizeInBytes, destination.Length); + GetCallback(OnExportDecapsulationKeyCore)(destination); + } + + protected override void ExportEncapsulationKeyCore(Span destination) + { + ExportEncapsulationKeyCoreCount++; + Assert.Equal(Suite.EncapsulationKeySizeInBytes, destination.Length); + GetCallback(OnExportEncapsulationKeyCore)(destination); + } + + protected override void SealCore( + ReadOnlySpan plaintext, Span encapsulatedSecret, Span ciphertext, + ReadOnlySpan associatedData, ReadOnlySpan info) + { + SealCoreCount++; + Assert.Equal(Suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + Assert.Equal(Suite.GetCiphertextLength(plaintext.Length), ciphertext.Length); + AssertInfo(info); + GetCallback(OnSealCore)(plaintext, encapsulatedSecret, ciphertext, associatedData, info); + } + + protected override void OpenCore( + ReadOnlySpan encapsulatedSecret, ReadOnlySpan ciphertext, Span plaintext, + ReadOnlySpan associatedData, ReadOnlySpan info) + { + OpenCoreCount++; + Assert.Equal(Suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + Assert.InRange(ciphertext.Length, Suite.AeadTagSizeInBytes, int.MaxValue); + Assert.Equal(ciphertext.Length - Suite.AeadTagSizeInBytes, plaintext.Length); + AssertInfo(info); + GetCallback(OnOpenCore)(encapsulatedSecret, ciphertext, plaintext, associatedData, info); + } + + protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) + { + CreateSenderCoreCount++; + Assert.Equal(Suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + AssertInfo(info); + return GetCallback(OnCreateSenderCore)(encapsulatedSecret, info); + } + + protected override HpkeRecipient CreateRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info) + { + CreateRecipientCoreCount++; + Assert.Equal(Suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + AssertInfo(info); + return GetCallback(OnCreateRecipientCore)(encapsulatedSecret, info); + } + + protected override HpkeSender CreatePskSenderCore( + Span encapsulatedSecret, ReadOnlySpan info, ReadOnlySpan psk, ReadOnlySpan pskId) + { + CreatePskSenderCoreCount++; + Assert.Equal(Suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + AssertInfo(info); + AssertPskInputs(psk, pskId); + return GetCallback(OnCreatePskSenderCore)(encapsulatedSecret, info, psk, pskId); + } + + protected override HpkeRecipient CreatePskRecipientCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId) + { + CreatePskRecipientCoreCount++; + Assert.Equal(Suite.EncapsulatedSecretSizeInBytes, encapsulatedSecret.Length); + AssertInfo(info); + AssertPskInputs(psk, pskId); + return GetCallback(OnCreatePskRecipientCore)(encapsulatedSecret, info, psk, pskId); + } + + protected override void Dispose(bool disposing) + { + GetCallback(OnDispose)(disposing); + VerifyCalled( + OnExportDecapsulationKeyCore, ExportDecapsulationKeyCoreCount, nameof(ExportDecapsulationKeyCore)); + VerifyCalled( + OnExportEncapsulationKeyCore, ExportEncapsulationKeyCoreCount, nameof(ExportEncapsulationKeyCore)); + VerifyCalled(OnSealCore, SealCoreCount, nameof(SealCore)); + VerifyCalled(OnOpenCore, OpenCoreCount, nameof(OpenCore)); + VerifyCalled(OnCreateSenderCore, CreateSenderCoreCount, nameof(CreateSenderCore)); + VerifyCalled(OnCreateRecipientCore, CreateRecipientCoreCount, nameof(CreateRecipientCore)); + VerifyCalled(OnCreatePskSenderCore, CreatePskSenderCoreCount, nameof(CreatePskSenderCore)); + VerifyCalled(OnCreatePskRecipientCore, CreatePskRecipientCoreCount, nameof(CreatePskRecipientCore)); + _disposed = true; + } + + internal static bool HasInputLengthLimit(HpkeKdf kdf) => kdf switch + { + HpkeKdf.HKDF_SHA256 or HpkeKdf.HKDF_SHA384 or HpkeKdf.HKDF_SHA512 => false, + HpkeKdf.SHAKE128 or HpkeKdf.SHAKE256 => true, + _ => throw new XunitException($"Unknown KDF {kdf}."), + }; + + private void AssertInfo(ReadOnlySpan info) + { + if (HasInputLengthLimit(Suite.KdfAlgorithm)) + { + Assert.InRange(info.Length, 0, ushort.MaxValue); + } + } + + private void AssertPskInputs(ReadOnlySpan psk, ReadOnlySpan pskId) + { + int maximumLength = HasInputLengthLimit(Suite.KdfAlgorithm) ? ushort.MaxValue : int.MaxValue; + Assert.InRange(psk.Length, 32, maximumLength); + Assert.InRange(pskId.Length, 1, maximumLength); + } + + private T GetCallback(T callback, [CallerMemberName] string caller = null) where T : Delegate + { + if (_disposed) + { + Assert.Fail($"Unexpected call to {caller} after Dispose."); + } + + return callback ?? throw new XunitException($"Unexpected call to {caller}."); + } + + private static void VerifyCalled(Delegate callback, int count, string name) + { + if (callback is not null && count == 0) + { + Assert.Fail($"Expected call to {name}."); + } + } + + internal delegate void ExportKeyCoreCallback(Span destination); + internal delegate void SealCoreCallback( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData, + ReadOnlySpan info); + internal delegate void OpenCoreCallback( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData, + ReadOnlySpan info); + internal delegate HpkeSender CreateSenderCoreCallback(Span encapsulatedSecret, ReadOnlySpan info); + internal delegate HpkeRecipient CreateRecipientCoreCallback( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info); + internal delegate HpkeSender CreatePskSenderCoreCallback(Span encapsulatedSecret, ReadOnlySpan info, + ReadOnlySpan psk, ReadOnlySpan pskId); + internal delegate HpkeRecipient CreatePskRecipientCoreCallback( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info, + ReadOnlySpan psk, + ReadOnlySpan pskId); + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index f15e57b268ff5d..e1a8a5a78cf13f 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -127,6 +127,8 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLDsaAlgorithmTests.cs" /> + + Date: Fri, 11 Sep 2026 14:08:51 -0400 Subject: [PATCH 33/42] Add a representative HPKE test-vector corpus Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeTestData.Generated.cs | 390 +++++++++ .../Cryptography/HpkeTestData.PqDraft.cs | 802 ++++++++++++++++++ .../Cryptography/HpkeTestData.Rfc9180.cs | 353 ++++++++ .../Security/Cryptography/HpkeTestData.cs | 67 ++ .../Cryptography/HpkeTestDataTests.cs | 113 +++ .../Microsoft.Bcl.Cryptography.Tests.csproj | 10 + .../System.Security.Cryptography.Tests.csproj | 10 + 7 files changed, 1745 insertions(+) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Generated.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.PqDraft.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Rfc9180.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Generated.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Generated.cs new file mode 100644 index 00000000000000..11a1057888d00f --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Generated.cs @@ -0,0 +1,390 @@ +// 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; + +namespace System.Security.Cryptography.Tests +{ + public static partial class HpkeTestData + { + private static IEnumerable GeneratedVectors() + { + yield return new HpkeTestVector( + Name: "Generated-EmptyInfo-NonceCarry", + Source: "Generated-EmptyInfo-NonceCarry", + Kem: HpkeKem.DHKEM_P384_HKDF_SHA384, + Kdf: HpkeKdf.HKDF_SHA384, + Aead: HpkeAead.AES_256_GCM, + UsePsk: false, + KeyMaterial: "65fca3ea3b6db29a62bff28ec53c08710fab10b3798e59b678d3224296d5883f03912347" + + "1784ce57b0d85a17cd521196", + DecapsulationKey: "679172205e04663f40fda1018cd46c18ebaa876ede6998ba86b051614ca4d5e4bfbea34b" + + "720617a4b958cc80f6305244", + EncapsulationKey: "04a5f53da8564364255bc36850df793672782a5c9e4a7fb5fb2e2146eb12e4d8477ab1f3" + + "26a361dfd1e41212109510e813380547c68c0964c1908f16f67b902a061be27b2f8b43f1" + + "fab1bf0dbf89f5167ce80aca2c210b8fc0f040699db9ee1229", + EncapsulatedSecret: "049f1da943827d165268869c842962c1feba1fb46402fd3fac50c002cf44bb103c1aa8fb" + + "15a848f9908554624b0eac4573ec258788335421dcbfa625bfc9136cfa0e335f0de018e4" + + "f9517ae0a8863f1b3631343c49c67fd240213f86af1b235ba4", + Info: "", + Psk: "", + PskId: "", + SharedSecret: "f609b68f1e65f077d9cca41ad41d45dd66284adfb8341b9ebdd0ff39c90917a1af423d5b" + + "70d6a917ebf469e093023850", + AeadKey: "bd5133daa3d6c3ceb9d7c92880b68e980837e4a92919dabc58a3080c8c8b6556", + BaseNonce: "85ba152cbfe9fe1fcdf84145", + ExporterSecret: "3a1e052f75515b099695976724cbd0149a0f420f70a647f45710b6939c1fbf927818630a" + + "165623226344bf08cc25e8d9", + Messages: + [ + new HpkeMessageVector("", "", "6c73e9049480742b178340a4e8037fc4"), + new HpkeMessageVector("00", "", "507ac932b2b9e11b1d9fb1450854513798"), + new HpkeMessageVector( + Plaintext: "00112233445566778899aabbccddee", + AssociatedData: "00ff", + Ciphertext: "4d5ce767f7f4429494a4e348d44aa7ffeb0a72bc3ceddae4cc35c52c4ccd8b"), + new HpkeMessageVector( + Plaintext: "00112233445566778899aabbccddeeff", + AssociatedData: "00ff", + Ciphertext: "a6a09da6b1c81c538b31f1d694a0f6140a360c2403c7ef96f06b938c820df51f"), + new HpkeMessageVector( + Plaintext: "00112233445566778899aabbccddeeff10", + AssociatedData: "00ff", + Ciphertext: "b49dc0a43ef7637b24250f3476d3fe5c1b0833de0d422b160b9e8bc1ecc0358d0d"), + new HpkeMessageVector("", "", "2c1225babad5211cc28920c03b2c75e3"), + new HpkeMessageVector("", "", "11b1b89f3730e7a116a224df0791ed19"), + new HpkeMessageVector("", "", "032b2290cf52a8e24e960be8d02ce272"), + new HpkeMessageVector("", "", "bf12ad7bda9c28e91423b7332b8b9203"), + new HpkeMessageVector("", "", "f376fc99b0a829a039db4f80f9830c38"), + new HpkeMessageVector("", "", "d8c632dcd3fcc00ff03832e37d6f6e66"), + new HpkeMessageVector("", "", "acfb351c7e08a193e20fb5d68021cd0a"), + new HpkeMessageVector("", "", "5f1a6c800022c8e031c895bd3b8eee60"), + new HpkeMessageVector("", "", "a0fe365f1b612dd203ea3bc4eb475636"), + new HpkeMessageVector("", "", "8ca537baa911d3967ea49e60ebe5ec71"), + new HpkeMessageVector("", "", "f580e03b1c5910b11ceac98dabfaaa2a"), + new HpkeMessageVector("", "", "b529d58b30a4667af24c8b22948633a8"), + new HpkeMessageVector("", "", "18616ba4e59c00a671c1f25850bcfaa8"), + new HpkeMessageVector("", "", "dc012fbbe9bb36441491e02945771971"), + new HpkeMessageVector("", "", "6ba748b207c3d766b25bde7eddb09957"), + new HpkeMessageVector("", "", "cb496b3cde5c3e3f008fb908109572eb"), + new HpkeMessageVector("", "", "908dfc592a95ee795f29ab8f8ddb6bac"), + new HpkeMessageVector("", "", "4975dd25ead35b8b5c070070b56e6f1c"), + new HpkeMessageVector("", "", "0dae18f660a5aa597252ad35495dab0c"), + new HpkeMessageVector("", "", "f752c3572053b57795752164fd3efc2f"), + new HpkeMessageVector("", "", "8fec6a36a31d0a38c763a1f175dea4c1"), + new HpkeMessageVector("", "", "8a0eb7340aa94a23e505c1617c564a64"), + new HpkeMessageVector("", "", "376b6a55626f3adb36d805c0a8551a6e"), + new HpkeMessageVector("", "", "9d92be353e5d42952f1c0c96cdb1a5d8"), + new HpkeMessageVector("", "", "936b554b4a485780e2fd662578a09c20"), + new HpkeMessageVector("", "", "4d02b3f33b2dd51e363cd5a21a238376"), + new HpkeMessageVector("", "", "e57c9349b2a41af89efc9e88cc0d87e8"), + new HpkeMessageVector("", "", "371af2c4ff14255b5294221deedb432c"), + new HpkeMessageVector("", "", "91eb08ced15d065a705ab50b273246ff"), + new HpkeMessageVector("", "", "c63ad8bac5ce67e5c0a555aa7aca6478"), + new HpkeMessageVector("", "", "2f3b852f909c2fff43739263a7a86e5c"), + new HpkeMessageVector("", "", "b6e59e83b4216f8b5cacde55d4da2754"), + new HpkeMessageVector("", "", "ac57438ea086ea2fff32b5a7498b266a"), + new HpkeMessageVector("", "", "c79dd726d8ac6414e2278fb616cdd189"), + new HpkeMessageVector("", "", "537dfa59d16fdcf011abbbba1e6e7087"), + new HpkeMessageVector("", "", "99cb93ec17547f7a7eb376bd82ad64ab"), + new HpkeMessageVector("", "", "542f78beea85884e834c896d6e0f903b"), + new HpkeMessageVector("", "", "6dbf269bba0df31d422aa025c57ffd92"), + new HpkeMessageVector("", "", "fcb5a9855eff8f98d3fdcc845bcca5ce"), + new HpkeMessageVector("", "", "1e24f0fc1e4b08900a7927332e4f7d1a"), + new HpkeMessageVector("", "", "bb46b20bc5b2a363ad315126396c6b1a"), + new HpkeMessageVector("", "", "2fb26e42f60f0e562c97b49079c09110"), + new HpkeMessageVector("", "", "a3ba22a1ae999d02461489b03619cd73"), + new HpkeMessageVector("", "", "046fa189d73b27ee97c8d19ce837b20d"), + new HpkeMessageVector("", "", "e8aa3c6ab9cbd5f05547b927311ba42e"), + new HpkeMessageVector("", "", "0626c9267022caa4a1187eedcef49177"), + new HpkeMessageVector("", "", "f841c988ca98f719ddc2d1c307a4007f"), + new HpkeMessageVector("", "", "81689e98ca7e127dcdf4e476589539c7"), + new HpkeMessageVector("", "", "348ab2c888924a7016e84732fb85b130"), + new HpkeMessageVector("", "", "539b613cd364291b856e0fce52971a70"), + new HpkeMessageVector("", "", "0c11d97a1d35ee5660272ef6eb4caefb"), + new HpkeMessageVector("", "", "cae0b80132c4531a8665b23bbadb25f9"), + new HpkeMessageVector("", "", "1732da5e7ab67924853d1a770f598cb6"), + new HpkeMessageVector("", "", "ab11d6cd2f89ff4a54a913b91cbecb2d"), + new HpkeMessageVector("", "", "9e8a379cceb4ef9a39620876db121492"), + new HpkeMessageVector("", "", "d341a0160fec81922e61f5591515f5a4"), + new HpkeMessageVector("", "", "cce01d29d7ef2af40c984d309db3c899"), + new HpkeMessageVector("", "", "90bbe873f894d2875771c5f3fe799559"), + new HpkeMessageVector("", "", "5395913e4053b5680723ba0c625f4867"), + new HpkeMessageVector("", "", "ce6ab6ae71c44a6e50fa34ff470267e0"), + new HpkeMessageVector("", "", "c03f324c929d168715520b1a5f65c161"), + new HpkeMessageVector("", "", "4e6652e40d48c1490d613d2fe3d64a71"), + new HpkeMessageVector("", "", "a5ba258a1b3a4b4e2f0a534e0e1cc2c4"), + new HpkeMessageVector("", "", "f0f3a07fde9458309140faa1a4efc7f9"), + new HpkeMessageVector("", "", "dd51e702ff6beac465d0c6b3cd6eb07f"), + new HpkeMessageVector("", "", "2bd48eb858b7379421188856f2bc7f79"), + new HpkeMessageVector("", "", "bf5794f127921c8d833ec0342abfcd70"), + new HpkeMessageVector("", "", "23c26960d339d57fffc3360d63cd9171"), + new HpkeMessageVector("", "", "b50b45dbe024851ab9b56be3dd52a299"), + new HpkeMessageVector("", "", "8a9499781e0fef40442787355c9d9be6"), + new HpkeMessageVector("", "", "4f7678cd03b744193e25a64928cb205a"), + new HpkeMessageVector("", "", "3f4f5e44f4f591389734f78b23c2cb99"), + new HpkeMessageVector("", "", "2f8433500661d80b4a660e593f983b5e"), + new HpkeMessageVector("", "", "5d60001f3d5a31932a6a79eb383bf9e0"), + new HpkeMessageVector("", "", "c82e38ad31ab76e46ce10504085451d8"), + new HpkeMessageVector("", "", "d8ce2a903095e7294edaefac66b67534"), + new HpkeMessageVector("", "", "d26b7bf221e06d1e7cb84df8876a6a52"), + new HpkeMessageVector("", "", "756beeb5062111cae28512212ca28d9a"), + new HpkeMessageVector("", "", "1eb6c07dba5735443ebc73ce9be15f7a"), + new HpkeMessageVector("", "", "368cd50f8ace7327e9092ac9d3f42068"), + new HpkeMessageVector("", "", "380f2f09c834171e1614c9d1fd3b2a13"), + new HpkeMessageVector("", "", "c7381aae3d55378eed0df485f0ac6b93"), + new HpkeMessageVector("", "", "8322727b60674803ad9f4b906d9a1f2f"), + new HpkeMessageVector("", "", "9ed08eb22a9b207af39a16ba295bce42"), + new HpkeMessageVector("", "", "0001193ff92b250eb3f463b902f07839"), + new HpkeMessageVector("", "", "0743a56a85de9ad0e36147f61022ed83"), + new HpkeMessageVector("", "", "248dff41b0588caa554a5bf14a5cb5b0"), + new HpkeMessageVector("", "", "af26d3b1672c5147762f02929eaa7c98"), + new HpkeMessageVector("", "", "2cdb9cf8da87012651365ddb71ea9cf9"), + new HpkeMessageVector("", "", "3295b3dbe0bd2ba4688895cce35847ca"), + new HpkeMessageVector("", "", "127f2440952e201bb140f77b2f6026ba"), + new HpkeMessageVector("", "", "d476b20552b0711fa51d7db5fe0930c0"), + new HpkeMessageVector("", "", "fb549ae2058be9e9252d1cd60fe68c06"), + new HpkeMessageVector("", "", "453b967df5a95b9db3f794f0f9fe62eb"), + new HpkeMessageVector("", "", "4d541789ecf4bab71db9365033a842d9"), + new HpkeMessageVector("", "", "003b34f7802749156063949eaa2d283a"), + new HpkeMessageVector("", "", "3c3d8883c41e050fb0dd008330b447b6"), + new HpkeMessageVector("", "", "3d5a366768d3921dc5cb0992f0992f48"), + new HpkeMessageVector("", "", "3b858e3c8c9a1110f18a28ad50d1ba07"), + new HpkeMessageVector("", "", "2f5cdbe7bcee8fc60e2e1a92db590a4f"), + new HpkeMessageVector("", "", "809e527b982f967926f34f5658c2bbbc"), + new HpkeMessageVector("", "", "366f94cdd157f05fc1d958c9d6d12ffb"), + new HpkeMessageVector("", "", "ae6ac16657c6a7e089f212ff73a13c3e"), + new HpkeMessageVector("", "", "94ecec7bc421f7bcf5b744bc95e2d3ce"), + new HpkeMessageVector("", "", "174a7b6cf363e74b0503be824ed82d02"), + new HpkeMessageVector("", "", "7be67120cb29e4df7c3991f5a62ed286"), + new HpkeMessageVector("", "", "951d51eb9771749d3ce39a3ded3f0447"), + new HpkeMessageVector("", "", "d618beaefd7e5540aad3e3687669f966"), + new HpkeMessageVector("", "", "6ff7f94e5f99f8515528320f6db00637"), + new HpkeMessageVector("", "", "a03b235b4db4c267e2bebe320d372fa2"), + new HpkeMessageVector("", "", "de5b0a65679e22b43ea56c6d198507c7"), + new HpkeMessageVector("", "", "344072c032047b7a195f485fb8dbc86e"), + new HpkeMessageVector("", "", "126872e363d08de409661f7f5d7bd6ce"), + new HpkeMessageVector("", "", "67541981ed2fd181903e3fc2019037e5"), + new HpkeMessageVector("", "", "9b6556337cde542fbbd6c49c99b02fd7"), + new HpkeMessageVector("", "", "bfb934471264c6742033983acbc6e0cd"), + new HpkeMessageVector("", "", "bf0d2088bddd139bae07606d3b58aa97"), + new HpkeMessageVector("", "", "399b7472f977901c2317a447d59dd7e8"), + new HpkeMessageVector("", "", "6da1cbfbb2e348cc5f1e58dd4f45b820"), + new HpkeMessageVector("", "", "3ae4f9f21e09d9eb94063269c422216e"), + new HpkeMessageVector("", "", "d325280f58bdd1bf2d684ad917354ad7"), + new HpkeMessageVector("", "", "703d85292de8ed92217edb4c4e22e423"), + new HpkeMessageVector("", "", "93448464fcd906e2a64820b6c3256d3e"), + new HpkeMessageVector("", "", "d25afe98adbb4b197a969d4261e6addf"), + new HpkeMessageVector("", "", "a22bd6fa2d66c5e03783d896eef12ded"), + new HpkeMessageVector("", "", "afe739fb178a733995f1673e9b6a6c96"), + new HpkeMessageVector("", "", "df09de4d36eeb93826c55011a23aac28"), + new HpkeMessageVector("", "", "e2593d17d7e6e49b2a90c9841a8d652e"), + new HpkeMessageVector("", "", "bed949f623203fabb71bbefd54b2225a"), + new HpkeMessageVector("", "", "efa45ec8a2390ba41492e553ccaaf477"), + new HpkeMessageVector("", "", "0738c47d8065985ba2230fd21fdea3a9"), + new HpkeMessageVector("", "", "38e24ec46e53639dc2e8aedd1c0de499"), + new HpkeMessageVector("", "", "2ee879a28622ea0cc5598ed6c1b356b3"), + new HpkeMessageVector("", "", "4f1e63a8daf7853e3a826cb41991ec04"), + new HpkeMessageVector("", "", "c9c7ed6d0561783ffd2e6bebc75194bd"), + new HpkeMessageVector("", "", "5af3ed3a0a7683eba78250cb341a5a3b"), + new HpkeMessageVector("", "", "c821332456edacf7f5cc20602c4f77fe"), + new HpkeMessageVector("", "", "6099f990e93aa09b12287b02595e68c2"), + new HpkeMessageVector("", "", "d1e02de5a8b4186deee11eb48cf5a977"), + new HpkeMessageVector("", "", "bfd763f5e86c511883f7bd4f4f30634d"), + new HpkeMessageVector("", "", "400aa307634f31bcd0a7cc7a545e65ff"), + new HpkeMessageVector("", "", "5d2444c88f4a5fe3405b9657e127a2b8"), + new HpkeMessageVector("", "", "60be92eeda1d68aea6f69e05f7377cce"), + new HpkeMessageVector("", "", "52a1b972896b0e86191fefa5525ec124"), + new HpkeMessageVector("", "", "79d934d47c6039cfac7b8e69102fe74b"), + new HpkeMessageVector("", "", "115a8f4f21377a213d444f0575050cfc"), + new HpkeMessageVector("", "", "6e51cdd9481067a0cd10f7fe642c0305"), + new HpkeMessageVector("", "", "2c53afd9ac128991a4a3a85b86364002"), + new HpkeMessageVector("", "", "77d8d67734a54a97c68bff070eef8aac"), + new HpkeMessageVector("", "", "236d7c1a4b3201d3e617264af1cbaf82"), + new HpkeMessageVector("", "", "965c5f8fee98af99f93bf1ea6932ac40"), + new HpkeMessageVector("", "", "afc4dc0871589fd65d73290357607963"), + new HpkeMessageVector("", "", "950c3fc9d7d0629542a8f65f3473f640"), + new HpkeMessageVector("", "", "8e78f447800b3c704a7bd4f32679cf7b"), + new HpkeMessageVector("", "", "2f9545e364ae45f4f60b600b56d48668"), + new HpkeMessageVector("", "", "d6657f2ffebc7740e2d2406e94fca4a8"), + new HpkeMessageVector("", "", "d0327cbf83d8e0cc0724ec682e166973"), + new HpkeMessageVector("", "", "580836014b7ce7c9b7961981c62166be"), + new HpkeMessageVector("", "", "edd413317605104acd5383425a7b4216"), + new HpkeMessageVector("", "", "b7c29b6f027e96e1f15c1fa4cf392655"), + new HpkeMessageVector("", "", "dff0094208de59ed1c981d5f732fa6d2"), + new HpkeMessageVector("", "", "b1880c8c4eacca2cebfd43e1a94ec72f"), + new HpkeMessageVector("", "", "15cb5b82e5846048c8476e40cf2392b0"), + new HpkeMessageVector("", "", "324427ce742abee4015a48fe4849714a"), + new HpkeMessageVector("", "", "e8deb09978663d5b97bd4ec3c79d8510"), + new HpkeMessageVector("", "", "c6e5281277ef52ea0d039cc8363f46d9"), + new HpkeMessageVector("", "", "a40aaee623b0da70e79aa0ca2081e618"), + new HpkeMessageVector("", "", "fbf87290d246d3f94a4891badd7b064f"), + new HpkeMessageVector("", "", "579e478f488f36e3fa8c7dfc02c32c73"), + new HpkeMessageVector("", "", "a5d7449592cbe74764c35c8572c427c5"), + new HpkeMessageVector("", "", "88216d5f82f5b5ce3bf60c99117670fa"), + new HpkeMessageVector("", "", "9d341aade2ca4e0b0d6d86de8a24ef05"), + new HpkeMessageVector("", "", "39f91128e4507f271eeaf16db11c6cb1"), + new HpkeMessageVector("", "", "5dca9bbafe6a8c172f212e51c2d4806e"), + new HpkeMessageVector("", "", "12b7c735afa217724258316fca83604b"), + new HpkeMessageVector("", "", "20303e64add07c09e5b32f26b0633b6b"), + new HpkeMessageVector("", "", "19420fa827342ff19e8ae054d77e22cc"), + new HpkeMessageVector("", "", "ec4ebfcf5a992d213cc1821c35293e2a"), + new HpkeMessageVector("", "", "52490aba517f9ac852b914fc8de89770"), + new HpkeMessageVector("", "", "c04fda5e4b96538944dc32d5fb1b052e"), + new HpkeMessageVector("", "", "db5118706ec7536dfd584941c8a1f75c"), + new HpkeMessageVector("", "", "2408b77c711ecf097aef5bd4388ac777"), + new HpkeMessageVector("", "", "20ef667b4b522b99a81224428c92d79f"), + new HpkeMessageVector("", "", "8fb9ea58484aca00927dbc9e8440566e"), + new HpkeMessageVector("", "", "dfbfadae07119d2f499004117caddd0f"), + new HpkeMessageVector("", "", "1dc8ea799aea65102736cb0cb251944c"), + new HpkeMessageVector("", "", "cf67cad00da0455600431f2ad52533be"), + new HpkeMessageVector("", "", "8a6a59fbe3a5834c1c551eca18b18ef3"), + new HpkeMessageVector("", "", "0f2d9ab416e3d502ec5f799bc44a83df"), + new HpkeMessageVector("", "", "2c22327f657cc1e955ae199e1e461e36"), + new HpkeMessageVector("", "", "8c09ddc54284b080299b82eab2f074f1"), + new HpkeMessageVector("", "", "39a6845ffcf2eef67467ced01ac8b338"), + new HpkeMessageVector("", "", "5378ba390922888219a6df9dd2b7f01f"), + new HpkeMessageVector("", "", "01bbf9b998715bb6d3969b4f386b4e91"), + new HpkeMessageVector("", "", "12a3e5baa22ea2eeb4c50bd51c172d3a"), + new HpkeMessageVector("", "", "843ede3861788530d62299335959d6d6"), + new HpkeMessageVector("", "", "23fa742cdc672bacc51931c583a7e5ea"), + new HpkeMessageVector("", "", "e3a7f890cd4e548ba305b3848add6da6"), + new HpkeMessageVector("", "", "294ff24400128d5559d0c7e6a4e39898"), + new HpkeMessageVector("", "", "e2a90ec4b7745333a0f6dde3a6b48da6"), + new HpkeMessageVector("", "", "107adef16dd9ae8e52033f190e219703"), + new HpkeMessageVector("", "", "eaaec506c2dce544af583338800099f2"), + new HpkeMessageVector("", "", "79f3569308118630d82cc890eeb257db"), + new HpkeMessageVector("", "", "0af812e3490611cdce164b8a61951da0"), + new HpkeMessageVector("", "", "f5784b138609d1d93686ce1d5f017d8a"), + new HpkeMessageVector("", "", "98a39ce9db8e52c0caddbfa75b08e1fc"), + new HpkeMessageVector("", "", "6e9943dc5f10f2ec983f9325e9a64b99"), + new HpkeMessageVector("", "", "6da48787db2552e7f7183524ed5d0a1a"), + new HpkeMessageVector("", "", "b3364e0973c368029cf51d4ca44dc5b0"), + new HpkeMessageVector("", "", "bfafbb87b492331a6929ed2dff8b19dc"), + new HpkeMessageVector("", "", "8621e54341680a3f6759f762c1be2f9b"), + new HpkeMessageVector("", "", "85fc9a485a71cec1c638767be0348049"), + new HpkeMessageVector("", "", "37bb8980e99ddfdd3523274900d44c9b"), + new HpkeMessageVector("", "", "f6424a76627d7c220bdacb9a18352c98"), + new HpkeMessageVector("", "", "9eb7178ce95606c5e97e17592b3971da"), + new HpkeMessageVector("", "", "f6d0b3a49a5902367e7e24c0ffc3b71f"), + new HpkeMessageVector("", "", "40801e6740bf14d35215cff1706fa289"), + new HpkeMessageVector("", "", "e741f34155642d30bf444a56f4a6eb27"), + new HpkeMessageVector("", "", "dce17c10d0523b6023896f9d4d4cef34"), + new HpkeMessageVector("", "", "831811106baec168bffc8ba3c189df5f"), + new HpkeMessageVector("", "", "7585e7df273d7dc371eae216d221042e"), + new HpkeMessageVector("", "", "2bce0f55a522305ae4cb4a7e6a8341af"), + new HpkeMessageVector("", "", "1a9e8ea6fff0774de5d0343c0bb5bc07"), + new HpkeMessageVector("", "", "2dc5b069308bde1a0c2025f12240fb18"), + new HpkeMessageVector("", "", "a94317f749c85e48cc57432e698f12e6"), + new HpkeMessageVector("", "", "a1a715ef4995f281473ea3c3fa9b9d24"), + new HpkeMessageVector("", "", "1bfe10f6faa3c1b98403b7172883bc96"), + new HpkeMessageVector("", "", "9dc28baba9b80525f01926bd760eea55"), + new HpkeMessageVector("", "", "d75413e87d39e2eb16a5386542fc0d4a"), + new HpkeMessageVector("", "", "85e00b6a0792559d0608615977b49d27"), + new HpkeMessageVector("", "", "652a0f5f8f27eb375806e17c462088e4"), + new HpkeMessageVector("", "", "b46e1f1a5a8dfd00125c6ab540b3e4c6"), + new HpkeMessageVector("", "", "ebdc083391ac9edaae9c30004000de9a"), + new HpkeMessageVector("", "", "21de9eed0c130004af1f417556c300c1"), + new HpkeMessageVector("", "", "232a18c94eb26ca49f509f106fa497d4"), + new HpkeMessageVector("", "", "b99f45eb85ac2fcdb10f9dacc33e8772"), + new HpkeMessageVector("", "", "d5bf9fc9c28a208e784aa3342c5814f1"), + new HpkeMessageVector("", "", "4c9181b998747dd04297970dfcb71b80"), + new HpkeMessageVector("", "", "d14b5aa2a639b8918be15915d0eaa556"), + new HpkeMessageVector("", "", "30871838b7f82ed4c8e1d8a359992fd7"), + new HpkeMessageVector("", "", "ba7f834a80d9e5301999732dc404a6f9"), + new HpkeMessageVector("", "", "e2ba45bbbd542838173021b3a85fe9d0"), + new HpkeMessageVector("", "", "9f28028d5bd76e1a5f9270dd21f1ee84"), + new HpkeMessageVector("", "", "3652e8592710c8dbb5fd221312df0943"), + new HpkeMessageVector("", "", "a9017568d262d420fef0786926c67d13"), + new HpkeMessageVector("", "", "5d6efba21a86c5464eedc2204466a801"), + new HpkeMessageVector("", "", "8d51b85d3b285e15b0ad81cfbf3668f6"), + new HpkeMessageVector("", "", "ac41307b502ef2f2d309dd856de63598"), + new HpkeMessageVector("", "", "ac245d316ee3b3e545c2a5d82237c579"), + new HpkeMessageVector("", "", "ae99bc16886977ef236e8e8094781e1f"), + new HpkeMessageVector("", "", "ae8674df95abe147f4e2901d051f53f6"), + new HpkeMessageVector("", "", "638c9d4c45fc9d592cdaadb9b7b990a4"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 0, + ExportedValue: ""), + new HpkeExportVector( + Context: "", + Length: 1, + ExportedValue: "14"), + new HpkeExportVector( + Context: "00ff", + Length: 257, + ExportedValue: "a56e126351393fe847c54721e4a2b4028909c7e05ed468ebfcdf9a01980996833befe63f" + + "340c7b0dfe385319be260589b44115c9ecbd4531261f46fb067b7c9a090476c572fcaf11" + + "4212e787983c8a8dd94ff987b2a918c82325fb5af996d55a2c6d180a8e379ed16bf65ade" + + "eae2b9d84c04f72240d0bd7b1a44311685032ac0d664f917d7daad7d1ddaed15c4cdec6f" + + "03bc0d000bc184159ad6d0c63ec3f66174a07e58e38462ad2b789752bcc4a56832957151" + + "9ace514a3822f544437c5d80396cf080c70b1c7ca2c185586edc3a40a9d71967743a5755" + + "ad21dc81ff0c95ee09faf7f19d00fdf1c97ce4529d2a49a524898d15b075157c53f9327a" + + "9af1f7b7da"), + ]); + + yield return new HpkeTestVector( + Name: "Generated-SHAKE256-Psk-MaxInputs", + Source: "Generated-SHAKE256-Psk-MaxInputs", + Kem: HpkeKem.DHKEM_X25519_HKDF_SHA256, + Kdf: HpkeKdf.SHAKE256, + Aead: HpkeAead.ChaCha20Poly1305, + UsePsk: true, + KeyMaterial: "1ac01f181fdf9f352797655161c58b75c656a6cc2716dcb66372da835542e1df", + DecapsulationKey: "8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb", + EncapsulationKey: "4310ee97d88cc1f088a5576c77ab0cf5c3ac797f3d95139c6c84b5429c59662a", + EncapsulatedSecret: "1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a", + Info: new string('a', 131070), + Psk: new string('5', 131070), + PskId: new string('f', 131070), + SharedSecret: "0bbe78490412b4bbea4812666f7916932b828bba79942424abb65244930d69a7", + AeadKey: "4924bb1b2198c1a7efa305bd67f9b9ba79d26197f8a4562754fbd1eafda9f38a", + BaseNonce: "5175d3d14646ba1cc11b0a8a", + ExporterSecret: "bfee9c59c405ad5529d174b326fb381e29abcfcd0381fb8b2cfc669e493a9c2457698375" + + "f7812922ce3bc6cc6d49984729b28c1089b118dc1192313a4c715640", + Messages: + [ + new HpkeMessageVector("", "00ff", "6de7030ae52fb2a277e5e7f5a655fbb5"), + new HpkeMessageVector("00", "00ff", "54848b8f81b282ced36e38632ddd41a712"), + new HpkeMessageVector( + Plaintext: "00112233445566778899aabbccddee", + AssociatedData: "00ff", + Ciphertext: "615806034f4fda020064e69a1f39f6412684fffc73222d5253276e00fabcfb"), + new HpkeMessageVector( + Plaintext: "00112233445566778899aabbccddeeff", + AssociatedData: "00ff", + Ciphertext: "3000616a9ebf56113cf94a7e543d9a01efcd639a2c251e4d5c86238e40bd3e80"), + new HpkeMessageVector( + Plaintext: "00112233445566778899aabbccddeeff10", + AssociatedData: "00ff", + Ciphertext: "e98c133856899ee1a5a8f8c484c91a19f5086c276dd24c2cfe7ed6d21bffb3d10a"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 0, + ExportedValue: ""), + new HpkeExportVector( + Context: "", + Length: 1, + ExportedValue: "6c"), + new HpkeExportVector( + Context: "00ff", + Length: 257, + ExportedValue: "8c3d68b61fd45ecdee4e821a880abbf339b11b3ddc73ba4e1b7d79b72fa2923387be8c59" + + "315b0d76d8070b75161ae7c2b9db571b654b2cbf40455af9b69179c3f4d255baf3a017fd" + + "bc8a1bff391c9a83757b24b6da741df4ec5b4a0a8dee268a0411a685dc134993d61a39e8" + + "6afd739118acfdf481e983f4a14e91d661a0d52ecfffe9426923a73ebe7cf40874408894" + + "8d059d0ec4870e9504d941186dfbe7d56bf15e1f21901bbe429dd5f9d1047f232f5f7591" + + "0f0d043623c5a22a9e2d90c82e69aa1814805423edaec75516152b5f9849bd693d7a4372" + + "949c3243dd3f2d61d7642c9c0e1a01c5201192497657463aeb7d38db27a57421ee651caa" + + "0abb46f261"), + ]); + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.PqDraft.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.PqDraft.cs new file mode 100644 index 00000000000000..d4e4cdf10895db --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.PqDraft.cs @@ -0,0 +1,802 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Generated by HpkeTestData.Generate.mjs. Do not edit by hand. +// Only messages 0-2 and the first three exports are retained. +using System.Collections.Generic; + +namespace System.Security.Cryptography.Tests +{ + public static partial class HpkeTestData + { + private static IEnumerable PqDraftVectors() + { + yield return new HpkeTestVector( + Name: "MLKEM512-HKDF256-AES128-Base", + Source: "https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#appendix-A.1" + + ".1", + Kem: HpkeKem.MLKEM_512, + Kdf: HpkeKdf.HKDF_SHA256, + Aead: HpkeAead.AES_128_GCM, + UsePsk: false, + KeyMaterial: "53c72362cd4c0d3c04fb963bb2d8fa3b61be2a83befb53883892f68d1e6af3ee2ab07a44" + + "5a87cd505fe27f3434e35c8ad26e6452b51f24e5c9d3d174b326fb0e", + DecapsulationKey: "0466a81fc187205d5925aaa518e98d6cbde2a1aa63d756da4a62f873f6a0b1f1418d0eec" + + "2620055b8537aca724d18ad436e47972f85f4c5c5d2cfb1c62b100bd", + EncapsulationKey: "3e774db858732c35a408388fceb66cc61777d361c85a72b1e844422cca0effcb5778cc5d" + + "e43acab0ec682b0b318fa4122bac224d10c193b5933758320587196fd50cf76c94a1222b" + + "2a330a9fdb32b0ec8a42931c531bb025095e49fc0df8a5205b32149e7354d63232c8199d" + + "d9e6654ec0bef0937484b0904950b05b29297dbb410be008ad441ebce23052c8cda593bb" + + "1bf4b5e0e520ac4a53e9a1bc38591c3f723e66c177a6715d3a365b0c156a5f72aa439ccb" + + "42944b8f47a32b446ab6d8ce58096a778a2322b3b467f2c5a17875fcd6a69ee74ea29709" + + "3798765f6851e6402b77c723b335c5c8857d94090d41fa2b5e54ce5b7194d29175f14171" + + "8c36959e6142402b2e816a856d914b1f2b3fc62329cccc7e23fb9d14828e44941997b323" + + "bcc90c497579d49462d79671809a38d79c3137cc4258563134d4662872260cc13b5c9902" + + "15959721082c827bab0cb9a2559b16eb704cdea7cfe60b24224b13d055b382ea9e0920ad" + + "b3592689b3635239ccf8db631f585957a37c57fa8d92fc7907d0266dca9b55fa5b68d308" + + "d8d6cbdcd8b583fa804c03ac620003911b506396709bea2ad1a7a83f697c9ec741cf6504" + + "64bac0093a0efd462207620c13b86cc0dc10dc9442870250390384c5e2b8f5294f9c88b2" + + "6b09c8d504c286d16109a56838830caa35b231811bda3a1677d3087ce216ed9c8ce5cc40" + + "20d290efcc60ded511bb990795d00674a12641e885ceda249e387ce470716188cd359c66" + + "f4e61908e12757b97c3095168fb8714681a96c54c1cd401231a5500896a8eabaa521da68" + + "0e759222d9765746624ab6c79a754b10477a0ae12ba2175f6f701569bb15d2ebc4e6e9b8" + + "e6f1c021b31edccb152ea23365db5ef396c893a9ba12cca8a3847e99f6c732523e55844a" + + "17ba34cbd6042d1b7ffdc47d6031a7587162ada1a283267eaaa31da17cc611038fff5144" + + "6e0384c7397450b05084859cb8f79e6a1775710a19f8e9896e83861debc6ffe1ba8ebb1c" + + "dee95da61c30f6c99091e31b4f3c593352a1253910261c187c60a420e1445951cd797b74" + + "a4a7b53b50b0c1370e69e65fcd29aa553682cc42f6802ac4b8a3bd7b1c482ff85523aa28" + + "48b95ee9654b55af", + EncapsulatedSecret: "a66b74747cbe84af3c6c824792211ab3b5ce0847f49090036c4ebf5b9767a6564c0c6cd5" + + "2137245582e773b5dbf530ade89b05e7df571c278476b5f874e5aa1590a47d9ee2c4d2c4" + + "47eb4a070e86ff448bec7d38412cb7df4463b2d42ce0691d59a97c25f0a2b6b39f07ab04" + + "e4c9b11e6a27a738e9c3b6869ca803602b5fb78f071e3f447845fde4d1d0893f650ea246" + + "eb599bcfbef61e3d5f03c6a20bcb99c610a22712045c8e37f549c353949ce27bfdd953bf" + + "ef97469e1a46696dde84326dd6a7eb79af9107ede1b61f4d5d17c8859a604dc0b67fc712" + + "f545efbc8ab6bba66931396769874794ad44654d63e57fb36a8ed188c9dff164100eb265" + + "81d0853719f88619220ba1815f8d737727a35bc33227e2580c5b68baec549e0cb722caf2" + + "4a4ee28cf585cc12e7de2a845a5b0599cccd94a49be72acd52e0eb1c26cb764bac0e25e9" + + "3fc015456ea2f6f2807a47a46fe5c1715394a5a913812ee17a4684b9857f229b61add744" + + "0301e12d00b4cb2c406a28de76bfc31b5c239dc96d94a3f29f3a85b507118d0c66fca652" + + "c33da63dc246024f429399c1cd9531e6e85c6ef30d6954270c895c5e318744b5ed728ea3" + + "26f242e1be5c519bccdb01668704d1328afd97476157e1322525b994a7a48d3ed1b9097a" + + "9eb632b84a92e257ef191fb5e469cfc9a5943175b7c52e3e0a83d6df64038fde3cc2938b" + + "ed141a5e19011247f87183602f5b98dc495fbdf463e8ede9da6f970632dad9cea242e91d" + + "1681ba801be84190e1a13e6d1820fe846945a41d920b1f717f12b10d70a9b203377316bf" + + "d3f6217b758a949a899de90119e934fdf0daae68f6a8b0b89f93064c727e14691c4149c1" + + "147d1ae457d127eda31a4b63fd8aa7f5e501b3e1de20ee1024c7cb0010a0996adebb9527" + + "c4919ac3903296ca8253facf1a225faa95aa7b26889a1302132cbfb519cda8cb60dd1464" + + "6c3e94cc335881015b0e63191b82711fd498442cb448cec3c2581e26019632f66c2d3012" + + "61e199026eebb351866d82212c1a5b4acef12e7c22f4597c185301cf606f6ce69482f81a" + + "630539d3cd1611875fa28a1d", + Info: "346636343635323036663665323036313230343737323635363336393631366532303535" + + "37323665", + Psk: "", + PskId: "", + SharedSecret: "996dfcdb0c50e9fa4748dfaf6a641ff4e26de2f84e1d19047f5bb0589043e194", + AeadKey: "7bdfa98081ef3777a154d3cd10018539", + BaseNonce: "b9bc4aba6b886a03673e7083", + ExporterSecret: "2e2fe69cfa6dc979c005cd7adeee7d44a76f2aee89210b36e7a967fab89069a7", + Messages: + [ + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d30", + Ciphertext: "c80cc04803277f688c29c5c0a9f222f1977c7bfc5cc5e66ff4210c5bc315ceb347135531" + + "581a411dc61bb35059a781fbf8c52e9539c1e55bef647086ea64a7cf3e5d6c4211f38f74" + + "7276"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d31", + Ciphertext: "f3c5de027b9b14bfe43fcfdd66c136b47ccfeb03096a212f480c74bdbd2987c9844b103d" + + "16ce2d98dfde273ac757a389bbe1ddf7295c1b6903495fe54caeb337f7a1856f3861a888" + + "a051"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d32", + Ciphertext: "cd629625b73b85743e4d88636a9459d0222e45704d7a5d0bb22b8aded487731c7173b090" + + "fe56293d06e822c087ea227271e08e1f6ef7a3b17e630f4c74545806723662fb03fa8b6d" + + "059e"), + ], + Exports: + [ + new HpkeExportVector( + Context: "70736575646f72616e646f6d30", + Length: 32, + ExportedValue: "c0186fd042852629d81ba939012f98d444a5c19bd7cee946389fa016cbb3d9a3"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d31", + Length: 32, + ExportedValue: "d4db343a5d04f812edac36da2b3bf29cbbb10e058b94de2a9a3ccabc621783bf"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d32", + Length: 32, + ExportedValue: "f9ba1dbc672d27b24880c74d16417c0e6e0e0ff68fd37684aa654b3e915289a3"), + ]); + + yield return new HpkeTestVector( + Name: "MLKEM768-HKDF256-AES128-Base", + Source: "https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#appendix-A.2" + + ".1", + Kem: HpkeKem.MLKEM_768, + Kdf: HpkeKdf.HKDF_SHA256, + Aead: HpkeAead.AES_128_GCM, + UsePsk: false, + KeyMaterial: "a60b35f174ce9ac7a4ff5b9f81e38125b03506ecbd56a3a55c31ece0f59070520729773a" + + "61a499d5137daaef824b493848b6e4dd332a815ff19aa9f58a381eb8", + DecapsulationKey: "80008d036609972cf761d7e2d3b831e48d3e941cda94fbf9bae09bca87373f9bb7411f58" + + "fd3324ba1d0daa5a7b42768c5b53e1df29c28d4f5428a8233a905089", + EncapsulationKey: "1a9664765a7f3322c86c451287f56dcafb799cc39a17e8c33f911a8703b90b3a99bbd712" + + "962c0eb0b9cca65843cd784ada958b261116dea17e0fa2533ca23498c0793078c5b8254d" + + "2a162e4042085d3c164d4615270bb56e4393672056c9f1babad3b95307b04ed54caa699b" + + "cc3cb24b1b488fcc5448d65bf9d8cdb9cc2140b7a18535232c14432e4bcba045bf80e00c" + + "8110679375406f278e96218de5662436d96b161829d23bceb66c338b78ab2eb956f90b86" + + "7738754763ce13f35eac655aeae10949a582810a625964a58f1a6d15ea52ceabb463b211" + + "e0e1b19f736e1af4a7d2c02485a4538b1551fcccb996a605ec93719c876a5c86a8782b78" + + "565c603c538856228257033831eb99072a1a2d0413b007a7fa9a013efca0ca9800bae097" + + "b12790506640cbbbb903d545b87a20adabaebc7c46c781a11fc08f3dea902a3064ceab86" + + "525a3e33758e1fc76d17eb9796e0afce3099e2b64300d4586b24ccf29185c1e26b0ae299" + + "25527e79607e48786d27d651a290174f966200b3cea6b28add1368a410c753b692f53887" + + "63e9530e3a4db61627a56b70d6fc545e5b8712d3546a7c0548a03efe3a13120914341335" + + "20b21dc04b6d8b70c01894ca5714cd811b6f8b671544575bb5016d5e8788abb0206b22a7" + + "d8f64a09ca4f4d773ec7029a6bec726aa612bec0213377b3cc8a9218b465abc8b242937f" + + "abe57ae932151092c3cba5ae2734088f67244346c93a024506485af5f7ae6a8257f04b91" + + "2e6c5ab9f12777473b8efb73a8056ee006294ae93ea03a204a09a858d87b09d425881819" + + "716b4cee09461daa8fdba8ad0ee81aeb60bd9ab59ba9c59381d3b147244c1d2285b5d79d" + + "5154abb794170a945c1a0acf2e4c82a5093371836bd153b6c9bb0fd3f21cc682bf1ad846" + + "7ebc88002840815a1bbd340603a8a45c002c3613503c6ab4e8620150c2331987559d6987" + + "900a803b05bd4b609f3ef6cbfb74c854eb83552c3db404af93746918eb7afd920434681a" + + "2eb618259c791ae58fa7e43fc083c14e3001e09c82e3e31efe61a2126936a7560a4239c8" + + "5d366d01c67d08a2158d3605e1a8335e42b4c1d8457be130451968ab3c43255539234428" + + "92c9bd2a238b942902b6d1cf5603674811020330c9b8f77d274c885a982167ea995aab3c" + + "4ad4c15a2368ab8bbcb3f27064e3cbea93b652c869db424f7722839f4b55e1acafd0a4b1" + + "d6b561c3a416cfa2c175933da440b01daa4970e3b1888814dc972fd27030b15051d8348a" + + "c75b91752304acf3961670341b334fe9591fd60469d1dac085ca745ae0cbd7d48f951746" + + "ac523c42679a105978af3a36dfe36bcccb9386b96c3c498483d7c8ad869204d7a298d085" + + "105c8cd40601782a874bb8337bd00a2cf39650dc499bf32e67233219bc597165124e1942" + + "52d47f23d78df18b43dcaaa29640269ff9197db15ebf919b3c702e16c01dd80881f62760" + + "0d35c60c059daa53cd6114cf29f739302996cb3132e17a2e852038569223e2fcc23f6a90" + + "559b9b4a1059d85b827fe2568558376cb946f9bca484030988e53f61431ab60a276d9b43" + + "a85b38637aa017535533a00f72064602948c83cb1e050b0584a7628b2697d23476af2b0e" + + "68b5d57d4718ede11de2080798a309cc8102fff10ad012bd3d32efdc2052d8fd", + EncapsulatedSecret: "86a740f005d8a10afb812bf6d0a97ea0c2a5c7a729af0c286418726ace66995445a5f6fc" + + "099b498ac3ef9e752ddb7ef88bec618c7fb4516385d681328381924c0723d92ffc9765a2" + + "5ee558e29b1f7e8a38aff5debec491ef6fbbdca10170f54c2bd08aec077920e59380e5cd" + + "81983cecc15b2c4b201f2c2cf70640eeee3224a7849d8efc6404b317ef3b81be28dc1334" + + "ab4c71b16682db94ae7115da8069fef82a9dba4ea1671cfb5707333e4e10763101693448" + + "0368514d3ead43fb3c916dba86da2071066d288b12b8c9397757e643c41ed7e240c5bdd9" + + "24e30d923e90af5d03b7adfe1a3bb055195d37309e28a55a10ee859f812d06145a634635" + + "4ca8dfd72a829ef348de166d5dde7e41f60d3387933b41ce33d29c134ab96f2982c51388" + + "4e7bcb790d31f0a8371e990c6cc9c1572d25d0efc7c0e979c88e1b6935d74b7ba78d5383" + + "7ca5e486e8da5e6055d13e0a1f566cbde09caa2e73c1c1f0afb2f7db73a820a738a99763" + + "9a96dac040e72eb8b18f48e7d9e964e3625ac12883a8a10d2f4f907e7703021347885081" + + "ef38c8445698717b947aa7df75f1dd3c320b71a431dd5d18b0de1ed307dba95a201aaf8a" + + "b37d8edd71c5da6097cfea602429abafd9924de42757fcd203b8bc6feafdac6d4178c7df" + + "eda5435eba612a0a0c603171ce6ec491363c706730667445f69c9754ec0922c9f5dd5938" + + "94f5c5a9284888376d22002242831babcb86feb2c7ec5df5b463e2469fd7a80d4aa6ce8a" + + "c21f48dbb95de64ddf15b1bd0f65548122f8f61c9f41a3ea75a2d66e970a04a79dc73c73" + + "b3e1ae0420677dcece4b36338d01ef7ff381a09c9aaffd1fc4f46c2461c0b8ee92de4337" + + "9f086151d7065c4acac7a3ffe9205db754f717f9108ae4e364f6f375acf5a565de7e2c12" + + "49a353df05258c8eafdda03410ba2778714cacb64f61d494c4d5bcb420f718954a1db1e9" + + "37df6b2483ca88b3d78476d3b2c78fe068ac2b584832c73fcccd2ac38aad93e35c717c1e" + + "172ac096eb2909f63ba780d009fc7c83b3acfe5be95be3ab11c25c86638a2cf3a2ebd23c" + + "b15b501c77eca0a28dc0a621ef1f337fcd7138e01d1324e985bd7eaf6d745530334b052b" + + "8d5c5b22064a623b9c7960d128e1665c8c7f8869d5afe7ed40f02f02edff64c577fd1084" + + "253aa40b1f8482725f6416f3e4d59add5002ba6b6d5b7be87a1aa3af89bba6c40838011f" + + "93dbb1bda2ecbd24a446619d15841f1ece8e0fb7769bd350d6bdd23ad074d0ef2d683bd7" + + "a6e64e97039aa92ea70f84f42ccc6ae0871da60362de0dfc6ea338dcae3eb2bf4c0e0746" + + "588faac369eb8b6b0e38a7a96d265fc5d4a91c994158168757e9750c80a90b571ec6c914" + + "eebf67901c7974732947d3871e41c9d59cd78bfd8e7e1fa6023545d54d2070ef64ecb70d" + + "aa01f0b508764f6a3ad19d680927e78f86a664e62313b57941524bd9b691cfc514ecca83" + + "e172bc1e0c2b2b62ea7d896b3f4218fe39c1fd892b9852ac170524524205ac19ac58102a" + + "4e7562d2dd453d80", + Info: "346636343635323036663665323036313230343737323635363336393631366532303535" + + "37323665", + Psk: "", + PskId: "", + SharedSecret: "750477fb7421ec8e787e4505a99278b0c8aa15b9783453e90939cf1527617dac", + AeadKey: "7d1031a2d6d232331f70495250fabb0d", + BaseNonce: "5974495634213151b309dfd3", + ExporterSecret: "abafcbaa704ff2bcd964ec7a3ad23cc66ff02f0df43576d744a4c2cf1f51e581", + Messages: + [ + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d30", + Ciphertext: "4b7dd443eecc37d978fb2e41808d8b3025e0afdefb57b96be0b2ee1c1e437a6a676e3798" + + "12eac544f55e463d07b20cbe88225ba97736c48ba39bdd96bcd783b43a67eedb77bcd612" + + "820b"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d31", + Ciphertext: "86e2d78ff8f07fc10e651796c0b51516200dfa224b35a99b460c9147eda0a42266cffa57" + + "63709ad8ff6ac0db08ac9a33ce4e8eab643380ea55fcd1d272dc8ecbed98d3b8e60a5380" + + "5187"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d32", + Ciphertext: "0908bf6f5163b23a220d8217ad53ec21dc8dbeb10d0ef86fb116c0c4a29a56a88b49b596" + + "ac7b90483b2bc64bdeb23006973992aaab358e825259acd3b56b80eafa635abc230911af" + + "016c"), + ], + Exports: + [ + new HpkeExportVector( + Context: "70736575646f72616e646f6d30", + Length: 32, + ExportedValue: "4ce822c6932f0cbc2f1fbc3a652bbef4976ac63833d35fcce20b35c4a3d05443"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d31", + Length: 32, + ExportedValue: "f8a7549322f1921e48ffc17b05b71d54640bb0253c6e4589b0ee748120d9e735"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d32", + Length: 32, + ExportedValue: "7be483d9a999b4ae759bc3ea1a713bc989540fdc376c36472a7c1038a6c6ee04"), + ]); + + yield return new HpkeTestVector( + Name: "MLKEM1024-HKDF384-AES256-Base", + Source: "https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#appendix-A.3" + + ".1", + Kem: HpkeKem.MLKEM_1024, + Kdf: HpkeKdf.HKDF_SHA384, + Aead: HpkeAead.AES_256_GCM, + UsePsk: false, + KeyMaterial: "d6688a981deeff1d1273426af8a44aab877c50b6e8ac74b11e01a5960d97c03bffd96348" + + "94d255c424c80c74e0930b85b9f4c60e22a3efb09f4bad4749be427b", + DecapsulationKey: "73a9ffe155d39edcc023b11171ad6cf541ff85eff68c33b521ba25cbb1b7079bf848b63f" + + "5b8ca53f809255b51f1bef24b342d706a77cb460981e16b2ce737552", + EncapsulationKey: "aa0a1b451a9aa747370a94ba416aa977c5bac5b19def1a59f1e9182564b1b8d4c761d30c" + + "598415ca200f30c2638d8635e4e8b67df03e207c2dcb2048e01590f5da7fff8c5d6657c3" + + "2f5c2cc5e6b807e647a50b252817c9063bc542282cc1fb6f400b2382e7b39407c5234b6e" + + "a2319828348dd1a8b92f506f67eb686726138781337573b6ae878133b7c8548c307656c8" + + "17f529d8f068f031c70b044ce1a09d3f8c8caec215dc3a68ad805617d11aff61c9a2d163" + + "2b4b7fdb8b67f37a2814e9974490b307b3ad3ccc9fbd3c577d9c147cbb69ea55c604f678" + + "95e2921f8b75d3b71e7126032acac6d2e5cbdad32587b6a99f9b12df949822e6392394a9" + + "761cc1bed0155bf28338e50c8cfaa1f0115f4c5384f6e8c46885588fc79cff4430fe745d" + + "4b74c1bf85cde60236e3b0b87dabac96677ca6c0cb7372bcf39708d5c1aff38235829b67" + + "d6d51eaaf36bcfc4c82980ca024a0495d8c3fd5a8ada25a3f7b33e78fb86701b710fd0b3" + + "9f4b9b5649a654d719e5a96d63ca306e3862ccb52937a3bc1cf8079a996a27ba34bd9c54" + + "cc9b99fd504f758abb3ca519c6521095b8964c478063a2a5e668c88eac1feda35ae0865b" + + "1cd83bc7d1513bb21188b7c81d71cac1048756680fe15493c79c5b3de8563698cc900a30" + + "833793a46a7237623cb70b151024855c69ac2124ce426c8375a0874c6718b55487560893" + + "5124a90357c277a6c4bc487266066bb0d7a7e76b5d3142a6c2aa8ce6da7540794eceb932" + + "9da1244e8b9fbfa0303ea9511d9c9a5ce39bd3c6358e4914d5a96a1c6bb47e7916b2c711" + + "4078c003d939e4a22118c7626d995b5455c6cb07ad59275e024944d599c817648ad08526" + + "25525a34fc1343325ebd286cfd2bcfd856cea2078415c49e485abb3bf8bbde7a93bcdb0a" + + "8ab84f4712370f71bc1f80c89fb9c548613ae9b267ade1c50915c3b6d9a464a6387ce71e" + + "2f268bae278e264acdc8f8ced1d85047aa2ee05a0c530117e1e06ff75624eae953938a7b" + + "ac13698bfc0139eb930ff799bad7ababd85b74babcd6d05212b55db227339694cb72258b" + + "c365860d56bae2e21c5c886286e9a10f747d9345b9ba25cc741a742dbc1dd1193eea58ab" + + "08496f1a8bac7520c5ad109ff3b8380f764c0ed84008e54cc56c96a32187f6ba972fab32" + + "3100b851e0586427223b67b4ec32011b33053bd1ab7d515b3ff02cef9c8994411085abc0" + + "7d4b43c16147f15877703598d5626a85e56e665724d4f62c07590c5354109d30b46f6b57" + + "64fcb45e2630d18b6bc1d767e814ccb215cce3861600d1b17be16cb1e05b26577ef53c7f" + + "1a0c4ea15488fdfc52c5808c9c2854fcf1633f977cd9333fb97933a9624bfee00872dcaa" + + "ce051c189551a45963d9f20eed23bbcedb37e9b29039a294a299795cf416e8990d76f73a" + + "f6474d157107afe0ac5cbb16a1241fc013a7350011b4ba70d9ac50c644b479f23e81ac98" + + "6bacbd462b980f7a17c8426dd16a8a1f6289221255a8437a1280bd06b4c8a8f1b566c13a" + + "5841abc6aab414264932ba7d56f94e8970570d8586ff6c7754822c0a0c21cd2b190a8a3c" + + "e6294346a430757b40361833628b603a9c331165c8c7c55a45ab8b57118c885c93eed7b7" + + "fc8736ddd0a10f8a0d1d4b1d3f4bb41b09c67269493b03ae004917a625c4ccd064a21a82" + + "2d45afb4d30b64a888460c37b1fc23082213cf468b10c60280ba9c91d68cab57433ab6cc" + + "b7474f0c0074ce23ac09e28e7795c72f496581878a999097fe940582b4af1784267bab2a" + + "f681119dd58ab35ca006c04e7345091cd489a0e4c42ad1cdd300bc13b395ee3121a9276c" + + "a01b6c2382212872729f9b9d10f2a7778085a22966f44601c72524b8b168e368cb83466f" + + "06746c0df330bcfc7019456a9740802cf6923b508a3326cb53265a4a3b961077884e38c0" + + "73d29de9c81189410467ac6c46f7c38e5142528a945ad9a510115122f519ca937e84e075" + + "260a323d900bb5e321080c264a95543106481cecc0d48275ebdbb4bdb6b5a9794a42d94b" + + "fc5c644fc8a5f3b7bbef1b3ba09a48473b3d2933b61cb474d292764531821fb72a0eb72d" + + "e454577a126ecd4778722c3c55e3b09d860a0941189e680847112b759963a1862612ab72" + + "36883d177067ae105d6db70d5aa86aa243004121baae838830b87c6ec203168987cbe6a9" + + "bf8765bc7ec4a51ca64cd7b22cd5a3877dfecc3c", + EncapsulatedSecret: "a9a4421ea715fc52329204452922e20220e14c1488bdc5f5b9e33916ff8c4b46481b8cad" + + "2a8b383f06b629908c71f7f7816afdb03c0a3e97fb58bcddad60cadd46582c182e4c75c6" + + "9283f5797efae3b0ba5d0957a8ee460828c53b925d836a1616e564e0c2df7342400fe16a" + + "2efd0d441764328be60229f172d3244102345367ace697c8332e931b32adbd47ed638543" + + "36a5eeb3128a4b555568dfe926206f93c52285fc036e26a5d55e1e40939f8504877e0eaf" + + "2744549e4c6fcc4bf8e85458dc66294699fb146d0be363b03444ee85cdf57cc373d46309" + + "7b8015121f91c00b66fd805d32ff0fb09a5c8c5af225f3c7c4d7fb7c4a39cf75878b16d3" + + "107edcd80ad10a450b1035b4144f3d662486b9e05f46f001ff8bf98688ceb4987bb0cdd7" + + "5f58e184419c80ef55bc4ec0295fb290119af95d95ba24c0e2d0c371af7ad7a6e4a34b63" + + "5dbcb2961571eb64e8087b8a2c16b2a2a4f71e94129bbd11bb4a2678dcafbd23bb6add7a" + + "3473880773f9b92812637b672edd418fb2630fe94d481789657d90afa4ceb7617ce2732e" + + "aba2c6a019b03ab7976e886ef9b50affc46676e536575b46dd39fa95e1f6d242914fde95" + + "2e07c789a6c41b0c53fa3423173bbaeba6b578c1ef84d5a49a044c69aa6cdba1a7c1b373" + + "f31ca39216c8713469b1f37ac6436f4ec3e202176f767416b45421eee5c9603b26be09dd" + + "cceadc052bbc71f5f32867627523772be84e62878bf6882b5b3c863e0a79c89a2efb0744" + + "ce880ebe3f5c729baec24ca2c6541cc79f6e32a8163386a99c527233cfe88521467c6c2d" + + "d786f4957834b4b24729235853622579ddb427929676b8e338de6e08b512c3b26bab191a" + + "3d7ea2f97f6b5c56d5d92df4e922fbfbe16c748b30ff1d2816d7d8431b79ae3432a9f8d0" + + "9e9e2577c1c3cf987cfaa17d699716892d4cbd8d5cb4fed656d58e1b3f5acdc6cac8afd3" + + "1dc50bdfc8260c379b6479df9770fe752a9c1a34c5da2671fec505d2da1dcfd3f2231d20" + + "a812908e73709d144717ba761ce5e200b65af01ac5fb13e86ccc72cdebbac15e0f45ffba" + + "10120b844b5c4619ef546d5b493bdbe4c90947bbd3023126c099cc6c5a916a46cef0ba46" + + "5c4d4734f2d0a4504ecc33f674d3e2560d2df0f201acad2988e454be3247aade5b5d7a2c" + + "a3059e75f1b09cb3653aed9139281aa66b21ad3ff8d8c4f331b253078c70173d907e4114" + + "0307b495cdd83de81b12ada65c441a50d834c32ed661a1686f2bbc57736b2b859302b545" + + "fb2c4214b5cc9b5b85e56c65ae02a1b15e561670019d477639773026e3d233578f6a61e7" + + "21cbb60c17a0d4035704b6dceb65c3e4e28772fd94df756b6a50931224e6d70f51993031" + + "fa96770b1d9df23b6fe1e0b6707e08a95f40357140287586b18f2cebc36544b90a82f086" + + "474fd1728f7d102e9f448f27fc632ec1805e0afe41061ac0501e91b5711e0431a856490e" + + "f6b2ce206d51d40dfcb2c6320aabf4904b9a58220b70bcc99b6a990a56398560dd0bb78a" + + "a84bf45e43e0ee4b3e03c5ab31608ead929df2e9fff6a4ff9e9f863592c471ab12d944ed" + + "3eb4ea10d80efc7ca22294b54bccc059f04170bdbd6d0a74f2366c0d26b97f0c508b3bbb" + + "913ce024b1bd3a5c6ec5f0643d2cab3cf78055334bd67e065564830a42c75590bdb5484f" + + "e758902ab79192255c250567b761bec6c6605fccaf50aec508103ea028065c34a799b208" + + "654a4b5260a4ff2ba8100c39ee128eef57ffbe36e009e530fcf215176184f956d875eca9" + + "4390fa1b3b264d4ce4d1dc0bc042596e4da23073a4a6fa4bcd2d95380ceca4b4411a5d37" + + "26f3e41e5c8c0792ca5b378414e3044df8fe7074245a610c59c8a741a110a54807d17250" + + "4ba9a0c078a88c33f610f7ac28e6ca0399fa8dd0a11c4c3bedf6ac81172dd7f6ee5dc6ae" + + "b9aaba4f48e0ffd604df818f06c09734a546f69661e9d0d544c7e4477dd644aa6ba9a243" + + "e5f6c941405a83216b2cb76e1e58cfc7566bafb11de4025cc40b7c24e0439c6ed791bc79" + + "4b996e7f0473da9a542ff4aa68c14f224400588b6e4337db6a78558a89ac54f93d2cc076" + + "fd15547f1f7618d738c63217e7453d861a7141019f75cd7ec5a6c0c8b690290ea3d2b61d" + + "142cd4803a3cd36b3b0d0b4ba545a454e23c14c723e7f21bc1e9d2b571ecdcd21a463a8a" + + "793e3013e211a404d414070a1aa635c35e8c8b87", + Info: "346636343635323036663665323036313230343737323635363336393631366532303535" + + "37323665", + Psk: "", + PskId: "", + SharedSecret: "ef4fb9e654c1f7cfe66da7f2d0ed39429067dfdf3b65723ae941221177f55552", + AeadKey: "85147d20f1ff72eb9a5d3de9a3c920ab0cac7b00300e6b07a7f53b87ef07e1b0", + BaseNonce: "75437389e6da148fdcaa309d", + ExporterSecret: "2bb8e6404f0df42e403505b7888d04bcdccf4cc33a93d90cdcde8b3604b5278a38aa272e" + + "5ae8aa4a0a8ed96eb4ee86f7", + Messages: + [ + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d30", + Ciphertext: "9d16979cb9ac997886c0ec51ed2c049d7ec53b369467026157ef061af23695b996e1893a" + + "fd2173c310546859e82eea9c16e0a1363bc994f2ff708e5d60089c1b233f38ce6a7fbd17" + + "6744"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d31", + Ciphertext: "36ac3e4d4b5709eb863f6cd257f046b2f36077a010952a9e2811494adc95667674880e67" + + "2d9cf1fa4e9e55245d22ca553c86a60cce2714108ba52865dc4addd1025c69b3206598f7" + + "8903"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d32", + Ciphertext: "4e9c9424c210f9cc0d2dd090bb44a022de0b52d3e475d6c4371104f2da02e4a5bc40e993" + + "d71f13e36d0b94a730e62198bd73195d688e68ca37dc4fc1cf6f0796e701ca7752204ba8" + + "06be"), + ], + Exports: + [ + new HpkeExportVector( + Context: "70736575646f72616e646f6d30", + Length: 32, + ExportedValue: "5bfa8896ed24e61987426ef9c223994f5ea8088f25f6cd46bfed4418a358c352"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d31", + Length: 32, + ExportedValue: "b9074bc3442b61a9d528f26685d741a37b7fae652c726a69f2f4a8d75c2dbfcb"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d32", + Length: 32, + ExportedValue: "c324cc1566312c5ed6d24d96a6c318efcf735828dacd615a2bcfb1a287d4f6d2"), + ]); + + yield return new HpkeTestVector( + Name: "MLKEM768-P256-HKDF256-AES128-Base", + Source: "https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#appendix-A.4" + + ".1", + Kem: HpkeKem.MLKEM768_P256, + Kdf: HpkeKdf.HKDF_SHA256, + Aead: HpkeAead.AES_128_GCM, + UsePsk: false, + KeyMaterial: "3bf888035cc5f48fa476c2ccdb73a5482e97a0d0578fa710b1e393ca9716b6f0", + DecapsulationKey: "1f25a59a6b22ef57b8e48a6cfe739b9ec13e9cf57e82dfd6e0480e0324cf905b", + EncapsulationKey: "67a132b24aba43d90a9ff65c02cc8446ca3131b7496348359647bc145b52f6db1253414e" + + "12515a17978a2437753fc754faf312ffea54d6c304f723989681037dfb24b6e89dd72c09" + + "5b5a2583e8656209845d1241876c4f98093d19d3ca36b8ccdef92471dacb96066dcec175" + + "03890456002bc43c60129cc9be2796e74ccb8cfcae45963cdb60890cf35ee6fb51b65c29" + + "e1461c89c849f8e3a7ca7a32b3e0318ec8381443950b386200c645fcb5065c0cae32871c" + + "18f4b0a587895a78bb7139ad14b238f34306d367b4e8590d1fc40ef30a30993b34b7c380" + + "c541653fb6b8f060cfabc149a3440ad61ac4248b162df284dfb9433e623435d64972d9c4" + + "09f37f4ad28ec20161ddf43ee9d61ac754747f8a0201cb6ed9657b87071e843b78954158" + + "1db891c8aa60bdf8965582a970300af8cc2a148630216b75d31ab5e3faa5ed5a38ec5c6c" + + "459812fbf2412afb96b9095aca552b2572430edb97dbc967ee142df0d0b83da744891a50" + + "760326eab584c1859fc42810d7c879fc67b78827313dc01b2a374e0442ca8f20249617b9" + + "d5042b2ee140a3c93e73072e9a1163b3d643230466e66871a8b42bc032475259037188c8" + + "939660994c900d99011cc83a308a65390827ed8bb5ec85b4b8226e44104b78c09d3b69bc" + + "2741cc579244b9763427809cdbd5aa8e838ffe3762eb0a9e23f2c3b25340c799c503609b" + + "50313c9f183a04a801ff1b883eb397d370a6c4b05d8f2cc549c40563d16bb01296c7c4af" + + "e17634226c95eba58a0af16d0d589a7a0c68ed887d36c98c8b75819a4674a79553f97639" + + "e194aa735b04e157127b251658965f141c7319990ebac3cb675a43f2554fdaaa211fd503" + + "7938935915c46a01cd0480a9bc624d2555b947d795e142608de987c7c8ae255ab71cb61f" + + "0ed7cd0ac88caca1a7b597a2800b15a61222c5f168315985e26378381168a885aa774a61" + + "51c065b8047bbf5aa9f3115f55343577782e75121c5d1bc59f4caeb180b5fd1c8811b967" + + "db91a5f8a1052adc0eb9941f51328fe6a9608c4bcad74a5168b23475ca1d4be73f99ea59" + + "d209bba4a747f589a054ca2854b22924b6bfc07b0a193a7883b2ab62248a0833800bac17" + + "59129dc465836fd65721cc08beca917807a3b0e29b0a8192a0a96fc4597177e6779f005b" + + "e566705a8a2daea40c8cb74b89030600466bbb62232c1a4fb0073257e9c8a518370d97c3" + + "7448989ce9ce08844cd5114b2db0906892cfac404ed54288796670e2516e9384ccb8aa5b" + + "d368bf7425241306082c59bc99942dd8144acf42904cbbb771162807ea07bd53c99ef876" + + "398c551a08488b82446ec13ffa257d47f59a96449f1962ba964c48f47130e0dc141b2110" + + "2a5b003e31945b513b88a887cea21a6f9ab3ca966254db342ad1a5cfc9071c0013f32aa2" + + "bcf0b1fb15a22b629f138720394c87b82c779ad5221df26b459a7c8a5737439013080663" + + "66e370f0480ffbe4a8db234288c1376341aebb98bc9bd1c9c8a702f42c99011c22ce3300" + + "6752167a4c2aa9649d6661b1ba30485eb35edb34a5f2c82045944a7dc85c6fc8b1f7f28e" + + "6c1a6b291591a6d9b9aae515dfb5277fa12d703a1481f44a88d760e6953150c67eb91543" + + "62d41aefbbbc2605ff478661dace2516aac53b17ef86e2022546b810d4b22aa2048b134f" + + "6b4783ce04cb9f5a67cfe41303954ee05abeab8aab18d97a790e60851fb2f11ab3886945" + + "28ee86713e8fb3a79c3cdf4852f11c95fe359a2a94af5a55e9", + EncapsulatedSecret: "19c8a22f31dcc098ed9a445222584c04c4254c8f87abdc0bc3a308a7c360fe50d133f394" + + "f48576f149cc272ea74cc07584186d36237e576ec55fbb49dbf1ec3164ae36675a815460" + + "039e17dbffef0ccc733bd554ff7b97fc9db1a98eeb1fdc503ec014ab4cc2d88ac9e1c53e" + + "f8796975908365d591dcc16aac61d37d803f53cadcb5005e7730cfa6849a4aab01e07044" + + "f69d29ccfe9966cee08b725537b5aad4b1a1e9d29b5061c32aabe077a5161e9a57fb1e8d" + + "c024be5f5e8cbed1f1ccfea1d34e302281c325f9b4ee87ad9095295be6d211a19d0f77e9" + + "e21ebe1f3ee032759d1a3b8a9589ba340512a0d4b61e112a1c291e0864fec755744b5b3a" + + "659920f82971470df89b25283ff19acdbf8ba9b087dc119f7d34f175cb1727bc4539abb2" + + "77e82680518c6ae1102f5c90bd0f17055b5f21c65be157740daf76d533fa9afe28250a3f" + + "e32a767514375f09df494e1d8507a79a7ce4d8d83aaf8addab70feb64a5f1c565b3320bc" + + "1ad7171115a050b6b0be8db0447c351e25443f8870c552d074a00b02e03e81ef21f3b7b1" + + "17ea44675f13c9cf9aa60f5a0941930094bf2787f46c65d314d19d722e10104899bc732d" + + "7025174826774e5f355405b2175013b5d0ab4adb980e776cbed35c93d26623ec08bc74a6" + + "229c6eb6e476ecf6e31800644589ccbbca7d8c46b138997144d5205e75237df59a0dc901" + + "ef3a3b4d3e45616da4761bfcbb7e38dce47ac631849702f66348090ea5a2ebe8e022939f" + + "8a108b0f6d89c71aedca58b1bf98b61467fe8862296f1a407dbe11c526b53b1757814545" + + "63670ec9b7c3b4c062c1af74b6c9f38197b0633e6fb304347b1b31b3110ad463ced8fe23" + + "50924f0d49d4cbad080bb4d440270482f5f9985ad16bd8b350fc6f2c6d9d4f1cb5710435" + + "41901d6aa1f30c0a8595d663a44438f460ab5cdf6504f06927ea71cb35f76a97c732071e" + + "234578560566c7d572393d33fb9e6e3401f6006a7cda5b33750465bd0e070b97f55ed540" + + "e884f597d3289fc21ed1928c869e263b82cda3cb3a7f06bd28ffad3d71b0a21b8626e0f8" + + "2223860642566959e3593f168cf623d783189863246910106450415818240f8c047ea864" + + "55ee8710c574296f8b698586cb2a067a6bfbbdb5f072483d26c082b96d0a39ddc71cf424" + + "a1907d69a1913f81d6c7dfeec1bb7d58d043beb7ad1429a97f9745f8ed207a2e30ddb29c" + + "a96561f0699cf3626cb471386674ca21a5a33b009da7fc0152383977bc7169b406571d31" + + "18c47fd6fe3224affbc1b8f118deb08e8b761633d96db58054b8fc5e2035c516a74645c3" + + "23362f8141edd50be3fa21f894483d4597cc046cd3de11b1c0fb2cb8eb0021dad74d404e" + + "c952c71ece3f50101b54678883402ee0c3c5533262a66cd1d0784b3fc7dabb28c29d3471" + + "94521fab5214455117f3f6d40215bfd901a34411a8985ae2fa74b75d61b3d9037a549946" + + "22be15ca1bb24a03f6a9464b32d094e78087e69b380dfd1dc137a108961ec564a28d4f08" + + "3b249c8004310bfc04fa6fa72b58b173345901cae4b54ff0860016232a46bc55d622880a" + + "a8a25216c58793cf2c94a1df461b758d43784e9c9cb8e67928b5c81a78903643508659e6" + + "ee", + Info: "346636343635323036663665323036313230343737323635363336393631366532303535" + + "37323665", + Psk: "", + PskId: "", + SharedSecret: "556ec9c8df352a315ec7fa6d72848b7f277a5f7181169a107d97b444d7bfa6ce", + AeadKey: "e1f50f15239d8c3cbd3fe992913bd365", + BaseNonce: "109dad0a50896f30a4cb478c", + ExporterSecret: "3dc613f4c647d912c18ffc90bc95025efa214264f6c1741587d044cb5a1f3e26", + Messages: + [ + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d30", + Ciphertext: "c55b375ecf13081a2448aefca58ca81ba771e04bc7299f9152aded351c76ac05cdc985a1" + + "609335f1399855f528adb21f48dfcc841fd7ef1c38bc64d9bdcd9c18c68d6d7a6c247429" + + "677d"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d31", + Ciphertext: "d3ef453d4ae5192c86d339c1f3ddd5e487c1553018da29de16e08b82bb4c0b82f606118a" + + "e9e11967d1cf572f27f64c4c29cdb4c70bee123664981a62169a3ed664f50229e8ec7726" + + "d1d9"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d32", + Ciphertext: "fec739822bc47a13b041e2e45720a5401b084bced934678f462fcd47c0494f1f5bacf0cd" + + "3417fb208e80b3ca1c5946000f84dd359434dcda5efc36bcc3a3872e569a95149ae74927" + + "3e27"), + ], + Exports: + [ + new HpkeExportVector( + Context: "70736575646f72616e646f6d30", + Length: 32, + ExportedValue: "a7e801ca7724275eea77f2e95340b7140b98aaa9f0035daa0be6d3325db4128f"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d31", + Length: 32, + ExportedValue: "4b6193c46d11cb047153e27e9cb43aa8ac1c107da4678ed3852ba8415ee3ff53"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d32", + Length: 32, + ExportedValue: "0f1490b86f762d3f7444072ea2cf5cf1641913950a6d81e4312af823b552d3c5"), + ]); + + yield return new HpkeTestVector( + Name: "MLKEM1024-P384-HKDF384-AES256-Base", + Source: "https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#appendix-A.6" + + ".1", + Kem: HpkeKem.MLKEM1024_P384, + Kdf: HpkeKdf.HKDF_SHA384, + Aead: HpkeAead.AES_256_GCM, + UsePsk: false, + KeyMaterial: "14c036a5e3c4af452baccdcd62cf818f250607076c299636e5c8074b3c757df1", + DecapsulationKey: "0ba4a1ff718a4444da0016d59f449e28d8abdeac107ee105e5ac0dc1e8219b37", + EncapsulationKey: "dd5407b4430e39f3416a6bc8e7d43b2a72c4f7480b1fd19e0c552af07b8f5bba626c2c46" + + "2724600d3b1598a254fa287661f14d1cf53a08019cb352b7078b2e6da44afb80467bc58c" + + "0b016933941a386609b66c8fecb0802ed41559dbb80ea5773d3c446f60c103d121049722" + + "f7106658422b1a074ccac083e3503a70a259ad15358b49b08f7b6502125cae1c18f3e596" + + "e4318297b3634e5a57a1275b60499b6fe88372520f3ce62e90d36300c7395769227075a7" + + "96943e9c257e213791a9fba955647167951bab3b8fd1a6baa6fca016ec51d59caed7bc45" + + "b77c1417ea1ae2662e96d2338cf5a81178071ea127891788511731dd450b60c125f3cb10" + + "a847b9e9558455608271a3b26ab3a996a697421441e28a4cc410c04b35379c04098f2415" + + "097481fb1c8a5e25392c56130e948c17d59bc0f01ac9353359ec6d5d19cbbe7558903371" + + "7f797fb967c01877272d4509fb3c8259a13dc7d78eebb51c152bab162b5259f0a36e3c2f" + + "8468ac14c279b65929070546f3b572d2b84324943c227011c878533629c96a235b4d88b4" + + "98c0bba890535f00c98c01b620e3c5b293014d2300f07b9d6c6a8e4c09c05532b5b40170" + + "99d1262f299f7b92663fbb074d115a29f7a7d56b366a6cc48c931b19d12cb6ea28487a0c" + + "27d0cd5df93349b8c5f8d5956196543e429d41f82d734c8dc5281d214c7b7ab32d9dba2c" + + "79b0007f229ad1b02e91c3ad1abc8d3ac16a9b951610d5293204522f4b049c7790511541" + + "8ce571c1ac43906799a2898f901105efe93c5acca2fbc72fc61a96a13b6869931009da8b" + + "2f472914828908d85fd4806fb12a28b254b8f600c9b9a41ada526e41c8719ffa71a7638c" + + "1ed4b4d86972e6da7442d98a5a9b8c5dd69fa5dc5c76a11f2a439436c779b255604f2947" + + "205434cd8a155852864d009fc062623f5c89f3c381be153e0479b032240e1c762e67d817" + + "28993f14431c8b0b3d3bb9103cb0262ef34d02d9a3cef9bd25845a392320156784adb54d" + + "f0691428d7b290827903abaa8fc009c57881a42c09dfa7bd9ec8c3e234b9f107ac00b429" + + "64266fd615b25bd906a1c762a9f7678b56af6b9598e9db1fd1573f6dd803885b3907a0c4" + + "d618a6337b29f37c8515509755e74e7364a6b32447cba852aa396ad9863a8d444aa39b51" + + "9855bd8ef40488776c02997033a4a6ac9520d898972b7aa49edc50d4b9143197974b1b3e" + + "36668bccc0be1754a263ba63ada761aa852717050eec3b36a46835828569c527bae50372" + + "5679654e652520c5369fb0b862174110d5b36cd5c91d78c6d49752efc5c10b8a6dd51ba2" + + "0dbc384cc20a30308912644090c8726536a35c906a75247663798b94a3647de50caea553" + + "24825719f99d2b649e7d77be5e242a3227c89f4198da3c59e81c5f1b999e5601968ef5b1" + + "ca6574b2501c33383fbe92ad8ff5a2ba98b60428153b463ff32131c49b6272681dbe2299" + + "e8d292a163b903d7911dfc61afa104e01433c2ab322b884743d338be8272e11561e51a4d" + + "53d371daf430d41c997192ac2c27197385cd5a373bfc3ca8d6db042e16076afa6ac31abc" + + "eea7bff73915c42acb25077405438ba8989ccd44b05d4043f8a3217b05bf899c8651bcc7" + + "40ba9a678c6b9bc9313bc21a8a38205e079d1051a6bf1071f6a0a38f3a0db88a7c9415cd" + + "5af72c3dd247bd20b14ea0a1d988816f101e302c9bf3918839e68a4f19c621e446940305" + + "c0567dbb2c8f6fb0257d09727e164a021b562a1bc2cde1b064dac144ba4f84f14ebee599" + + "9db03b2d892d629c134fca1457b851b1891f055437caeba081814b029672b21c9a428338" + + "4bc93a8579ca5eb99067d84c0e10c3b5bc1d7311829f95458a0b40e52681b1c262112b6c" + + "7f374ea86978359c096dfcafaf097d520b10c021c2d0e0ab9dd8570d9b3f884448379367" + + "caabbbe924478f72b0470c7c1ed956fe4b791db29ac9b6b905a8aac1b904fff757fddb27" + + "348932a6560f4b776c2a74510b11379800b538b27114715273f43f9f7acb63b3addf3019" + + "66591b963979028449cef3802269c8f94282364546ce91957b509c5f5696cfa49bb1eaca" + + "e0219c963b802b35986a0b857c0144a9d1cab7d29d851970bda11b2c86811023b216d493" + + "0ca94f97d80b7b65a0cb7a9ada2b37bde4355c5b65c5ec15b1eb61530b04566a4299e91a" + + "fbef01cc121f19ab90b705b5bf2e57373c16c68604a70238ee7c3f7e7d364957e5f4e32f" + + "a6747c811fb22b7efde99966fd0da372510c6e1fce1b25ce287707e5b3b0a2384e884cc6" + + "a3ac281fa6f5705d09ef0a823bfed3af81d8e5a38bb914d2269de2a4e47a1d7cdc6d85cb" + + "c009ea787f2eefed4b", + EncapsulatedSecret: "6252bc46bca0a8fea250a751deef5ebcd053d86881cab58afe159028253fa5bed2fb7eca" + + "382831b2e9a0714629521466d6092509a0892e93d927d177c9b0ccfe66e2fa44f2f1426c" + + "e7148cab999bcdae2e3db25ced0d669c078772346cdf7b12fca942f5ea27ab175e74b861" + + "d1aac098384e848537627d21b64f460e008b8c5a15c6811c892d49a053f8a1c06a8b1960" + + "b4650a8c7f91ecaf50079e34e2aeb1bb45935cd4b578cb7a2578b2cd4215f803a02353d9" + + "bc83f096e2982b41e9e089d158b4dade7959915d2ae7b66c9ec4aa9f5f85faa62a8d4ced" + + "aace187eef5da43ff523b4de139cfb7ee3edda8d2e45af7b591646920836ac97d83067a5" + + "f3ccc9bdf6b10958b2542a600dd5e27d51d3a3179aa82260b272f3580bd76c19d6c7f996" + + "0a04d72197904800a35234b84c50c142e68ddccc5a89dcb94491a1f03981bc1c4d033f48" + + "fb18b4da14bdfb64b4e2e9985f21d634e3a4a88ca9f2782a2f11c79632e23139b2c26d16" + + "de006c211f09493a7985e5eac0952a65449ecb84c2d0b7c7ee27c5c127851b9b8061f8f9" + + "c64d6e98650bbe7321a2fad69fa6ced8adffdad8f40dbd7122406211c09957d37eeab172" + + "1200abf815e66b0afa5d2986f66afa9b80bfcd0bbdd6b848a19486f5a2daff4793b54d0a" + + "1ab99593977dbf561959919978f21d6b924fc19cdd54572b72f1f6fb4f765501b955dc83" + + "3bf627684f367e0bb02232ef428bd3aaa20aaebf36861432f6b1eaa022f5db22566d0065" + + "cc78f2059f777ded29c2f7218c8995a988fc81af98b9d97551efef39b72b84cab58154c9" + + "03ff724959d286d8159d1a0aee218ede82edb148286f7ff8fc8ec4a8fb48fe912851a3a6" + + "77f6c27529edb36d811402a9e5658aade9e91df8c13765e41aca064b14397613426dfb51" + + "f7971c29d8d688233a4e3a1e6e1c96e1c1ec39b4d2c0fbc5258e1f363ce11c803183e4af" + + "52777ec4750dde7d499f4d8e1a69f78af8e3c2e75bb8de85376ab29d7f3da499a8480196" + + "258436386151b57252d104a061112721b73ce1f2f5bb0334fd417d88bab0fdac368f46b2" + + "db22330adb6cd8e747dc14eff8cef6353c94f9525f6d0c1d32ec20b7ec624ad8df4b5e82" + + "b72375bee995fad8c9694e765e2c5ff3d97e9cddf8848618bf08c7680f1a9f2cef663f81" + + "ad95f8aca6855f8aec99157ce9758883877326d08d75872a549f524cc5abadc3b007f9ad" + + "37072376e97f7c7997b1548dcef72ebb751251a1f499c6d79bd4a6ce83331d449aff880e" + + "19473fea5ec9387ff984f24d56a7dd58426af98203506a7d7e8c00399144e91d9283e4cd" + + "a4c3d7b7baa58bd7b58028101e57ef0410eb6bdf15aaad25949f0e4e3610655197a6b6a6" + + "d9941109828299c567cb68bd18e2359552959bea6ac6d51c181caf35b0f5fa0a6b075c33" + + "09bb06ff3bf36c6110241be25bb26b5c36b74059fb0c72a0af36e65e8ea7cd4836f79931" + + "fa72b0606941a7f1474cab150b90ef2f7bd69b994e128177f387e6963dd7d5c158992301" + + "63b743cda48777fe95d64d5a61278e2375e77a556e0ee52ef944e36a141123d6d03ba39d" + + "b4481db40a545aa14b91bb14663d717f93d2db3fbf838c4dfda0ad866b652a1f2dba6bec" + + "b1f856e1583305447824396bd2c8eb7ad02c86c9779aec904e85732141bef525fe5c271c" + + "cb655e7dbc5f82327971905e9e8c52bbddac260500a8e7667e2069947da3d62405fa357a" + + "0a96a937fd6b6ab9b0fc52fec997e63819fb1666db67429fc2971f8aa53ff690877fe1b4" + + "c334a82c416822cfd06e2eb783e7bc20a76c6596990b12f06e3a597764e2ca85b14f511e" + + "63eee821338d80451d714dc8fb2f3cc9a5077553f121ae5edc0ac2e37f70e6454bc5bc35" + + "82b4da9872fddde5a0abc3f981fb5af044a78ec102827bccdca891218faeb27b0ccde8fb" + + "71f0b32dda854f737dfb7811c386c7d833d3bc81952b83b964dd61464477fb50f86ff5eb" + + "5b6f3929928ab7cdec9974cfb97086fdf21ae4fd0d137ccb825d45584b5cadaac383abd8" + + "d8d7b97229aea44e0985db277fc8c38bc93b520dfb197e5a9106c48e903c2120e12a7102" + + "61db45a17d41342a4053ce23b80fdfd90278dc64e0f6dc794d3740b34a28041f00a5b70e" + + "3c1dcc6e60944fd1cf6dbad0907c55b5501cea7acbce32c02066dab5a7f3bc2c2c237689" + + "b0299e18269df7252eab5e543ab03a777faef62f04d1b38e73e0254b09c72a40c7a1e073" + + "dc3725a32f5d9de0e9de45d907b4cba48c3e078b4dcd78668b3ebd5c67b1682aa5beaed5" + + "e02473d713ff3181acd63c98fde2f301e53c92b751c7dae053d7914f5c0c4633c0b16377" + + "c47fe22c64ec4bae84", + Info: "346636343635323036663665323036313230343737323635363336393631366532303535" + + "37323665", + Psk: "", + PskId: "", + SharedSecret: "226311ca7023793ede9bd9503137298e036add770ea5a6c46efbd17e2c1a0855", + AeadKey: "26143789a8c64c529d174ee0a614460bdefdcb82dfae5eb82821deb7bab61dc8", + BaseNonce: "b0dc993388b766c96e7a8267", + ExporterSecret: "5b92680d4c918985d6184e85b2696079047c2ecc21c19f58ed7bbbbed68a203720120ce3" + + "4ae2dc8aac2e992b484f3738", + Messages: + [ + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d30", + Ciphertext: "1af5c6176d191f913bb9a39ae6af2c5847d5effca2d794242de5464ef287bfd6d5f6735b" + + "ab1b42b3d29a6b131a91b180b04dbf6afc395bdc35f2b8558db9c62ce54c81872b42d222" + + "459a"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d31", + Ciphertext: "9e34298676cbe51af56ba3dbf356292f35189305f123b59f1fb4825f4d1746d84f4440ed" + + "957cd610b6aa0208956c9664a8297751377c909160df88bd33908f962593333727f83766" + + "f42b"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d32", + Ciphertext: "c2e427b4fc917ad8fa5cc2c2d63802a287be09d75e3c220bdd802a2365d087043058b6bb" + + "d64a966d51326646cc58ef6e0e5a4057f2082f305d96d9017482d292b21ac25ab2bb500f" + + "2c9f"), + ], + Exports: + [ + new HpkeExportVector( + Context: "70736575646f72616e646f6d30", + Length: 32, + ExportedValue: "29c8d4342d91ac6b7be5167cd58db0d6f0db21356c4dda73964e0d1bcca575fb"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d31", + Length: 32, + ExportedValue: "8218e9f4d94056911a6e0b46446ea36b02f16ed7b8f2d7333a153dd7d914c422"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d32", + Length: 32, + ExportedValue: "9470d784dddfa4c994942dbbc6466d7bf557253f1055018a7c0e7c11d1f91b19"), + ]); + + yield return new HpkeTestVector( + Name: "P256-SHAKE128-AES128-Base", + Source: "https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#appendix-A.7" + + ".1", + Kem: HpkeKem.DHKEM_P256_HKDF_SHA256, + Kdf: HpkeKdf.SHAKE128, + Aead: HpkeAead.AES_128_GCM, + UsePsk: false, + KeyMaterial: "baea9ef03113b6b3eae42055d1153824e0d6ce292c7a7776c46164b3d7ff472d", + DecapsulationKey: "940a1692f2c9bdcc71c563304d019359c08d9cf031c97ff731accace45298abb", + EncapsulationKey: "0499c51fe81dd142193be7ebfb9bbead8da7c5014364f07d70b6947003b037a77d1d2ab7" + + "664e4456baf9ae18617731c5217ab5ba724df2c6ee06e167d6f8ad3430", + EncapsulatedSecret: "040d6b7d55773a677961fcd20a94a428cce3887a0eadccff4177afae894d13457b9a6c6a" + + "ce3afbcb3a8a7b6dcf341fad4f8c4a46594994765a493123ef00564eb3", + Info: "346636343635323036663665323036313230343737323635363336393631366532303535" + + "37323665", + Psk: "", + PskId: "", + SharedSecret: "aa92abe0c252ce7357b0c3eb6b31f8e5934bcbdcd5d1291dd0ca238aa678244f", + AeadKey: "c7a6a81a2a59761aade2149116f463f1", + BaseNonce: "66429e34404232db6ac64888", + ExporterSecret: "4603c7eacbc8bc64150037769c56f246b2473dbcc1a73775ddd2e24d0daa19df", + Messages: + [ + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d30", + Ciphertext: "b6bbe209cf13d2e491651b4e01a70421cb63f509c4f54b468338ebdc9cbe09e5342145c1" + + "c367b1ead479b804823ba1ea640df5f9f7bebfeae4cf596f786dc4c80acc4ce56e4ef72e" + + "53a2"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d31", + Ciphertext: "fd31b952b731aad4f43597b0cc6ba2c3a3e56f78abc201b86bed80798c5cb874d14dbac6" + + "f33c8700d0a629e1267c76ed6f101b1326c3acdb125c7eb6ead45a3148b86766d2ced80e" + + "2da0"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d32", + Ciphertext: "d0c5f0d1fc43c8644600dcfd667d7cbd899c6c68f1862efab6e8fd6f2559b2e486a4993a" + + "3f83edc83c17c795709ec0192d58593983ba2c47999cee42c78e61c07baf68824e9cd83b" + + "51a7"), + ], + Exports: + [ + new HpkeExportVector( + Context: "70736575646f72616e646f6d30", + Length: 32, + ExportedValue: "33367f44b8561d2be9a67535926bc2f52949267b70f4a76d9294c69056196ee5"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d31", + Length: 32, + ExportedValue: "2c07c3d1d68ef711c380700bf019bab6d88616b39060ced822c666ad0dd679e9"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d32", + Length: 32, + ExportedValue: "2fadebf4f18368f7d5d270562daa449e31c6c843e87a21451667bdfcd016255e"), + ]); + + yield return new HpkeTestVector( + Name: "P384-SHAKE256-AES256-Base", + Source: "https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#appendix-A.8" + + ".1", + Kem: HpkeKem.DHKEM_P384_HKDF_SHA384, + Kdf: HpkeKdf.SHAKE256, + Aead: HpkeAead.AES_256_GCM, + UsePsk: false, + KeyMaterial: "65fca3ea3b6db29a62bff28ec53c08710fab10b3798e59b678d3224296d5883f03912347" + + "1784ce57b0d85a17cd521196", + DecapsulationKey: "679172205e04663f40fda1018cd46c18ebaa876ede6998ba86b051614ca4d5e4bfbea34b" + + "720617a4b958cc80f6305244", + EncapsulationKey: "04a5f53da8564364255bc36850df793672782a5c9e4a7fb5fb2e2146eb12e4d8477ab1f3" + + "26a361dfd1e41212109510e813380547c68c0964c1908f16f67b902a061be27b2f8b43f1" + + "fab1bf0dbf89f5167ce80aca2c210b8fc0f040699db9ee1229", + EncapsulatedSecret: "049f1da943827d165268869c842962c1feba1fb46402fd3fac50c002cf44bb103c1aa8fb" + + "15a848f9908554624b0eac4573ec258788335421dcbfa625bfc9136cfa0e335f0de018e4" + + "f9517ae0a8863f1b3631343c49c67fd240213f86af1b235ba4", + Info: "346636343635323036663665323036313230343737323635363336393631366532303535" + + "37323665", + Psk: "", + PskId: "", + SharedSecret: "f609b68f1e65f077d9cca41ad41d45dd66284adfb8341b9ebdd0ff39c90917a1af423d5b" + + "70d6a917ebf469e093023850", + AeadKey: "4c314eaf3ad5fc2c6ec5478d159c566a209c36d22828e8a51e4c84537cfb7c5a", + BaseNonce: "77459442b645123943d74d7b", + ExporterSecret: "a2c1e1738982407a75c68acffd70d2d63cc3f753ff437947e56337fd6e612d09a6f776a3" + + "628f236c91c2b39c0e30ce70730bcf8379fabac484540eaf89cec1ea", + Messages: + [ + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d30", + Ciphertext: "3c7922016241555d76d87b725f17058f9c309cb3b793b3d8b503cd99a6174130aa6fc679" + + "2f94345bfd5e8ec4cfc3641bf6a672b5285598e49dab91ebd71c38d703d4e41c0c6cd23b" + + "8cf7"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d31", + Ciphertext: "a1745c52e73ed7c7c05cd4d712094dc5c3ec84d316a82ec0e338b64dd11742f42f7b2b39" + + "1cd0d3397d451ed48c32b5d1b5392db62c6a2c9f829ed9f937ed64452fc5e5c108c09899" + + "910c"), + new HpkeMessageVector( + Plaintext: "343236353631373537343739323036393733323037343732373537343638326332303734" + + "37323735373436383230363236353631373537343739", + AssociatedData: "436f756e742d32", + Ciphertext: "a70388a907779a2b59fd6f041541925127745559e2da6b2ab7ac9a49132bb027f1918a3b" + + "a93c7b01b0028cab840213f8d1c023c57665770db8ea535c8a58b6035f07acb658b3b8ae" + + "611e"), + ], + Exports: + [ + new HpkeExportVector( + Context: "70736575646f72616e646f6d30", + Length: 32, + ExportedValue: "a28eac67f1c7d8e0a7d10da1c3e65c7e7e7b6e788fdcd33aa3eed6f6037631a0"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d31", + Length: 32, + ExportedValue: "3929d2c79d0993cce923b502ff03811dc8328360b0dece71485a7994603dd3be"), + new HpkeExportVector( + Context: "70736575646f72616e646f6d32", + Length: 32, + ExportedValue: "999f41ef5b39e0faf7b2fee973b18e2018c8d4da259949d4bea9a595da070269"), + ]); + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Rfc9180.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Rfc9180.cs new file mode 100644 index 00000000000000..9524a69a04fecb --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.Rfc9180.cs @@ -0,0 +1,353 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Generated by HpkeTestData.Generate.mjs. Do not edit by hand. +// Only messages 0-2 and the first three exports are retained. +using System.Collections.Generic; + +namespace System.Security.Cryptography.Tests +{ + public static partial class HpkeTestData + { + private static IEnumerable Rfc9180Vectors() + { + yield return new HpkeTestVector( + Name: "X25519-HKDF256-ChaCha-Base", + Source: "https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.2.1", + Kem: HpkeKem.DHKEM_X25519_HKDF_SHA256, + Kdf: HpkeKdf.HKDF_SHA256, + Aead: HpkeAead.ChaCha20Poly1305, + UsePsk: false, + KeyMaterial: "1ac01f181fdf9f352797655161c58b75c656a6cc2716dcb66372da835542e1df", + DecapsulationKey: "8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb", + EncapsulationKey: "4310ee97d88cc1f088a5576c77ab0cf5c3ac797f3d95139c6c84b5429c59662a", + EncapsulatedSecret: "1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a", + Info: "4f6465206f6e2061204772656369616e2055726e", + Psk: "", + PskId: "", + SharedSecret: "0bbe78490412b4bbea4812666f7916932b828bba79942424abb65244930d69a7", + AeadKey: "ad2744de8e17f4ebba575b3f5f5a8fa1f69c2a07f6e7500bc60ca6e3e3ec1c91", + BaseNonce: "5c4d98150661b848853b547f", + ExporterSecret: "a3b010d4994890e2c6968a36f64470d3c824c8f5029942feb11e7a74b2921922", + Messages: + [ + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d30", + Ciphertext: "1c5250d8034ec2b784ba2cfd69dbdb8af406cfe3ff938e131f0def8c8b60b4db21993c62" + + "ce81883d2dd1b51a28"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d31", + Ciphertext: "6b53c051e4199c518de79594e1c4ab18b96f081549d45ce015be002090bb119e85285337" + + "cc95ba5f59992dc98c"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d32", + Ciphertext: "71146bd6795ccc9c49ce25dda112a48f202ad220559502cef1f34271e0cb4b02b4f10eca" + + "c6f48c32f878fae86b"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 32, + ExportedValue: "4bbd6243b8bb54cec311fac9df81841b6fd61f56538a775e7c80a9f40160606e"), + new HpkeExportVector( + Context: "00", + Length: 32, + ExportedValue: "8c1df14732580e5501b00f82b10a1647b40713191b7c1240ac80e2b68808ba69"), + new HpkeExportVector( + Context: "54657374436f6e74657874", + Length: 32, + ExportedValue: "5acb09211139c43b3090489a9da433e8a30ee7188ba8b0a9a1ccf0c229283e53"), + ]); + + yield return new HpkeTestVector( + Name: "X25519-HKDF256-ChaCha-Psk", + Source: "https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.2.2", + Kem: HpkeKem.DHKEM_X25519_HKDF_SHA256, + Kdf: HpkeKdf.HKDF_SHA256, + Aead: HpkeAead.ChaCha20Poly1305, + UsePsk: true, + KeyMaterial: "26b923eade72941c8a85b09986cdfa3f1296852261adedc52d58d2930269812b", + DecapsulationKey: "77d114e0212be51cb1d76fa99dd41cfd4d0166b08caa09074430a6c59ef17879", + EncapsulationKey: "13640af826b722fc04feaa4de2f28fbd5ecc03623b317834e7ff4120dbe73062", + EncapsulatedSecret: "2261299c3f40a9afc133b969a97f05e95be2c514e54f3de26cbe5644ac735b04", + Info: "4f6465206f6e2061204772656369616e2055726e", + Psk: "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82", + PskId: "456e6e796e20447572696e206172616e204d6f726961", + SharedSecret: "4be079c5e77779d0215b3f689595d59e3e9b0455d55662d1f3666ec606e50ea7", + AeadKey: "600d2fdb0313a7e5c86a9ce9221cd95bed069862421744cfb4ab9d7203a9c019", + BaseNonce: "112e0465562045b7368653e7", + ExporterSecret: "73b506dc8b6b4269027f80b0362def5cbb57ee50eed0c2873dac9181f453c5ac", + Messages: + [ + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d30", + Ciphertext: "4a177f9c0d6f15cfdf533fb65bf84aecdc6ab16b8b85b4cf65a370e07fc1d78d28fb0732" + + "14525276f4a89608ff"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d31", + Ciphertext: "5c3cabae2f0b3e124d8d864c116fd8f20f3f56fda988c3573b40b09997fd6c769e77c8ed" + + "a6cda4f947f5b704a8"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d32", + Ciphertext: "14958900b44bdae9cbe5a528bf933c5c990dbb8e282e6e495adf8205d19da9eb270e3a6f" + + "1e0613ab7e757962a4"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 32, + ExportedValue: "813c1bfc516c99076ae0f466671f0ba5ff244a41699f7b2417e4c59d46d39f40"), + new HpkeExportVector( + Context: "00", + Length: 32, + ExportedValue: "2745cf3d5bb65c333658732954ee7af49eb895ce77f8022873a62a13c94cb4e1"), + new HpkeExportVector( + Context: "54657374436f6e74657874", + Length: 32, + ExportedValue: "ad40e3ae14f21c99bfdebc20ae14ab86f4ca2dc9a4799d200f43a25f99fa78ae"), + ]); + + yield return new HpkeTestVector( + Name: "P256-HKDF512-AES128-Base", + Source: "https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.4.1", + Kem: HpkeKem.DHKEM_P256_HKDF_SHA256, + Kdf: HpkeKdf.HKDF_SHA512, + Aead: HpkeAead.AES_128_GCM, + UsePsk: false, + KeyMaterial: "ea9ff7cc5b2705b188841c7ace169290ff312a9cb31467784ca92d7a2e6e1be8", + DecapsulationKey: "3ac8530ad1b01885960fab38cf3cdc4f7aef121eaa239f222623614b4079fb38", + EncapsulationKey: "04085aa5b665dc3826f9650ccbcc471be268c8ada866422f739e2d531d4a8818a9466bc6" + + "b449357096232919ec4fe9070ccbac4aac30f4a1a53efcf7af90610edd", + EncapsulatedSecret: "0493ed86735bdfb978cc055c98b45695ad7ce61ce748f4dd63c525a3b8d53a15565c6897" + + "888070070c1579db1f86aaa56deb8297e64db7e8924e72866f9a472580", + Info: "4f6465206f6e2061204772656369616e2055726e", + Psk: "", + PskId: "", + SharedSecret: "02f584736390fc93f5b4ad039826a3fa08e9911bd1215a3db8e8791ba533cafd", + AeadKey: "090ca96e5f8aa02b69fac360da50ddf9", + BaseNonce: "9c995e621bf9a20c5ca45546", + ExporterSecret: "4a7abb2ac43e6553f129b2c5750a7e82d149a76ed56dc342d7bca61e26d494f4855dff0d" + + "0165f27ce57756f7f16baca006539bb8e4518987ba610480ac03efa8", + Messages: + [ + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d30", + Ciphertext: "d3cf4984931484a080f74c1bb2a6782700dc1fef9abe8442e44a6f09044c88907200b332" + + "003543754eb51917ba"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d31", + Ciphertext: "d14414555a47269dfead9fbf26abb303365e40709a4ed16eaefe1f2070f1ddeb1bdd94d9" + + "e41186f124e0acc62d"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d32", + Ciphertext: "9bba136cade5c4069707ba91a61932e2cbedda2d9c7bdc33515aa01dd0e0f7e9d3579bf4" + + "016dec37da4aafa800"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 32, + ExportedValue: "a32186b8946f61aeead1c093fe614945f85833b165b28c46bf271abf16b57208"), + new HpkeExportVector( + Context: "00", + Length: 32, + ExportedValue: "84998b304a0ea2f11809398755f0abd5f9d2c141d1822def79dd15c194803c2a"), + new HpkeExportVector( + Context: "54657374436f6e74657874", + Length: 32, + ExportedValue: "93fb9411430b2cfa2cf0bed448c46922a5be9beff20e2e621df7e4655852edbc"), + ]); + + yield return new HpkeTestVector( + Name: "P256-HKDF512-AES128-Psk", + Source: "https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.4.2", + Kem: HpkeKem.DHKEM_P256_HKDF_SHA256, + Kdf: HpkeKdf.HKDF_SHA512, + Aead: HpkeAead.AES_128_GCM, + UsePsk: true, + KeyMaterial: "75bfc2a3a3541170a54c0b06444e358d0ee2b4fb78a401fd399a47a33723b700", + DecapsulationKey: "bc6f0b5e22429e5ff47d5969003f3cae0f4fec50e23602e880038364f33b8522", + EncapsulationKey: "043f5266fba0742db649e1043102b8a5afd114465156719cea90373229aabdd84d7f45da" + + "bfc1f55664b888a7e86d594853a6cccdc9b189b57839cbbe3b90b55873", + EncapsulatedSecret: "04a307934180ad5287f95525fe5bc6244285d7273c15e061f0f2efb211c35057f3079f6e" + + "0abae200992610b25f48b63aacfcb669106ddee8aa023feed301901371", + Info: "4f6465206f6e2061204772656369616e2055726e", + Psk: "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82", + PskId: "456e6e796e20447572696e206172616e204d6f726961", + SharedSecret: "2912aacc6eaebd71ff715ea50f6ef3a6637856b2a4c58ea61e0c3fc159e3bc16", + AeadKey: "0b910ba8d9cfa17e5f50c211cb32839a", + BaseNonce: "0c29e714eb52de5b7415a1b7", + ExporterSecret: "50c0a182b6f94b4c0bd955c4aa20df01f282cc12c43065a0812fe4d4352790171ed2b2c4" + + "756ad7f5a730ba336c8f1edd0089d8331192058c385bae39c7cc8b57", + Messages: + [ + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d30", + Ciphertext: "57624b6e320d4aba0afd11f548780772932f502e2ba2a8068676b2a0d3b5129a45b9faa8" + + "8de39e8306da41d4cc"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d31", + Ciphertext: "159d6b4c24bacaf2f5049b7863536d8f3ffede76302dace42080820fa51925d4e1c72a64" + + "f87b14291a3057e00a"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d32", + Ciphertext: "bd24140859c99bf0055075e9c460032581dd1726d52cf980d308e9b20083ca62e700b178" + + "92bcf7fa82bac751d0"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 32, + ExportedValue: "8158bea21a6700d37022bb7802866edca30ebf2078273757b656ef7fc2e428cf"), + new HpkeExportVector( + Context: "00", + Length: 32, + ExportedValue: "6a348ba6e0e72bb3ef22479214a139ef8dac57be34509a61087a12565473da8d"), + new HpkeExportVector( + Context: "54657374436f6e74657874", + Length: 32, + ExportedValue: "2f6d4f7a18ec48de1ef4469f596aada4afdf6d79b037ed3c07e0118f8723bffc"), + ]); + + yield return new HpkeTestVector( + Name: "P521-HKDF512-AES256-Base", + Source: "https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.6.1", + Kem: HpkeKem.DHKEM_P521_HKDF_SHA512, + Kdf: HpkeKdf.HKDF_SHA512, + Aead: HpkeAead.AES_256_GCM, + UsePsk: false, + KeyMaterial: "2ad954bbe39b7122529f7dde780bff626cd97f850d0784a432784e69d86eccaade43b6c1" + + "0a8ffdb94bf943c6da479db137914ec835a7e715e36e45e29b587bab3bf1", + DecapsulationKey: "01462680369ae375e4b3791070a7458ed527842f6a98a79ff5e0d4cbde83c27196a39169" + + "56655523a6a2556a7af62c5cadabe2ef9da3760bb21e005202f7b2462847", + EncapsulationKey: "0401b45498c1714e2dce167d3caf162e45e0642afc7ed435df7902ccae0e84ba0f7d373f" + + "646b7738bbbdca11ed91bdeae3cdcba3301f2457be452f271fa6837580e661012af49583" + + "a62e48d44bed350c7118c0d8dc861c238c72a2bda17f64704f464b57338e7f40b6095948" + + "0c0e58e6559b190d81663ed816e523b6b6a418f66d2451ec64", + EncapsulatedSecret: "040138b385ca16bb0d5fa0c0665fbbd7e69e3ee29f63991d3e9b5fa740aab8900aaeed46" + + "ed73a49055758425a0ce36507c54b29cc5b85a5cee6bae0cf1c21f2731ece2013dc3fb7c" + + "8d21654bb161b463962ca19e8c654ff24c94dd2898de12051f1ed0692237fb02b2f8d1dc" + + "1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0", + Info: "4f6465206f6e2061204772656369616e2055726e", + Psk: "", + PskId: "", + SharedSecret: "776ab421302f6eff7d7cb5cb1adaea0cd50872c71c2d63c30c4f1d5e43653336fef33b10" + + "3c67e7a98add2d3b66e2fda95b5b2a667aa9dac7e59cc1d46d30e818", + AeadKey: "751e346ce8f0ddb2305c8a2a85c70d5cf559c53093656be636b9406d4d7d1b70", + BaseNonce: "55ff7a7d739c69f44b25447b", + ExporterSecret: "e4ff9dfbc732a2b9c75823763c5ccc954a2c0648fc6de80a58581252d0ee3215388a4455" + + "e69086b50b87eb28c169a52f42e71de4ca61c920e7bd24c95cc3f992", + Messages: + [ + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d30", + Ciphertext: "170f8beddfe949b75ef9c387e201baf4132fa7374593dfafa90768788b7b2b200aafcc6d" + + "80ea4c795a7c5b841a"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d31", + Ciphertext: "d9ee248e220ca24ac00bbbe7e221a832e4f7fa64c4fbab3945b6f3af0c5ecd5e16815b32" + + "8be4954a05fd352256"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d32", + Ciphertext: "142cf1e02d1f58d9285f2af7dcfa44f7c3f2d15c73d460c48c6e0e506a3144bae35284e7" + + "e221105b61d24e1c7a"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 32, + ExportedValue: "05e2e5bd9f0c30832b80a279ff211cc65eceb0d97001524085d609ead60d0412"), + new HpkeExportVector( + Context: "00", + Length: 32, + ExportedValue: "fca69744bb537f5b7a1596dbf34eaa8d84bf2e3ee7f1a155d41bd3624aa92b63"), + new HpkeExportVector( + Context: "54657374436f6e74657874", + Length: 32, + ExportedValue: "f389beaac6fcf6c0d9376e20f97e364f0609a88f1bc76d7328e9104df8477013"), + ]); + + yield return new HpkeTestVector( + Name: "P521-HKDF512-AES256-Psk", + Source: "https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.6.2", + Kem: HpkeKem.DHKEM_P521_HKDF_SHA512, + Kdf: HpkeKdf.HKDF_SHA512, + Aead: HpkeAead.AES_256_GCM, + UsePsk: true, + KeyMaterial: "a2a2458705e278e574f835effecd18232f8a4c459e7550a09d44348ae5d3b1ea9d95c519" + + "95e657ad6f7cae659f5e186126a471c017f8f5e41da9eba74d4e0473e179", + DecapsulationKey: "011bafd9c7a52e3e71afbdab0d2f31b03d998a0dc875dd7555c63560e142bde264428de0" + + "3379863b4ec6138f813fa009927dc5d15f62314c56d4e7ff2b485753eb72", + EncapsulationKey: "04006917e049a2be7e1482759fb067ddb94e9c4f7f5976f655088dec45246614ff924ed3" + + "b385fc2986c0ecc39d14f907bf837d7306aada59dd5889086125ecd038ead400603394b5" + + "d81f89ebfd556a898cc1d6a027e143d199d3db845cb91c5289fb26c5ff80832935b0e8dd" + + "08d37c6185a6f77683347e472d1edb6daa6bd7652fea628fae", + EncapsulatedSecret: "040085eff0835cc84351f32471d32aa453cdc1f6418eaaecf1c2824210eb1d48d0768b36" + + "8110fab21407c324b8bb4bec63f042cfa4d0868d19b760eb4beba1bff793b30036d2c614" + + "d55730bd2a40c718f9466faf4d5f8170d22b6df98dfe0c067d02b349ae4a142e0c03418f" + + "0a1479ff78a3db07ae2c2e89e5840f712c174ba2118e90fdcb", + Info: "4f6465206f6e2061204772656369616e2055726e", + Psk: "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82", + PskId: "456e6e796e20447572696e206172616e204d6f726961", + SharedSecret: "0d52de997fdaa4797720e8b1bebd3df3d03c4cf38cc8c1398168d36c3fc7626428c9c254" + + "dd3f9274450909c64a5b3acbe45e2d850a2fd69ac0605fe5c8a057a5", + AeadKey: "f764a5a4b17e5d1ffba6e699d65560497ebaea6eb0b0d9010a6d979e298a39ff", + BaseNonce: "479afdf3546ddba3a9841f38", + ExporterSecret: "5c3d4b65a13570502b93095ef196c42c8211a4a188c4590d35863665c705bb140ecba6ce" + + "9256be3fad35b4378d41643867454612adfd0542a684b61799bf293f", + Messages: + [ + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d30", + Ciphertext: "de69e9d943a5d0b70be3359a19f317bd9aca4a2ebb4332a39bcdfc97d5fe62f3a77702f4" + + "822c3be531aa7843a1"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d31", + Ciphertext: "77a16162831f90de350fea9152cfc685ecfa10acb4f7994f41aed43fa5431f2382d078ec" + + "88baec53943984553e"), + new HpkeMessageVector( + Plaintext: "4265617574792069732074727574682c20747275746820626561757479", + AssociatedData: "436f756e742d32", + Ciphertext: "f1d48d09f126b9003b4c7d3fe6779c7c92173188a2bb7465ba43d899a6398a333914d2bb" + + "19fd769d53f3ec7336"), + ], + Exports: + [ + new HpkeExportVector( + Context: "", + Length: 32, + ExportedValue: "62691f0f971e34de38370bff24deb5a7d40ab628093d304be60946afcdb3a936"), + new HpkeExportVector( + Context: "00", + Length: 32, + ExportedValue: "76083c6d1b6809da088584674327b39488eaf665f0731151128452e04ce81bff"), + new HpkeExportVector( + Context: "54657374436f6e74657874", + Length: 32, + ExportedValue: "0c7cfc0976e25ae7680cf909ae2de1859cd9b679610a14bec40d69b91785b2f6"), + ]); + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs new file mode 100644 index 00000000000000..cd29583131a893 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs @@ -0,0 +1,67 @@ +// 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; + +namespace System.Security.Cryptography.Tests +{ + public static partial class HpkeTestData + { + // Representative coverage, not a Cartesian product. RFC cases include different KEM and outer HKDF hashes. + // Generated cases add empty inputs/nonce carry and SHAKE PSK mode at the length-prefix boundaries. + public static IReadOnlyList Vectors { get; } = Array.AsReadOnly( + Rfc9180Vectors().Concat(PqDraftVectors()).Concat(GeneratedVectors()).ToArray()); + + public static IEnumerable VectorNames => + Vectors.Select(vector => new object[] { vector.Name }); + + public static IEnumerable RepresentativeSuites + { + get + { + HashSet<(HpkeKem, HpkeKdf, HpkeAead)> seen = new(); + + foreach (HpkeTestVector vector in Vectors) + { + if (seen.Add((vector.Kem, vector.Kdf, vector.Aead))) + { + yield return new object[] { vector.Kem, vector.Kdf, vector.Aead }; + } + } + } + } + + public static HpkeTestVector GetVector(string name) => Vectors.Single(vector => vector.Name == name); + } + + // Binary values are hex strings so consumers can obtain independent mutable buffers when needed. + // KeyMaterial is the recipient's DeriveKey input; Messages are consecutive, starting at sequence zero. + public sealed record class HpkeTestVector( + string Name, + string Source, + HpkeKem Kem, + HpkeKdf Kdf, + HpkeAead Aead, + bool UsePsk, + string KeyMaterial, + string DecapsulationKey, + string EncapsulationKey, + string EncapsulatedSecret, + string Info, + string Psk, + string PskId, + string SharedSecret, + string AeadKey, + string BaseNonce, + string ExporterSecret, + IReadOnlyList Messages, + IReadOnlyList Exports) + { + public override string ToString() => Name; + } + + public sealed record class HpkeMessageVector(string Plaintext, string AssociatedData, string Ciphertext); + + public sealed record class HpkeExportVector(string Context, int Length, string ExportedValue); +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs new file mode 100644 index 00000000000000..b1a2e5810e73da --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs @@ -0,0 +1,113 @@ +// 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 Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static class HpkeTestDataTests + { + [Fact] + public static void Corpus_CoversEveryDeclaredComponent() + { + Assert.Equal( + Enum.GetValues(typeof(HpkeKem)).Cast().OrderBy(value => value), + HpkeTestData.Vectors.Select(vector => vector.Kem).Distinct().OrderBy(value => value)); + Assert.Equal( + Enum.GetValues(typeof(HpkeKdf)).Cast().OrderBy(value => value), + HpkeTestData.Vectors.Select(vector => vector.Kdf).Distinct().OrderBy(value => value)); + Assert.Equal( + Enum.GetValues(typeof(HpkeAead)).Cast().OrderBy(value => value), + HpkeTestData.Vectors.Select(vector => vector.Aead).Distinct().OrderBy(value => value)); + Assert.Equal( + HpkeTestData.Vectors.Count, + HpkeTestData.Vectors.Select(vector => vector.Name).Distinct().Count()); + } + + [Theory] + [MemberData(nameof(HpkeTestData.VectorNames), MemberType = typeof(HpkeTestData))] + public static void Vector_HasValidShape(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = new(vector.Kem, vector.Kdf, vector.Aead); + int hashLength = vector.Kdf switch + { + HpkeKdf.HKDF_SHA256 or HpkeKdf.SHAKE128 => 32, + HpkeKdf.HKDF_SHA384 => 48, + HpkeKdf.HKDF_SHA512 or HpkeKdf.SHAKE256 => 64, + _ => throw new InvalidOperationException(), + }; + int keyLength = vector.Aead == HpkeAead.AES_128_GCM ? 16 : 32; + bool oneStage = vector.Kdf is HpkeKdf.SHAKE128 or HpkeKdf.SHAKE256; + + Assert.NotEmpty(vector.Name); + Assert.NotEmpty(vector.Source); + Assert.NotEmpty(vector.KeyMaterial.HexToByteArray()); + Assert.Equal(suite.DecapsulationKeySizeInBytes, vector.DecapsulationKey.HexToByteArray().Length); + Assert.Equal(suite.EncapsulationKeySizeInBytes, vector.EncapsulationKey.HexToByteArray().Length); + Assert.Equal(suite.EncapsulatedSecretSizeInBytes, vector.EncapsulatedSecret.HexToByteArray().Length); + Assert.NotEmpty(vector.SharedSecret.HexToByteArray()); + Assert.Equal(keyLength, vector.AeadKey.HexToByteArray().Length); + Assert.Equal(12, vector.BaseNonce.HexToByteArray().Length); + Assert.Equal(hashLength, vector.ExporterSecret.HexToByteArray().Length); + Assert.InRange(vector.Info.HexToByteArray().Length, 0, oneStage ? ushort.MaxValue : int.MaxValue); + + if (vector.UsePsk) + { + Assert.InRange(vector.Psk.HexToByteArray().Length, 32, oneStage ? ushort.MaxValue : int.MaxValue); + Assert.InRange(vector.PskId.HexToByteArray().Length, 1, oneStage ? ushort.MaxValue : int.MaxValue); + } + else + { + Assert.Empty(vector.Psk); + Assert.Empty(vector.PskId); + } + + Assert.NotEmpty(vector.Messages); + + foreach (HpkeMessageVector message in vector.Messages) + { + byte[] plaintext = message.Plaintext.HexToByteArray(); + byte[] ciphertext = message.Ciphertext.HexToByteArray(); + _ = message.AssociatedData.HexToByteArray(); + Assert.Equal(suite.GetCiphertextLength(plaintext.Length), ciphertext.Length); + } + + Assert.NotEmpty(vector.Exports); + + foreach (HpkeExportVector export in vector.Exports) + { + _ = export.Context.HexToByteArray(); + Assert.InRange(export.Length, 0, oneStage ? ushort.MaxValue : 255 * hashLength); + Assert.Equal(export.Length, export.ExportedValue.HexToByteArray().Length); + } + } + + [Fact] + public static void GeneratedCases_CoverMissingBoundaries() + { + HpkeTestVector carry = HpkeTestData.GetVector("Generated-EmptyInfo-NonceCarry"); + Assert.False(carry.UsePsk); + Assert.Empty(carry.Info); + Assert.Equal(257, carry.Messages.Count); + Assert.Empty(carry.Messages[0].Plaintext); + Assert.Empty(carry.Messages[255].Plaintext); + Assert.Empty(carry.Messages[256].Plaintext); + Assert.Contains(carry.Messages, message => message.Plaintext.HexToByteArray().Length == 15); + Assert.Contains(carry.Messages, message => message.Plaintext.HexToByteArray().Length == 16); + Assert.Contains(carry.Messages, message => message.Plaintext.HexToByteArray().Length == 17); + Assert.Contains(carry.Exports, export => export.Length == 0); + Assert.Contains(carry.Exports, export => export.Length == 1); + Assert.Contains(carry.Exports, export => export.Length == 257); + + HpkeTestVector psk = HpkeTestData.GetVector("Generated-SHAKE256-Psk-MaxInputs"); + Assert.True(psk.UsePsk); + Assert.Equal(HpkeKdf.SHAKE256, psk.Kdf); + Assert.Equal(ushort.MaxValue, psk.Psk.HexToByteArray().Length); + Assert.Equal(ushort.MaxValue, psk.PskId.HexToByteArray().Length); + Assert.Equal(ushort.MaxValue, psk.Info.HexToByteArray().Length); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index e1a8a5a78cf13f..b123fb3a8a7ab6 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -131,6 +131,16 @@ Link="CommonTest\System\Security\Cryptography\HpkeContractTests.cs" /> + + + + + + + + + + Date: Fri, 11 Sep 2026 15:21:47 -0400 Subject: [PATCH 34/42] Bound HPKE test exporter contexts to 1024 bytes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../tests/System/Security/Cryptography/HpkeTestData.cs | 3 +++ .../System/Security/Cryptography/HpkeTestDataTests.cs | 2 +- .../System.Security.Cryptography/tests/HpkeTests.cs | 10 ++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs index cd29583131a893..6faeef0da7cd14 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs @@ -8,6 +8,9 @@ namespace System.Security.Cryptography.Tests { public static partial class HpkeTestData { + // A test-data portability bound, not a limit of the HPKE API. + internal const int MaxExporterContextLength = 1024; + // Representative coverage, not a Cartesian product. RFC cases include different KEM and outer HKDF hashes. // Generated cases add empty inputs/nonce carry and SHAKE PSK mode at the length-prefix boundaries. public static IReadOnlyList Vectors { get; } = Array.AsReadOnly( diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs index b1a2e5810e73da..b60ebcfdb4dc1c 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestDataTests.cs @@ -79,7 +79,7 @@ public static void Vector_HasValidShape(string name) foreach (HpkeExportVector export in vector.Exports) { - _ = export.Context.HexToByteArray(); + Assert.InRange(export.Context.HexToByteArray().Length, 0, HpkeTestData.MaxExporterContextLength); Assert.InRange(export.Length, 0, oneStage ? ushort.MaxValue : 255 * hashLength); Assert.Equal(export.Length, export.ExportedValue.HexToByteArray().Length); } diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs index a801ccc6027801..fb45f2623c3d01 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs @@ -1691,7 +1691,10 @@ public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) Assert.NotEqual(referenceExport, sender.Export(new byte[] { 0 }, 32)); Assert.False(referenceExport.AsSpan().SequenceEqual(sender.Export(context, 33).AsSpan(0, 32))); // HKDF export framing adds 22 bytes; the first two cases straddle the 256-byte stack limit. - foreach (int contextLength in new[] { 234, 235, 65536 }) + foreach (int contextLength in new[] + { + 234, 235, HpkeTestData.MaxExporterContextLength + }) { byte[] longContext = new byte[contextLength]; longContext.AsSpan().Fill(0x39); @@ -2562,8 +2565,7 @@ public static void Sender_Export(HpkeKdf kdf, int maximumLength) using (RecordingHpkeSender sender = new(suite)) { - // Unlike setup info, a SHAKE exporter context is not length-prefixed. - byte[] exporterContext = new byte[65536]; + byte[] exporterContext = new byte[HpkeTestData.MaxExporterContextLength]; exporterContext.AsSpan().Fill(0x39); foreach (int length in new[] { 0, 1, maximumLength }) @@ -2759,7 +2761,7 @@ public static void Recipient_Export(HpkeKdf kdf, int maximumLength) using (RecordingHpkeRecipient recipient = new(suite)) { - byte[] exporterContext = new byte[65536]; + byte[] exporterContext = new byte[HpkeTestData.MaxExporterContextLength]; exporterContext.AsSpan().Fill(0x39); foreach (int length in new[] { 0, 1, maximumLength }) From b2b03392c2403c6dfdd68b33bebe045cd99640ab Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 11 Sep 2026 15:53:20 -0400 Subject: [PATCH 35/42] Add shared HPKE sender and recipient contract tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../HpkeRecipientContractTests.cs | 614 ++++++++++++++++++ .../Cryptography/HpkeSenderContractTests.cs | 560 ++++++++++++++++ .../Security/Cryptography/HpkeTestData.cs | 9 + .../Microsoft.Bcl.Cryptography.Tests.csproj | 4 + .../System.Security.Cryptography.Tests.csproj | 4 + 5 files changed, 1191 insertions(+) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeRecipientContractTests.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeSenderContractTests.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeRecipientContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeRecipientContractTests.cs new file mode 100644 index 00000000000000..2d742027e350b5 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeRecipientContractTests.cs @@ -0,0 +1,614 @@ +// 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.Runtime.CompilerServices; +using Xunit; +using Xunit.Sdk; + +namespace System.Security.Cryptography.Tests +{ + public static class HpkeRecipientContractTests + { + private static readonly HpkeSuite s_suite = new( + HpkeKem.MLKEM_768, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + [Fact] + public static void Constructor_NullSuite() + { + AssertExtensions.Throws("suite", () => new HpkeRecipientContract(null)); + } + + [Theory] + [MemberData(nameof(HpkeTestData.RepresentativeSuites), MemberType = typeof(HpkeTestData))] + public static void Constructor_SetsSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + using (HpkeRecipientContract recipient = new(suite)) + { + Assert.Equal(suite, recipient.Suite); + } + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + public static void Dispose_CallsCoreOnce(int disposeCalls) + { + int calls = 0; + HpkeRecipientContract recipient = new(s_suite) + { + OnDispose = disposing => + { + Assert.True(disposing); + calls++; + }, + }; + + for (int i = 0; i < disposeCalls; i++) + { + recipient.Dispose(); + } + + Assert.Equal(1, calls); + } + + [Fact] + public static void Disposed_OperationsDoNotCallCore() + { + using (HpkeRecipientContract recipient = new(s_suite)) + { + recipient.Dispose(); + + foreach (Action operation in Operations(recipient)) + { + Assert.Throws(operation); + } + } + } + + [Fact] + public static void Dispose_FailurePropagatesAndDoesNotRepeat() + { + InvalidOperationException exception = new(); + int calls = 0; + HpkeRecipientContract recipient = new(s_suite) + { + OnDispose = disposing => + { + Assert.True(disposing); + calls++; + throw exception; + }, + }; + + Assert.Same(exception, Assert.Throws(() => recipient.Dispose())); + recipient.Dispose(); + Assert.Equal(1, calls); + + foreach (Action operation in Operations(recipient)) + { + Assert.Throws(operation); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.RepresentativeSuites), MemberType = typeof(HpkeTestData))] + public static void Open_Allocated(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 16, 17, 32 }) + foreach (bool useSpan in new[] { false, true }) + { + byte[] ciphertext = Data(suite.GetCiphertextLength(length)); + byte[] expectedCiphertext = (byte[])ciphertext.Clone(); + byte[] associatedData = [0x71, 0x72, 0x73]; + + using (HpkeRecipientContract recipient = new(suite) + { + OnOpenCore = (ct, p, aad) => + { + AssertExtensions.SequenceEqual(expectedCiphertext.AsSpan(), ct); + AssertExtensions.SequenceEqual(associatedData.AsSpan(), aad); + p.Fill(0xE7); + }, + }) + { + byte[] plaintext = useSpan + ? recipient.Open(ciphertext.AsSpan(), associatedData: associatedData.AsSpan()) + : recipient.Open(ciphertext, associatedData: associatedData); + Assert.Equal(length, plaintext.Length); + AssertExtensions.FilledWith(0xE7, plaintext); + Assert.Equal(expectedCiphertext, ciphertext); + Assert.Equal(new byte[] { 0x71, 0x72, 0x73 }, associatedData); + Assert.Equal(1, recipient.OpenCoreCount); + Assert.Equal(0, recipient.ExportCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.RepresentativeSuites), MemberType = typeof(HpkeTestData))] + public static void Open_Exact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 32 }) + { + byte[] input = Data(suite.GetCiphertextLength(length) + 2); + Memory ciphertext = input.AsMemory(1, input.Length - 2); + byte[] expected = ciphertext.ToArray(); + byte[] aadBuffer = [0xA5, 0x71, 0x72, 0x73, 0xA5]; + Memory associatedData = aadBuffer.AsMemory(1, 3); + byte[] output = new byte[length + 2]; + output.AsSpan().Fill(0xA5); + + using (HpkeRecipientContract recipient = new(suite) + { + OnOpenCore = (ct, p, aad) => + { + AssertExtensions.SequenceEqual(expected.AsSpan(), ct); + AssertExtensions.SequenceEqual(associatedData.Span, aad); + p.Fill(0xE7); + }, + }) + { + recipient.Open(ciphertext.Span, output.AsSpan(1, length), associatedData.Span); + AssertGuardedOutput(output); + Assert.Equal(Data(input.Length), input); + Assert.Equal(new byte[] { 0xA5, 0x71, 0x72, 0x73, 0xA5 }, aadBuffer); + Assert.Equal(1, recipient.OpenCoreCount); + Assert.Equal(0, recipient.ExportCoreCount); + } + } + } + + [Fact] + public static void Open_OptionalAssociatedDataIsEmpty() + { + byte[] ciphertext = Data(s_suite.GetCiphertextLength(1)); + + using (HpkeRecipientContract recipient = new(s_suite) + { + OnOpenCore = (ct, p, aad) => + { + AssertExtensions.SequenceEqual(ciphertext.AsSpan(), ct); + Assert.True(aad.IsEmpty); + p.Fill(0xE7); + }, + }) + { + byte[] first = recipient.Open(ciphertext); + byte[] second = recipient.Open(ciphertext.AsSpan()); + byte[] third = recipient.Open(ciphertext, associatedData: null); + byte[] destination = new byte[1]; + recipient.Open(ciphertext, destination.AsSpan()); + Assert.Equal(first, second); + Assert.Equal(first, third); + Assert.Equal(first, destination); + AssertExtensions.FilledWith(0xE7, destination); + Assert.Equal(4, recipient.OpenCoreCount); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void NullArgumentsBeforeDisposal(bool disposed) + { + using (HpkeRecipientContract recipient = new(s_suite)) + { + if (disposed) + { + recipient.Dispose(); + } + + AssertExtensions.Throws("ciphertext", () => recipient.Open((byte[])null)); + AssertExtensions.Throws("exporterContext", + () => recipient.Export((byte[])null, 0)); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Open_ShortCiphertextBeforeDisposal(bool disposed) + { + using (HpkeRecipientContract recipient = new(s_suite)) + { + if (disposed) + { + recipient.Dispose(); + } + + foreach (int length in new[] { 0, s_suite.AeadTagSizeInBytes - 1 }) + { + byte[] ciphertext = Data(length); + byte[] destination = Data(1); + AssertExtensions.Throws("ciphertext", () => recipient.Open(ciphertext)); + AssertExtensions.Throws("ciphertext", + () => recipient.Open(ciphertext.AsSpan())); + AssertExtensions.Throws("ciphertext", + () => recipient.Open(ciphertext, destination.AsSpan())); + Assert.Equal(Data(1), destination); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Open_InvalidDestinationBeforeDisposal(bool disposed) + { + byte[] ciphertext = Data(s_suite.GetCiphertextLength(32)); + + using (HpkeRecipientContract recipient = new(s_suite)) + { + if (disposed) + { + recipient.Dispose(); + } + + foreach (int length in new[] { 0, 31, 33 }) + { + byte[] destination = new byte[length]; + destination.AsSpan().Fill(0xA5); + AssertExtensions.Throws("plaintext", + () => recipient.Open(ciphertext, destination.AsSpan())); + AssertExtensions.FilledWith(0xA5, destination); + } + } + } + + public static IEnumerable OpenOverlaps() + { + foreach (bool overlapCiphertext in new[] { false, true }) + { + int inputLength = overlapCiphertext ? s_suite.GetCiphertextLength(32) : 16; + + foreach (int offset in new[] { -1, 0, 1, inputLength - 1, 1 - 32 }) + { + yield return new object[] { overlapCiphertext, offset }; + } + } + } + + [Theory] + [MemberData(nameof(OpenOverlaps))] + public static void Open_OverlapsRejectedBeforeDisposal(bool overlapCiphertext, int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[] ciphertext = new byte[256]; + byte[] aad = new byte[256]; + byte[] output = overlapCiphertext ? ciphertext : aad; + output.AsSpan().Fill(0xA5); + + using (HpkeRecipientContract recipient = new(s_suite)) + { + if (disposed) + { + recipient.Dispose(); + } + + Assert.Throws(() => recipient.Open( + ciphertext.AsSpan(80, s_suite.GetCiphertextLength(32)), + output.AsSpan(80 + offset, 32), + aad.AsSpan(80, 16))); + AssertExtensions.FilledWith(0xA5, output); + } + } + } + + [Fact] + public static void Open_ReadOnlyOverlapAndAdjacentOutput() + { + int ciphertextLength = s_suite.GetCiphertextLength(32); + byte[] buffer = Data(ciphertextLength + 32); + byte[] expected = buffer.AsSpan(0, ciphertextLength).ToArray(); + + using (HpkeRecipientContract recipient = new(s_suite) + { + OnOpenCore = (ct, p, aad) => + { + AssertExtensions.SequenceEqual(expected.AsSpan(), ct); + AssertExtensions.SequenceEqual(expected.AsSpan(0, 16), aad); + p.Fill(0xE7); + }, + }) + { + recipient.Open( + buffer.AsSpan(0, ciphertextLength), buffer.AsSpan(ciphertextLength), buffer.AsSpan(0, 16)); + AssertExtensions.SequenceEqual(expected.AsSpan(), buffer.AsSpan(0, ciphertextLength)); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(ciphertextLength)); + Assert.Equal(1, recipient.OpenCoreCount); + } + } + + [Fact] + public static void Open_EmptyOutputMayShareInputBuffer() + { + byte[] buffer = Data(s_suite.AeadTagSizeInBytes); + byte[] original = (byte[])buffer.Clone(); + + using (HpkeRecipientContract recipient = new(s_suite) + { + OnOpenCore = (ct, p, aad) => + { + AssertExtensions.SequenceEqual(original.AsSpan(), ct); + Assert.True(p.IsEmpty); + Assert.True(aad.IsEmpty); + }, + }) + { + recipient.Open(buffer.AsSpan(), buffer.AsSpan(1, 0), buffer.AsSpan(2, 0)); + Assert.Equal(original, buffer); + Assert.Equal(1, recipient.OpenCoreCount); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.ExportLimits), MemberType = typeof(HpkeTestData))] + public static void Export_AllocatedAndExact(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + foreach (int contextLength in new[] { 0, 1, HpkeTestData.MaxExporterContextLength }) + foreach (int length in new[] { 0, 1, maximumLength }) + { + byte[] context = Data(contextLength); + byte[] expectedContext = (byte[])context.Clone(); + byte[] output = new byte[length + 2]; + output.AsSpan().Fill(0xA5); + + using (HpkeRecipientContract recipient = new(suite) + { + OnExportCore = (c, destination) => + { + AssertExtensions.SequenceEqual(expectedContext.AsSpan(), c); + Assert.Equal(length, destination.Length); + destination.Fill(0xE7); + }, + }) + { + byte[] first = recipient.Export(context, length); + byte[] second = recipient.Export(context.AsSpan(), length); + recipient.Export(context, output.AsSpan(1, length)); + Assert.Equal(length, first.Length); + AssertExtensions.FilledWith(0xE7, first); + Assert.Equal(first, second); + AssertGuardedOutput(output); + Assert.Equal(expectedContext, context); + Assert.Equal(3, recipient.ExportCoreCount); + Assert.Equal(0, recipient.OpenCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.ExportLimits), MemberType = typeof(HpkeTestData))] + public static void Export_InvalidLengthsBeforeDisposal(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + foreach (bool disposed in new[] { false, true }) + { + using (HpkeRecipientContract recipient = new(suite)) + { + if (disposed) + { + recipient.Dispose(); + } + + foreach (int length in new[] { -1, int.MinValue, maximumLength + 1, int.MaxValue }) + { + AssertExtensions.Throws("length", + () => recipient.Export(Array.Empty(), length)); + AssertExtensions.Throws("length", + () => recipient.Export(ReadOnlySpan.Empty, length)); + } + + byte[] destination = new byte[maximumLength + 1]; + destination.AsSpan().Fill(0xA5); + AssertExtensions.Throws("destination", + () => recipient.Export(ReadOnlySpan.Empty, destination.AsSpan())); + AssertExtensions.FilledWith(0xA5, destination); + } + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + [InlineData(15)] + [InlineData(-31)] + public static void Export_OverlapsRejectedBeforeDisposal(int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[] buffer = new byte[128]; + buffer.AsSpan().Fill(0xA5); + + using (HpkeRecipientContract recipient = new(s_suite)) + { + if (disposed) + { + recipient.Dispose(); + } + + Assert.Throws(() => + recipient.Export(buffer.AsSpan(48, 16), buffer.AsSpan(48 + offset, 32))); + AssertExtensions.FilledWith(0xA5, buffer); + } + } + } + + [Theory] + [InlineData(0, 32)] + [InlineData(32, 0)] + [InlineData(32, 32)] + public static void Export_EmptyAndAdjacentBuffers(int contextLength, int outputLength) + { + byte[] buffer = Data(contextLength + outputLength + 1); + byte[] original = (byte[])buffer.Clone(); + + using (HpkeRecipientContract recipient = new(s_suite) + { + OnExportCore = (context, destination) => + { + AssertExtensions.SequenceEqual(original.AsSpan(0, contextLength), context); + Assert.Equal(outputLength, destination.Length); + destination.Fill(0xE7); + }, + }) + { + recipient.Export(buffer.AsSpan(0, contextLength), buffer.AsSpan(contextLength, outputLength)); + AssertExtensions.SequenceEqual(original.AsSpan(0, contextLength), buffer.AsSpan(0, contextLength)); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(contextLength, outputLength)); + Assert.Equal(original[original.Length - 1], buffer[buffer.Length - 1]); + Assert.Equal(1, recipient.ExportCoreCount); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Open_CoreFailurePropagatesUnchanged(bool authenticationFailure) + { + CryptographicException exception = authenticationFailure + ? new AuthenticationTagMismatchException() + : new CryptographicException(); + byte[] ciphertext = new byte[s_suite.AeadTagSizeInBytes]; + + using (HpkeRecipientContract recipient = new(s_suite) + { + OnOpenCore = (ct, p, aad) => throw exception, + }) + { + Assert.Same(exception, Assert.Throws(exception.GetType(), () => recipient.Open(ciphertext))); + Assert.Same(exception, Assert.Throws(exception.GetType(), () => recipient.Open(ciphertext.AsSpan()))); + Assert.Same(exception, Assert.Throws(exception.GetType(), + () => recipient.Open(ciphertext, Span.Empty))); + Assert.Equal(3, recipient.OpenCoreCount); + Assert.Equal(0, recipient.ExportCoreCount); + } + } + + [Fact] + public static void Export_CoreFailurePropagatesUnchanged() + { + CryptographicException exception = new(); + + using (HpkeRecipientContract recipient = new(s_suite) + { + OnExportCore = (context, destination) => throw exception, + }) + { + Assert.Same(exception, Assert.Throws( + () => recipient.Export(Array.Empty(), 0))); + Assert.Same(exception, Assert.Throws( + () => recipient.Export(ReadOnlySpan.Empty, 1))); + Assert.Same(exception, Assert.Throws( + () => recipient.Export(ReadOnlySpan.Empty, new byte[1].AsSpan()))); + Assert.Equal(3, recipient.ExportCoreCount); + Assert.Equal(0, recipient.OpenCoreCount); + } + } + + private static IEnumerable Operations(HpkeRecipient recipient) + { + byte[] ciphertext = new byte[recipient.Suite.AeadTagSizeInBytes]; + yield return () => recipient.Open(ciphertext); + yield return () => recipient.Open(ciphertext.AsSpan()); + yield return () => recipient.Open(ciphertext, Span.Empty); + yield return () => recipient.Export(Array.Empty(), 0); + yield return () => recipient.Export(ReadOnlySpan.Empty, 1); + yield return () => recipient.Export(ReadOnlySpan.Empty, new byte[1].AsSpan()); + } + + private static byte[] Data(int length) + { + byte[] data = new byte[length]; + + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)(i * 17 + 3); + } + + return data; + } + + private static void AssertGuardedOutput(byte[] buffer) + { + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[buffer.Length - 1]); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(1, buffer.Length - 2)); + } + } + + internal sealed class HpkeRecipientContract : HpkeRecipient + { + private bool _disposed; + + internal OpenCoreCallback OnOpenCore { get; set; } + internal ExportCoreCallback OnExportCore { get; set; } + internal Action OnDispose { get; set; } = static disposing => { }; + internal int OpenCoreCount { get; private set; } + internal int ExportCoreCount { get; private set; } + + internal HpkeRecipientContract(HpkeSuite suite) : base(suite) + { + } + + protected override void OpenCore( + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData) + { + OpenCoreCount++; + Assert.InRange(ciphertext.Length, Suite.AeadTagSizeInBytes, int.MaxValue); + Assert.Equal(ciphertext.Length - Suite.AeadTagSizeInBytes, plaintext.Length); + GetCallback(OnOpenCore)(ciphertext, plaintext, associatedData); + } + + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) + { + ExportCoreCount++; + GetCallback(OnExportCore)(exporterContext, destination); + } + + protected override void Dispose(bool disposing) + { + GetCallback(OnDispose)(disposing); + + if (OnOpenCore is not null && OpenCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(OpenCore)}."); + } + + if (OnExportCore is not null && ExportCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(ExportCore)}."); + } + + _disposed = true; + } + + private T GetCallback(T callback, [CallerMemberName] string caller = null) where T : Delegate + { + if (_disposed) + { + Assert.Fail($"Unexpected call to {caller} after Dispose."); + } + + return callback ?? throw new XunitException($"Unexpected call to {caller}."); + } + + internal delegate void OpenCoreCallback( + ReadOnlySpan ciphertext, Span plaintext, ReadOnlySpan associatedData); + internal delegate void ExportCoreCallback(ReadOnlySpan exporterContext, Span destination); + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeSenderContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSenderContractTests.cs new file mode 100644 index 00000000000000..988b5fc19b8824 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSenderContractTests.cs @@ -0,0 +1,560 @@ +// 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.Runtime.CompilerServices; +using Xunit; +using Xunit.Sdk; + +namespace System.Security.Cryptography.Tests +{ + public static class HpkeSenderContractTests + { + private static readonly HpkeSuite s_suite = new( + HpkeKem.MLKEM_768, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + [Fact] + public static void Constructor_NullSuite() + { + AssertExtensions.Throws("suite", () => new HpkeSenderContract(null)); + } + + [Theory] + [MemberData(nameof(HpkeTestData.RepresentativeSuites), MemberType = typeof(HpkeTestData))] + public static void Constructor_SetsSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + using (HpkeSenderContract sender = new(suite)) + { + Assert.Equal(suite, sender.Suite); + } + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + public static void Dispose_CallsCoreOnce(int disposeCalls) + { + int calls = 0; + HpkeSenderContract sender = new(s_suite) + { + OnDispose = disposing => + { + Assert.True(disposing); + calls++; + }, + }; + + for (int i = 0; i < disposeCalls; i++) + { + sender.Dispose(); + } + + Assert.Equal(1, calls); + } + + [Fact] + public static void Disposed_OperationsDoNotCallCore() + { + using (HpkeSenderContract sender = new(s_suite)) + { + sender.Dispose(); + + foreach (Action operation in Operations(sender)) + { + Assert.Throws(operation); + } + } + } + + [Fact] + public static void Dispose_FailurePropagatesAndDoesNotRepeat() + { + InvalidOperationException exception = new(); + int calls = 0; + HpkeSenderContract sender = new(s_suite) + { + OnDispose = disposing => + { + Assert.True(disposing); + calls++; + throw exception; + }, + }; + + Assert.Same(exception, Assert.Throws(() => sender.Dispose())); + sender.Dispose(); + Assert.Equal(1, calls); + + foreach (Action operation in Operations(sender)) + { + Assert.Throws(operation); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.RepresentativeSuites), MemberType = typeof(HpkeTestData))] + public static void Seal_Allocated(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 16, 17, 32 }) + foreach (bool useSpan in new[] { false, true }) + { + byte[] plaintext = Data(length); + byte[] associatedData = [0x71, 0x72, 0x73]; + + using (HpkeSenderContract sender = new(suite) + { + OnSealCore = (p, ct, aad) => + { + AssertExtensions.SequenceEqual(plaintext.AsSpan(), p); + AssertExtensions.SequenceEqual(associatedData.AsSpan(), aad); + ct.Fill(0xE7); + }, + }) + { + byte[] ciphertext = useSpan + ? sender.Seal(plaintext.AsSpan(), associatedData: associatedData.AsSpan()) + : sender.Seal(plaintext, associatedData: associatedData); + Assert.Equal(suite.GetCiphertextLength(length), ciphertext.Length); + AssertExtensions.FilledWith(0xE7, ciphertext); + Assert.Equal(Data(length), plaintext); + Assert.Equal(new byte[] { 0x71, 0x72, 0x73 }, associatedData); + Assert.Equal(1, sender.SealCoreCount); + Assert.Equal(0, sender.ExportCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.RepresentativeSuites), MemberType = typeof(HpkeTestData))] + public static void Seal_Exact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + foreach (int length in new[] { 0, 1, 32 }) + { + byte[] input = Data(length + 2); + Memory plaintext = input.AsMemory(1, length); + byte[] expected = plaintext.ToArray(); + byte[] aadBuffer = [0xA5, 0x71, 0x72, 0x73, 0xA5]; + Memory associatedData = aadBuffer.AsMemory(1, 3); + byte[] output = new byte[suite.GetCiphertextLength(length) + 2]; + output.AsSpan().Fill(0xA5); + + using (HpkeSenderContract sender = new(suite) + { + OnSealCore = (p, ct, aad) => + { + AssertExtensions.SequenceEqual(expected.AsSpan(), p); + AssertExtensions.SequenceEqual(associatedData.Span, aad); + ct.Fill(0xE7); + }, + }) + { + sender.Seal(plaintext.Span, output.AsSpan(1, output.Length - 2), associatedData.Span); + AssertGuardedOutput(output); + Assert.Equal(Data(length + 2), input); + Assert.Equal(new byte[] { 0xA5, 0x71, 0x72, 0x73, 0xA5 }, aadBuffer); + Assert.Equal(1, sender.SealCoreCount); + Assert.Equal(0, sender.ExportCoreCount); + } + } + } + + [Fact] + public static void Seal_OptionalAssociatedDataIsEmpty() + { + using (HpkeSenderContract sender = new(s_suite) + { + OnSealCore = (p, ct, aad) => + { + Assert.True(p.IsEmpty); + Assert.True(aad.IsEmpty); + ct.Fill(0xE7); + }, + }) + { + byte[] first = sender.Seal(Array.Empty()); + byte[] second = sender.Seal(ReadOnlySpan.Empty); + byte[] third = sender.Seal(Array.Empty(), associatedData: null); + byte[] destination = new byte[s_suite.AeadTagSizeInBytes]; + sender.Seal(ReadOnlySpan.Empty, destination.AsSpan()); + Assert.Equal(first, second); + Assert.Equal(first, third); + Assert.Equal(first, destination); + AssertExtensions.FilledWith(0xE7, destination); + Assert.Equal(4, sender.SealCoreCount); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void NullArgumentsBeforeDisposal(bool disposed) + { + using (HpkeSenderContract sender = new(s_suite)) + { + if (disposed) + { + sender.Dispose(); + } + + AssertExtensions.Throws("plaintext", () => sender.Seal((byte[])null)); + AssertExtensions.Throws("exporterContext", + () => sender.Export((byte[])null, 0)); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Seal_InvalidDestinationBeforeDisposal(bool disposed) + { + byte[] plaintext = Data(32); + + using (HpkeSenderContract sender = new(s_suite)) + { + if (disposed) + { + sender.Dispose(); + } + + int size = s_suite.GetCiphertextLength(plaintext.Length); + + foreach (int length in new[] { 0, size - 1, size + 1 }) + { + byte[] destination = new byte[length]; + destination.AsSpan().Fill(0xA5); + AssertExtensions.Throws("ciphertext", + () => sender.Seal(plaintext, destination.AsSpan())); + AssertExtensions.FilledWith(0xA5, destination); + } + } + } + + public static IEnumerable SealOverlaps() + { + foreach (bool overlapPlaintext in new[] { false, true }) + { + int inputLength = overlapPlaintext ? 32 : 16; + int outputLength = s_suite.GetCiphertextLength(32); + + foreach (int offset in new[] { -1, 0, 1, inputLength - 1, 1 - outputLength }) + { + yield return new object[] { overlapPlaintext, offset }; + } + } + } + + [Theory] + [MemberData(nameof(SealOverlaps))] + public static void Seal_OverlapsRejectedBeforeDisposal(bool overlapPlaintext, int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[] plaintext = new byte[256]; + byte[] aad = new byte[256]; + byte[] output = overlapPlaintext ? plaintext : aad; + output.AsSpan().Fill(0xA5); + + using (HpkeSenderContract sender = new(s_suite)) + { + if (disposed) + { + sender.Dispose(); + } + + Assert.Throws(() => sender.Seal( + plaintext.AsSpan(80, 32), + output.AsSpan(80 + offset, s_suite.GetCiphertextLength(32)), + aad.AsSpan(80, 16))); + AssertExtensions.FilledWith(0xA5, output); + } + } + } + + [Fact] + public static void Seal_ReadOnlyOverlapAndAdjacentOutput() + { + byte[] buffer = Data(32 + s_suite.GetCiphertextLength(32)); + byte[] expected = buffer.AsSpan(0, 32).ToArray(); + + using (HpkeSenderContract sender = new(s_suite) + { + OnSealCore = (p, ct, aad) => + { + AssertExtensions.SequenceEqual(expected.AsSpan(), p); + AssertExtensions.SequenceEqual(expected.AsSpan(0, 16), aad); + ct.Fill(0xE7); + }, + }) + { + sender.Seal(buffer.AsSpan(0, 32), buffer.AsSpan(32), buffer.AsSpan(0, 16)); + AssertExtensions.SequenceEqual(expected.AsSpan(), buffer.AsSpan(0, 32)); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(32)); + Assert.Equal(1, sender.SealCoreCount); + } + } + + [Fact] + public static void Seal_EmptyInputsMayShareOutputBuffer() + { + byte[] buffer = new byte[s_suite.AeadTagSizeInBytes]; + + using (HpkeSenderContract sender = new(s_suite) + { + OnSealCore = (p, ct, aad) => + { + Assert.True(p.IsEmpty); + Assert.True(aad.IsEmpty); + ct.Fill(0xE7); + }, + }) + { + sender.Seal(buffer.AsSpan(0, 0), buffer.AsSpan(), buffer.AsSpan(1, 0)); + AssertExtensions.FilledWith(0xE7, buffer); + Assert.Equal(1, sender.SealCoreCount); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.ExportLimits), MemberType = typeof(HpkeTestData))] + public static void Export_AllocatedAndExact(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + foreach (int contextLength in new[] { 0, 1, HpkeTestData.MaxExporterContextLength }) + foreach (int length in new[] { 0, 1, maximumLength }) + { + byte[] context = Data(contextLength); + byte[] expectedContext = (byte[])context.Clone(); + byte[] output = new byte[length + 2]; + output.AsSpan().Fill(0xA5); + + using (HpkeSenderContract sender = new(suite) + { + OnExportCore = (c, destination) => + { + AssertExtensions.SequenceEqual(expectedContext.AsSpan(), c); + Assert.Equal(length, destination.Length); + destination.Fill(0xE7); + }, + }) + { + byte[] first = sender.Export(context, length); + byte[] second = sender.Export(context.AsSpan(), length); + sender.Export(context, output.AsSpan(1, length)); + Assert.Equal(length, first.Length); + AssertExtensions.FilledWith(0xE7, first); + Assert.Equal(first, second); + AssertGuardedOutput(output); + Assert.Equal(expectedContext, context); + Assert.Equal(3, sender.ExportCoreCount); + Assert.Equal(0, sender.SealCoreCount); + } + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.ExportLimits), MemberType = typeof(HpkeTestData))] + public static void Export_InvalidLengthsBeforeDisposal(HpkeKdf kdf, int maximumLength) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + foreach (bool disposed in new[] { false, true }) + { + using (HpkeSenderContract sender = new(suite)) + { + if (disposed) + { + sender.Dispose(); + } + + foreach (int length in new[] { -1, int.MinValue, maximumLength + 1, int.MaxValue }) + { + AssertExtensions.Throws("length", + () => sender.Export(Array.Empty(), length)); + AssertExtensions.Throws("length", + () => sender.Export(ReadOnlySpan.Empty, length)); + } + + byte[] destination = new byte[maximumLength + 1]; + destination.AsSpan().Fill(0xA5); + AssertExtensions.Throws("destination", + () => sender.Export(ReadOnlySpan.Empty, destination.AsSpan())); + AssertExtensions.FilledWith(0xA5, destination); + } + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + [InlineData(15)] + [InlineData(-31)] + public static void Export_OverlapsRejectedBeforeDisposal(int offset) + { + foreach (bool disposed in new[] { false, true }) + { + byte[] buffer = new byte[128]; + buffer.AsSpan().Fill(0xA5); + + using (HpkeSenderContract sender = new(s_suite)) + { + if (disposed) + { + sender.Dispose(); + } + + Assert.Throws(() => + sender.Export(buffer.AsSpan(48, 16), buffer.AsSpan(48 + offset, 32))); + AssertExtensions.FilledWith(0xA5, buffer); + } + } + } + + [Theory] + [InlineData(0, 32)] + [InlineData(32, 0)] + [InlineData(32, 32)] + public static void Export_EmptyAndAdjacentBuffers(int contextLength, int outputLength) + { + byte[] buffer = Data(contextLength + outputLength + 1); + byte[] original = (byte[])buffer.Clone(); + + using (HpkeSenderContract sender = new(s_suite) + { + OnExportCore = (context, destination) => + { + AssertExtensions.SequenceEqual(original.AsSpan(0, contextLength), context); + Assert.Equal(outputLength, destination.Length); + destination.Fill(0xE7); + }, + }) + { + sender.Export(buffer.AsSpan(0, contextLength), buffer.AsSpan(contextLength, outputLength)); + AssertExtensions.SequenceEqual(original.AsSpan(0, contextLength), buffer.AsSpan(0, contextLength)); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(contextLength, outputLength)); + Assert.Equal(original[original.Length - 1], buffer[buffer.Length - 1]); + Assert.Equal(1, sender.ExportCoreCount); + } + } + + [Fact] + public static void CoreFailures_PropagateUnchanged() + { + CryptographicException exception = new(); + + using (HpkeSenderContract sender = new(s_suite) + { + OnSealCore = (p, ct, aad) => throw exception, + OnExportCore = (context, destination) => throw exception, + }) + { + foreach (Action operation in Operations(sender)) + { + Assert.Same(exception, Assert.Throws(operation)); + } + + Assert.Equal(3, sender.SealCoreCount); + Assert.Equal(3, sender.ExportCoreCount); + } + } + + private static IEnumerable Operations(HpkeSender sender) + { + yield return () => sender.Seal(Array.Empty()); + yield return () => sender.Seal(ReadOnlySpan.Empty); + yield return () => sender.Seal( + ReadOnlySpan.Empty, new byte[sender.Suite.AeadTagSizeInBytes].AsSpan()); + yield return () => sender.Export(Array.Empty(), 0); + yield return () => sender.Export(ReadOnlySpan.Empty, 1); + yield return () => sender.Export(ReadOnlySpan.Empty, new byte[1].AsSpan()); + } + + private static byte[] Data(int length) + { + byte[] data = new byte[length]; + + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)(i * 17 + 3); + } + + return data; + } + + private static void AssertGuardedOutput(byte[] buffer) + { + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[buffer.Length - 1]); + AssertExtensions.FilledWith(0xE7, buffer.AsSpan(1, buffer.Length - 2)); + } + } + + internal sealed class HpkeSenderContract : HpkeSender + { + private bool _disposed; + + internal SealCoreCallback OnSealCore { get; set; } + internal ExportCoreCallback OnExportCore { get; set; } + internal Action OnDispose { get; set; } = static disposing => { }; + internal int SealCoreCount { get; private set; } + internal int ExportCoreCount { get; private set; } + + internal HpkeSenderContract(HpkeSuite suite) : base(suite) + { + } + + protected override void SealCore( + ReadOnlySpan plaintext, + Span ciphertext, + ReadOnlySpan associatedData) + { + SealCoreCount++; + Assert.Equal(Suite.GetCiphertextLength(plaintext.Length), ciphertext.Length); + GetCallback(OnSealCore)(plaintext, ciphertext, associatedData); + } + + protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) + { + ExportCoreCount++; + GetCallback(OnExportCore)(exporterContext, destination); + } + + protected override void Dispose(bool disposing) + { + GetCallback(OnDispose)(disposing); + + if (OnSealCore is not null && SealCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(SealCore)}."); + } + + if (OnExportCore is not null && ExportCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(ExportCore)}."); + } + + _disposed = true; + } + + private T GetCallback(T callback, [CallerMemberName] string caller = null) where T : Delegate + { + if (_disposed) + { + Assert.Fail($"Unexpected call to {caller} after Dispose."); + } + + return callback ?? throw new XunitException($"Unexpected call to {caller}."); + } + + internal delegate void SealCoreCallback( + ReadOnlySpan plaintext, Span ciphertext, ReadOnlySpan associatedData); + internal delegate void ExportCoreCallback(ReadOnlySpan exporterContext, Span destination); + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs index 6faeef0da7cd14..7bab0b14e848a9 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs @@ -19,6 +19,15 @@ public static partial class HpkeTestData public static IEnumerable VectorNames => Vectors.Select(vector => new object[] { vector.Name }); + public static IEnumerable ExportLimits => + [ + [HpkeKdf.HKDF_SHA256, 8160], + [HpkeKdf.HKDF_SHA384, 12240], + [HpkeKdf.HKDF_SHA512, 16320], + [HpkeKdf.SHAKE128, 65535], + [HpkeKdf.SHAKE256, 65535], + ]; + public static IEnumerable RepresentativeSuites { get diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index b123fb3a8a7ab6..462c862dc81136 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -129,6 +129,10 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLKemAlgorithmTests.cs" /> + + + + Date: Sat, 12 Sep 2026 11:46:16 -0400 Subject: [PATCH 36/42] Add shared HPKE key tests and prune legacy tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Security/Cryptography/HpkeKeyTests.cs | 463 ++++ .../Microsoft.Bcl.Cryptography.Tests.csproj | 2 + .../tests/HpkeTests.cs | 2437 +---------------- .../System.Security.Cryptography.Tests.csproj | 2 + 4 files changed, 614 insertions(+), 2290 deletions(-) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeKeyTests.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeKeyTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeKeyTests.cs new file mode 100644 index 00000000000000..faa5ec680f8927 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeKeyTests.cs @@ -0,0 +1,463 @@ +// 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 Microsoft.DotNet.XUnitExtensions; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(PlatformDetection), + nameof(PlatformDetection.IsNotBrowser), + nameof(PlatformDetection.IsNotWasi), + nameof(PlatformDetection.IsNotNetFramework))] + public static class HpkeKeyTests + { + private static IEnumerable SupportedKeyAlgorithms => + Enum.GetValues(typeof(HpkeKem)).Cast().Where(kem => Hpke.IsSupported(KeySuite(kem))); + + private static IEnumerable SupportedNistAlgorithms => + SupportedKeyAlgorithms.Where(kem => kem is HpkeKem.DHKEM_P256_HKDF_SHA256 + or HpkeKem.DHKEM_P384_HKDF_SHA384 or HpkeKem.DHKEM_P521_HKDF_SHA512); + + public static bool HasX25519 => Hpke.IsSupported(KeySuite(HpkeKem.DHKEM_X25519_HKDF_SHA256)); + + public static IEnumerable SupportedKems => + SupportedKeyAlgorithms.Select(kem => new object[] { kem }); + + public static IEnumerable SupportedNistKems => + SupportedNistAlgorithms.Select(kem => new object[] { kem }); + + public static IEnumerable SupportedKeyVectorNames + { + get + { + HashSet<(HpkeKem, string, string, string)> seen = new(); + + foreach (HpkeTestVector vector in HpkeTestData.Vectors) + { + if (Hpke.IsSupported(KeySuite(vector.Kem)) && + seen.Add((vector.Kem, vector.KeyMaterial, vector.DecapsulationKey, vector.EncapsulationKey))) + { + yield return new object[] { vector.Name }; + } + } + } + } + + public static IEnumerable KeyDerivationSuiteVariants + { + get + { + HashSet<(HpkeKem, HpkeKdf, HpkeAead)> seen = new(); + + foreach (HpkeTestVector vector in HpkeTestData.Vectors) + { + HpkeSuite suite = new(vector.Kem, vector.Kdf, vector.Aead); + + if (!suite.Equals(KeySuite(vector.Kem)) && + Hpke.IsSupported(suite) && + Hpke.IsSupported(KeySuite(vector.Kem)) && + seen.Add((vector.Kem, vector.Kdf, vector.Aead))) + { + yield return new object[] { vector.Name }; + } + } + } + } + + [Theory] + [MemberData(nameof(SupportedKems))] + public static void GenerateKey_Roundtrip(HpkeKem kem) + { + HpkeSuite suite = KeySuite(kem); + + using (Hpke generated = Hpke.GenerateKey(suite)) + { + byte[] privateKey = generated.ExportDecapsulationKey(); + byte[] publicKey = generated.ExportEncapsulationKey(); + + Assert.Equal(suite, generated.Suite); + Assert.Equal(suite.DecapsulationKeySizeInBytes, privateKey.Length); + Assert.Equal(suite.EncapsulationKeySizeInBytes, publicKey.Length); + AssertKeyExports(generated, privateKey, publicKey); + + foreach (bool useSpan in new[] { false, true }) + { + using (Hpke importedPrivate = ImportPrivate(suite, privateKey, useSpan)) + using (Hpke importedPublic = ImportPublic(suite, publicKey, useSpan)) + { + AssertKeyExports(importedPrivate, privateKey, publicKey); + AssertPublicKeyExports(importedPublic, publicKey); + AssertKeyPairWorks(generated, importedPublic); + AssertKeyPairWorks(importedPrivate, generated); + } + } + } + } + + [Theory] + [MemberData(nameof(SupportedKeyVectorNames))] + public static void DeriveKey_KnownAnswerAndInputOwnership(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = KeySuite(vector.Kem); + byte[] material = vector.KeyMaterial.HexToByteArray(); + byte[] expectedPrivate = vector.DecapsulationKey.HexToByteArray(); + byte[] expectedPublic = vector.EncapsulationKey.HexToByteArray(); + + foreach (bool useSpan in new[] { false, true }) + { + int offset = useSpan ? 1 : 0; + byte[] input = InputBuffer(material, offset); + + using (Hpke key = useSpan + ? Hpke.DeriveKey(suite, input.AsSpan(offset, material.Length)) + : Hpke.DeriveKey(suite, input)) + { + input.AsSpan().Fill(0xEC); + Assert.Equal(suite, key.Suite); + AssertKeyExports(key, expectedPrivate, expectedPublic); + } + } + } + + [Theory] + [MemberData(nameof(KeyDerivationSuiteVariants))] + public static void DeriveKey_IndependentOfOuterKdfAndAead(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = new(vector.Kem, vector.Kdf, vector.Aead); + byte[] material = vector.KeyMaterial.HexToByteArray(); + byte[] expectedPrivate = vector.DecapsulationKey.HexToByteArray(); + byte[] expectedPublic = vector.EncapsulationKey.HexToByteArray(); + + using (Hpke baseline = Hpke.DeriveKey(KeySuite(vector.Kem), material)) + using (Hpke variant = Hpke.DeriveKey(suite, material.AsSpan())) + { + AssertKeyExports(baseline, expectedPrivate, expectedPublic); + AssertKeyExports(variant, expectedPrivate, expectedPublic); + } + } + + [Theory] + [MemberData(nameof(SupportedKeyVectorNames))] + public static void ImportDecapsulationKey_KnownAnswerAndInputOwnership(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = KeySuite(vector.Kem); + byte[] expectedPrivate = vector.DecapsulationKey.HexToByteArray(); + byte[] expectedPublic = vector.EncapsulationKey.HexToByteArray(); + + foreach (bool useSpan in new[] { false, true }) + { + int offset = useSpan ? 1 : 0; + byte[] input = InputBuffer(expectedPrivate, offset); + + using (Hpke key = useSpan + ? Hpke.ImportDecapsulationKey(suite, input.AsSpan(offset, expectedPrivate.Length)) + : Hpke.ImportDecapsulationKey(suite, input)) + using (Hpke peer = Hpke.ImportEncapsulationKey(suite, expectedPublic)) + { + input.AsSpan().Fill(0xEC); + Assert.Equal(suite, key.Suite); + AssertKeyExports(key, expectedPrivate, expectedPublic); + AssertKeyPairWorks(key, peer); + } + } + } + + [Theory] + [MemberData(nameof(SupportedKeyVectorNames))] + public static void ImportEncapsulationKey_KnownAnswerAndInputOwnership(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = KeySuite(vector.Kem); + byte[] privateKey = vector.DecapsulationKey.HexToByteArray(); + byte[] expected = vector.EncapsulationKey.HexToByteArray(); + + using (Hpke recipient = Hpke.ImportDecapsulationKey(suite, privateKey)) + { + foreach (bool useSpan in new[] { false, true }) + { + int offset = useSpan ? 1 : 0; + byte[] input = InputBuffer(expected, offset); + + using (Hpke key = useSpan + ? Hpke.ImportEncapsulationKey(suite, input.AsSpan(offset, expected.Length)) + : Hpke.ImportEncapsulationKey(suite, input)) + { + input.AsSpan().Fill(0xEC); + Assert.Equal(suite, key.Suite); + AssertPublicKeyExports(key, expected); + AssertKeyPairWorks(recipient, key); + } + } + } + } + + [Theory] + [MemberData(nameof(SupportedKeyVectorNames))] + public static void ExportKeys_IndependentBuffers(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + byte[] privateKey = vector.DecapsulationKey.HexToByteArray(); + + using (Hpke key = Hpke.ImportDecapsulationKey(KeySuite(vector.Kem), privateKey)) + { + Assert.NotSame(key.ExportDecapsulationKey(), key.ExportDecapsulationKey()); + Assert.NotSame(key.ExportEncapsulationKey(), key.ExportEncapsulationKey()); + } + } + + [Theory] + [MemberData(nameof(SupportedKems))] + public static void PublicOnlyKey_CapabilitiesAndContextLifetimes(HpkeKem kem) + { + HpkeSuite suite = KeySuite(kem); + HpkeTestVector vector = HpkeTestData.Vectors.First(value => value.Kem == kem); + byte[] privateBytes = vector.DecapsulationKey.HexToByteArray(); + byte[] publicBytes = vector.EncapsulationKey.HexToByteArray(); + + Hpke privateKey = Hpke.ImportDecapsulationKey(suite, privateBytes); + Hpke publicKey = Hpke.ImportEncapsulationKey(suite, publicBytes); + byte[] message = [1, 2, 3, 4, 5]; + byte[] aad = [0x71, 0x72]; + byte[] info = [0x91, 0x92, 0x93]; + byte[] psk = new byte[32]; + byte[] pskId = [1]; + publicKey.Seal(message, out byte[] enc, out byte[] ciphertext, aad, info); + Assert.Equal(message, privateKey.Open(enc, ciphertext, associatedData: aad, info: info)); + Assert.ThrowsAny(() => publicKey.ExportDecapsulationKey()); + Assert.ThrowsAny(() => + publicKey.ExportDecapsulationKey(new byte[suite.DecapsulationKeySizeInBytes])); + Assert.ThrowsAny(() => + publicKey.Open(enc, ciphertext, associatedData: aad, info: info)); + Assert.ThrowsAny(() => publicKey.CreateRecipient(enc, info)); + + using (HpkeSender sender = publicKey.CreateSender(out byte[] baseEnc, info)) + using (HpkeRecipient recipient = privateKey.CreateRecipient(baseEnc, info)) + using (HpkeSender pskSender = publicKey.CreatePskSender(psk, pskId, out byte[] pskEnc, info)) + using (HpkeRecipient pskRecipient = privateKey.CreatePskRecipient(pskEnc, psk, pskId, info)) + { + Assert.ThrowsAny(() => + publicKey.CreatePskRecipient(pskEnc, psk, pskId, info)); + privateKey.Dispose(); + publicKey.Dispose(); + Assert.Equal(message, recipient.Open(sender.Seal(message, associatedData: aad), + associatedData: aad)); + Assert.Equal(message, pskRecipient.Open(pskSender.Seal(message, associatedData: aad), + associatedData: aad)); + AssertMatchingExports(sender, recipient); + AssertMatchingExports(pskSender, pskRecipient); + } + } + + [Theory] + [MemberData(nameof(SupportedNistKems))] + public static void ImportDecapsulationKey_NistScalarBoundaries(HpkeKem kem) + { + HpkeSuite suite = KeySuite(kem); + byte[] order = Curve(kem).Order; + byte[] belowOrder = (byte[])order.Clone(); + byte[] aboveOrder = (byte[])order.Clone(); + belowOrder[belowOrder.Length - 1]--; + aboveOrder[aboveOrder.Length - 1]++; + byte[] allBitsSet = new byte[order.Length]; + allBitsSet.AsSpan().Fill(0xFF); + + foreach (byte[] invalid in new[] { new byte[order.Length], order, aboveOrder, allBitsSet }) + { + Assert.ThrowsAny(() => Hpke.ImportDecapsulationKey(suite, invalid)); + Assert.ThrowsAny(() => Hpke.ImportDecapsulationKey(suite, invalid.AsSpan())); + } + + foreach (bool useSpan in new[] { false, true }) + { + using (Hpke key = ImportPrivate(suite, belowOrder, useSpan)) + using (Hpke peer = Hpke.ImportEncapsulationKey(suite, key.ExportEncapsulationKey())) + { + AssertKeyExports(key, belowOrder, peer.ExportEncapsulationKey()); + AssertKeyPairWorks(key, peer); + } + } + } + + [Theory] + [MemberData(nameof(SupportedNistKems))] + public static void ImportDecapsulationKey_OneProducesGeneratorPoint(HpkeKem kem) + { + HpkeSuite suite = KeySuite(kem); + ECCurve curve = Curve(kem); + byte[] scalar = new byte[suite.DecapsulationKeySizeInBytes]; + scalar[scalar.Length - 1] = 1; + byte[] expectedPublic = new byte[suite.EncapsulationKeySizeInBytes]; + expectedPublic[0] = 4; + curve.G.X.CopyTo(expectedPublic, 1); + curve.G.Y.CopyTo(expectedPublic, 1 + curve.G.X.Length); + + foreach (bool useSpan in new[] { false, true }) + { + using (Hpke key = ImportPrivate(suite, scalar, useSpan)) + { + AssertKeyExports(key, scalar, expectedPublic); + } + } + } + + [Theory] + [MemberData(nameof(SupportedNistKems))] + public static void ImportEncapsulationKey_RejectsInvalidNistPoints(HpkeKem kem) + { + HpkeSuite suite = KeySuite(kem); + byte[] publicKey = HpkeTestData.Vectors.First(vector => vector.Kem == kem) + .EncapsulationKey.HexToByteArray(); + List invalidPoints = new(); + + foreach (byte prefix in new byte[] { 0, 2, 3, 6, 7, 0xFF }) + { + byte[] invalid = (byte[])publicKey.Clone(); + invalid[0] = prefix; + invalidPoints.Add(invalid); + } + + byte[] zeroPoint = new byte[publicKey.Length]; + zeroPoint[0] = 4; + invalidPoints.Add(zeroPoint); + byte[] outOfRange = new byte[publicKey.Length]; + outOfRange.AsSpan().Fill(0xFF); + outOfRange[0] = 4; + invalidPoints.Add(outOfRange); + + foreach (byte[] invalid in invalidPoints) + { + Assert.ThrowsAny(() => Hpke.ImportEncapsulationKey(suite, invalid)); + Assert.ThrowsAny(() => Hpke.ImportEncapsulationKey(suite, invalid.AsSpan())); + } + } + + [ConditionalTheory(typeof(HpkeKeyTests), nameof(HasX25519))] + [InlineData(0)] + [InlineData(255)] + public static void ImportDecapsulationKey_X25519RawBytes(byte value) + { + HpkeSuite suite = KeySuite(HpkeKem.DHKEM_X25519_HKDF_SHA256); + byte[] scalar = new byte[suite.DecapsulationKeySizeInBytes]; + scalar.AsSpan().Fill(value); + + foreach (bool useSpan in new[] { false, true }) + { + using (Hpke key = ImportPrivate(suite, scalar, useSpan)) + using (Hpke peer = Hpke.ImportEncapsulationKey(suite, key.ExportEncapsulationKey())) + { + AssertKeyExports(key, scalar, peer.ExportEncapsulationKey()); + AssertKeyPairWorks(key, peer); + } + } + } + + [Theory] + [MemberData(nameof(SupportedKeyVectorNames))] + public static void Dispose_KeysFromFactories(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = KeySuite(vector.Kem); + byte[] material = vector.KeyMaterial.HexToByteArray(); + byte[] privateKey = vector.DecapsulationKey.HexToByteArray(); + byte[] publicKey = vector.EncapsulationKey.HexToByteArray(); + + using (Hpke derived = Hpke.DeriveKey(suite, material)) + using (Hpke importedPrivate = Hpke.ImportDecapsulationKey(suite, privateKey)) + using (Hpke importedPublic = Hpke.ImportEncapsulationKey(suite, publicKey)) + using (Hpke generated = Hpke.GenerateKey(suite)) + { + foreach (Hpke key in new[] { derived, importedPrivate, importedPublic, generated }) + { + key.Dispose(); + key.Dispose(); + Assert.Throws(() => key.ExportEncapsulationKey()); + Assert.Throws(() => + key.ExportEncapsulationKey(new byte[suite.EncapsulationKeySizeInBytes])); + Assert.Throws(() => key.ExportDecapsulationKey()); + Assert.Throws(() => + key.ExportDecapsulationKey(new byte[suite.DecapsulationKeySizeInBytes])); + } + } + } + + // Key material depends on the KEM only; do not make key coverage depend on SHAKE or ChaCha availability. + private static HpkeSuite KeySuite(HpkeKem kem) => new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + + private static Hpke ImportPrivate(HpkeSuite suite, byte[] key, bool useSpan) => useSpan + ? Hpke.ImportDecapsulationKey(suite, key.AsSpan()) + : Hpke.ImportDecapsulationKey(suite, key); + + private static Hpke ImportPublic(HpkeSuite suite, byte[] key, bool useSpan) => useSpan + ? Hpke.ImportEncapsulationKey(suite, key.AsSpan()) + : Hpke.ImportEncapsulationKey(suite, key); + + private static byte[] InputBuffer(ReadOnlySpan value, int padding) + { + byte[] input = new byte[value.Length + 2 * padding]; + input.AsSpan().Fill(0xA5); + value.CopyTo(input.AsSpan(padding)); + return input; + } + + private static void AssertKeyExports(Hpke key, ReadOnlySpan privateKey, ReadOnlySpan publicKey) + { + byte[] allocated = key.ExportDecapsulationKey(); + byte[] buffer = new byte[privateKey.Length + 2]; + buffer.AsSpan().Fill(0xA5); + + AssertExtensions.SequenceEqual(privateKey, allocated.AsSpan()); + key.ExportDecapsulationKey(buffer.AsSpan(1, privateKey.Length)); + AssertExtensions.SequenceEqual(privateKey, buffer.AsSpan(1, privateKey.Length)); + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[buffer.Length - 1]); + AssertPublicKeyExports(key, publicKey); + } + + private static void AssertPublicKeyExports(Hpke key, ReadOnlySpan expected) + { + AssertExtensions.SequenceEqual(expected, key.ExportEncapsulationKey().AsSpan()); + byte[] buffer = new byte[expected.Length + 2]; + buffer.AsSpan().Fill(0xA5); + key.ExportEncapsulationKey(buffer.AsSpan(1, expected.Length)); + AssertExtensions.SequenceEqual(expected, buffer.AsSpan(1, expected.Length)); + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[buffer.Length - 1]); + } + + private static void AssertKeyPairWorks(Hpke privateKey, Hpke publicKey) + { + byte[] message = [0, 0x11, 0x7F, 0xFF]; + byte[] aad = [0x71, 0x72]; + byte[] info = [0x91, 0x92, 0x93]; + publicKey.Seal(message, out byte[] enc, out byte[] ciphertext, aad, info); + Assert.Equal(message, privateKey.Open(enc, ciphertext, associatedData: aad, info: info)); + byte[] plaintext = new byte[message.Length]; + privateKey.Open(enc, ciphertext, plaintext.AsSpan(), aad, info); + Assert.Equal(message, plaintext); + } + + private static void AssertMatchingExports(HpkeSender sender, HpkeRecipient recipient) + { + byte[] context = [0x11, 0x22, 0x33]; + byte[] senderSecret = sender.Export(context, 32); + byte[] recipientSecret = recipient.Export(context, 32); + + Assert.Equal(senderSecret, recipientSecret); + } + + private static ECCurve Curve(HpkeKem kem) => kem switch + { + HpkeKem.DHKEM_P256_HKDF_SHA256 => EccTestData.GetNistP256ExplicitCurve(), + HpkeKem.DHKEM_P384_HKDF_SHA384 => EccTestData.GetNistP384ExplicitCurve(), + HpkeKem.DHKEM_P521_HKDF_SHA512 => EccTestData.GetNistP521ExplicitCurve(), + _ => throw new InvalidOperationException(), + }; + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index 462c862dc81136..2b111ecab38108 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -129,6 +129,8 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLKemAlgorithmTests.cs" /> + ("suite", () => Hpke.GenerateKey((HpkeSuite)null)); - } - - [Fact] - public static void DeriveKey_NullArguments() - { - HpkeSuite suite = new( - HpkeKem.DHKEM_P256_HKDF_SHA256, - HpkeKdf.HKDF_SHA256, - HpkeAead.AES_128_GCM); - - AssertExtensions.Throws( - "suite", - () => Hpke.DeriveKey((HpkeSuite)null, Array.Empty())); - AssertExtensions.Throws( - "ikm", - () => Hpke.DeriveKey(suite, (byte[])null)); - } - - [Theory] - [InlineData( - HpkeKem.DHKEM_P256_HKDF_SHA256, - "4270e54ffd08d79d5928020af4686d8f6b7d35dbe470265f1f5aa22816ce860e", - "4995788ef4b9d6132b249ce59a77281493eb39af373d236a1fe415cb0c2d7beb", - "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325a" + - "c98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4")] - [InlineData( - HpkeKem.DHKEM_P384_HKDF_SHA384, - "65fca3ea3b6db29a62bff28ec53c08710fab10b3798e59b678d3224296d5883f" + - "039123471784ce57b0d85a17cd521196", - "679172205e04663f40fda1018cd46c18ebaa876ede6998ba86b051614ca4d5e4" + - "bfbea34b720617a4b958cc80f6305244", - "04a5f53da8564364255bc36850df793672782a5c9e4a7fb5fb2e2146eb12e4d8" + - "477ab1f326a361dfd1e41212109510e813380547c68c0964c1908f16f67b902a" + - "061be27b2f8b43f1fab1bf0dbf89f5167ce80aca2c210b8fc0f040699db9ee1229")] - [InlineData( - HpkeKem.DHKEM_P521_HKDF_SHA512, - "2ad954bbe39b7122529f7dde780bff626cd97f850d0784a432784e69d86eccaa" + - "de43b6c10a8ffdb94bf943c6da479db137914ec835a7e715e36e45e29b587bab3bf1", - "01462680369ae375e4b3791070a7458ed527842f6a98a79ff5e0d4cbde83c2719" + - "6a3916956655523a6a2556a7af62c5cadabe2ef9da3760bb21e005202f7b2462847", - "0401b45498c1714e2dce167d3caf162e45e0642afc7ed435df7902ccae0e84ba0f7d" + - "373f646b7738bbbdca11ed91bdeae3cdcba3301f2457be452f271fa6837580e661" + - "012af49583a62e48d44bed350c7118c0d8dc861c238c72a2bda17f64704f464b573" + - "38e7f40b60959480c0e58e6559b190d81663ed816e523b6b6a418f66d2451ec64")] - [InlineData( - HpkeKem.DHKEM_P521_HKDF_SHA512, - "39a28dc317c3e48b908948f99d608059f882d3d09c0541824bc25f94e6dee7aa0" + - "df1c644296b06fbb76e84aef5008f8a908e08fbabadf70658538d74753a85f8856a", - "009227b4b91cf1eb6eecb6c0c0bae93a272d24e11c63bd4c34a581c49f9c3ca0" + - "1c16bbd32a0a1fac22784f2ae985c85f183baad103b2d02aee787179dfc1a94fea11", - "0400b81073b1612cf7fdb6db07b35cf4bc17bda5854f3d270ecd9ea99f6c07b46795" + - "b8014b66c523ceed6f4829c18bc3886c891b63fa902500ce3ddeb1fbec7e608ac7" + - "0050b76a0a7fc081dbf1cb30b005981113e635eb501a973aba662d7f16fcc12897d" + - "d752d657d37774bb16197c0d9724eecc1ed65349fb6ac1f280749e7669766f8cd")] - [InlineData( - HpkeKem.DHKEM_X25519_HKDF_SHA256, - "7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234", - "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736", - "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431")] - public static void DeriveKey_KnownAnswer(HpkeKem kem, string ikmHex, string privateKeyHex, string publicKeyHex) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws( - () => Hpke.DeriveKey(suite, Convert.FromHexString(ikmHex))); - return; - } - - byte[] ikm = Convert.FromHexString(ikmHex); - byte[] expectedPrivateKey = Convert.FromHexString(privateKeyHex); - byte[] expectedPublicKey = Convert.FromHexString(publicKeyHex); - - try - { - using (Hpke keyFromArray = Hpke.DeriveKey(suite, ikm)) - using (Hpke keyFromSpan = Hpke.DeriveKey(suite, ikm.AsSpan())) - { - byte[] arrayPrivateKey = keyFromArray.ExportDecapsulationKey(); - byte[] spanPrivateKey = new byte[suite.DecapsulationKeySizeInBytes]; - - try - { - byte[] arrayPublicKey = keyFromArray.ExportEncapsulationKey(); - byte[] spanPublicKey = new byte[suite.EncapsulationKeySizeInBytes]; - - keyFromSpan.ExportDecapsulationKey(spanPrivateKey); - keyFromSpan.ExportEncapsulationKey(spanPublicKey); - Assert.Equal(expectedPrivateKey, arrayPrivateKey); - Assert.Equal(arrayPrivateKey, spanPrivateKey); - Assert.Equal(expectedPublicKey, arrayPublicKey); - Assert.Equal(arrayPublicKey, spanPublicKey); - - using (Hpke privateFromArray = Hpke.ImportDecapsulationKey(suite, expectedPrivateKey)) - using (Hpke privateFromSpan = Hpke.ImportDecapsulationKey(suite, expectedPrivateKey.AsSpan())) - using (Hpke publicFromArray = Hpke.ImportEncapsulationKey(suite, expectedPublicKey)) - using (Hpke publicFromSpan = Hpke.ImportEncapsulationKey(suite, expectedPublicKey.AsSpan())) - { - privateFromArray.ExportDecapsulationKey(spanPrivateKey); - Assert.Equal(expectedPrivateKey, spanPrivateKey); - privateFromSpan.ExportDecapsulationKey(spanPrivateKey); - Assert.Equal(expectedPrivateKey, spanPrivateKey); - Assert.Equal(expectedPublicKey, privateFromArray.ExportEncapsulationKey()); - Assert.Equal(expectedPublicKey, privateFromSpan.ExportEncapsulationKey()); - Assert.Equal(expectedPublicKey, publicFromArray.ExportEncapsulationKey()); - Assert.Equal(expectedPublicKey, publicFromSpan.ExportEncapsulationKey()); - } - } - finally - { - CryptographicOperations.ZeroMemory(arrayPrivateKey); - CryptographicOperations.ZeroMemory(spanPrivateKey); - } - } - } - finally - { - CryptographicOperations.ZeroMemory(ikm); - CryptographicOperations.ZeroMemory(expectedPrivateKey); - } - } - - [Fact] - public static void ImportKey_ArgumentValidation() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, (byte[])null)); - AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, (byte[])null)); - AssertExtensions.Throws("suite", () => Hpke.ImportDecapsulationKey(null, Array.Empty())); - AssertExtensions.Throws("suite", () => Hpke.ImportDecapsulationKey(null, ReadOnlySpan.Empty)); - AssertExtensions.Throws("suite", () => Hpke.ImportEncapsulationKey(null, Array.Empty())); - AssertExtensions.Throws("suite", () => Hpke.ImportEncapsulationKey(null, ReadOnlySpan.Empty)); - foreach (HpkeKem kem in Enum.GetValues()) { - suite = new HpkeSuite(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - foreach (int length in new[] { 0, suite.DecapsulationKeySizeInBytes - 1, suite.DecapsulationKeySizeInBytes + 1 }) - { - byte[] source = new byte[length]; - AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, source)); - AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, source.AsSpan())); - } - - foreach (int length in new[] { 0, suite.EncapsulationKeySizeInBytes - 1, suite.EncapsulationKeySizeInBytes + 1 }) - { - byte[] source = new byte[length]; - AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, source)); - AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, source.AsSpan())); - } + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); if (!Hpke.IsSupported(suite)) { byte[] privateKey = new byte[suite.DecapsulationKeySizeInBytes]; byte[] publicKey = new byte[suite.EncapsulationKeySizeInBytes]; + Assert.Throws(() => Hpke.GenerateKey(suite)); + Assert.Throws(() => Hpke.DeriveKey(suite, privateKey)); + Assert.Throws(() => Hpke.DeriveKey(suite, privateKey.AsSpan())); Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, privateKey)); - Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, privateKey.AsSpan())); + Assert.Throws( + () => Hpke.ImportDecapsulationKey(suite, privateKey.AsSpan())); Assert.Throws(() => Hpke.ImportEncapsulationKey(suite, publicKey)); - Assert.Throws(() => Hpke.ImportEncapsulationKey(suite, publicKey.AsSpan())); - } - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - public static void ImportDecapsulationKey_ScalarBoundaries(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] order = kem switch - { - HpkeKem.DHKEM_P256_HKDF_SHA256 => EccTestData.GetNistP256ExplicitCurve().Order, - HpkeKem.DHKEM_P384_HKDF_SHA384 => EccTestData.GetNistP384ExplicitCurve().Order, - HpkeKem.DHKEM_P521_HKDF_SHA512 => EccTestData.GetNistP521ExplicitCurve().Order, - _ => throw new InvalidOperationException(), - }; - byte[] allBitsSet = new byte[order.Length]; - allBitsSet.AsSpan().Fill(0xFF); - byte[] orderPlusOne = (byte[])order.Clone(); - orderPlusOne[^1]++; - - foreach (byte[] invalid in new[] { new byte[order.Length], order, orderPlusOne, allBitsSet }) - { - Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, invalid)); - Assert.Throws(() => Hpke.ImportDecapsulationKey(suite, invalid.AsSpan())); - } - - byte[] one = new byte[order.Length]; - one[^1] = 1; - byte[] orderMinusOne = (byte[])order.Clone(); - orderMinusOne[^1]--; - - foreach (byte[] valid in new[] { one, orderMinusOne }) - { - byte[] exported = new byte[valid.Length]; - - using (Hpke fromArray = Hpke.ImportDecapsulationKey(suite, valid)) - using (Hpke fromSpan = Hpke.ImportDecapsulationKey(suite, valid.AsSpan())) - { - fromArray.ExportDecapsulationKey(exported); - Assert.Equal(valid, exported); - fromSpan.ExportDecapsulationKey(exported); - Assert.Equal(valid, exported); - fromArray.Seal("message"u8, out byte[] enc, out byte[] ciphertext); - AssertExtensions.SequenceEqual("message"u8, fromSpan.Open(enc, ciphertext)); - } - - CryptographicOperations.ZeroMemory(exported); - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - public static void ImportEncapsulationKey_InvalidNistPoint(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - foreach (byte prefix in new byte[] { 0, 2, 3, 4, 6, 7, 0xFF }) - { - byte[] source = new byte[suite.EncapsulationKeySizeInBytes]; - source[0] = prefix; - byte[] original = (byte[])source.Clone(); - Assert.ThrowsAny(() => Hpke.ImportEncapsulationKey(suite, source)); - Assert.ThrowsAny(() => Hpke.ImportEncapsulationKey(suite, source.AsSpan())); - Assert.Equal(original, source); - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void ImportKey_OperationsAndOwnership(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws( - () => Hpke.ImportDecapsulationKey(suite, new byte[suite.DecapsulationKeySizeInBytes])); - Assert.Throws( - () => Hpke.ImportEncapsulationKey(suite, new byte[suite.EncapsulationKeySizeInBytes])); - return; - } - - using (Hpke original = Hpke.GenerateKey(suite)) - { - byte[] privateKey = original.ExportDecapsulationKey(); - byte[] publicKey = original.ExportEncapsulationKey(); - - try - { - foreach (bool useSpan in new[] { false, true }) - { - int padding = useSpan ? 2 : 0; - int offset = useSpan ? 1 : 0; - byte[] privateSource = new byte[privateKey.Length + padding]; - byte[] publicSource = new byte[publicKey.Length + padding]; - privateSource.AsSpan().Fill(0xA5); - publicSource.AsSpan().Fill(0xA5); - privateKey.CopyTo(privateSource, offset); - publicKey.CopyTo(publicSource, offset); - - try - { - using (Hpke importedPrivate = useSpan - ? Hpke.ImportDecapsulationKey(suite, privateSource.AsSpan(1, privateKey.Length)) - : Hpke.ImportDecapsulationKey(suite, privateSource)) - using (Hpke importedPublic = useSpan - ? Hpke.ImportEncapsulationKey(suite, publicSource.AsSpan(1, publicKey.Length)) - : Hpke.ImportEncapsulationKey(suite, publicSource)) - { - Assert.Same(suite, importedPrivate.Suite); - Assert.Same(suite, importedPublic.Suite); - Assert.Equal(publicKey, importedPrivate.ExportEncapsulationKey()); - Assert.Equal(publicKey, importedPublic.ExportEncapsulationKey()); - AssertExtensions.SequenceEqual(privateKey.AsSpan(), privateSource.AsSpan(offset, privateKey.Length)); - AssertExtensions.SequenceEqual(publicKey.AsSpan(), publicSource.AsSpan(offset, publicKey.Length)); - - if (useSpan) - { - Assert.Equal(0xA5, privateSource[0]); - Assert.Equal(0xA5, privateSource[^1]); - Assert.Equal(0xA5, publicSource[0]); - Assert.Equal(0xA5, publicSource[^1]); - } - - privateSource.AsSpan().Clear(); - publicSource.AsSpan().Clear(); - original.Dispose(); - byte[] plaintext = "message"u8.ToArray(); - byte[] aad = "associated data"u8.ToArray(); - byte[] info = "application context"u8.ToArray(); - importedPublic.Seal(plaintext, out byte[] enc, out byte[] ciphertext, aad, info); - Assert.Equal(plaintext, importedPrivate.Open(enc, ciphertext, aad, info)); - byte[] opened = new byte[plaintext.Length]; - importedPrivate.Open(enc, ciphertext, opened.AsSpan(), aad, info); - Assert.Equal(plaintext, opened); - - byte[] psk = new byte[32]; - byte[] pskId = [1]; - using (HpkeSender sender = importedPublic.CreateSender(out byte[] contextEnc, info)) - using (HpkeRecipient recipient = importedPrivate.CreateRecipient(contextEnc, info)) - using (HpkeSender pskSender = importedPublic.CreatePskSender(psk, pskId, out byte[] pskEnc, info)) - using (HpkeRecipient pskRecipient = importedPrivate.CreatePskRecipient(pskEnc, psk, pskId, info)) - { - Assert.ThrowsAny(() => importedPublic.ExportDecapsulationKey()); - Assert.ThrowsAny( - () => importedPublic.ExportDecapsulationKey(new byte[privateKey.Length])); - Assert.ThrowsAny(() => importedPublic.Open(enc, ciphertext, aad, info)); - Assert.ThrowsAny( - () => importedPublic.Open(enc, ciphertext, opened.AsSpan(), aad, info)); - Assert.ThrowsAny(() => importedPublic.CreateRecipient(contextEnc, info)); - Assert.ThrowsAny( - () => importedPublic.CreatePskRecipient(pskEnc, psk, pskId, info)); - - for (int i = 0; i < 2; i++) - { - Assert.Equal(plaintext, recipient.Open(sender.Seal(plaintext, aad), aad)); - Assert.Equal(plaintext, pskRecipient.Open(pskSender.Seal(plaintext, aad), aad)); - } - - Assert.Equal(sender.Export([], 32), recipient.Export([], 32)); - Assert.Equal(pskSender.Export([], 32), pskRecipient.Export([], 32)); - importedPublic.Dispose(); - importedPrivate.Dispose(); - Assert.Throws(() => importedPublic.ExportEncapsulationKey()); - Assert.Throws(() => importedPrivate.ExportDecapsulationKey()); - Assert.Equal(plaintext, recipient.Open(sender.Seal(plaintext, aad), aad)); - Assert.Equal(plaintext, pskRecipient.Open(pskSender.Seal(plaintext, aad), aad)); - } - } - } - finally - { - CryptographicOperations.ZeroMemory(privateSource); - } - } - } - finally - { - CryptographicOperations.ZeroMemory(privateKey); - } - } - } - - [Theory] - [InlineData(0)] - [InlineData(255)] - public static void ImportDecapsulationKey_X25519RawKey(byte value) - { - HpkeSuite suite = new(HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] source = new byte[suite.DecapsulationKeySizeInBytes]; - source.AsSpan().Fill(value); - byte[] exported = new byte[source.Length]; - - try - { - using (Hpke fromArray = Hpke.ImportDecapsulationKey(suite, source)) - using (Hpke fromSpan = Hpke.ImportDecapsulationKey(suite, source.AsSpan())) - { - fromArray.ExportDecapsulationKey(exported); - Assert.Equal(source, exported); - fromSpan.ExportDecapsulationKey(exported); - Assert.Equal(source, exported); - fromArray.Seal("message"u8, out byte[] enc, out byte[] ciphertext); - AssertExtensions.SequenceEqual("message"u8, fromSpan.Open(enc, ciphertext)); + Assert.Throws( + () => Hpke.ImportEncapsulationKey(suite, publicKey.AsSpan())); } } - finally - { - CryptographicOperations.ZeroMemory(source); - CryptographicOperations.ZeroMemory(exported); - } } // https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/b1f7cb0cdeab6906c61b3d6574e8bdfdbe1cd3fb/test-vectors.json @@ -478,34 +100,27 @@ public static void Open_KnownAnswer( byte[] associatedData = "Count-0"u8.ToArray(); byte[] info = "Ode on a Grecian Urn"u8.ToArray(); - try + using (Hpke key = Hpke.DeriveKey(suite, ikm)) { - using (Hpke key = Hpke.DeriveKey(suite, ikm)) - { - Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); - Assert.Equal(plaintext, key.Open( - new ReadOnlySpan(enc), ciphertext, new ReadOnlySpan(associatedData), info)); + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); + Assert.Equal(plaintext, key.Open( + new ReadOnlySpan(enc), ciphertext, new ReadOnlySpan(associatedData), info)); - byte[] destination = new byte[plaintext.Length]; - key.Open(enc, ciphertext, destination.AsSpan(), associatedData, info); - Assert.Equal(plaintext, destination); + byte[] destination = new byte[plaintext.Length]; + key.Open(enc, ciphertext, destination.AsSpan(), associatedData, info); + Assert.Equal(plaintext, destination); - using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) - { - AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); - Assert.Equal(plaintext, recipient.Open( - new ReadOnlySpan(Convert.FromHexString(secondCiphertextHex)), "Count-1"u8)); - recipient.Open(Convert.FromHexString(thirdCiphertextHex), destination.AsSpan(), "Count-2"u8); - Assert.Equal(plaintext, destination); - AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - } + using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) + { + AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); + Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); + Assert.Equal(plaintext, recipient.Open( + new ReadOnlySpan(Convert.FromHexString(secondCiphertextHex)), "Count-1"u8)); + recipient.Open(Convert.FromHexString(thirdCiphertextHex), destination.AsSpan(), "Count-2"u8); + Assert.Equal(plaintext, destination); + AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); } } - finally - { - CryptographicOperations.ZeroMemory(ikm); - } } public static IEnumerable OpenSuiteData() @@ -680,93 +295,80 @@ public static void Open_InvalidEncapsulatedSecret(HpkeKem kem, byte firstByte) } [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void Open_ArgumentValidation(HpkeKem kem) + [MemberData(nameof(OpenSuiteData))] + public static void CreateContexts_SealAndOpen(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + HpkeSuite suite = new(kem, kdf, aead); - using (RecordingHpke key = new(suite)) + if (!Hpke.IsSupported(suite)) { - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; - byte[] plaintext = new byte[1]; + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } - AssertExtensions.Throws("encapsulatedSecret", () => key.Open((byte[])null, ciphertext)); - AssertExtensions.Throws("ciphertext", () => key.Open(enc, (byte[])null)); + using (Hpke key = Hpke.GenerateKey(suite)) + { + byte[] info = new byte[1024]; + info.AsSpan().Fill(0x3C); + byte[] associatedData = "associated data"u8.ToArray(); - foreach (int length in new[] { 0, enc.Length - 1, enc.Length + 1 }) + foreach (int length in new[] { 0, 1, 257 }) { - byte[] invalidEnc = new byte[length]; - AssertExtensions.Throws("encapsulatedSecret", () => key.Open(invalidEnc, ciphertext)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.Open(invalidEnc.AsSpan(), ciphertext)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.Open(invalidEnc, ciphertext, plaintext.AsSpan())); - } + byte[] plaintext = new byte[length]; + plaintext.AsSpan().Fill(0xA7); + int ciphertextLength = suite.GetCiphertextLength(length); - foreach (int length in new[] { 0, suite.AeadTagSizeInBytes - 1 }) - { - byte[] invalidCiphertext = new byte[length]; - AssertExtensions.Throws("ciphertext", () => key.Open(enc, invalidCiphertext)); - AssertExtensions.Throws( - "ciphertext", () => key.Open(enc.AsSpan(), invalidCiphertext)); - AssertExtensions.Throws( - "ciphertext", () => key.Open(enc, invalidCiphertext, plaintext.AsSpan())); - } + using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) + using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) + { + Assert.Same(suite, sender.Suite); + Assert.Same(suite, recipient.Suite); + Assert.Equal(suite.EncapsulatedSecretSizeInBytes, enc.Length); - foreach (int length in new[] { 0, 2 }) - { - byte[] invalidPlaintext = new byte[length]; - AssertExtensions.Throws( - "plaintext", () => key.Open(enc, ciphertext, invalidPlaintext.AsSpan())); - } + byte[] ciphertext = sender.Seal(plaintext, associatedData); + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); + Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); - Assert.False(key.OpenCoreCalled); - key.Dispose(); - Assert.Throws(() => key.Open(enc, ciphertext)); - Assert.Throws(() => key.Open(enc.AsSpan(), ciphertext)); - Assert.Throws(() => key.Open(enc, ciphertext, plaintext.AsSpan())); - Assert.False(key.OpenCoreCalled); - } - } + byte[] nextCiphertext = sender.Seal( + new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData)); + Assert.Equal(ciphertextLength, nextCiphertext.Length); + Assert.NotEqual(ciphertext, nextCiphertext); + Assert.Throws( + () => key.Open(enc, nextCiphertext, associatedData, info)); + Assert.Equal(plaintext, recipient.Open( + new ReadOnlySpan(nextCiphertext), new ReadOnlySpan(associatedData))); + } - [Theory] - [InlineData(HpkeKdf.HKDF_SHA256, 65536, true)] - [InlineData(HpkeKdf.HKDF_SHA384, 65536, true)] - [InlineData(HpkeKdf.HKDF_SHA512, 65536, true)] - [InlineData(HpkeKdf.SHAKE128, 65535, true)] - [InlineData(HpkeKdf.SHAKE128, 65536, false)] - [InlineData(HpkeKdf.SHAKE256, 65535, true)] - [InlineData(HpkeKdf.SHAKE256, 65536, false)] - public static void Open_InfoLength(HpkeKdf kdf, int infoLength, bool valid) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); + byte[] encBuffer = new byte[suite.EncapsulatedSecretSizeInBytes + 2]; + encBuffer.AsSpan().Fill(0xA5); - using (RecordingHpke key = new(suite)) - { - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; - byte[] info = new byte[infoLength]; + using (HpkeSender sender = key.CreateSender( + encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes), info)) + { + Assert.Same(suite, sender.Suite); + Assert.Equal(0xA5, encBuffer[0]); + Assert.Equal(0xA5, encBuffer[^1]); + byte[] enc = encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes).ToArray(); + byte[] ciphertextBuffer = new byte[ciphertextLength + 2]; + ciphertextBuffer.AsSpan().Fill(0xA5); + sender.Seal(plaintext, ciphertextBuffer.AsSpan(1, ciphertextLength), associatedData); + Assert.Equal(0xA5, ciphertextBuffer[0]); + Assert.Equal(0xA5, ciphertextBuffer[^1]); + Assert.Equal(plaintext, key.Open( + enc, ciphertextBuffer.AsSpan(1, ciphertextLength), new ReadOnlySpan(associatedData), info)); - if (valid) - { - Assert.Empty(key.Open(enc, ciphertext, info: info)); - Assert.Empty(key.Open(enc.AsSpan(), ciphertext, info: info)); - key.Open(enc, ciphertext, Span.Empty, info: info); - } - else - { - AssertExtensions.Throws("info", () => key.Open(enc, ciphertext, info: info)); - AssertExtensions.Throws( - "info", () => key.Open(enc.AsSpan(), ciphertext, info: info)); - AssertExtensions.Throws( - "info", () => key.Open(enc, ciphertext, Span.Empty, info: info)); + using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) + { + Assert.Same(suite, recipient.Suite); + byte[] destination = new byte[length + 2]; + destination.AsSpan().Fill(0xA5); + recipient.Open(ciphertextBuffer.AsSpan(1, ciphertextLength), destination.AsSpan(1, length), associatedData); + AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); + } + } } - - Assert.Equal(valid, key.OpenCoreCalled); } } @@ -775,375 +377,37 @@ public static void Open_InfoLength(HpkeKdf kdf, int infoLength, bool valid) [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - [InlineData(HpkeKem.MLKEM_512)] - [InlineData(HpkeKem.MLKEM_768)] - [InlineData(HpkeKem.MLKEM_1024)] - [InlineData(HpkeKem.MLKEM768_P256)] - [InlineData(HpkeKem.MLKEM1024_P384)] - public static void CreateSender_Overloads(HpkeKem kem) + public static void CreateContexts_IndependentLifetime(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - using (RecordingHpke key = new(suite)) + if (!Hpke.IsSupported(suite)) { - byte[] info = [1, 2, 3]; - byte[] expected = new byte[suite.EncapsulatedSecretSizeInBytes]; - expected.AsSpan().Fill(0xD7); + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; + } - using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) - { - Assert.IsType(sender); - Assert.Same(suite, sender.Suite); - Assert.Equal(expected, enc); - Assert.Equal(info, key.LastSenderInfo); - } + byte[] ikm = new byte[suite.DecapsulationKeySizeInBytes]; - byte[] destination = new byte[expected.Length + 2]; - destination.AsSpan().Fill(0xA5); + using (Hpke key = Hpke.DeriveKey(suite, ikm)) + using (Hpke peer = Hpke.DeriveKey(suite, ikm)) + using (HpkeSender first = key.CreateSender(out byte[] firstEnc)) + using (HpkeSender second = key.CreateSender(out byte[] secondEnc)) + using (HpkeRecipient firstRecipient = key.CreateRecipient(firstEnc)) + using (HpkeRecipient secondRecipient = key.CreateRecipient(secondEnc.AsSpan())) + { + key.Dispose(); + Assert.NotEqual(firstEnc, secondEnc); + byte[] plaintext = "message"u8.ToArray(); + byte[] firstCiphertext = first.Seal(plaintext); + Assert.Equal(plaintext, peer.Open(firstEnc, firstCiphertext)); + Assert.Equal(plaintext, firstRecipient.Open(firstCiphertext)); - using (HpkeSender sender = key.CreateSender(destination.AsSpan(1, expected.Length), info)) - { - Assert.Same(suite, sender.Suite); - AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - Assert.Equal(info, key.LastSenderInfo); - } - - using (HpkeSender sender = key.CreateSender(out _)) - { - Assert.Empty(key.LastSenderInfo); - } - - using (HpkeSender sender = key.CreateSender(destination.AsSpan(1, expected.Length))) - { - Assert.Empty(key.LastSenderInfo); - } - - Assert.Equal(4, key.CreateSenderCalls); - } - } - - [Fact] - public static void CreateSender_Validation() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpke key = new(suite)) - { - foreach (int length in new[] { 0, suite.EncapsulatedSecretSizeInBytes - 1, suite.EncapsulatedSecretSizeInBytes + 1 }) - { - byte[] destination = new byte[length]; - destination.AsSpan().Fill(0xA5); - byte[] original = (byte[])destination.Clone(); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreateSender(destination)); - Assert.Equal(original, destination); - } - - key.Dispose(); - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - Assert.Throws(() => key.CreateSender(out _)); - Assert.Throws(() => key.CreateSender(enc)); - Assert.Equal(0, key.CreateSenderCalls); - } - } - - [Theory] - [InlineData(HpkeKdf.HKDF_SHA256, 65536, true)] - [InlineData(HpkeKdf.HKDF_SHA384, 65536, true)] - [InlineData(HpkeKdf.HKDF_SHA512, 65536, true)] - [InlineData(HpkeKdf.SHAKE128, 65535, true)] - [InlineData(HpkeKdf.SHAKE128, 65536, false)] - [InlineData(HpkeKdf.SHAKE256, 65535, true)] - [InlineData(HpkeKdf.SHAKE256, 65536, false)] - public static void CreateSender_InfoLength(HpkeKdf kdf, int infoLength, bool valid) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - - using (RecordingHpke key = new(suite)) - { - byte[] info = new byte[infoLength]; - info.AsSpan().Fill(0x39); - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - - if (valid) - { - using (HpkeSender sender = key.CreateSender(out _, info)) - { - Assert.Equal(info, key.LastSenderInfo); - } - - using (HpkeSender sender = key.CreateSender(enc, info)) - { - Assert.Equal(info, key.LastSenderInfo); - } - } - else - { - AssertExtensions.Throws("info", () => key.CreateSender(out _, info)); - AssertExtensions.Throws("info", () => key.CreateSender(enc, info)); - } - - Assert.Equal(valid ? 2 : 0, key.CreateSenderCalls); - } - } - - [Fact] - public static void CreateSender_CoreFailureDoesNotPublishOutput() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpke key = new(suite) { ThrowOnCreateSender = true }) - { - byte[] original = [0xA5]; - byte[] enc = original; - Assert.Throws(() => key.CreateSender(out enc)); - Assert.Same(original, enc); - Assert.Throws(() => key.CreateSender(new byte[suite.EncapsulatedSecretSizeInBytes])); - Assert.Equal(2, key.CreateSenderCalls); - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void CreateContexts_SealAndOpen(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - using (Hpke key = Hpke.GenerateKey(suite)) - { - byte[] info = new byte[1024]; - info.AsSpan().Fill(0x3C); - byte[] associatedData = "associated data"u8.ToArray(); - - foreach (int length in new[] { 0, 1, 257 }) - { - byte[] plaintext = new byte[length]; - plaintext.AsSpan().Fill(0xA7); - int ciphertextLength = suite.GetCiphertextLength(length); - - using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) - using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) - { - Assert.Same(suite, sender.Suite); - Assert.Same(suite, recipient.Suite); - Assert.Equal(suite.EncapsulatedSecretSizeInBytes, enc.Length); - AssertExtensions.Throws( - "ciphertext", () => sender.Seal(plaintext, new byte[ciphertextLength - 1].AsSpan(), associatedData)); - - byte[] ciphertext = sender.Seal(plaintext, associatedData); - Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); - Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); - - byte[] nextCiphertext = sender.Seal( - new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData)); - Assert.Equal(ciphertextLength, nextCiphertext.Length); - Assert.NotEqual(ciphertext, nextCiphertext); - Assert.Throws( - () => key.Open(enc, nextCiphertext, associatedData, info)); - Assert.Equal(plaintext, recipient.Open( - new ReadOnlySpan(nextCiphertext), new ReadOnlySpan(associatedData))); - } - - byte[] encBuffer = new byte[suite.EncapsulatedSecretSizeInBytes + 2]; - encBuffer.AsSpan().Fill(0xA5); - - using (HpkeSender sender = key.CreateSender( - encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes), info)) - { - Assert.Same(suite, sender.Suite); - Assert.Equal(0xA5, encBuffer[0]); - Assert.Equal(0xA5, encBuffer[^1]); - byte[] enc = encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes).ToArray(); - byte[] ciphertextBuffer = new byte[ciphertextLength + 2]; - ciphertextBuffer.AsSpan().Fill(0xA5); - sender.Seal(plaintext, ciphertextBuffer.AsSpan(1, ciphertextLength), associatedData); - Assert.Equal(0xA5, ciphertextBuffer[0]); - Assert.Equal(0xA5, ciphertextBuffer[^1]); - Assert.Equal(plaintext, key.Open( - enc, ciphertextBuffer.AsSpan(1, ciphertextLength), new ReadOnlySpan(associatedData), info)); - - using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) - { - Assert.Same(suite, recipient.Suite); - byte[] destination = new byte[length + 2]; - destination.AsSpan().Fill(0xA5); - recipient.Open(ciphertextBuffer.AsSpan(1, ciphertextLength), destination.AsSpan(1, length), associatedData); - AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - } - } - } - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void CreateContexts_IndependentLifetime(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] ikm = new byte[suite.DecapsulationKeySizeInBytes]; - - try - { - using (Hpke key = Hpke.DeriveKey(suite, ikm)) - using (Hpke peer = Hpke.DeriveKey(suite, ikm)) - using (HpkeSender first = key.CreateSender(out byte[] firstEnc)) - using (HpkeSender second = key.CreateSender(out byte[] secondEnc)) - using (HpkeRecipient firstRecipient = key.CreateRecipient(firstEnc)) - using (HpkeRecipient secondRecipient = key.CreateRecipient(secondEnc.AsSpan())) - { - key.Dispose(); - Assert.NotEqual(firstEnc, secondEnc); - byte[] plaintext = "message"u8.ToArray(); - byte[] firstCiphertext = first.Seal(plaintext); - Assert.Equal(plaintext, peer.Open(firstEnc, firstCiphertext)); - Assert.Equal(plaintext, firstRecipient.Open(firstCiphertext)); - - first.Dispose(); - firstRecipient.Dispose(); - Assert.Throws(() => first.Seal(plaintext)); - Assert.Throws(() => firstRecipient.Open(firstCiphertext)); - byte[] secondCiphertext = second.Seal(plaintext); - Assert.Equal(plaintext, peer.Open(secondEnc, secondCiphertext)); - Assert.Equal(plaintext, secondRecipient.Open(secondCiphertext)); - } - } - finally - { - CryptographicOperations.ZeroMemory(ikm); - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - [InlineData(HpkeKem.MLKEM_512)] - [InlineData(HpkeKem.MLKEM_768)] - [InlineData(HpkeKem.MLKEM_1024)] - [InlineData(HpkeKem.MLKEM768_P256)] - [InlineData(HpkeKem.MLKEM1024_P384)] - public static void CreateRecipient_Overloads(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpke key = new(suite)) - { - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - enc.AsSpan().Fill(0xD7); - byte[] info = [1, 2, 3]; - - using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) - { - Assert.IsType(recipient); - Assert.Same(suite, recipient.Suite); - Assert.Equal(enc, key.LastRecipientEncapsulatedSecret); - Assert.Equal(info, key.LastRecipientInfo); - } - - using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) - { - Assert.Same(suite, recipient.Suite); - Assert.Equal(enc, key.LastRecipientEncapsulatedSecret); - Assert.Equal(info, key.LastRecipientInfo); - } - - using (HpkeRecipient recipient = key.CreateRecipient(enc, info: null)) - { - Assert.Empty(key.LastRecipientInfo); - } - - using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan())) - { - Assert.Empty(key.LastRecipientInfo); - } - - Assert.Equal(4, key.CreateRecipientCalls); - } - } - - [Fact] - public static void CreateRecipient_Validation() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpke key = new(suite)) - { - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreateRecipient((byte[])null)); - - foreach (int length in new[] { 0, suite.EncapsulatedSecretSizeInBytes - 1, suite.EncapsulatedSecretSizeInBytes + 1 }) - { - byte[] enc = new byte[length]; - AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(enc)); - AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(enc.AsSpan())); - } - - key.Dispose(); - byte[] validLengthEnc = new byte[suite.EncapsulatedSecretSizeInBytes]; - Assert.Throws(() => key.CreateRecipient(validLengthEnc)); - Assert.Throws(() => key.CreateRecipient(validLengthEnc.AsSpan())); - Assert.Equal(0, key.CreateRecipientCalls); - } - } - - [Theory] - [InlineData(HpkeKdf.HKDF_SHA256, 65536, true)] - [InlineData(HpkeKdf.HKDF_SHA384, 65536, true)] - [InlineData(HpkeKdf.HKDF_SHA512, 65536, true)] - [InlineData(HpkeKdf.SHAKE128, 65535, true)] - [InlineData(HpkeKdf.SHAKE128, 65536, false)] - [InlineData(HpkeKdf.SHAKE256, 65535, true)] - [InlineData(HpkeKdf.SHAKE256, 65536, false)] - public static void CreateRecipient_InfoLength(HpkeKdf kdf, int infoLength, bool valid) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - - using (RecordingHpke key = new(suite)) - { - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - byte[] info = new byte[infoLength]; - info.AsSpan().Fill(0x39); - - if (valid) - { - using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) - { - Assert.Equal(info, key.LastRecipientInfo); - } - - using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) - { - Assert.Equal(info, key.LastRecipientInfo); - } - } - else - { - AssertExtensions.Throws("info", () => key.CreateRecipient(enc, info)); - AssertExtensions.Throws("info", () => key.CreateRecipient(enc.AsSpan(), info)); - } - - Assert.Equal(valid ? 2 : 0, key.CreateRecipientCalls); + first.Dispose(); + firstRecipient.Dispose(); + byte[] secondCiphertext = second.Seal(plaintext); + Assert.Equal(plaintext, peer.Open(secondEnc, secondCiphertext)); + Assert.Equal(plaintext, secondRecipient.Open(secondCiphertext)); } } @@ -1181,9 +445,6 @@ public static void Recipient_AuthenticationFailureAndOrdering(HpkeKem kem, HpkeK Assert.Throws(() => wrongInfoRecipient.Open(firstCiphertext, associatedData)); } - AssertExtensions.Throws( - "plaintext", () => recipient.Open(firstCiphertext, new byte[first.Length - 1].AsSpan(), associatedData)); - for (int tamper = 0; tamper < 3; tamper++) { byte[] modifiedCiphertext = (byte[])firstCiphertext.Clone(); @@ -1289,29 +550,21 @@ public static void Psk_KnownAnswer( byte[] enc = Convert.FromHexString(encHex); byte[] plaintext = "Beauty is truth, truth beauty"u8.ToArray(); - try - { - using (Hpke key = Hpke.DeriveKey(suite, ikm)) - using (HpkeRecipient fromArray = key.CreatePskRecipient(enc, psk, pskId, info)) - using (HpkeRecipient fromSpan = key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)) - { - AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - byte[] firstCiphertext = Convert.FromHexString(firstCiphertextHex); - byte[] secondCiphertext = Convert.FromHexString(secondCiphertextHex); - Assert.Equal(plaintext, fromArray.Open(firstCiphertext, "Count-0"u8.ToArray())); - Assert.Equal(plaintext, fromArray.Open(new ReadOnlySpan(secondCiphertext), "Count-1"u8)); - byte[] destination = new byte[plaintext.Length]; - fromSpan.Open(firstCiphertext, destination.AsSpan(), "Count-0"u8); - Assert.Equal(plaintext, destination); - Assert.Equal(plaintext, fromSpan.Open(secondCiphertext, "Count-1"u8.ToArray())); - AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - AssertRecipientExports(fromSpan, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - } - } - finally + using (Hpke key = Hpke.DeriveKey(suite, ikm)) + using (HpkeRecipient fromArray = key.CreatePskRecipient(enc, psk, pskId, info)) + using (HpkeRecipient fromSpan = key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)) { - CryptographicOperations.ZeroMemory(ikm); - CryptographicOperations.ZeroMemory(psk); + AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); + byte[] firstCiphertext = Convert.FromHexString(firstCiphertextHex); + byte[] secondCiphertext = Convert.FromHexString(secondCiphertextHex); + Assert.Equal(plaintext, fromArray.Open(firstCiphertext, "Count-0"u8.ToArray())); + Assert.Equal(plaintext, fromArray.Open(new ReadOnlySpan(secondCiphertext), "Count-1"u8)); + byte[] destination = new byte[plaintext.Length]; + fromSpan.Open(firstCiphertext, destination.AsSpan(), "Count-0"u8); + Assert.Equal(plaintext, destination); + Assert.Equal(plaintext, fromSpan.Open(secondCiphertext, "Count-1"u8.ToArray())); + AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); + AssertRecipientExports(fromSpan, emptyContextExportHex, zeroContextExportHex, testContextExportHex); } } @@ -1389,8 +642,6 @@ public static void Psk_RoundtripAndLifetime(HpkeKem kem, HpkeKdf kdf, HpkeAead a } } } - - CryptographicOperations.ZeroMemory(originalPsk); } } @@ -1446,191 +697,39 @@ public static void Psk_AuthenticationAndModeSeparation(HpkeKem kem, HpkeKdf kdf, } } - [Fact] - public static void Psk_ArgumentValidation() + private static void AssertRecipientExports( + HpkeRecipient recipient, string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + byte[][] contexts = [[], [0], "TestContext"u8.ToArray()]; + string[] expectedHex = [emptyContextExportHex, zeroContextExportHex, testContextExportHex]; - using (RecordingHpke key = new(suite)) + for (int i = 0; i < contexts.Length; i++) { - byte[] psk = new byte[32]; - byte[] pskId = [1]; - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - AssertExtensions.Throws("psk", () => key.CreatePskSender((byte[])null, pskId, out _)); - AssertExtensions.Throws("pskId", () => key.CreatePskSender(psk, (byte[])null, out _)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreatePskRecipient((byte[])null, psk, pskId)); - AssertExtensions.Throws("psk", () => key.CreatePskRecipient(enc, (byte[])null, pskId)); - AssertExtensions.Throws("pskId", () => key.CreatePskRecipient(enc, psk, (byte[])null)); - - foreach (int length in new[] { 0, 1, 31 }) - { - AssertPskArgumentException(key, "psk", new byte[length], pskId, enc, []); - } - - AssertPskArgumentException(key, "pskId", psk, [], enc, []); - foreach (int length in new[] { 0, enc.Length - 1, enc.Length + 1 }) - { - byte[] invalidEnc = new byte[length]; - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreatePskSender(psk, pskId, invalidEnc.AsSpan())); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreatePskRecipient(invalidEnc, psk, pskId)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreatePskRecipient(invalidEnc.AsSpan(), psk, pskId)); - } - - key.Dispose(); - Assert.Throws(() => key.CreatePskSender(psk, pskId, out _)); - Assert.Throws(() => key.CreatePskSender(psk.AsSpan(), pskId, out _)); - Assert.Throws(() => key.CreatePskSender(psk, pskId, enc.AsSpan())); - Assert.Throws(() => key.CreatePskRecipient(enc, psk, pskId)); - Assert.Throws(() => key.CreatePskRecipient(enc.AsSpan(), psk, pskId)); - Assert.Equal(0, key.PskCalls); + byte[] expected = Convert.FromHexString(expectedHex[i]); + Assert.Equal(expected, recipient.Export(contexts[i], expected.Length)); + Assert.Equal(expected, recipient.Export(contexts[i].AsSpan(), expected.Length)); + byte[] destination = new byte[expected.Length + 2]; + destination.AsSpan().Fill(0xA5); + recipient.Export(contexts[i], destination.AsSpan(1, expected.Length)); + AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); + Assert.Equal(0xA5, destination[0]); + Assert.Equal(0xA5, destination[^1]); } } - public static IEnumerable PskInputLengthData() + [Theory] + [MemberData(nameof(OpenSuiteData))] + public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) { - foreach (HpkeKdf kdf in Enum.GetValues()) + HpkeSuite suite = new(kem, kdf, aead); + + if (!Hpke.IsSupported(suite)) { - yield return new object[] { kdf, 32, 1, 0, null }; - yield return new object[] { kdf, 33, 1, 0, null }; - yield return new object[] { kdf, 65535, 65535, 65535, null }; - bool shake = kdf is HpkeKdf.SHAKE128 or HpkeKdf.SHAKE256; - yield return new object[] { kdf, 65536, 1, 0, shake ? "psk" : null }; - yield return new object[] { kdf, 32, 65536, 0, shake ? "pskId" : null }; - yield return new object[] { kdf, 32, 1, 65536, shake ? "info" : null }; + Assert.Throws(() => Hpke.GenerateKey(suite)); + return; } - } - [Theory] - [MemberData(nameof(PskInputLengthData))] - public static void Psk_InputLengths(HpkeKdf kdf, int pskLength, int idLength, int infoLength, string invalidParameter) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - byte[] psk = new byte[pskLength]; - psk.AsSpan().Fill(0x3C); - byte[] pskId = new byte[idLength]; - pskId.AsSpan().Fill(0x1D); - byte[] info = new byte[infoLength]; - info.AsSpan().Fill(0x39); - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - - using (RecordingHpke key = new(suite)) - { - if (invalidParameter is not null) - { - AssertPskArgumentException(key, invalidParameter, psk, pskId, enc, info); - Assert.Equal(0, key.PskCalls); - return; - } - - byte[] expectedEnc = new byte[enc.Length]; - expectedEnc.AsSpan().Fill(0xD7); - using (HpkeSender sender = key.CreatePskSender(psk, pskId, out byte[] arrayEnc, info)) - { - Assert.Equal(expectedEnc, arrayEnc); - Assert.Same(suite, sender.Suite); - } - - using (HpkeSender sender = key.CreatePskSender(psk.AsSpan(), pskId, out byte[] spanEnc, info)) - { - Assert.Equal(expectedEnc, spanEnc); - } - - using (HpkeSender sender = key.CreatePskSender(psk, pskId, enc.AsSpan(), info)) - { - Assert.Equal(expectedEnc, enc); - } - - Assert.Equal(info, key.LastSenderInfo); - using (HpkeRecipient recipient = key.CreatePskRecipient(enc, psk, pskId, info)) - using (HpkeRecipient spanRecipient = key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)) - { - Assert.Same(suite, recipient.Suite); - Assert.Same(suite, spanRecipient.Suite); - Assert.Equal(enc, key.LastRecipientEncapsulatedSecret); - Assert.Equal(info, key.LastRecipientInfo); - Assert.Equal(psk, key.LastPsk); - Assert.Equal(pskId, key.LastPskId); - } - - Assert.Equal(5, key.PskCalls); - } - } - - private static void AssertPskArgumentException( - Hpke key, string parameter, byte[] psk, byte[] pskId, byte[] enc, byte[] info) - { - byte[] original = [0xA5]; - byte[] result = original; - AssertExtensions.Throws(parameter, () => key.CreatePskSender(psk, pskId, out result, info)); - Assert.Same(original, result); - AssertExtensions.Throws( - parameter, () => key.CreatePskSender(psk.AsSpan(), pskId, out result, info)); - Assert.Same(original, result); - byte[] originalEnc = (byte[])enc.Clone(); - AssertExtensions.Throws(parameter, () => key.CreatePskSender(psk, pskId, enc.AsSpan(), info)); - Assert.Equal(originalEnc, enc); - AssertExtensions.Throws(parameter, () => key.CreatePskRecipient(enc, psk, pskId, info)); - AssertExtensions.Throws(parameter, () => key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)); - } - - [Fact] - public static void Psk_CoreFailureDoesNotPublishOutput() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpke key = new(suite) { ThrowOnCreateSender = true }) - { - byte[] psk = new byte[32]; - byte[] pskId = [1]; - byte[] original = [0xA5]; - byte[] enc = original; - Assert.Throws(() => key.CreatePskSender(psk, pskId, out enc)); - Assert.Same(original, enc); - Assert.Throws(() => key.CreatePskSender(psk.AsSpan(), pskId, out enc)); - Assert.Same(original, enc); - Assert.Throws( - () => key.CreatePskSender(psk, pskId, new byte[suite.EncapsulatedSecretSizeInBytes].AsSpan())); - Assert.Equal(3, key.PskCalls); - } - } - - private static void AssertRecipientExports( - HpkeRecipient recipient, string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) - { - byte[][] contexts = [[], [0], "TestContext"u8.ToArray()]; - string[] expectedHex = [emptyContextExportHex, zeroContextExportHex, testContextExportHex]; - - for (int i = 0; i < contexts.Length; i++) - { - byte[] expected = Convert.FromHexString(expectedHex[i]); - Assert.Equal(expected, recipient.Export(contexts[i], expected.Length)); - Assert.Equal(expected, recipient.Export(contexts[i].AsSpan(), expected.Length)); - byte[] destination = new byte[expected.Length + 2]; - destination.AsSpan().Fill(0xA5); - recipient.Export(contexts[i], destination.AsSpan(1, expected.Length)); - AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - int maximumLength = kdf switch + int maximumLength = kdf switch { HpkeKdf.HKDF_SHA256 => 8160, HpkeKdf.HKDF_SHA384 => 12240, @@ -1701,19 +800,6 @@ public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) Assert.Equal(sender.Export(longContext, 32), recipient.Export(longContext, 32)); } - AssertExtensions.Throws( - "length", () => sender.Export(context, maximumLength + 1)); - AssertExtensions.Throws( - "length", () => recipient.Export(context, maximumLength + 1)); - byte[] invalidDestination = new byte[maximumLength + 1]; - invalidDestination.AsSpan().Fill(0xA5); - byte[] originalDestination = (byte[])invalidDestination.Clone(); - AssertExtensions.Throws( - "destination", () => sender.Export(context, invalidDestination.AsSpan())); - AssertExtensions.Throws( - "destination", () => recipient.Export(context, invalidDestination.AsSpan())); - Assert.Equal(originalDestination, invalidDestination); - byte[] message = "message"u8.ToArray(); for (int i = 0; i < 3; i++) { @@ -1725,1239 +811,10 @@ public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) } sender.Dispose(); - Assert.Throws(() => sender.Export(context, 0)); - Assert.Throws(() => sender.Export(context.AsSpan(), 32)); - Assert.Throws(() => sender.Export(context, new byte[32].AsSpan())); Assert.Equal(referenceExport, recipient.Export(context, 32)); - recipient.Dispose(); - Assert.Throws(() => recipient.Export(context, 0)); - Assert.Throws(() => recipient.Export(context.AsSpan(), 32)); - Assert.Throws(() => recipient.Export(context, new byte[32].AsSpan())); - } - } - } - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - public static void Seal_RejectsOverlappingBuffers(int offset) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - // Indices: plaintext, associatedData, info, encapsulatedSecret, ciphertext. - (int First, int Second)[] pairs = [(0, 3), (1, 3), (2, 3), (0, 4), (1, 4), (2, 4), (3, 4)]; - - foreach ((int first, int second) in pairs) - { - byte[][] buffers = [new byte[128], new byte[128], new byte[128], new byte[128], new byte[128]]; - buffers[second] = buffers[first]; - buffers[first].AsSpan().Fill(0xA5); - byte[] original = (byte[])buffers[first].Clone(); - int[] starts = [16, 16, 16, 16, 16]; - starts[second] += offset; - - using (RecordingHpke key = new(suite)) - { - Assert.Throws(() => key.Seal( - buffers[0].AsSpan(starts[0], 32), - buffers[3].AsSpan(starts[3], suite.EncapsulatedSecretSizeInBytes), - buffers[4].AsSpan(starts[4], suite.GetCiphertextLength(32)), - buffers[1].AsSpan(starts[1], 32), - buffers[2].AsSpan(starts[2], 32))); - Assert.Equal(0, key.SealCalls); - Assert.Equal(original, buffers[first]); - } - } - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - public static void CreateSender_RejectsOverlappingBuffers(int offset) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - byte[] buffer = new byte[128]; - buffer.AsSpan().Fill(0xA5); - byte[] original = (byte[])buffer.Clone(); - - using (RecordingHpke key = new(suite)) - { - Assert.Throws(() => key.CreateSender( - buffer.AsSpan(16 + offset, suite.EncapsulatedSecretSizeInBytes), - buffer.AsSpan(16, 32))); - Assert.Equal(0, key.CreateSenderCalls); - Assert.Equal(original, buffer); - } - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - public static void CreatePskSender_RejectsOverlappingBuffers(int offset) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - for (int input = 0; input < 3; input++) - { - byte[][] inputs = [new byte[128], new byte[128], new byte[128]]; - byte[] output = inputs[input]; - output.AsSpan().Fill(0xA5); - byte[] original = (byte[])output.Clone(); - - using (RecordingHpke key = new(suite)) - { - Assert.Throws(() => key.CreatePskSender( - inputs[0].AsSpan(16, 32), - inputs[1].AsSpan(16, 32), - output.AsSpan(16 + offset, suite.EncapsulatedSecretSizeInBytes), - inputs[2].AsSpan(16, 32))); - Assert.Equal(0, key.PskCalls); - Assert.Equal(0, key.CreateSenderCalls); - Assert.Equal(original, output); - } - } - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - public static void Sender_SealRejectsOverlappingBuffers(int offset) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - for (int input = 0; input < 2; input++) - { - byte[][] inputs = [new byte[128], new byte[128]]; - byte[] output = inputs[input]; - output.AsSpan().Fill(0xA5); - byte[] original = (byte[])output.Clone(); - - using (RecordingHpkeSender sender = new(suite)) - { - Assert.Throws(() => sender.Seal( - inputs[0].AsSpan(16, 32), - output.AsSpan(16 + offset, suite.GetCiphertextLength(32)), - inputs[1].AsSpan(16, 32))); - Assert.Equal(0, sender.SealCalls); - Assert.Equal(original, output); - } - } - } - - [Theory] - [InlineData(0)] - [InlineData(32)] - public static void Seal_AllowsReadOnlyOverlapAndAdjacentOutputs(int inputLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - int encLength = suite.EncapsulatedSecretSizeInBytes; - int ciphertextLength = suite.GetCiphertextLength(inputLength); - byte[] buffer = new byte[inputLength + encLength + ciphertextLength]; - buffer.AsSpan().Fill(0xA5); - byte[] originalInput = buffer.AsSpan(0, inputLength).ToArray(); - - using (RecordingHpke key = new(suite)) - { - key.Seal( - buffer.AsSpan(0, inputLength), - buffer.AsSpan(inputLength, encLength), - buffer.AsSpan(inputLength + encLength, ciphertextLength), - buffer.AsSpan(0, inputLength), - buffer.AsSpan(0, inputLength)); - Assert.Equal(1, key.SealCalls); - AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); - Assert.Equal(0xD7, buffer[inputLength]); - Assert.Equal(0xC8, buffer[^1]); - } - - using (RecordingHpkeSender sender = new(suite)) - { - sender.Seal( - buffer.AsSpan(0, inputLength), - buffer.AsSpan(inputLength, ciphertextLength), - buffer.AsSpan(0, inputLength)); - Assert.Equal(1, sender.SealCalls); - AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); - } - } - - [Fact] - public static void CreateSender_AllowsReadOnlyOverlapAndAdjacentOutput() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - byte[] buffer = new byte[32 + suite.EncapsulatedSecretSizeInBytes]; - buffer.AsSpan().Fill(0xA5); - byte[] originalInput = buffer.AsSpan(0, 32).ToArray(); - - using (RecordingHpke key = new(suite)) - using (HpkeSender sender = key.CreateSender(buffer.AsSpan(32), buffer.AsSpan(0, 32))) - using (HpkeSender pskSender = key.CreatePskSender( - buffer.AsSpan(0, 32), buffer.AsSpan(0, 32), buffer.AsSpan(32), buffer.AsSpan(0, 32))) - { - Assert.Equal(2, key.CreateSenderCalls); - Assert.Equal(1, key.PskCalls); - Assert.Equal(originalInput, key.LastPsk); - Assert.Equal(originalInput, key.LastPskId); - Assert.Equal(originalInput, key.LastSenderInfo); - AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, 32)); - Assert.Equal(0xD7, buffer[^1]); - } - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - [InlineData(null)] - public static void Open_RejectsOverlappingBuffers(int? offset) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - int[] lengths = [suite.EncapsulatedSecretSizeInBytes, suite.GetCiphertextLength(32), 32, 32]; - - for (int input = 0; input < lengths.Length; input++) - { - byte[][] inputs = [new byte[128], new byte[128], new byte[128], new byte[128]]; - byte[] output = inputs[input]; - output.AsSpan().Fill(0xA5); - byte[] original = (byte[])output.Clone(); - int outputStart = 16 + (offset ?? lengths[input] - 1); - - using (RecordingHpke key = new(suite)) - { - Assert.Throws(() => key.Open( - inputs[0].AsSpan(16, lengths[0]), - inputs[1].AsSpan(16, lengths[1]), - output.AsSpan(outputStart, 32), - inputs[2].AsSpan(16, lengths[2]), - inputs[3].AsSpan(16, lengths[3]))); - Assert.False(key.OpenCoreCalled); - Assert.Equal(original, output); - } - } - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - [InlineData(null)] - public static void Recipient_OpenRejectsOverlappingBuffers(int? offset) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - int[] lengths = [suite.GetCiphertextLength(32), 32]; - - for (int input = 0; input < lengths.Length; input++) - { - byte[][] inputs = [new byte[128], new byte[128]]; - byte[] output = inputs[input]; - output.AsSpan().Fill(0xA5); - byte[] original = (byte[])output.Clone(); - int outputStart = 16 + (offset ?? lengths[input] - 1); - - using (RecordingHpkeRecipient recipient = new(suite)) - { - Assert.Throws(() => recipient.Open( - inputs[0].AsSpan(16, lengths[0]), - output.AsSpan(outputStart, 32), - inputs[1].AsSpan(16, lengths[1]))); - Assert.Equal(0, recipient.OpenCalls); - Assert.Equal(original, output); - } - } - } - - [Theory] - [InlineData(0)] - [InlineData(32)] - public static void Open_AllowsReadOnlyOverlapAndAdjacentOutput(int plaintextLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - int encLength = suite.EncapsulatedSecretSizeInBytes; - int ciphertextLength = suite.GetCiphertextLength(plaintextLength); - int inputLength = Math.Max(encLength, ciphertextLength); - byte[] buffer = new byte[inputLength + plaintextLength + 1]; - buffer.AsSpan().Fill(0xA5); - byte[] originalInput = buffer.AsSpan(0, inputLength).ToArray(); - - using (RecordingHpke key = new(suite)) - { - key.Open( - buffer.AsSpan(0, encLength), - buffer.AsSpan(0, ciphertextLength), - buffer.AsSpan(inputLength, plaintextLength), - buffer.AsSpan(0, inputLength), - buffer.AsSpan(0, inputLength)); - Assert.True(key.OpenCoreCalled); - AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); - AssertExtensions.SequenceEqual(new byte[plaintextLength].AsSpan(), buffer.AsSpan(inputLength, plaintextLength)); - Assert.Equal(0xA5, buffer[^1]); - } - - using (RecordingHpkeRecipient recipient = new(suite)) - { - recipient.Open( - buffer.AsSpan(0, ciphertextLength), - buffer.AsSpan(inputLength, plaintextLength), - buffer.AsSpan(0, inputLength)); - Assert.Equal(1, recipient.OpenCalls); - AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength)); - Assert.Equal(0xA5, buffer[^1]); - } - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - [InlineData(31)] - public static void Export_RejectsOverlappingBuffers(int offset) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - byte[] buffer = new byte[96]; - buffer.AsSpan().Fill(0xA5); - byte[] original = (byte[])buffer.Clone(); - - using (RecordingHpkeSender sender = new(suite)) - { - Assert.Throws(() => sender.Export( - buffer.AsSpan(16, 32), buffer.AsSpan(16 + offset, 32))); - Assert.Equal(0, sender.ExportCalls); - Assert.Equal(original, buffer); - } - - using (RecordingHpkeRecipient recipient = new(suite)) - { - Assert.Throws(() => recipient.Export( - buffer.AsSpan(16, 32), buffer.AsSpan(16 + offset, 32))); - Assert.Equal(0, recipient.ExportCalls); - Assert.Equal(original, buffer); - } - } - - [Theory] - [InlineData(0, 32)] - [InlineData(32, 0)] - [InlineData(32, 32)] - public static void Export_AllowsEmptyAndAdjacentBuffers(int contextLength, int outputLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - byte[] buffer = new byte[contextLength + outputLength + 1]; - buffer.AsSpan().Fill(0xA5); - byte[] originalContext = buffer.AsSpan(0, contextLength).ToArray(); - byte[] expected = new byte[outputLength]; - expected.AsSpan().Fill(0xE7); - - using (RecordingHpkeSender sender = new(suite)) - { - sender.Export(buffer.AsSpan(0, contextLength), buffer.AsSpan(contextLength, outputLength)); - Assert.Equal(1, sender.ExportCalls); - Assert.Equal(originalContext, sender.LastExporterContext); - AssertExtensions.SequenceEqual(originalContext.AsSpan(), buffer.AsSpan(0, contextLength)); - AssertExtensions.SequenceEqual(expected.AsSpan(), buffer.AsSpan(contextLength, outputLength)); - Assert.Equal(0xA5, buffer[^1]); - } - - buffer.AsSpan(contextLength).Fill(0xA5); - - using (RecordingHpkeRecipient recipient = new(suite)) - { - recipient.Export(buffer.AsSpan(0, contextLength), buffer.AsSpan(contextLength, outputLength)); - Assert.Equal(1, recipient.ExportCalls); - Assert.Equal(originalContext, recipient.LastExporterContext); - AssertExtensions.SequenceEqual(originalContext.AsSpan(), buffer.AsSpan(0, contextLength)); - AssertExtensions.SequenceEqual(expected.AsSpan(), buffer.AsSpan(contextLength, outputLength)); - Assert.Equal(0xA5, buffer[^1]); - } - } - - [Fact] - public static void DisposedKey_ValidatesArgumentsFirst() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.SHAKE128, HpkeAead.AES_128_GCM); - using (RecordingHpke key = new(suite)) - { - key.Dispose(); - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; - byte[] plaintext = new byte[1]; - byte[] psk = new byte[32]; - byte[] pskId = [1]; - byte[] invalidInfo = new byte[65536]; - - AssertExtensions.Throws("destination", () => key.ExportDecapsulationKey(Span.Empty)); - AssertExtensions.Throws("destination", () => key.ExportEncapsulationKey(Span.Empty)); - AssertExtensions.Throws("plaintext", () => key.Seal((byte[])null, out _, out _)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.Seal(plaintext, Span.Empty, ciphertext)); - AssertExtensions.Throws( - "ciphertext", () => key.Seal(plaintext, enc, Span.Empty)); - Assert.Throws(() => key.Seal(enc.AsSpan(0, 1), enc, ciphertext)); - - AssertExtensions.Throws("encapsulatedSecret", () => key.Open((byte[])null, ciphertext)); - AssertExtensions.Throws("ciphertext", () => key.Open(enc, (byte[])null)); - AssertExtensions.Throws("encapsulatedSecret", () => key.Open(Array.Empty(), ciphertext)); - AssertExtensions.Throws("encapsulatedSecret", () => key.Open(ReadOnlySpan.Empty, ciphertext)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.Open(ReadOnlySpan.Empty, ciphertext, plaintext.AsSpan())); - AssertExtensions.Throws("ciphertext", () => key.Open(enc, Array.Empty())); - AssertExtensions.Throws("ciphertext", () => key.Open(enc.AsSpan(), ReadOnlySpan.Empty)); - AssertExtensions.Throws( - "ciphertext", () => key.Open(enc, ReadOnlySpan.Empty, plaintext.AsSpan())); - AssertExtensions.Throws("plaintext", () => key.Open(enc, ciphertext, Span.Empty)); - Assert.Throws(() => key.Open(enc, ciphertext, ciphertext.AsSpan(0, 1))); - - AssertExtensions.Throws("encapsulatedSecret", () => key.CreateSender(Span.Empty)); - Assert.Throws(() => key.CreateSender(enc, enc.AsSpan(0, 1))); - AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient((byte[])null)); - AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(Array.Empty())); - AssertExtensions.Throws("encapsulatedSecret", () => key.CreateRecipient(ReadOnlySpan.Empty)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreatePskSender(psk, pskId, Span.Empty)); - Assert.Throws(() => key.CreatePskSender(enc.AsSpan(0, 32), pskId, enc)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreatePskRecipient(Array.Empty(), psk, pskId)); - AssertExtensions.Throws( - "encapsulatedSecret", () => key.CreatePskRecipient(ReadOnlySpan.Empty, psk, pskId)); - - AssertExtensions.Throws("info", () => key.Seal(plaintext, out _, out _, info: invalidInfo)); - AssertExtensions.Throws("info", () => key.Seal(plaintext.AsSpan(), out _, out _, info: invalidInfo)); - AssertExtensions.Throws("info", () => key.CreateSender(out _, invalidInfo)); - AssertExtensions.Throws("info", () => key.CreateSender(enc, invalidInfo)); - AssertExtensions.Throws("info", () => key.Open(enc, ciphertext, info: invalidInfo)); - AssertExtensions.Throws("info", () => key.CreateRecipient(enc, invalidInfo)); - AssertPskArgumentException(key, "psk", [], pskId, enc, []); - AssertPskArgumentException(key, "pskId", psk, [], enc, []); - AssertPskArgumentException(key, "info", psk, pskId, enc, invalidInfo); - - Assert.Throws(() => key.Seal(plaintext, out _, out _)); - Assert.Throws(() => key.Seal(plaintext.AsSpan(), out _, out _)); - Assert.Throws(() => key.Seal(plaintext, enc, ciphertext)); - Assert.Throws(() => key.Open(enc, ciphertext)); - Assert.Throws(() => key.Open(enc.AsSpan(), ciphertext)); - Assert.Throws(() => key.Open(enc, ciphertext, plaintext.AsSpan())); - Assert.Throws(() => key.CreateSender(out _)); - Assert.Throws(() => key.CreateSender(enc)); - Assert.Throws(() => key.CreateRecipient(enc)); - Assert.Throws(() => key.CreatePskSender(psk, pskId, out _)); - Assert.Throws(() => key.CreatePskSender(psk, pskId, enc)); - Assert.Throws(() => key.CreatePskRecipient(enc, psk, pskId)); - Assert.Equal(0, key.SealCalls); - Assert.False(key.OpenCoreCalled); - Assert.Equal(0, key.CreateSenderCalls); - Assert.Equal(0, key.CreateRecipientCalls); - Assert.Equal(0, key.PskCalls); - } - } - - [Fact] - public static void DisposedSender_ValidatesArgumentsFirst() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - using (RecordingHpkeSender sender = new(suite)) - { - sender.Dispose(); - byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; - AssertExtensions.Throws("plaintext", () => sender.Seal((byte[])null)); - AssertExtensions.Throws( - "ciphertext", () => sender.Seal(ReadOnlySpan.Empty, Span.Empty)); - Assert.Throws(() => sender.Seal(ciphertext.AsSpan(0, 1), ciphertext.AsSpan())); - AssertExtensions.Throws("exporterContext", () => sender.Export((byte[])null, 1)); - - foreach (int length in new[] { -1, 8161 }) - { - AssertExtensions.Throws("length", () => sender.Export(Array.Empty(), length)); - AssertExtensions.Throws("length", () => sender.Export(ReadOnlySpan.Empty, length)); - } - - AssertExtensions.Throws( - "destination", () => sender.Export(ReadOnlySpan.Empty, new byte[8161].AsSpan())); - Assert.Throws(() => sender.Export(ciphertext.AsSpan(), ciphertext.AsSpan())); - Assert.Throws(() => sender.Seal(ciphertext)); - Assert.Throws(() => sender.Seal(ciphertext.AsSpan())); - Assert.Throws(() => sender.Seal(new byte[1], ciphertext.AsSpan())); - Assert.Throws(() => sender.Export(Array.Empty(), 0)); - Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, Span.Empty)); - Assert.Equal(0, sender.SealCalls); - Assert.Equal(0, sender.ExportCalls); - } - } - - [Fact] - public static void DisposedRecipient_ValidatesArgumentsFirst() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - using (RecordingHpkeRecipient recipient = new(suite)) - { - recipient.Dispose(); - byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; - AssertExtensions.Throws("ciphertext", () => recipient.Open((byte[])null)); - AssertExtensions.Throws("ciphertext", () => recipient.Open(Array.Empty())); - AssertExtensions.Throws("ciphertext", () => recipient.Open(ReadOnlySpan.Empty)); - AssertExtensions.Throws( - "ciphertext", () => recipient.Open(ReadOnlySpan.Empty, Span.Empty)); - AssertExtensions.Throws("plaintext", () => recipient.Open(ciphertext, Span.Empty)); - Assert.Throws(() => recipient.Open(ciphertext, ciphertext.AsSpan(0, 1))); - AssertExtensions.Throws("exporterContext", () => recipient.Export((byte[])null, 1)); - - foreach (int length in new[] { -1, 8161 }) - { - AssertExtensions.Throws("length", () => recipient.Export(Array.Empty(), length)); - AssertExtensions.Throws("length", () => recipient.Export(ReadOnlySpan.Empty, length)); - } - - AssertExtensions.Throws( - "destination", () => recipient.Export(ReadOnlySpan.Empty, new byte[8161].AsSpan())); - Assert.Throws(() => recipient.Export(ciphertext.AsSpan(), ciphertext.AsSpan())); - Assert.Throws(() => recipient.Open(ciphertext)); - Assert.Throws(() => recipient.Open(ciphertext.AsSpan())); - Assert.Throws(() => recipient.Open(ciphertext, new byte[1].AsSpan())); - Assert.Throws(() => recipient.Export(Array.Empty(), 0)); - Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, Span.Empty)); - Assert.Equal(0, recipient.OpenCalls); - Assert.Equal(0, recipient.ExportCalls); - } - } - - private sealed class RecordingHpke : Hpke - { - internal bool OpenCoreCalled { get; private set; } - internal int SealCalls { get; private set; } - internal int CreateSenderCalls { get; private set; } - internal byte[] LastSenderInfo { get; private set; } = []; - internal bool ThrowOnCreateSender { get; set; } - internal int CreateRecipientCalls { get; private set; } - internal byte[] LastRecipientEncapsulatedSecret { get; private set; } = []; - internal byte[] LastRecipientInfo { get; private set; } = []; - internal int PskCalls { get; private set; } - internal byte[] LastPsk { get; private set; } = []; - internal byte[] LastPskId { get; private set; } = []; - - internal RecordingHpke(HpkeSuite suite) : base(suite) - { - } - - protected override void OpenCore( - ReadOnlySpan encapsulatedSecret, - ReadOnlySpan ciphertext, - Span plaintext, - ReadOnlySpan associatedData, - ReadOnlySpan info) - { - OpenCoreCalled = true; - plaintext.Clear(); - } - - protected override HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) - { - CreateSenderCalls++; - LastSenderInfo = info.ToArray(); - encapsulatedSecret.Fill(0xD7); - - if (ThrowOnCreateSender) - { - throw new CryptographicException("Sender creation test failure."); - } - - return new RecordingHpkeSender(Suite); - } - - protected override HpkeRecipient CreateRecipientCore( - ReadOnlySpan encapsulatedSecret, - ReadOnlySpan info) - { - CreateRecipientCalls++; - LastRecipientEncapsulatedSecret = encapsulatedSecret.ToArray(); - LastRecipientInfo = info.ToArray(); - return new RecordingHpkeRecipient(Suite); - } - - protected override HpkeSender CreatePskSenderCore( - Span encapsulatedSecret, - ReadOnlySpan info, - ReadOnlySpan psk, - ReadOnlySpan pskId) - { - RecordPskInputs(psk, pskId); - return CreateSenderCore(encapsulatedSecret, info); - } - - protected override HpkeRecipient CreatePskRecipientCore( - ReadOnlySpan encapsulatedSecret, - ReadOnlySpan info, - ReadOnlySpan psk, - ReadOnlySpan pskId) - { - RecordPskInputs(psk, pskId); - return CreateRecipientCore(encapsulatedSecret, info); - } - - private void RecordPskInputs(ReadOnlySpan psk, ReadOnlySpan pskId) - { - PskCalls++; - LastPsk = psk.ToArray(); - LastPskId = pskId.ToArray(); - } - - protected override void ExportDecapsulationKeyCore(Span destination) => - throw new InvalidOperationException("Unexpected key export."); - - protected override void ExportEncapsulationKeyCore(Span destination) => - throw new InvalidOperationException("Unexpected key export."); - - protected override void SealCore( - ReadOnlySpan plaintext, - Span encapsulatedSecret, - Span ciphertext, - ReadOnlySpan associatedData, - ReadOnlySpan info) - { - SealCalls++; - encapsulatedSecret.Fill(0xD7); - ciphertext.Fill(0xC8); - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void GenerateKey(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - Hpke key = Hpke.GenerateKey(suite); - - try - { - Assert.Same(suite, key.Suite); - - byte[] privateKey = key.ExportDecapsulationKey(); - - try - { - Assert.Equal(suite.DecapsulationKeySizeInBytes, privateKey.Length); - } - finally - { - CryptographicOperations.ZeroMemory(privateKey); - } - } - finally - { - key.Dispose(); - } - - key.Dispose(); - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void ExportDecapsulationKey_BufferAndLifetime(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - using (Hpke key = Hpke.GenerateKey(suite)) - { - int keySize = suite.DecapsulationKeySizeInBytes; - byte[] buffer = new byte[keySize + 2]; - byte[] exported = key.ExportDecapsulationKey(); - - try - { - buffer.AsSpan().Fill(0xA5); - key.ExportDecapsulationKey(buffer.AsSpan(1, keySize)); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - Assert.Equal(0xA5, buffer[0]); - Assert.Equal(0xA5, buffer[^1]); - - exported.AsSpan().Clear(); - key.ExportDecapsulationKey(exported); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - - AssertExtensions.Throws( - "destination", () => key.ExportDecapsulationKey(Span.Empty)); - AssertExtensions.Throws( - "destination", () => key.ExportDecapsulationKey(buffer.AsSpan(0, keySize - 1))); - AssertExtensions.Throws( - "destination", () => key.ExportDecapsulationKey(buffer.AsSpan(0, keySize + 1))); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - - key.Dispose(); - Assert.Throws(() => key.ExportDecapsulationKey()); - Assert.Throws(() => key.ExportDecapsulationKey(exported)); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - } - finally - { - CryptographicOperations.ZeroMemory(exported); - CryptographicOperations.ZeroMemory(buffer); - } - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void ExportEncapsulationKey_BufferAndLifetime(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - using (Hpke key = Hpke.GenerateKey(suite)) - { - int keySize = suite.EncapsulationKeySizeInBytes; - byte[] buffer = new byte[keySize + 2]; - byte[] exported = key.ExportEncapsulationKey(); - - buffer.AsSpan().Fill(0xA5); - key.ExportEncapsulationKey(buffer.AsSpan(1, keySize)); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - Assert.Equal(0xA5, buffer[0]); - Assert.Equal(0xA5, buffer[^1]); - - exported.AsSpan().Clear(); - key.ExportEncapsulationKey(exported); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - - AssertExtensions.Throws( - "destination", () => key.ExportEncapsulationKey(Span.Empty)); - AssertExtensions.Throws( - "destination", () => key.ExportEncapsulationKey(buffer.AsSpan(0, keySize - 1))); - AssertExtensions.Throws( - "destination", () => key.ExportEncapsulationKey(buffer.AsSpan(0, keySize + 1))); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - - key.Dispose(); - Assert.Throws(() => key.ExportEncapsulationKey()); - Assert.Throws(() => key.ExportEncapsulationKey(exported)); - AssertExtensions.SequenceEqual(exported.AsSpan(), buffer.AsSpan(1, keySize)); - } - } - - [Fact] - public static void Sender_ConstructorAndDisposal() - { - AssertExtensions.Throws("suite", () => new RecordingHpkeSender(null)); - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpkeSender sender = new(suite)) - { - Assert.Same(suite, sender.Suite); - sender.Dispose(); - sender.Dispose(); - Assert.Equal(1, sender.DisposeCalls); - - byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; - Assert.Throws(() => sender.Seal(Array.Empty())); - Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty)); - Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty, ciphertext.AsSpan())); - Assert.Throws(() => sender.Export(Array.Empty(), 0)); - Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, 0)); - Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, Span.Empty)); - Assert.Equal(0, sender.SealCalls); - Assert.Equal(0, sender.ExportCalls); - } - } - - [Theory] - [InlineData(HpkeAead.AES_128_GCM, 0)] - [InlineData(HpkeAead.AES_128_GCM, 5)] - [InlineData(HpkeAead.AES_256_GCM, 5)] - [InlineData(HpkeAead.ChaCha20Poly1305, 5)] - public static void Sender_Seal(HpkeAead aead, int plaintextLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, aead); - - using (RecordingHpkeSender sender = new(suite)) - { - byte[] plaintext = new byte[plaintextLength]; - plaintext.AsSpan().Fill(0x3C); - byte[] associatedData = [1, 2, 3]; - byte[] expected = new byte[suite.GetCiphertextLength(plaintextLength)]; - expected.AsSpan().Fill(0xD3); - - Assert.Equal(expected, sender.Seal(plaintext, associatedData)); - Assert.Equal(plaintext, sender.LastPlaintext); - Assert.Equal(associatedData, sender.LastAssociatedData); - - Assert.Equal(expected, sender.Seal( - new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData))); - Assert.Equal(plaintext, sender.LastPlaintext); - Assert.Equal(associatedData, sender.LastAssociatedData); - - byte[] destination = new byte[expected.Length + 2]; - destination.AsSpan().Fill(0xA5); - sender.Seal(plaintext, destination.AsSpan(1, expected.Length), associatedData); - AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - Assert.Equal(plaintext, sender.LastPlaintext); - Assert.Equal(associatedData, sender.LastAssociatedData); - - Assert.Equal(expected, sender.Seal(plaintext)); - Assert.Empty(sender.LastAssociatedData); - Assert.Equal(expected, sender.Seal(plaintext.AsSpan())); - Assert.Empty(sender.LastAssociatedData); - Assert.Equal(5, sender.SealCalls); - Assert.Equal(0, sender.ExportCalls); - } - } - - [Fact] - public static void Sender_SealValidation() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpkeSender sender = new(suite)) - { - AssertExtensions.Throws("plaintext", () => sender.Seal((byte[])null)); - - foreach (int length in new[] { 0, suite.AeadTagSizeInBytes - 1, suite.AeadTagSizeInBytes + 1 }) - { - byte[] ciphertext = new byte[length]; - ciphertext.AsSpan().Fill(0xA5); - byte[] originalCiphertext = (byte[])ciphertext.Clone(); - AssertExtensions.Throws( - "ciphertext", () => sender.Seal(ReadOnlySpan.Empty, ciphertext.AsSpan())); - Assert.Equal(originalCiphertext, ciphertext); - } - - Assert.Equal(0, sender.SealCalls); - } - } - - [Theory] - [InlineData(HpkeKdf.HKDF_SHA256, 8160)] - [InlineData(HpkeKdf.HKDF_SHA384, 12240)] - [InlineData(HpkeKdf.HKDF_SHA512, 16320)] - [InlineData(HpkeKdf.SHAKE128, 65535)] - [InlineData(HpkeKdf.SHAKE256, 65535)] - public static void Sender_Export(HpkeKdf kdf, int maximumLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - - using (RecordingHpkeSender sender = new(suite)) - { - byte[] exporterContext = new byte[HpkeTestData.MaxExporterContextLength]; - exporterContext.AsSpan().Fill(0x39); - - foreach (int length in new[] { 0, 1, maximumLength }) - { - byte[] expected = new byte[length]; - expected.AsSpan().Fill(0xE7); - Assert.Equal(expected, sender.Export(exporterContext, length)); - Assert.Equal(exporterContext, sender.LastExporterContext); - Assert.Equal(expected, sender.Export(exporterContext.AsSpan(), length)); - Assert.Equal(exporterContext, sender.LastExporterContext); - - byte[] destination = new byte[length + 2]; - destination.AsSpan().Fill(0xA5); - sender.Export(exporterContext, destination.AsSpan(1, length)); - AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - Assert.Equal(exporterContext, sender.LastExporterContext); - } - - Assert.Equal(9, sender.ExportCalls); - Assert.Equal(0, sender.SealCalls); - Assert.Empty(sender.Export(Array.Empty(), 0)); - Assert.Empty(sender.LastExporterContext); - } - } - - [Theory] - [InlineData(HpkeKdf.HKDF_SHA256, 8160)] - [InlineData(HpkeKdf.HKDF_SHA384, 12240)] - [InlineData(HpkeKdf.HKDF_SHA512, 16320)] - [InlineData(HpkeKdf.SHAKE128, 65535)] - [InlineData(HpkeKdf.SHAKE256, 65535)] - public static void Sender_ExportValidation(HpkeKdf kdf, int maximumLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - - using (RecordingHpkeSender sender = new(suite)) - { - AssertExtensions.Throws("exporterContext", () => sender.Export((byte[])null, 0)); - - foreach (int length in new[] { -1, int.MinValue, maximumLength + 1, int.MaxValue }) - { - AssertExtensions.Throws( - "length", () => sender.Export(Array.Empty(), length)); - AssertExtensions.Throws( - "length", () => sender.Export(ReadOnlySpan.Empty, length)); - } - - byte[] destination = new byte[maximumLength + 1]; - destination.AsSpan().Fill(0xA5); - byte[] originalDestination = (byte[])destination.Clone(); - AssertExtensions.Throws( - "destination", () => sender.Export(ReadOnlySpan.Empty, destination.AsSpan())); - Assert.Equal(originalDestination, destination); - Assert.Equal(0, sender.ExportCalls); - } - } - - [Fact] - public static void Sender_CoreFailuresPropagate() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpkeSender sender = new(suite) { ThrowOnCoreCall = true }) - { - byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; - Assert.Throws(() => sender.Seal(Array.Empty())); - Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty)); - Assert.Throws(() => sender.Seal(ReadOnlySpan.Empty, ciphertext.AsSpan())); - Assert.Throws(() => sender.Export(Array.Empty(), 1)); - Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, 1)); - Assert.Throws(() => sender.Export(ReadOnlySpan.Empty, new byte[1].AsSpan())); - Assert.Equal(3, sender.SealCalls); - Assert.Equal(3, sender.ExportCalls); - } - } - - [Fact] - public static void Recipient_ConstructorAndDisposal() - { - AssertExtensions.Throws("suite", () => new RecordingHpkeRecipient(null)); - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpkeRecipient recipient = new(suite)) - { - Assert.Same(suite, recipient.Suite); - recipient.Dispose(); - recipient.Dispose(); - Assert.Equal(1, recipient.DisposeCalls); - - byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; - Assert.Throws(() => recipient.Open(ciphertext)); - Assert.Throws(() => recipient.Open(ciphertext.AsSpan())); - Assert.Throws(() => recipient.Open(ciphertext, Span.Empty)); - Assert.Throws(() => recipient.Export(Array.Empty(), 0)); - Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, 0)); - Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, Span.Empty)); - Assert.Equal(0, recipient.OpenCalls); - Assert.Equal(0, recipient.ExportCalls); - } - } - - [Theory] - [InlineData(HpkeAead.AES_128_GCM, 0)] - [InlineData(HpkeAead.AES_128_GCM, 5)] - [InlineData(HpkeAead.AES_256_GCM, 5)] - [InlineData(HpkeAead.ChaCha20Poly1305, 5)] - public static void Recipient_Open(HpkeAead aead, int plaintextLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, aead); - - using (RecordingHpkeRecipient recipient = new(suite)) - { - byte[] ciphertext = new byte[suite.GetCiphertextLength(plaintextLength)]; - ciphertext.AsSpan().Fill(0x3C); - byte[] associatedData = [1, 2, 3]; - byte[] expected = new byte[plaintextLength]; - expected.AsSpan().Fill(0xD3); - - Assert.Equal(expected, recipient.Open(ciphertext, associatedData)); - Assert.Equal(ciphertext, recipient.LastCiphertext); - Assert.Equal(associatedData, recipient.LastAssociatedData); - - Assert.Equal(expected, recipient.Open( - new ReadOnlySpan(ciphertext), new ReadOnlySpan(associatedData))); - Assert.Equal(ciphertext, recipient.LastCiphertext); - Assert.Equal(associatedData, recipient.LastAssociatedData); - - byte[] destination = new byte[plaintextLength + 2]; - destination.AsSpan().Fill(0xA5); - recipient.Open(ciphertext, destination.AsSpan(1, plaintextLength), associatedData); - AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, plaintextLength)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - Assert.Equal(ciphertext, recipient.LastCiphertext); - Assert.Equal(associatedData, recipient.LastAssociatedData); - - Assert.Equal(expected, recipient.Open(ciphertext)); - Assert.Empty(recipient.LastAssociatedData); - Assert.Equal(expected, recipient.Open(ciphertext.AsSpan())); - Assert.Empty(recipient.LastAssociatedData); - Assert.Equal(5, recipient.OpenCalls); - Assert.Equal(0, recipient.ExportCalls); - } - } - - [Fact] - public static void Recipient_OpenValidation() - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpkeRecipient recipient = new(suite)) - { - AssertExtensions.Throws("ciphertext", () => recipient.Open((byte[])null)); - byte[] plaintext = [0xA5]; - - foreach (int length in new[] { 0, suite.AeadTagSizeInBytes - 1 }) - { - byte[] ciphertext = new byte[length]; - AssertExtensions.Throws("ciphertext", () => recipient.Open(ciphertext)); - AssertExtensions.Throws("ciphertext", () => recipient.Open(ciphertext.AsSpan())); - AssertExtensions.Throws( - "ciphertext", () => recipient.Open(ciphertext, plaintext.AsSpan())); - Assert.Equal(0xA5, plaintext[0]); - } - - byte[] validLengthCiphertext = new byte[suite.GetCiphertextLength(1)]; - - foreach (int length in new[] { 0, 2 }) - { - byte[] destination = new byte[length]; - destination.AsSpan().Fill(0xA5); - byte[] originalDestination = (byte[])destination.Clone(); - AssertExtensions.Throws( - "plaintext", () => recipient.Open(validLengthCiphertext, destination.AsSpan())); - Assert.Equal(originalDestination, destination); - } - - Assert.Equal(0, recipient.OpenCalls); - } - } - - [Theory] - [InlineData(HpkeKdf.HKDF_SHA256, 8160)] - [InlineData(HpkeKdf.HKDF_SHA384, 12240)] - [InlineData(HpkeKdf.HKDF_SHA512, 16320)] - [InlineData(HpkeKdf.SHAKE128, 65535)] - [InlineData(HpkeKdf.SHAKE256, 65535)] - public static void Recipient_Export(HpkeKdf kdf, int maximumLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - - using (RecordingHpkeRecipient recipient = new(suite)) - { - byte[] exporterContext = new byte[HpkeTestData.MaxExporterContextLength]; - exporterContext.AsSpan().Fill(0x39); - - foreach (int length in new[] { 0, 1, maximumLength }) - { - byte[] expected = new byte[length]; - expected.AsSpan().Fill(0xE7); - Assert.Equal(expected, recipient.Export(exporterContext, length)); - Assert.Equal(exporterContext, recipient.LastExporterContext); - Assert.Equal(expected, recipient.Export(exporterContext.AsSpan(), length)); - Assert.Equal(exporterContext, recipient.LastExporterContext); - - byte[] destination = new byte[length + 2]; - destination.AsSpan().Fill(0xA5); - recipient.Export(exporterContext, destination.AsSpan(1, length)); - AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - Assert.Equal(exporterContext, recipient.LastExporterContext); - } - - Assert.Equal(9, recipient.ExportCalls); - Assert.Equal(0, recipient.OpenCalls); - Assert.Empty(recipient.Export(Array.Empty(), 0)); - Assert.Empty(recipient.LastExporterContext); - } - } - - [Theory] - [InlineData(HpkeKdf.HKDF_SHA256, 8160)] - [InlineData(HpkeKdf.HKDF_SHA384, 12240)] - [InlineData(HpkeKdf.HKDF_SHA512, 16320)] - [InlineData(HpkeKdf.SHAKE128, 65535)] - [InlineData(HpkeKdf.SHAKE256, 65535)] - public static void Recipient_ExportValidation(HpkeKdf kdf, int maximumLength) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, kdf, HpkeAead.AES_128_GCM); - - using (RecordingHpkeRecipient recipient = new(suite)) - { - AssertExtensions.Throws("exporterContext", () => recipient.Export((byte[])null, 0)); - - foreach (int length in new[] { -1, int.MinValue, maximumLength + 1, int.MaxValue }) - { - AssertExtensions.Throws( - "length", () => recipient.Export(Array.Empty(), length)); - AssertExtensions.Throws( - "length", () => recipient.Export(ReadOnlySpan.Empty, length)); - } - - byte[] destination = new byte[maximumLength + 1]; - destination.AsSpan().Fill(0xA5); - byte[] originalDestination = (byte[])destination.Clone(); - AssertExtensions.Throws( - "destination", () => recipient.Export(ReadOnlySpan.Empty, destination.AsSpan())); - Assert.Equal(originalDestination, destination); - Assert.Equal(0, recipient.ExportCalls); - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public static void Recipient_CoreFailuresPropagate(bool authenticationFailure) - { - HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - using (RecordingHpkeRecipient recipient = new(suite) - { - ThrowOnCoreCall = true, - AuthenticationFailure = authenticationFailure, - }) - { - Type expectedException = authenticationFailure - ? typeof(AuthenticationTagMismatchException) - : typeof(CryptographicException); - byte[] ciphertext = new byte[suite.GetCiphertextLength(1)]; - byte[] plaintext = [0xA5, 0xA5, 0xA5]; - - Assert.Throws(expectedException, () => recipient.Open(ciphertext)); - Assert.Throws(expectedException, () => recipient.Open(ciphertext.AsSpan())); - Assert.Throws(expectedException, () => recipient.Open(ciphertext, plaintext.AsSpan(1, 1))); - Assert.Equal(new byte[] { 0xA5, 0, 0xA5 }, plaintext); - Assert.Throws(() => recipient.Export(Array.Empty(), 1)); - Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, 1)); - Assert.Throws(() => recipient.Export(ReadOnlySpan.Empty, new byte[1].AsSpan())); - Assert.Equal(3, recipient.OpenCalls); - Assert.Equal(3, recipient.ExportCalls); - } - } - - private sealed class RecordingHpkeRecipient : HpkeRecipient - { - internal int OpenCalls { get; private set; } - internal int ExportCalls { get; private set; } - internal int DisposeCalls { get; private set; } - internal byte[] LastCiphertext { get; private set; } = []; - internal byte[] LastAssociatedData { get; private set; } = []; - internal byte[] LastExporterContext { get; private set; } = []; - internal bool ThrowOnCoreCall { get; set; } - internal bool AuthenticationFailure { get; set; } - - internal RecordingHpkeRecipient(HpkeSuite suite) : base(suite) - { - } - - protected override void OpenCore( - ReadOnlySpan ciphertext, - Span plaintext, - ReadOnlySpan associatedData) - { - OpenCalls++; - LastCiphertext = ciphertext.ToArray(); - LastAssociatedData = associatedData.ToArray(); - plaintext.Fill(0xD3); - - if (ThrowOnCoreCall) - { - plaintext.Clear(); - - if (AuthenticationFailure) - { - throw new AuthenticationTagMismatchException("Recipient test authentication failure."); } - - throw new CryptographicException("Recipient test failure."); } } - - protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) - { - ExportCalls++; - LastExporterContext = exporterContext.ToArray(); - destination.Fill(0xE7); - - if (ThrowOnCoreCall) - { - throw new CryptographicException("Recipient test failure."); - } - } - - protected override void Dispose(bool disposing) - { - Assert.True(disposing); - DisposeCalls++; - base.Dispose(disposing); - } - } - - private sealed class RecordingHpkeSender : HpkeSender - { - internal int SealCalls { get; private set; } - internal int ExportCalls { get; private set; } - internal int DisposeCalls { get; private set; } - internal byte[] LastPlaintext { get; private set; } = []; - internal byte[] LastAssociatedData { get; private set; } = []; - internal byte[] LastExporterContext { get; private set; } = []; - internal bool ThrowOnCoreCall { get; set; } - - internal RecordingHpkeSender(HpkeSuite suite) : base(suite) - { - } - - protected override void SealCore( - ReadOnlySpan plaintext, - Span ciphertext, - ReadOnlySpan associatedData) - { - SealCalls++; - LastPlaintext = plaintext.ToArray(); - LastAssociatedData = associatedData.ToArray(); - ciphertext.Fill(0xD3); - - if (ThrowOnCoreCall) - { - throw new CryptographicException("Sender test failure."); - } - } - - protected override void ExportCore(ReadOnlySpan exporterContext, Span destination) - { - ExportCalls++; - LastExporterContext = exporterContext.ToArray(); - destination.Fill(0xE7); - - if (ThrowOnCoreCall) - { - throw new CryptographicException("Sender test failure."); - } - } - - protected override void Dispose(bool disposing) - { - Assert.True(disposing); - DisposeCalls++; - base.Dispose(disposing); - } } } } diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index bc043d04a2ca55..e7b85021a232a7 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -232,6 +232,8 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLKemAlgorithmTests.cs" /> + Date: Sat, 12 Sep 2026 14:21:52 -0400 Subject: [PATCH 37/42] Add shared HPKE implementation tests Replace legacy functional tests with shared corpus-driven coverage and simplify HPKE API documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../src/System/Security/Cryptography/Hpke.cs | 163 +--- .../Cryptography/HpkeImplementationTests.cs | 744 +++++++++++++++++ .../Microsoft.Bcl.Cryptography.Tests.csproj | 2 + .../tests/HpkeTests.cs | 790 +----------------- .../System.Security.Cryptography.Tests.csproj | 2 + 5 files changed, 785 insertions(+), 916 deletions(-) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeImplementationTests.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs index e33cb5ce678db8..4c2e8adf1ef8a6 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -149,12 +149,6 @@ public static Hpke GenerateKey(HpkeSuite suite) /// /// is not supported on the current platform. /// - /// - /// The key must use the format returned by , - /// not the input keying material accepted by . - /// The imported key does not retain a reference to . - /// The caller remains responsible for protecting and clearing the source key bytes. - /// public static Hpke ImportDecapsulationKey(HpkeSuite suite, byte[] source) { ArgumentNullException.ThrowIfNull(source); @@ -185,12 +179,6 @@ public static Hpke ImportDecapsulationKey(HpkeSuite suite, byte[] source) /// /// is not supported on the current platform. /// - /// - /// The key must use the format returned by , - /// not the input keying material accepted by . - /// The imported key does not retain a reference to . - /// The caller remains responsible for protecting and clearing the source key bytes. - /// public static Hpke ImportDecapsulationKey(HpkeSuite suite, ReadOnlySpan source) { ArgumentNullException.ThrowIfNull(suite); @@ -228,12 +216,6 @@ public static Hpke ImportDecapsulationKey(HpkeSuite suite, ReadOnlySpan so /// /// is not supported on the current platform. /// - /// - /// The key must use the format returned by . - /// The imported key can encrypt messages and create sender contexts, but cannot decrypt messages, - /// create recipient contexts, or export a decapsulation key. - /// The imported key does not retain a reference to . - /// public static Hpke ImportEncapsulationKey(HpkeSuite suite, byte[] source) { ArgumentNullException.ThrowIfNull(source); @@ -264,12 +246,6 @@ public static Hpke ImportEncapsulationKey(HpkeSuite suite, byte[] source) /// /// is not supported on the current platform. /// - /// - /// The key must use the format returned by . - /// The imported key can encrypt messages and create sender contexts, but cannot decrypt messages, - /// create recipient contexts, or export a decapsulation key. - /// The imported key does not retain a reference to . - /// public static Hpke ImportEncapsulationKey(HpkeSuite suite, ReadOnlySpan source) { ArgumentNullException.ThrowIfNull(suite); @@ -296,19 +272,6 @@ public static Hpke ImportEncapsulationKey(HpkeSuite suite, ReadOnlySpan so /// /// The object has already been disposed. /// - /// - /// - /// The key is exported in the private-key format defined by the cipher suite's KEM, - /// without a PKCS#8 or other ASN.1 wrapper. For DHKEM with NIST curves, this is the fixed-width, - /// big-endian private scalar. For DHKEM with X25519, this is the raw 32-byte X25519 private key. - /// For ML-KEM and hybrid ML-KEM cipher suites, this is the private seed. - /// - /// - /// The returned key is not the original input keying material supplied to - /// . - /// The caller is responsible for protecting the returned secret bytes and clearing them when no longer needed. - /// - /// public byte[] ExportDecapsulationKey() { ThrowIfDisposed(); @@ -342,11 +305,6 @@ public byte[] ExportDecapsulationKey() /// /// The object has already been disposed. /// - /// - /// The key format is the same as for . - /// On success, the entire destination is filled with the serialized key. - /// The caller is responsible for protecting the secret bytes and clearing the buffer when no longer needed. - /// public void ExportDecapsulationKey(Span destination) { if (destination.Length != Suite.DecapsulationKeySizeInBytes) @@ -369,14 +327,6 @@ public void ExportDecapsulationKey(Span destination) /// /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key. /// - /// - /// The calling method has verified that this instance is not disposed and that - /// is exactly - /// bytes long. - /// Implementations must fill the entire destination using the key format described by - /// and throw - /// if the decapsulation key cannot be exported. - /// protected abstract void ExportDecapsulationKeyCore(Span destination); /// @@ -386,7 +336,7 @@ public void ExportDecapsulationKey(Span destination) /// The encapsulation key. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while exporting the key. + /// An error occurred while exporting the key. /// /// /// The object has already been disposed. @@ -410,7 +360,7 @@ public byte[] ExportEncapsulationKey() /// bytes long. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while exporting the key. + /// An error occurred while exporting the key. /// /// /// The object has already been disposed. @@ -435,27 +385,23 @@ public void ExportEncapsulationKey(Span destination) /// The buffer to receive the encapsulation key. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while exporting the key. + /// An error occurred while exporting the key. /// - /// - /// is exactly - /// bytes long. - /// protected abstract void ExportEncapsulationKeyCore(Span destination); /// - /// Encrypts and authenticates a single message using Base mode. + /// Encrypts and authenticates a single message using Base mode. /// /// /// The message to encrypt. /// /// /// When this method returns, contains a new byte array containing the encapsulated secret to send - /// to the recipient. This parameter is treated as uninitialized. + /// to the recipient. /// /// /// When this method returns, contains a new byte array containing the ciphertext followed by its - /// authentication tag. This parameter is treated as uninitialized. + /// authentication tag. /// /// /// The additional data to authenticate without encrypting. @@ -470,7 +416,7 @@ public void ExportEncapsulationKey(Span destination) /// The ciphertext length would exceed . /// /// - /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// An error occurred during encryption. /// /// /// The object has already been disposed. @@ -496,18 +442,18 @@ public void Seal( } /// - /// Encrypts and authenticates a single message using Base mode. + /// Encrypts and authenticates a single message using Base mode. /// /// /// The message to encrypt. /// /// /// When this method returns, contains a new byte array containing the encapsulated secret to send - /// to the recipient. This parameter is treated as uninitialized. + /// to the recipient. /// /// /// When this method returns, contains a new byte array containing the ciphertext followed by its - /// authentication tag. This parameter is treated as uninitialized. + /// authentication tag. /// /// /// The additional data to authenticate without encrypting, @@ -527,7 +473,7 @@ public void Seal( /// The ciphertext length would exceed . /// /// - /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// An error occurred during encryption. /// /// /// The object has already been disposed. @@ -555,7 +501,7 @@ public void Seal( } /// - /// Encrypts and authenticates a single message into the provided buffers using Base mode. + /// Encrypts and authenticates a single message into the provided buffers using Base mode. /// /// /// The message to encrypt. @@ -596,7 +542,7 @@ public void Seal( /// /// -or- /// - /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// An error occurred during encryption. /// /// /// @@ -643,7 +589,7 @@ public void Seal( } /// - /// When overridden in a derived class, encrypts and authenticates a single message using Base mode. + /// When overridden in a derived class, encrypts and authenticates a single message using Base mode. /// /// /// The message to encrypt. @@ -661,13 +607,8 @@ public void Seal( /// The application context. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred during encryption. + /// An error occurred during encryption. /// - /// - /// The calling method has verified that this instance is not disposed, the output buffers - /// have the exact required lengths for , and - /// satisfies the KDF's length limit. Implementations must fill both output buffers on success. - /// protected abstract void SealCore( ReadOnlySpan plaintext, Span encapsulatedSecret, @@ -676,7 +617,7 @@ protected abstract void SealCore( ReadOnlySpan info); /// - /// Decrypts and authenticates a single HPKE ciphertext using Base mode. + /// Decrypts and authenticates a single HPKE ciphertext using Base mode. /// /// /// The encapsulated secret produced by the sender. @@ -740,7 +681,7 @@ public byte[] Open( } /// - /// Decrypts and authenticates a single HPKE ciphertext using Base mode. + /// Decrypts and authenticates a single HPKE ciphertext using Base mode. /// /// /// The encapsulated secret produced by the sender. @@ -802,7 +743,7 @@ public byte[] Open( } /// - /// Decrypts and authenticates a single HPKE ciphertext into the provided buffer using Base mode. + /// Decrypts and authenticates a single HPKE ciphertext into the provided buffer using Base mode. /// /// /// The encapsulated secret produced by the sender. @@ -883,7 +824,8 @@ public void Open( } /// - /// When overridden in a derived class, decrypts and authenticates a single HPKE ciphertext using Base mode. + /// When overridden in a derived class, decrypts and authenticates a single HPKE ciphertext + /// using Base mode. /// /// /// The encapsulated secret produced by the sender. @@ -907,12 +849,6 @@ public void Open( /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, /// or an error occurred during decryption. /// - /// - /// The calling method has verified that this instance is not disposed, the input and output lengths - /// are valid for , and satisfies the KDF's length limit. - /// Implementations must fill the entire plaintext buffer on success and must not leave - /// unauthenticated plaintext in the buffer when authentication fails. - /// protected abstract void OpenCore( ReadOnlySpan encapsulatedSecret, ReadOnlySpan ciphertext, @@ -921,11 +857,10 @@ protected abstract void OpenCore( ReadOnlySpan info); /// - /// Creates an HPKE sender context using Base mode. + /// Creates an HPKE sender context using Base mode. /// /// /// When this method returns, contains the encapsulated secret to send to the recipient. - /// This parameter is treated as uninitialized. /// /// /// The application context, which must match the value used by the recipient. @@ -937,7 +872,7 @@ protected abstract void OpenCore( /// exceeds the maximum length supported by the cipher suite's KDF. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// An error occurred while creating the sender. /// /// /// Creating a sender is not supported on the current platform. @@ -957,7 +892,8 @@ public HpkeSender CreateSender(out byte[] encapsulatedSecret, ReadOnlySpan } /// - /// Creates an HPKE sender context using Base mode and writes the encapsulated secret into the provided buffer. + /// Creates an HPKE sender context using Base mode and writes the encapsulated secret + /// into the provided buffer. /// /// /// The buffer to receive the encapsulated secret to send to the recipient. @@ -984,7 +920,7 @@ public HpkeSender CreateSender(out byte[] encapsulatedSecret, ReadOnlySpan /// /// -or- /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// An error occurred while creating the sender. /// /// /// @@ -1014,7 +950,7 @@ public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan } /// - /// When overridden in a derived class, creates an HPKE sender context using Base mode. + /// When overridden in a derived class, creates an HPKE sender context using Base mode. /// /// /// The buffer to receive the encapsulated secret. @@ -1026,20 +962,15 @@ public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan /// A new sender context for this key's cipher suite. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// An error occurred while creating the sender. /// /// /// Creating a sender is not supported on the current platform. /// - /// - /// The calling method has verified that this instance is not disposed, the encapsulated secret buffer - /// has the exact required length, and satisfies the KDF's length limit. - /// Implementations must fill the entire buffer and return an initialized sender for . - /// protected abstract HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info); /// - /// Creates an HPKE recipient context using Base mode. + /// Creates an HPKE recipient context using Base mode. /// /// /// The encapsulated secret produced by the sender. @@ -1081,7 +1012,7 @@ public HpkeRecipient CreateRecipient( } /// - /// Creates an HPKE recipient context using Base mode. + /// Creates an HPKE recipient context using Base mode. /// /// /// The encapsulated secret produced by the sender. @@ -1123,7 +1054,7 @@ public HpkeRecipient CreateRecipient(byte[] encapsulatedSecret, byte[]? info = n } /// - /// When overridden in a derived class, creates an HPKE recipient context using Base mode. + /// When overridden in a derived class, creates an HPKE recipient context using Base mode. /// /// /// The encapsulated secret produced by the sender. @@ -1141,11 +1072,6 @@ public HpkeRecipient CreateRecipient(byte[] encapsulatedSecret, byte[]? info = n /// /// Creating a recipient is not supported on the current platform. /// - /// - /// The calling method has verified that this instance is not disposed, the encapsulated secret - /// has the exact required length, and satisfies the KDF's length limit. - /// Implementations must return an initialized recipient for . - /// protected abstract HpkeRecipient CreateRecipientCore( ReadOnlySpan encapsulatedSecret, ReadOnlySpan info); @@ -1161,7 +1087,6 @@ protected abstract HpkeRecipient CreateRecipientCore( /// /// /// When this method returns, contains the encapsulated secret to send to the recipient. - /// This parameter is treated as uninitialized. /// /// /// The application context, which must match the value used by the recipient. @@ -1174,7 +1099,7 @@ protected abstract HpkeRecipient CreateRecipientCore( /// or an input exceeds the maximum length supported by the cipher suite's KDF. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// An error occurred while creating the sender. /// /// /// Creating a PSK sender is not supported on the current platform. @@ -1182,11 +1107,6 @@ protected abstract HpkeRecipient CreateRecipientCore( /// /// The object has already been disposed. /// - /// - /// The sender and recipient must use the same pre-shared key and identifier. - /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy; - /// length validation alone does not guarantee this. A low-entropy password is not a suitable pre-shared key. - /// public HpkeSender CreatePskSender( ReadOnlySpan psk, ReadOnlySpan pskId, @@ -1214,7 +1134,6 @@ public HpkeSender CreatePskSender( /// /// /// When this method returns, contains the encapsulated secret to send to the recipient. - /// This parameter is treated as uninitialized. /// /// /// The application context, which must match the value used by the recipient, @@ -1231,7 +1150,7 @@ public HpkeSender CreatePskSender( /// or an input exceeds the maximum length supported by the cipher suite's KDF. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// An error occurred while creating the sender. /// /// /// Creating a PSK sender is not supported on the current platform. @@ -1239,10 +1158,6 @@ public HpkeSender CreatePskSender( /// /// The object has already been disposed. /// - /// - /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy. - /// The sender and recipient must use the same pre-shared key and identifier. - /// public HpkeSender CreatePskSender( byte[] psk, byte[] pskId, @@ -1284,7 +1199,7 @@ public HpkeSender CreatePskSender( /// /// -or- /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// An error occurred while creating the sender. /// /// /// @@ -1293,10 +1208,6 @@ public HpkeSender CreatePskSender( /// /// The object has already been disposed. /// - /// - /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy. - /// The sender and recipient must use the same pre-shared key and identifier. - /// public HpkeSender CreatePskSender( ReadOnlySpan psk, ReadOnlySpan pskId, @@ -1343,17 +1254,11 @@ public HpkeSender CreatePskSender( /// A new sender context for this key's cipher suite. /// /// - /// The current instance does not contain an encapsulation key, or an error occurred while creating the sender. + /// An error occurred while creating the sender. /// /// /// Creating a PSK sender is not supported on the current platform. /// - /// - /// The calling method has verified that this instance is not disposed, the encapsulated secret buffer - /// has the exact required length, the pre-shared key is at least 32 bytes long, the identifier is nonempty, - /// and all inputs satisfy the KDF's length limits. Implementations must fill the entire buffer - /// and return an initialized sender for . - /// protected abstract HpkeSender CreatePskSenderCore( Span encapsulatedSecret, ReadOnlySpan info, diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeImplementationTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeImplementationTests.cs new file mode 100644 index 00000000000000..26a285dba116da --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeImplementationTests.cs @@ -0,0 +1,744 @@ +// 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 Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(PlatformDetection), + nameof(PlatformDetection.IsNotBrowser), + nameof(PlatformDetection.IsNotWasi), + nameof(PlatformDetection.IsNotNetFramework))] + public static class HpkeImplementationTests + { + public static IEnumerable SupportedVectorNames + { + get + { + foreach (HpkeTestVector vector in HpkeTestData.Vectors) + { + if (Hpke.IsSupported(Suite(vector))) + { + yield return [vector.Name]; + } + } + } + } + + public static IEnumerable BaseVectorNames + { + get + { + foreach (HpkeTestVector vector in HpkeTestData.Vectors) + { + if (!vector.UsePsk && Hpke.IsSupported(Suite(vector))) + { + yield return [vector.Name]; + } + } + } + } + + public static IEnumerable SupportedSuites + { + get + { + foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) + foreach (HpkeKdf kdf in Enum.GetValues(typeof(HpkeKdf))) + foreach (HpkeAead aead in Enum.GetValues(typeof(HpkeAead))) + { + if (Hpke.IsSupported(new HpkeSuite(kem, kdf, aead))) + { + yield return [kem, kdf, aead]; + } + } + } + } + + public static IEnumerable RepresentativeSuiteModes + { + get + { + foreach (HpkeSuite suite in HpkeTestData.Vectors.Select(Suite).Distinct().Where(Hpke.IsSupported)) + foreach (bool usePsk in new[] { false, true }) + { + yield return [suite.KemAlgorithm, suite.KdfAlgorithm, suite.AeadAlgorithm, usePsk]; + } + } + } + + public static IEnumerable InvalidEncapsulatedSecrets + { + get + { + HpkeKem[] kems = + [ + HpkeKem.DHKEM_P256_HKDF_SHA256, + HpkeKem.DHKEM_P384_HKDF_SHA384, + HpkeKem.DHKEM_P521_HKDF_SHA512, + HpkeKem.DHKEM_X25519_HKDF_SHA256, + ]; + + foreach (HpkeKem kem in kems) + { + if (Hpke.IsSupported(new HpkeSuite(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM))) + { + byte[] prefixes = kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 ? [0, 1] : [0, 4]; + + foreach (byte prefix in prefixes) + { + yield return [kem, prefix]; + } + } + } + } + } + + [Theory] + [MemberData(nameof(BaseVectorNames))] + public static void Open_KnownAnswer(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeMessageVector message = vector.Messages[0]; + byte[] enc = vector.EncapsulatedSecret.HexToByteArray(); + byte[] info = vector.Info.HexToByteArray(); + byte[] ciphertext = message.Ciphertext.HexToByteArray(); + byte[] plaintext = message.Plaintext.HexToByteArray(); + byte[] aad = message.AssociatedData.HexToByteArray(); + + using (Hpke key = Hpke.ImportDecapsulationKey(Suite(vector), vector.DecapsulationKey.HexToByteArray())) + { + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData: aad, info: info)); + Assert.Equal(plaintext, key.Open(enc.AsSpan(), ciphertext, associatedData: aad, info: info)); + byte[] destination = GuardedBuffer(plaintext.Length); + key.Open(enc, ciphertext, destination.AsSpan(1, plaintext.Length), aad, info); + AssertGuardedOutput(plaintext, destination); + } + } + + [Theory] + [MemberData(nameof(SupportedVectorNames))] + public static void Recipient_KnownAnswer(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + byte[] enc = vector.EncapsulatedSecret.HexToByteArray(); + + using (Hpke key = Hpke.ImportDecapsulationKey(Suite(vector), vector.DecapsulationKey.HexToByteArray())) + using (HpkeRecipient fromArray = CreateRecipient(key, vector, enc, useSpan: false)) + using (HpkeRecipient fromSpan = CreateRecipient(key, vector, enc, useSpan: true)) + using (HpkeRecipient toDestination = CreateRecipient(key, vector, enc, useSpan: true)) + { + AssertKnownExports(fromArray, vector.Exports); + + foreach (HpkeMessageVector message in vector.Messages) + { + byte[] plaintext = message.Plaintext.HexToByteArray(); + byte[] ciphertext = message.Ciphertext.HexToByteArray(); + byte[] aad = message.AssociatedData.HexToByteArray(); + + Assert.Equal(plaintext, fromArray.Open(ciphertext, aad)); + Assert.Equal(plaintext, fromSpan.Open(ciphertext.AsSpan(), associatedData: aad)); + byte[] destination = GuardedBuffer(plaintext.Length); + toDestination.Open(ciphertext, destination.AsSpan(1, plaintext.Length), aad); + AssertGuardedOutput(plaintext, destination); + } + + AssertKnownExports(fromArray, vector.Exports); + AssertKnownExports(fromSpan, vector.Exports); + AssertKnownExports(toDestination, vector.Exports); + } + } + + [Theory] + [MemberData(nameof(SupportedSuites))] + public static void SingleShot_Roundtrip_Array(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + using (Hpke privateKey = Hpke.GenerateKey(suite)) + using (Hpke publicKey = Hpke.ImportEncapsulationKey(suite, privateKey.ExportEncapsulationKey())) + { + foreach (int length in new[] { 0, 1, 257 }) + { + byte[] plaintext = new byte[length]; + plaintext.AsSpan().Fill(0xA7); + byte[] aad = length == 0 ? [] : "associated data"u8.ToArray(); + byte[] info = new byte[length == 0 ? 0 : 1024]; + info.AsSpan().Fill(0x3C); + + publicKey.Seal(plaintext, out byte[] enc, out byte[] ciphertext, + length == 0 ? null : aad, length == 0 ? null : info); + Assert.Equal(plaintext, privateKey.Open(enc, ciphertext, + associatedData: length == 0 ? null : aad, info: length == 0 ? null : info)); + Assert.Equal(plaintext, privateKey.Open(enc.AsSpan(), ciphertext, + associatedData: aad, info: info)); + byte[] destination = GuardedBuffer(length); + privateKey.Open(enc, ciphertext, destination.AsSpan(1, length), aad, info); + AssertGuardedOutput(plaintext, destination); + + using (HpkeRecipient recipient = privateKey.CreateRecipient(enc, info)) + { + Assert.Equal(plaintext, recipient.Open(ciphertext, aad)); + } + } + } + } + + [Theory] + [MemberData(nameof(SupportedSuites))] + public static void SingleShot_Roundtrip_Span(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + using (Hpke privateKey = Hpke.GenerateKey(suite)) + using (Hpke publicKey = Hpke.ImportEncapsulationKey(suite, privateKey.ExportEncapsulationKey())) + { + foreach (int length in new[] { 0, 1, 257 }) + { + byte[] plaintext = new byte[length]; + plaintext.AsSpan().Fill(0xA7); + byte[] aad = length == 0 ? [] : "associated data"u8.ToArray(); + byte[] info = new byte[length == 0 ? 0 : 1024]; + info.AsSpan().Fill(0x3C); + + publicKey.Seal(plaintext.AsSpan(), out byte[] enc, out byte[] ciphertext, aad, info); + Assert.Equal(plaintext, privateKey.Open(enc, ciphertext, + associatedData: length == 0 ? null : aad, info: length == 0 ? null : info)); + Assert.Equal(plaintext, privateKey.Open(enc.AsSpan(), ciphertext, + associatedData: aad, info: info)); + byte[] destination = GuardedBuffer(length); + privateKey.Open(enc, ciphertext, destination.AsSpan(1, length), aad, info); + AssertGuardedOutput(plaintext, destination); + + using (HpkeRecipient recipient = privateKey.CreateRecipient(enc, info)) + { + Assert.Equal(plaintext, recipient.Open(ciphertext, aad)); + } + } + } + } + + [Theory] + [MemberData(nameof(SupportedSuites))] + public static void SingleShot_Roundtrip_Destination(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + + using (Hpke privateKey = Hpke.GenerateKey(suite)) + using (Hpke publicKey = Hpke.ImportEncapsulationKey(suite, privateKey.ExportEncapsulationKey())) + { + foreach (int length in new[] { 0, 1, 257 }) + { + byte[] plaintext = new byte[length]; + plaintext.AsSpan().Fill(0xA7); + byte[] aad = length == 0 ? [] : "associated data"u8.ToArray(); + byte[] info = new byte[length == 0 ? 0 : 1024]; + info.AsSpan().Fill(0x3C); + byte[] encBuffer = GuardedBuffer(suite.EncapsulatedSecretSizeInBytes); + byte[] ciphertextBuffer = GuardedBuffer(suite.GetCiphertextLength(length)); + + publicKey.Seal(plaintext, encBuffer.AsSpan(1, encBuffer.Length - 2), + ciphertextBuffer.AsSpan(1, ciphertextBuffer.Length - 2), aad, info); + AssertGuards(encBuffer); + AssertGuards(ciphertextBuffer); + byte[] enc = encBuffer.AsSpan(1, encBuffer.Length - 2).ToArray(); + byte[] ciphertext = ciphertextBuffer.AsSpan(1, ciphertextBuffer.Length - 2).ToArray(); + Assert.Equal(plaintext, privateKey.Open(enc, ciphertext, + associatedData: length == 0 ? null : aad, info: length == 0 ? null : info)); + Assert.Equal(plaintext, privateKey.Open(enc.AsSpan(), ciphertext, + associatedData: aad, info: info)); + byte[] destination = GuardedBuffer(length); + privateKey.Open(enc, ciphertext, destination.AsSpan(1, length), aad, info); + AssertGuardedOutput(plaintext, destination); + + using (HpkeRecipient recipient = privateKey.CreateRecipient(enc, info)) + { + Assert.Equal(plaintext, recipient.Open(ciphertext, aad)); + } + } + } + } + + [Theory] + [MemberData(nameof(SupportedVectorNames))] + public static void Contexts_Roundtrip_Array(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = Suite(vector); + byte[] info = vector.Info.HexToByteArray(); + byte[] psk = vector.Psk.HexToByteArray(); + byte[] pskId = vector.PskId.HexToByteArray(); + byte[] enc; + + using (Hpke privateKey = Hpke.ImportDecapsulationKey(suite, vector.DecapsulationKey.HexToByteArray())) + using (Hpke publicKey = Hpke.ImportEncapsulationKey(suite, vector.EncapsulationKey.HexToByteArray())) + using (HpkeSender sender = vector.UsePsk + ? publicKey.CreatePskSender(psk, pskId, out enc, info) + : publicKey.CreateSender(out enc, info)) + using (HpkeRecipient recipient = vector.UsePsk + ? privateKey.CreatePskRecipient(enc, psk, pskId, info) + : privateKey.CreateRecipient(enc, info)) + { + for (int sequence = 0; sequence < vector.Messages.Count; sequence++) + { + HpkeMessageVector message = vector.Messages[sequence]; + byte[] plaintext = message.Plaintext.HexToByteArray(); + byte[] aad = message.AssociatedData.HexToByteArray(); + byte[] ciphertext = sender.Seal(plaintext, aad); + Assert.Equal(plaintext, recipient.Open(ciphertext, aad)); + + if (!vector.UsePsk && sequence == 0) + { + Assert.Equal(plaintext, privateKey.Open(enc, ciphertext, + associatedData: aad, info: info)); + } + else if (!vector.UsePsk && sequence == 1) + { + Assert.Throws(() => + privateKey.Open(enc, ciphertext, associatedData: aad, info: info)); + } + } + + Assert.Equal(sender.Export(Array.Empty(), 32), recipient.Export(Array.Empty(), 32)); + } + } + + [Theory] + [MemberData(nameof(SupportedVectorNames))] + public static void Contexts_Roundtrip_Span(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = Suite(vector); + byte[] info = vector.Info.HexToByteArray(); + byte[] psk = vector.Psk.HexToByteArray(); + byte[] pskId = vector.PskId.HexToByteArray(); + byte[] enc; + + using (Hpke privateKey = Hpke.ImportDecapsulationKey(suite, vector.DecapsulationKey.HexToByteArray())) + using (Hpke publicKey = Hpke.ImportEncapsulationKey(suite, vector.EncapsulationKey.HexToByteArray())) + using (HpkeSender sender = vector.UsePsk + ? publicKey.CreatePskSender(psk.AsSpan(), pskId, out enc, info) + : publicKey.CreateSender(out enc, info)) + using (HpkeRecipient recipient = vector.UsePsk + ? privateKey.CreatePskRecipient(enc.AsSpan(), psk, pskId, info) + : privateKey.CreateRecipient(enc.AsSpan(), info)) + { + for (int sequence = 0; sequence < vector.Messages.Count; sequence++) + { + HpkeMessageVector message = vector.Messages[sequence]; + byte[] plaintext = message.Plaintext.HexToByteArray(); + byte[] aad = message.AssociatedData.HexToByteArray(); + byte[] ciphertext = sender.Seal(plaintext.AsSpan(), associatedData: aad); + Assert.Equal(plaintext, recipient.Open(ciphertext.AsSpan(), associatedData: aad)); + + if (!vector.UsePsk && sequence == 0) + { + Assert.Equal(plaintext, privateKey.Open(enc, ciphertext, + associatedData: aad, info: info)); + } + else if (!vector.UsePsk && sequence == 1) + { + Assert.Throws(() => + privateKey.Open(enc, ciphertext, associatedData: aad, info: info)); + } + } + + Assert.Equal(sender.Export(Array.Empty(), 32), recipient.Export(Array.Empty(), 32)); + } + } + + [Theory] + [MemberData(nameof(SupportedVectorNames))] + public static void Contexts_Roundtrip_Destination(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = Suite(vector); + byte[] info = vector.Info.HexToByteArray(); + byte[] psk = vector.Psk.HexToByteArray(); + byte[] pskId = vector.PskId.HexToByteArray(); + byte[] encBuffer = GuardedBuffer(suite.EncapsulatedSecretSizeInBytes); + + using (Hpke privateKey = Hpke.ImportDecapsulationKey(suite, vector.DecapsulationKey.HexToByteArray())) + using (Hpke publicKey = Hpke.ImportEncapsulationKey(suite, vector.EncapsulationKey.HexToByteArray())) + using (HpkeSender sender = vector.UsePsk + ? publicKey.CreatePskSender(psk, pskId, + encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes), info) + : publicKey.CreateSender(encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes), info)) + { + AssertGuards(encBuffer); + byte[] enc = encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes).ToArray(); + + using (HpkeRecipient recipient = vector.UsePsk + ? privateKey.CreatePskRecipient(enc.AsSpan(), psk, pskId, info) + : privateKey.CreateRecipient(enc.AsSpan(), info)) + { + for (int sequence = 0; sequence < vector.Messages.Count; sequence++) + { + HpkeMessageVector message = vector.Messages[sequence]; + byte[] plaintext = message.Plaintext.HexToByteArray(); + byte[] aad = message.AssociatedData.HexToByteArray(); + byte[] ciphertextBuffer = GuardedBuffer(suite.GetCiphertextLength(plaintext.Length)); + sender.Seal(plaintext, ciphertextBuffer.AsSpan(1, ciphertextBuffer.Length - 2), aad); + AssertGuards(ciphertextBuffer); + byte[] ciphertext = ciphertextBuffer.AsSpan(1, ciphertextBuffer.Length - 2).ToArray(); + byte[] destination = GuardedBuffer(plaintext.Length); + recipient.Open(ciphertext, destination.AsSpan(1, plaintext.Length), aad); + AssertGuardedOutput(plaintext, destination); + + if (!vector.UsePsk && sequence == 0) + { + Assert.Equal(plaintext, privateKey.Open(enc, ciphertext, + associatedData: aad, info: info)); + } + else if (!vector.UsePsk && sequence == 1) + { + Assert.Throws(() => + privateKey.Open(enc, ciphertext, associatedData: aad, info: info)); + } + } + + Assert.Equal(sender.Export(Array.Empty(), 32), recipient.Export(Array.Empty(), 32)); + } + } + } + + [Theory] + [MemberData(nameof(BaseVectorNames))] + public static void Open_AuthenticationFailure(string name) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = Suite(vector); + byte[] plaintext = "plaintext"u8.ToArray(); + byte[] aad = "associated data"u8.ToArray(); + byte[] info = vector.Info.HexToByteArray(); + + using (Hpke key = Hpke.ImportDecapsulationKey(suite, vector.DecapsulationKey.HexToByteArray())) + using (Hpke wrongKey = Hpke.GenerateKey(suite)) + using (HpkeSender unrelated = key.CreateSender(out byte[] differentEnc, info)) + { + key.Seal(plaintext, out byte[] enc, out byte[] ciphertext, aad, info); + + for (int tamper = 0; tamper < 6; tamper++) + { + Hpke recipient = key; + byte[] modifiedEnc = enc; + byte[] modifiedCiphertext = (byte[])ciphertext.Clone(); + byte[] modifiedAad = aad; + byte[] modifiedInfo = info; + + switch (tamper) + { + case 0: + modifiedCiphertext[0] ^= 1; + break; + case 1: + modifiedCiphertext[modifiedCiphertext.Length - 1] ^= 1; + break; + case 2: + modifiedAad = Different(aad); + break; + case 3: + modifiedInfo = Different(info); + break; + case 4: + modifiedEnc = differentEnc; + break; + case 5: + recipient = wrongKey; + break; + } + + Assert.Throws(() => + recipient.Open(modifiedEnc, modifiedCiphertext, + associatedData: modifiedAad, info: modifiedInfo)); + Assert.Throws(() => + recipient.Open(modifiedEnc.AsSpan(), modifiedCiphertext, + associatedData: modifiedAad, info: modifiedInfo)); + byte[] destination = GuardedBuffer(plaintext.Length); + Assert.Throws(() => + recipient.Open(modifiedEnc, modifiedCiphertext, destination.AsSpan(1, plaintext.Length), + modifiedAad, modifiedInfo)); + AssertGuardedOutput(new byte[plaintext.Length], destination); + } + + Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData: aad, info: info)); + } + } + + [Theory] + [MemberData(nameof(RepresentativeSuiteModes))] + public static void Recipient_AuthenticationFailureAndOrdering( + HpkeKem kem, HpkeKdf kdf, HpkeAead aead, bool usePsk) + { + HpkeSuite suite = new(kem, kdf, aead); + byte[] info = "application context"u8.ToArray(); + byte[] aad = "associated data"u8.ToArray(); + byte[] psk = new byte[32]; + byte[] pskId = "identifier"u8.ToArray(); + byte[] first = "first"u8.ToArray(); + byte[] second = "second"u8.ToArray(); + byte[] third = "third"u8.ToArray(); + byte[] enc; + + using (Hpke key = Hpke.GenerateKey(suite)) + using (Hpke wrongKey = Hpke.GenerateKey(suite)) + using (HpkeSender sender = usePsk + ? key.CreatePskSender(psk, pskId, out enc, info) + : key.CreateSender(out enc, info)) + using (HpkeRecipient recipient = usePsk + ? key.CreatePskRecipient(enc, psk, pskId, info) + : key.CreateRecipient(enc, info)) + using (HpkeRecipient badKey = usePsk + ? wrongKey.CreatePskRecipient(enc, psk, pskId, info) + : wrongKey.CreateRecipient(enc, info)) + using (HpkeRecipient badInfo = usePsk + ? key.CreatePskRecipient(enc, psk, pskId, Different(info)) + : key.CreateRecipient(enc, Different(info))) + using (HpkeRecipient wrongMode = usePsk + ? key.CreateRecipient(enc, info) + : key.CreatePskRecipient(enc, psk, pskId, info)) + { + byte[] firstCiphertext = sender.Seal(first, aad); + byte[] secondCiphertext = sender.Seal(second, aad); + byte[] thirdCiphertext = sender.Seal(third, aad); + byte[] export = recipient.Export(Array.Empty(), 32); + + foreach (HpkeRecipient incorrect in new[] { badKey, badInfo, wrongMode }) + { + AssertAuthenticationFailure(incorrect, firstCiphertext, aad); + } + + if (usePsk) + { + using (HpkeRecipient badPsk = key.CreatePskRecipient(enc, Different(psk), pskId, info)) + using (HpkeRecipient badId = key.CreatePskRecipient(enc, psk, Different(pskId), info)) + { + AssertAuthenticationFailure(badPsk, firstCiphertext, aad); + AssertAuthenticationFailure(badId, firstCiphertext, aad); + } + } + + byte[] badTag = (byte[])firstCiphertext.Clone(); + badTag[badTag.Length - 1] ^= 1; + AssertAuthenticationFailure(recipient, Different(firstCiphertext), aad); + AssertAuthenticationFailure(recipient, badTag, aad); + AssertAuthenticationFailure(recipient, firstCiphertext, Different(aad)); + Assert.Equal(export, recipient.Export(Array.Empty(), 32)); + Assert.Equal(first, recipient.Open(firstCiphertext, aad)); + AssertAuthenticationFailure(recipient, firstCiphertext, aad); + AssertAuthenticationFailure(recipient, thirdCiphertext, aad); + Assert.Equal(second, recipient.Open(secondCiphertext.AsSpan(), associatedData: aad)); + byte[] destination = GuardedBuffer(third.Length); + recipient.Open(thirdCiphertext, destination.AsSpan(1, third.Length), aad); + AssertGuardedOutput(third, destination); + } + } + + [Theory] + [MemberData(nameof(RepresentativeSuiteModes))] + public static void Contexts_IndependentLifetime(HpkeKem kem, HpkeKdf kdf, HpkeAead aead, bool usePsk) + { + HpkeSuite suite = new(kem, kdf, aead); + byte[] info = "application context"u8.ToArray(); + byte[] psk = new byte[32]; + psk.AsSpan().Fill(0x3C); + byte[] pskId = "identifier"u8.ToArray(); + byte[] message = "message"u8.ToArray(); + byte[] firstEnc; + byte[] secondEnc; + Hpke key = Hpke.GenerateKey(suite); + HpkeSender first = usePsk + ? key.CreatePskSender(psk, pskId, out firstEnc, info) + : key.CreateSender(out firstEnc, info); + HpkeRecipient firstRecipient = usePsk + ? key.CreatePskRecipient(firstEnc, psk, pskId, info) + : key.CreateRecipient(firstEnc, info); + HpkeSender second = usePsk + ? key.CreatePskSender(psk, pskId, out secondEnc, info) + : key.CreateSender(out secondEnc, info); + + using (HpkeRecipient secondRecipient = usePsk + ? key.CreatePskRecipient(secondEnc, psk, pskId, info) + : key.CreateRecipient(secondEnc, info)) + { + byte[] firstExport = first.Export(Array.Empty(), 32); + byte[] secondExport = second.Export(Array.Empty(), 32); + key.Dispose(); + info.AsSpan().Clear(); + psk.AsSpan().Clear(); + pskId.AsSpan().Clear(); + firstEnc.AsSpan().Clear(); + secondEnc.AsSpan().Clear(); + + Assert.Equal(message, firstRecipient.Open(first.Seal(message))); + Assert.Equal(message, firstRecipient.Open(first.Seal(message))); + Assert.Equal(firstExport, first.Export(Array.Empty(), 32)); + Assert.Equal(firstExport, firstRecipient.Export(Array.Empty(), 32)); + first.Dispose(); + firstRecipient.Dispose(); + + Assert.Equal(message, secondRecipient.Open(second.Seal(message))); + Assert.Equal(secondExport, second.Export(Array.Empty(), 32)); + second.Dispose(); + Assert.Equal(secondExport, secondRecipient.Export(Array.Empty(), 32)); + } + } + + [Theory] + [MemberData(nameof(RepresentativeSuiteModes))] + public static void Contexts_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead, bool usePsk) + { + HpkeSuite suite = new(kem, kdf, aead); + int maximumLength = (int)HpkeTestData.ExportLimits.Single(row => row[0].Equals(kdf))[1]; + byte[] info = "application context"u8.ToArray(); + byte[] psk = new byte[32]; + byte[] pskId = "identifier"u8.ToArray(); + byte[] context = "exporter context"u8.ToArray(); + byte[] enc; + + using (Hpke key = Hpke.GenerateKey(suite)) + using (HpkeSender sender = usePsk + ? key.CreatePskSender(psk, pskId, out enc, info) + : key.CreateSender(out enc, info)) + using (HpkeRecipient recipient = usePsk + ? key.CreatePskRecipient(enc, psk, pskId, info) + : key.CreateRecipient(enc, info)) + { + byte[] reference = sender.Export(context, 32); + + foreach (int length in new[] { 0, 1, 31, 32, 33, 65, maximumLength }) + { + byte[] expected = sender.Export(context, length); + Assert.Equal(length, expected.Length); + Assert.Equal(expected, sender.Export(context.AsSpan(), length)); + Assert.Equal(expected, recipient.Export(context, length)); + Assert.Equal(expected, recipient.Export(context.AsSpan(), length)); + byte[] senderBuffer = GuardedBuffer(length); + byte[] recipientBuffer = GuardedBuffer(length); + sender.Export(context, senderBuffer.AsSpan(1, length)); + recipient.Export(context, recipientBuffer.AsSpan(1, length)); + AssertGuardedOutput(expected, senderBuffer); + AssertGuardedOutput(expected, recipientBuffer); + } + + Assert.NotEqual(reference, sender.Export(Array.Empty(), 32)); + Assert.NotEqual(reference, sender.Export([0], 32)); + Assert.False(reference.AsSpan().SequenceEqual(sender.Export(context, 33).AsSpan(0, 32))); + + foreach (int contextLength in new[] { 234, 235, HpkeTestData.MaxExporterContextLength }) + { + byte[] longContext = new byte[contextLength]; + longContext.AsSpan().Fill(0x39); + Assert.Equal(sender.Export(longContext, 32), recipient.Export(longContext, 32)); + } + + byte[] message = "message"u8.ToArray(); + + for (int i = 0; i < 3; i++) + { + byte[] ciphertext = sender.Seal(message); + Assert.Equal(reference, sender.Export(context, 32)); + Assert.Equal(reference, recipient.Export(context, 32)); + Assert.Equal(message, recipient.Open(ciphertext)); + Assert.Equal(reference, recipient.Export(context, 32)); + } + } + } + + [Theory] + [MemberData(nameof(InvalidEncapsulatedSecrets))] + public static void InvalidEncapsulation_Rejected(HpkeKem kem, byte prefix) + { + HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; + enc[0] = prefix; + byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; + byte[] psk = new byte[32]; + byte[] pskId = [1]; + + using (Hpke key = Hpke.GenerateKey(suite)) + { + Assert.ThrowsAny(() => key.Open(enc, ciphertext)); + Assert.ThrowsAny(() => key.Open(enc.AsSpan(), ciphertext)); + Assert.ThrowsAny(() => key.Open(enc, ciphertext, Span.Empty)); + Assert.ThrowsAny(() => key.CreateRecipient(enc)); + Assert.ThrowsAny(() => key.CreateRecipient(enc.AsSpan())); + Assert.ThrowsAny(() => key.CreatePskRecipient(enc, psk, pskId)); + Assert.ThrowsAny(() => key.CreatePskRecipient(enc.AsSpan(), psk, pskId)); + } + } + + private static HpkeSuite Suite(HpkeTestVector vector) => new(vector.Kem, vector.Kdf, vector.Aead); + + private static HpkeRecipient CreateRecipient(Hpke key, HpkeTestVector vector, byte[] enc, bool useSpan) + { + byte[] info = vector.Info.HexToByteArray(); + + if (vector.UsePsk) + { + byte[] psk = vector.Psk.HexToByteArray(); + byte[] pskId = vector.PskId.HexToByteArray(); + return useSpan + ? key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info) + : key.CreatePskRecipient(enc, psk, pskId, info); + } + + return useSpan ? key.CreateRecipient(enc.AsSpan(), info) : key.CreateRecipient(enc, info); + } + + private static void AssertKnownExports(HpkeRecipient recipient, IReadOnlyList exports) + { + foreach (HpkeExportVector export in exports) + { + byte[] context = export.Context.HexToByteArray(); + byte[] expected = export.ExportedValue.HexToByteArray(); + Assert.Equal(expected, recipient.Export(context, export.Length)); + Assert.Equal(expected, recipient.Export(context.AsSpan(), export.Length)); + byte[] destination = GuardedBuffer(export.Length); + recipient.Export(context, destination.AsSpan(1, export.Length)); + AssertGuardedOutput(expected, destination); + } + } + + private static void AssertAuthenticationFailure(HpkeRecipient recipient, byte[] ciphertext, byte[] aad) + { + Assert.Throws(() => recipient.Open(ciphertext, aad)); + Assert.Throws(() => + recipient.Open(ciphertext.AsSpan(), associatedData: aad)); + int length = ciphertext.Length - recipient.Suite.AeadTagSizeInBytes; + byte[] destination = GuardedBuffer(length); + Assert.Throws(() => + recipient.Open(ciphertext, destination.AsSpan(1, length), aad)); + AssertGuardedOutput(new byte[length], destination); + } + + private static byte[] Different(byte[] input) + { + byte[] result = input.Length == 0 ? [1] : (byte[])input.Clone(); + result[0] ^= 0x80; + return result; + } + + private static byte[] GuardedBuffer(int length) + { + byte[] buffer = new byte[length + 2]; + buffer.AsSpan().Fill(0xA5); + return buffer; + } + + private static void AssertGuardedOutput(ReadOnlySpan expected, byte[] buffer) + { + AssertExtensions.SequenceEqual(expected, buffer.AsSpan(1, buffer.Length - 2)); + AssertGuards(buffer); + } + + private static void AssertGuards(byte[] buffer) + { + Assert.Equal(0xA5, buffer[0]); + Assert.Equal(0xA5, buffer[buffer.Length - 1]); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index 2b111ecab38108..c776c8ae672464 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -129,6 +129,8 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLKemAlgorithmTests.cs" /> + ()) + foreach (HpkeKdf kdf in Enum.GetValues()) + foreach (HpkeAead aead in Enum.GetValues()) { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); + HpkeSuite suite = new(kem, kdf, aead); if (!Hpke.IsSupported(suite)) { @@ -31,790 +32,5 @@ public static void KeyFactories_NotSupported() } } } - - // https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/b1f7cb0cdeab6906c61b3d6574e8bdfdbe1cd3fb/test-vectors.json - [Theory] - [InlineData( - HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM, - "668b37171f1072f3cf12ea8a236a45df23fc13b82af3609ad1e354f6ef817550", - "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325a" + - "c98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4", - "5ad590bb8baa577f8619db35a36311226a896e7342a6d836d8b7bcd2f20b6c7f9076ac232e3ab2523f39513434", - "fa6f037b47fc21826b610172ca9637e82d6e5801eb31cbd3748271affd4ecb06646e0329cbdf3c3cd655b28e82", - "895cabfac50ce6c6eb02ffe6c048bf53b7f7be9a91fc559402cbc5b8dcaeb52b2ccc93e466c28fb55fed7a7fec", - "5e9bc3d236e1911d95e65b576a8a86d478fb827e8bdfe77b741b289890490d4d", - "6cff87658931bda83dc857e6353efe4987a201b849658d9b047aab4cf216e796", - "d8f1ea7942adbba7412c6d431c62d01371ea476b823eb697e1f6e6cae1dab85a")] - [InlineData( - HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, - "a2f6e7c4d9e108e03be268a64fe73e11a320963c85375a30bfc9ec4a214c6a55", - "0404dc39344526dbfa728afba96986d575811b5af199c11f821a0e603a4d191b2554" + - "4a402f25364964b2c129cb417b3c1dab4dfc0854f3084e843f731654392726", - "949f58e87c39b3f55390b6a970de27dfac44aadc2fbc9d623dcde1a08b628c83ad07dbbee6aede7fcfbf955670", - "2b122485c81e76277b6fb7d96d85e1e2f0d41c8b6659dbbd2fad77d4a2318ceb88a350b02f7fdb242af6ee6222", - "24612f7a27e9a8a0ddffcc18e769f5e03c9ebb658071b558058172d81336d151933f3d80846596d99f67994822", - "c9d634be6e873105fc38fae1f86e195a0aa025c5cf1672acd2a358e7e2a84244", - "d51a7dee4bb7da5e8d6271c5d6755967bbade71c4ceddab1acded3e6e5f642d0", - "1a677fc144ec3f0df86cfebd6578a0a1a402beeb6f6c36235006369f1211edfa")] - [InlineData( - HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, - "2ad954bbe39b7122529f7dde780bff626cd97f850d0784a432784e69d86eccaa" + - "de43b6c10a8ffdb94bf943c6da479db137914ec835a7e715e36e45e29b587bab3bf1", - "040138b385ca16bb0d5fa0c0665fbbd7e69e3ee29f63991d3e9b5fa740aab8900aa" + - "eed46ed73a49055758425a0ce36507c54b29cc5b85a5cee6bae0cf1c21f2731ece2" + - "013dc3fb7c8d21654bb161b463962ca19e8c654ff24c94dd2898de12051f1ed0692" + - "237fb02b2f8d1dc1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0", - "170f8beddfe949b75ef9c387e201baf4132fa7374593dfafa90768788b7b2b200aafcc6d80ea4c795a7c5b841a", - "d9ee248e220ca24ac00bbbe7e221a832e4f7fa64c4fbab3945b6f3af0c5ecd5e16815b328be4954a05fd352256", - "142cf1e02d1f58d9285f2af7dcfa44f7c3f2d15c73d460c48c6e0e506a3144bae35284e7e221105b61d24e1c7a", - "05e2e5bd9f0c30832b80a279ff211cc65eceb0d97001524085d609ead60d0412", - "fca69744bb537f5b7a1596dbf34eaa8d84bf2e3ee7f1a155d41bd3624aa92b63", - "f389beaac6fcf6c0d9376e20f97e364f0609a88f1bc76d7328e9104df8477013")] - [InlineData( - HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, - "969bb169aa9c24a501ee9d962e96c310226d427fb6eb3fc579d9882dbc708315", - "1d38fc578d4209ea0ef3ee5f1128ac4876a9549d74dc2d2f46e75942a6188244", - "72da9627fd7eb3a8b7169c6d97419b80adefca751c6b52b39a2e084d35ce3eb4487aadaca5a9c590e0938c48b9", - "bf59c5bfd8b31c3debc4a050388f7a047a24c18559902512d1146177a320616a6b527b194c92cf91d8832db1d5", - "a80cdfe1a370a2db7e664c4acc69948d3a095be78bbfb0160f1aa0313cf0ed440154e913e5f9bc6756d7693982", - "5b6120165c82456080db3c730b886b07129e0aec9b5f7beae9e5bbd103c67f2d", - "30890b81a37b14b818c462ae5b680b4273cdc7a1ce5ca86d30d482fbe4323e7a", - "b0b5c19ae0daf8d005593f5755d6e8cab29bd3c5c8245823586d009d15aa5237")] - public static void Open_KnownAnswer( - HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, - string ciphertextHex, string secondCiphertextHex, string thirdCiphertextHex, - string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] ikm = Convert.FromHexString(ikmHex); - byte[] enc = Convert.FromHexString(encHex); - byte[] ciphertext = Convert.FromHexString(ciphertextHex); - byte[] plaintext = "Beauty is truth, truth beauty"u8.ToArray(); - byte[] associatedData = "Count-0"u8.ToArray(); - byte[] info = "Ode on a Grecian Urn"u8.ToArray(); - - using (Hpke key = Hpke.DeriveKey(suite, ikm)) - { - Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); - Assert.Equal(plaintext, key.Open( - new ReadOnlySpan(enc), ciphertext, new ReadOnlySpan(associatedData), info)); - - byte[] destination = new byte[plaintext.Length]; - key.Open(enc, ciphertext, destination.AsSpan(), associatedData, info); - Assert.Equal(plaintext, destination); - - using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) - { - AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); - Assert.Equal(plaintext, recipient.Open( - new ReadOnlySpan(Convert.FromHexString(secondCiphertextHex)), "Count-1"u8)); - recipient.Open(Convert.FromHexString(thirdCiphertextHex), destination.AsSpan(), "Count-2"u8); - Assert.Equal(plaintext, destination); - AssertRecipientExports(recipient, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - } - } - } - - public static IEnumerable OpenSuiteData() - { - HpkeKem[] kems = - [ - HpkeKem.DHKEM_P256_HKDF_SHA256, - HpkeKem.DHKEM_P384_HKDF_SHA384, - HpkeKem.DHKEM_P521_HKDF_SHA512, - HpkeKem.DHKEM_X25519_HKDF_SHA256, - ]; - - foreach (HpkeKem kem in kems) - { - foreach (HpkeKdf kdf in Enum.GetValues()) - { - foreach (HpkeAead aead in Enum.GetValues()) - { - yield return new object[] { kem, kdf, aead }; - } - } - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void Open_Roundtrip(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - using (Hpke key = Hpke.GenerateKey(suite)) - { - foreach (int length in new[] { 0, 1, 257 }) - { - byte[] plaintext = new byte[length]; - plaintext.AsSpan().Fill(0xA7); - byte[] associatedData = length == 0 ? [] : "associated data"u8.ToArray(); - byte[] info = new byte[length == 0 ? 0 : 1024]; - info.AsSpan().Fill(0x3C); - - key.Seal(plaintext, out byte[] enc, out byte[] ciphertext, associatedData, info); - byte[] originalEnc = (byte[])enc.Clone(); - byte[] originalCiphertext = (byte[])ciphertext.Clone(); - - Assert.Equal(plaintext, key.Open( - enc, ciphertext, length == 0 ? null : associatedData, length == 0 ? null : info)); - Assert.Equal(plaintext, key.Open( - new ReadOnlySpan(enc), ciphertext, new ReadOnlySpan(associatedData), info)); - - byte[] destination = new byte[length + 2]; - destination.AsSpan().Fill(0xA5); - key.Open(enc, ciphertext, destination.AsSpan(1, length), associatedData, info); - AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - - Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); - Assert.Equal(originalEnc, enc); - Assert.Equal(originalCiphertext, ciphertext); - } - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void Open_AuthenticationFailure(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - using (Hpke key = Hpke.GenerateKey(suite)) - using (Hpke wrongKey = Hpke.GenerateKey(suite)) - { - byte[] plaintext = "plaintext"u8.ToArray(); - byte[] associatedData = "associated data"u8.ToArray(); - byte[] info = "application context"u8.ToArray(); - key.Seal(plaintext, out byte[] enc, out byte[] ciphertext, associatedData, info); - - for (int tamper = 0; tamper < 6; tamper++) - { - Hpke recipient = key; - byte[] modifiedEnc = (byte[])enc.Clone(); - byte[] modifiedCiphertext = (byte[])ciphertext.Clone(); - byte[] modifiedAssociatedData = (byte[])associatedData.Clone(); - byte[] modifiedInfo = (byte[])info.Clone(); - - switch (tamper) - { - case 0: - modifiedCiphertext[0] ^= 1; - break; - case 1: - modifiedCiphertext[^1] ^= 1; - break; - case 2: - modifiedAssociatedData[0] ^= 1; - break; - case 3: - modifiedInfo[0] ^= 1; - break; - case 4: - modifiedEnc = wrongKey.ExportEncapsulationKey(); - break; - case 5: - recipient = wrongKey; - break; - } - - Assert.Throws(() => recipient.Open( - modifiedEnc, modifiedCiphertext, modifiedAssociatedData, modifiedInfo)); - Assert.Throws(() => recipient.Open( - new ReadOnlySpan(modifiedEnc), modifiedCiphertext, - new ReadOnlySpan(modifiedAssociatedData), modifiedInfo)); - - byte[] destination = new byte[plaintext.Length + 2]; - destination.AsSpan().Fill(0xA5); - Assert.Throws(() => recipient.Open( - modifiedEnc, modifiedCiphertext, destination.AsSpan(1, plaintext.Length), - modifiedAssociatedData, modifiedInfo)); - AssertExtensions.SequenceEqual( - new byte[plaintext.Length].AsSpan(), destination.AsSpan(1, plaintext.Length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - } - - Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, 0)] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256, 4)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 0)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384, 4)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512, 0)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512, 4)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 0)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256, 1)] - public static void Open_InvalidEncapsulatedSecret(HpkeKem kem, byte firstByte) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - using (Hpke key = Hpke.GenerateKey(suite)) - { - byte[] enc = new byte[suite.EncapsulatedSecretSizeInBytes]; - enc[0] = firstByte; - byte[] ciphertext = new byte[suite.AeadTagSizeInBytes]; - - Assert.ThrowsAny(() => key.Open(enc, ciphertext)); - Assert.ThrowsAny(() => key.Open(enc.AsSpan(), ciphertext)); - Assert.ThrowsAny(() => key.Open(enc, ciphertext, Span.Empty)); - Assert.ThrowsAny(() => key.CreateRecipient(enc)); - Assert.ThrowsAny(() => key.CreateRecipient(enc.AsSpan())); - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void CreateContexts_SealAndOpen(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - using (Hpke key = Hpke.GenerateKey(suite)) - { - byte[] info = new byte[1024]; - info.AsSpan().Fill(0x3C); - byte[] associatedData = "associated data"u8.ToArray(); - - foreach (int length in new[] { 0, 1, 257 }) - { - byte[] plaintext = new byte[length]; - plaintext.AsSpan().Fill(0xA7); - int ciphertextLength = suite.GetCiphertextLength(length); - - using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) - using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) - { - Assert.Same(suite, sender.Suite); - Assert.Same(suite, recipient.Suite); - Assert.Equal(suite.EncapsulatedSecretSizeInBytes, enc.Length); - - byte[] ciphertext = sender.Seal(plaintext, associatedData); - Assert.Equal(plaintext, key.Open(enc, ciphertext, associatedData, info)); - Assert.Equal(plaintext, recipient.Open(ciphertext, associatedData)); - - byte[] nextCiphertext = sender.Seal( - new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData)); - Assert.Equal(ciphertextLength, nextCiphertext.Length); - Assert.NotEqual(ciphertext, nextCiphertext); - Assert.Throws( - () => key.Open(enc, nextCiphertext, associatedData, info)); - Assert.Equal(plaintext, recipient.Open( - new ReadOnlySpan(nextCiphertext), new ReadOnlySpan(associatedData))); - } - - byte[] encBuffer = new byte[suite.EncapsulatedSecretSizeInBytes + 2]; - encBuffer.AsSpan().Fill(0xA5); - - using (HpkeSender sender = key.CreateSender( - encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes), info)) - { - Assert.Same(suite, sender.Suite); - Assert.Equal(0xA5, encBuffer[0]); - Assert.Equal(0xA5, encBuffer[^1]); - byte[] enc = encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes).ToArray(); - byte[] ciphertextBuffer = new byte[ciphertextLength + 2]; - ciphertextBuffer.AsSpan().Fill(0xA5); - sender.Seal(plaintext, ciphertextBuffer.AsSpan(1, ciphertextLength), associatedData); - Assert.Equal(0xA5, ciphertextBuffer[0]); - Assert.Equal(0xA5, ciphertextBuffer[^1]); - Assert.Equal(plaintext, key.Open( - enc, ciphertextBuffer.AsSpan(1, ciphertextLength), new ReadOnlySpan(associatedData), info)); - - using (HpkeRecipient recipient = key.CreateRecipient(enc.AsSpan(), info)) - { - Assert.Same(suite, recipient.Suite); - byte[] destination = new byte[length + 2]; - destination.AsSpan().Fill(0xA5); - recipient.Open(ciphertextBuffer.AsSpan(1, ciphertextLength), destination.AsSpan(1, length), associatedData); - AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - } - } - } - } - } - - [Theory] - [InlineData(HpkeKem.DHKEM_P256_HKDF_SHA256)] - [InlineData(HpkeKem.DHKEM_P384_HKDF_SHA384)] - [InlineData(HpkeKem.DHKEM_P521_HKDF_SHA512)] - [InlineData(HpkeKem.DHKEM_X25519_HKDF_SHA256)] - public static void CreateContexts_IndependentLifetime(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] ikm = new byte[suite.DecapsulationKeySizeInBytes]; - - using (Hpke key = Hpke.DeriveKey(suite, ikm)) - using (Hpke peer = Hpke.DeriveKey(suite, ikm)) - using (HpkeSender first = key.CreateSender(out byte[] firstEnc)) - using (HpkeSender second = key.CreateSender(out byte[] secondEnc)) - using (HpkeRecipient firstRecipient = key.CreateRecipient(firstEnc)) - using (HpkeRecipient secondRecipient = key.CreateRecipient(secondEnc.AsSpan())) - { - key.Dispose(); - Assert.NotEqual(firstEnc, secondEnc); - byte[] plaintext = "message"u8.ToArray(); - byte[] firstCiphertext = first.Seal(plaintext); - Assert.Equal(plaintext, peer.Open(firstEnc, firstCiphertext)); - Assert.Equal(plaintext, firstRecipient.Open(firstCiphertext)); - - first.Dispose(); - firstRecipient.Dispose(); - byte[] secondCiphertext = second.Seal(plaintext); - Assert.Equal(plaintext, peer.Open(secondEnc, secondCiphertext)); - Assert.Equal(plaintext, secondRecipient.Open(secondCiphertext)); - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void Recipient_AuthenticationFailureAndOrdering(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] info = "application context"u8.ToArray(); - byte[] associatedData = "associated data"u8.ToArray(); - byte[] first = "first"u8.ToArray(); - byte[] second = "second"u8.ToArray(); - byte[] third = "third"u8.ToArray(); - - using (Hpke key = Hpke.GenerateKey(suite)) - using (Hpke wrongKey = Hpke.GenerateKey(suite)) - using (HpkeSender sender = key.CreateSender(out byte[] enc, info)) - using (HpkeRecipient recipient = key.CreateRecipient(enc, info)) - { - byte[] firstCiphertext = sender.Seal(first, associatedData); - byte[] secondCiphertext = sender.Seal(second, associatedData); - byte[] thirdCiphertext = sender.Seal(third, associatedData); - - using (HpkeRecipient wrongKeyRecipient = wrongKey.CreateRecipient(enc, info)) - using (HpkeRecipient wrongInfoRecipient = key.CreateRecipient(enc, "different context"u8.ToArray())) - { - Assert.Throws(() => wrongKeyRecipient.Open(firstCiphertext, associatedData)); - Assert.Throws(() => wrongInfoRecipient.Open(firstCiphertext, associatedData)); - } - - for (int tamper = 0; tamper < 3; tamper++) - { - byte[] modifiedCiphertext = (byte[])firstCiphertext.Clone(); - byte[] modifiedAssociatedData = (byte[])associatedData.Clone(); - - switch (tamper) - { - case 0: - modifiedCiphertext[0] ^= 1; - break; - case 1: - modifiedCiphertext[^1] ^= 1; - break; - case 2: - modifiedAssociatedData[0] ^= 1; - break; - } - - Assert.Throws(() => recipient.Open(modifiedCiphertext, modifiedAssociatedData)); - Assert.Throws(() => recipient.Open( - new ReadOnlySpan(modifiedCiphertext), new ReadOnlySpan(modifiedAssociatedData))); - - byte[] destination = new byte[first.Length + 2]; - destination.AsSpan().Fill(0xA5); - Assert.Throws(() => recipient.Open( - modifiedCiphertext, destination.AsSpan(1, first.Length), modifiedAssociatedData)); - AssertExtensions.SequenceEqual(new byte[first.Length].AsSpan(), destination.AsSpan(1, first.Length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - } - - Assert.Equal(first, recipient.Open(firstCiphertext, associatedData)); - Assert.Throws(() => recipient.Open(firstCiphertext, associatedData)); - Assert.Throws(() => recipient.Open(thirdCiphertext, associatedData)); - Assert.Equal(second, recipient.Open(new ReadOnlySpan(secondCiphertext), new ReadOnlySpan(associatedData))); - byte[] thirdDestination = new byte[third.Length]; - recipient.Open(thirdCiphertext, thirdDestination.AsSpan(), associatedData); - Assert.Equal(third, thirdDestination); - } - } - - // https://github.com/cfrg/draft-irtf-cfrg-hpke/blob/b1f7cb0cdeab6906c61b3d6574e8bdfdbe1cd3fb/test-vectors.json - [Theory] - [InlineData( - HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM, - "d42ef874c1913d9568c9405407c805baddaffd0898a00f1e84e154fa787b2429", - "04305d35563527bce037773d79a13deabed0e8e7cde61eecee403496959e89e4d0" + - "ca701726696d1485137ccb5341b3c1c7aaee90a4a02449725e744b1193b53b5f", - "90c4deb5b75318530194e4bb62f890b019b1397bbf9d0d6eb918890e1fb2be1ac2603193b60a49c2126b75d0eb", - "9e223384a3620f4a75b5a52f546b7262d8826dea18db5a365feb8b997180b22d72dc1287f7089a1073a7102c27", - "a115a59bf4dd8dc49332d6a0093af8efca1bcbfd3627d850173f5c4a55d0c185", - "4517eaede0669b16aac7c92d5762dd459c301fa10e02237cd5aeb9be969430c4", - "164e02144d44b607a7722e58b0f4156e67c0c2874d74cf71da6ca48a4cbdc5e0")] - [InlineData( - HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, - "509212d2ac43d399abd9050ae3c41c030b82623da0494c0d9f8f26ac56b7e188", - "048739ebbaea3156cbd5e39b4ef41ee7e3b52c8cb4958d087112b17b778897152c" + - "7e99307095b1cee54b807077f6f5092970a27fbb57ce2835263132c75e52e7e0", - "351d83aa6f2ba77c4b9b89aa22fcb18aff3f792bb04e999de9f76f03f99e92c8d9203605cc0dcbb5eb08a9db6b", - "e9deb7896d9414ea4d3e01763e425b5bce3b43874d9121f33441f601a8f7faafb0687512f8782f23ea7aa25b4d", - "850caf7336dd83d41fdee7cb133c7c12b62bf7111d3c5d3d60b20128484adada", - "50121f10b5674e3dc46eed39616ff502ef0d6d7f356783808887a867f6a717c6", - "32b9b0b8315cfc2415852b21e9353e79c233233f400def9623404e21657bdab5")] - [InlineData( - HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeKdf.HKDF_SHA512, HpkeAead.AES_256_GCM, - "a2a2458705e278e574f835effecd18232f8a4c459e7550a09d44348ae5d3b1ea" + - "9d95c51995e657ad6f7cae659f5e186126a471c017f8f5e41da9eba74d4e0473e179", - "040085eff0835cc84351f32471d32aa453cdc1f6418eaaecf1c2824210eb1d48d076" + - "8b368110fab21407c324b8bb4bec63f042cfa4d0868d19b760eb4beba1bff793b3" + - "0036d2c614d55730bd2a40c718f9466faf4d5f8170d22b6df98dfe0c067d02b349" + - "ae4a142e0c03418f0a1479ff78a3db07ae2c2e89e5840f712c174ba2118e90fdcb", - "de69e9d943a5d0b70be3359a19f317bd9aca4a2ebb4332a39bcdfc97d5fe62f3a77702f4822c3be531aa7843a1", - "77a16162831f90de350fea9152cfc685ecfa10acb4f7994f41aed43fa5431f2382d078ec88baec53943984553e", - "62691f0f971e34de38370bff24deb5a7d40ab628093d304be60946afcdb3a936", - "76083c6d1b6809da088584674327b39488eaf665f0731151128452e04ce81bff", - "0c7cfc0976e25ae7680cf909ae2de1859cd9b679610a14bec40d69b91785b2f6")] - [InlineData( - HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA512, HpkeAead.ChaCha20Poly1305, - "92c0e581f1b0ad231dd7346d69071afa23eb4dacdf0b868b644a20bd5121dc07", - "bc441a64a700843a8efd5cd574c20e9909c3a2ff7d35e260f9328cbb8e555d56", - "65a46e483d921343f20cba85da69976b2e0e52f450db7919f7796604977d6708d884a40d5e4fd5b820211264aa", - "02019423af9256981bc0a8a7675494efee2244faa2be5b572d9470e451ea3f831e2c08cd47bfc78d6d1f11cfb1", - "722aa34bd26f69aa1763f46d7eae6cf461ce74b6952483f3ea7d490c88882982", - "ea0c03bea28f6a22f5c93c52a999fdbd386572920a2838304e987d6f930d5fa4", - "3a3980d8a63287c12db540669ded019a0643e236e25896f2f3197edda044b3ce")] - public static void Psk_KnownAnswer( - HpkeKem kem, HpkeKdf kdf, HpkeAead aead, string ikmHex, string encHex, - string firstCiphertextHex, string secondCiphertextHex, - string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] ikm = Convert.FromHexString(ikmHex); - byte[] psk = Convert.FromHexString("0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"); - byte[] pskId = "Ennyn Durin aran Moria"u8.ToArray(); - byte[] info = "Ode on a Grecian Urn"u8.ToArray(); - byte[] enc = Convert.FromHexString(encHex); - byte[] plaintext = "Beauty is truth, truth beauty"u8.ToArray(); - - using (Hpke key = Hpke.DeriveKey(suite, ikm)) - using (HpkeRecipient fromArray = key.CreatePskRecipient(enc, psk, pskId, info)) - using (HpkeRecipient fromSpan = key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info)) - { - AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - byte[] firstCiphertext = Convert.FromHexString(firstCiphertextHex); - byte[] secondCiphertext = Convert.FromHexString(secondCiphertextHex); - Assert.Equal(plaintext, fromArray.Open(firstCiphertext, "Count-0"u8.ToArray())); - Assert.Equal(plaintext, fromArray.Open(new ReadOnlySpan(secondCiphertext), "Count-1"u8)); - byte[] destination = new byte[plaintext.Length]; - fromSpan.Open(firstCiphertext, destination.AsSpan(), "Count-0"u8); - Assert.Equal(plaintext, destination); - Assert.Equal(plaintext, fromSpan.Open(secondCiphertext, "Count-1"u8.ToArray())); - AssertRecipientExports(fromArray, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - AssertRecipientExports(fromSpan, emptyContextExportHex, zeroContextExportHex, testContextExportHex); - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void Psk_RoundtripAndLifetime(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - for (int overload = 0; overload < 3; overload++) - { - byte[] psk = new byte[overload == 0 ? 32 : overload == 1 ? 33 : 64]; - psk.AsSpan().Fill(0x3C); - byte[] originalPsk = (byte[])psk.Clone(); - byte[] pskId = "psk identifier"u8.ToArray(); - byte[] originalPskId = (byte[])pskId.Clone(); - byte[] info = overload == 0 ? null : new byte[1024]; - - using (Hpke key = Hpke.GenerateKey(suite)) - { - HpkeSender sender; - byte[] enc; - - if (overload == 0) - { - sender = key.CreatePskSender(psk, pskId, out enc, info); - } - else if (overload == 1) - { - sender = key.CreatePskSender(psk.AsSpan(), pskId, out enc, info); - } - else - { - byte[] buffer = new byte[suite.EncapsulatedSecretSizeInBytes + 2]; - buffer.AsSpan().Fill(0xA5); - sender = key.CreatePskSender(psk, pskId, buffer.AsSpan(1, buffer.Length - 2), info); - Assert.Equal(0xA5, buffer[0]); - Assert.Equal(0xA5, buffer[^1]); - enc = buffer.AsSpan(1, buffer.Length - 2).ToArray(); - } - - using (sender) - using (HpkeRecipient recipient = overload == 1 - ? key.CreatePskRecipient(enc.AsSpan(), psk, pskId, info) - : key.CreatePskRecipient(enc, psk, pskId, info)) - { - Assert.Same(suite, sender.Suite); - Assert.Same(suite, recipient.Suite); - Assert.Equal(originalPsk, psk); - Assert.Equal(originalPskId, pskId); - key.Dispose(); - psk.AsSpan().Clear(); - pskId.AsSpan().Clear(); - enc.AsSpan().Clear(); - info?.AsSpan().Clear(); - - foreach (int length in new[] { 0, 1, 257 }) - { - byte[] plaintext = new byte[length]; - plaintext.AsSpan().Fill(0xA7); - byte[] aad = length == 0 ? [] : "associated data"u8.ToArray(); - byte[] ciphertext = sender.Seal(plaintext, aad); - byte[] destination = new byte[length + 2]; - destination.AsSpan().Fill(0xA5); - recipient.Open(ciphertext, destination.AsSpan(1, length), aad); - AssertExtensions.SequenceEqual(plaintext.AsSpan(), destination.AsSpan(1, length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - } - } - } - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void Psk_AuthenticationAndModeSeparation(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - byte[] psk = new byte[32]; - byte[] differentPsk = new byte[32]; - differentPsk[0] = 1; - byte[] pskId = "identifier"u8.ToArray(); - byte[] info = "info"u8.ToArray(); - byte[] plaintext = "plaintext"u8.ToArray(); - byte[] aad = "associated data"u8.ToArray(); - - using (Hpke key = Hpke.GenerateKey(suite)) - using (Hpke wrongKey = Hpke.GenerateKey(suite)) - using (HpkeSender sender = key.CreatePskSender(psk, pskId, out byte[] enc, info)) - using (HpkeRecipient recipient = key.CreatePskRecipient(enc, psk, pskId, info)) - using (HpkeRecipient badPsk = key.CreatePskRecipient(enc, differentPsk, pskId, info)) - using (HpkeRecipient badId = key.CreatePskRecipient(enc, psk, "different identifier"u8.ToArray(), info)) - using (HpkeRecipient badInfo = key.CreatePskRecipient(enc, psk, pskId, "different info"u8.ToArray())) - using (HpkeRecipient badKey = wrongKey.CreatePskRecipient(enc, psk, pskId, info)) - using (HpkeRecipient baseRecipient = key.CreateRecipient(enc, info)) - using (HpkeSender baseSender = key.CreateSender(out byte[] baseEnc, info)) - using (HpkeRecipient pskRecipientForBase = key.CreatePskRecipient(baseEnc, psk, pskId, info)) - { - byte[] ciphertext = sender.Seal(plaintext, aad); - foreach (HpkeRecipient incorrect in new[] { badPsk, badId, badInfo, badKey, baseRecipient }) - { - Assert.Throws(() => incorrect.Open(ciphertext, aad)); - } - - byte[] baseCiphertext = baseSender.Seal(plaintext, aad); - Assert.Throws(() => pskRecipientForBase.Open(baseCiphertext, aad)); - byte[] tamperedCiphertext = (byte[])ciphertext.Clone(); - tamperedCiphertext[^1] ^= 1; - byte[] destination = new byte[plaintext.Length]; - destination.AsSpan().Fill(0xA5); - Assert.Throws( - () => recipient.Open(tamperedCiphertext, destination.AsSpan(), aad)); - Assert.Equal(new byte[destination.Length], destination); - Assert.Equal(plaintext, recipient.Open(ciphertext, aad)); - Assert.Equal(plaintext, recipient.Open(sender.Seal(plaintext, aad), aad)); - } - } - - private static void AssertRecipientExports( - HpkeRecipient recipient, string emptyContextExportHex, string zeroContextExportHex, string testContextExportHex) - { - byte[][] contexts = [[], [0], "TestContext"u8.ToArray()]; - string[] expectedHex = [emptyContextExportHex, zeroContextExportHex, testContextExportHex]; - - for (int i = 0; i < contexts.Length; i++) - { - byte[] expected = Convert.FromHexString(expectedHex[i]); - Assert.Equal(expected, recipient.Export(contexts[i], expected.Length)); - Assert.Equal(expected, recipient.Export(contexts[i].AsSpan(), expected.Length)); - byte[] destination = new byte[expected.Length + 2]; - destination.AsSpan().Fill(0xA5); - recipient.Export(contexts[i], destination.AsSpan(1, expected.Length)); - AssertExtensions.SequenceEqual(expected.AsSpan(), destination.AsSpan(1, expected.Length)); - Assert.Equal(0xA5, destination[0]); - Assert.Equal(0xA5, destination[^1]); - } - } - - [Theory] - [MemberData(nameof(OpenSuiteData))] - public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) - { - HpkeSuite suite = new(kem, kdf, aead); - - if (!Hpke.IsSupported(suite)) - { - Assert.Throws(() => Hpke.GenerateKey(suite)); - return; - } - - int maximumLength = kdf switch - { - HpkeKdf.HKDF_SHA256 => 8160, - HpkeKdf.HKDF_SHA384 => 12240, - HpkeKdf.HKDF_SHA512 => 16320, - HpkeKdf.SHAKE128 or HpkeKdf.SHAKE256 => 65535, - _ => throw new InvalidOperationException(), - }; - - foreach (bool usePsk in new[] { false, true }) - { - using (Hpke key = Hpke.GenerateKey(suite)) - { - byte[] psk = new byte[32]; - byte[] pskId = "identifier"u8.ToArray(); - byte[] info = "application context"u8.ToArray(); - byte[] enc; - using (HpkeSender sender = usePsk - ? key.CreatePskSender(psk, pskId, out enc, info) - : key.CreateSender(out enc, info)) - using (HpkeRecipient recipient = usePsk - ? key.CreatePskRecipient(enc, psk, pskId, info) - : key.CreateRecipient(enc, info)) - { - byte[] context = "exporter context"u8.ToArray(); - byte[] originalContext = (byte[])context.Clone(); - byte[] referenceExport = sender.Export(context, 32); - Assert.Equal(referenceExport, recipient.Export(context, 32)); - - key.Dispose(); - psk.AsSpan().Clear(); - pskId.AsSpan().Clear(); - info.AsSpan().Clear(); - - foreach (int length in new[] { 0, 1, 31, 32, 33, 65, maximumLength }) - { - byte[] expected = sender.Export(context, length); - Assert.Equal(length, expected.Length); - Assert.Equal(expected, sender.Export(context.AsSpan(), length)); - Assert.Equal(expected, recipient.Export(context, length)); - Assert.Equal(expected, recipient.Export(context.AsSpan(), length)); - - byte[] senderBuffer = new byte[length + 2]; - byte[] recipientBuffer = new byte[length + 2]; - senderBuffer.AsSpan().Fill(0xA5); - recipientBuffer.AsSpan().Fill(0xA5); - sender.Export(context, senderBuffer.AsSpan(1, length)); - recipient.Export(context, recipientBuffer.AsSpan(1, length)); - AssertExtensions.SequenceEqual(expected.AsSpan(), senderBuffer.AsSpan(1, length)); - Assert.Equal(senderBuffer, recipientBuffer); - Assert.Equal(0xA5, senderBuffer[0]); - Assert.Equal(0xA5, senderBuffer[^1]); - expected.AsSpan().Clear(); - } - - Assert.Equal(originalContext, context); - Assert.Equal(referenceExport, sender.Export(context, 32)); - Assert.NotEqual(referenceExport, sender.Export(Array.Empty(), 32)); - Assert.NotEqual(referenceExport, sender.Export(new byte[] { 0 }, 32)); - Assert.False(referenceExport.AsSpan().SequenceEqual(sender.Export(context, 33).AsSpan(0, 32))); - // HKDF export framing adds 22 bytes; the first two cases straddle the 256-byte stack limit. - foreach (int contextLength in new[] - { - 234, 235, HpkeTestData.MaxExporterContextLength - }) - { - byte[] longContext = new byte[contextLength]; - longContext.AsSpan().Fill(0x39); - Assert.Equal(sender.Export(longContext, 32), recipient.Export(longContext, 32)); - } - - byte[] message = "message"u8.ToArray(); - for (int i = 0; i < 3; i++) - { - byte[] ciphertext = sender.Seal(message); - Assert.Equal(referenceExport, sender.Export(context, 32)); - Assert.Equal(referenceExport, recipient.Export(context, 32)); - Assert.Equal(message, recipient.Open(ciphertext)); - Assert.Equal(referenceExport, recipient.Export(context, 32)); - } - - sender.Dispose(); - Assert.Equal(referenceExport, recipient.Export(context, 32)); - } - } - } - } } } diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index e7b85021a232a7..76c648e1bbcc9b 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -232,6 +232,8 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLKemAlgorithmTests.cs" /> + Date: Sat, 12 Sep 2026 14:26:13 -0400 Subject: [PATCH 38/42] Simplify HPKE recipient documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Security/Cryptography/HpkeRecipient.cs | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs index 90ead47bf7b179..7acd136e70e218 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs @@ -109,6 +109,9 @@ public byte[] Open(ReadOnlySpan ciphertext, ReadOnlySpan associatedD /// /// The object has already been disposed. /// + /// + /// Messages must be supplied in the same order in which the corresponding sender context encrypted them. + /// public byte[] Open(byte[] ciphertext, byte[]? associatedData = null) { ArgumentNullException.ThrowIfNull(ciphertext); @@ -152,6 +155,9 @@ public byte[] Open(byte[] ciphertext, byte[]? associatedData = null) /// /// The object has already been disposed. /// + /// + /// Messages must be supplied in the same order in which the corresponding sender context encrypted them. + /// public void Open( ReadOnlySpan ciphertext, Span plaintext, @@ -193,13 +199,6 @@ public void Open( /// /// The recipient's message limit has been reached, or an error occurred during decryption. /// - /// - /// The calling method has verified that this instance is not disposed, the ciphertext contains - /// enough bytes for the authentication tag, and the plaintext buffer has the exact required length. - /// Implementations must maintain the recipient's message sequence, reject decryption when the message - /// limit is reached, and fill the entire plaintext buffer on success. Authentication failures must not - /// advance the message sequence or leave unauthenticated plaintext in the buffer. - /// protected abstract void OpenCore( ReadOnlySpan ciphertext, Span plaintext, @@ -226,11 +225,6 @@ protected abstract void OpenCore( /// /// The object has already been disposed. /// - /// - /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. - /// Exporting a secret does not advance the recipient's message sequence. - /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. - /// public byte[] Export(ReadOnlySpan exporterContext, int length) { ArgumentOutOfRangeException.ThrowIfNegative(length); @@ -282,10 +276,6 @@ public byte[] Export(ReadOnlySpan exporterContext, int length) /// /// The object has already been disposed. /// - /// - /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. - /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. - /// public byte[] Export(byte[] exporterContext, int length) { ArgumentNullException.ThrowIfNull(exporterContext); @@ -316,11 +306,6 @@ public byte[] Export(byte[] exporterContext, int length) /// /// The object has already been disposed. /// - /// - /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. - /// Exporting a secret does not advance the recipient's message sequence. - /// The caller is responsible for protecting the secret and clearing the buffer when no longer needed. - /// public void Export(ReadOnlySpan exporterContext, Span destination) { int maximumLength = Suite.KdfMetadata.MaximumExportLength; @@ -353,11 +338,6 @@ public void Export(ReadOnlySpan exporterContext, Span destination) /// /// An error occurred while deriving the exported secret. /// - /// - /// The calling method has verified that this instance is not disposed and the destination length - /// does not exceed the KDF's maximum export length. The destination may be empty. - /// Implementations must fill the entire destination on success without advancing the recipient's message sequence. - /// protected abstract void ExportCore(ReadOnlySpan exporterContext, Span destination); /// From 93cf8e5d4dc8ff78344ce1c9c684fa5674be7ba2 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 12 Sep 2026 14:32:02 -0400 Subject: [PATCH 39/42] Simplify HPKE sender and suite documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Security/Cryptography/HpkeSender.cs | 32 +++++-------------- .../System/Security/Cryptography/HpkeSuite.cs | 8 +---- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs index 282a600f1c5053..af54410691cf81 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs @@ -95,6 +95,10 @@ public byte[] Seal(ReadOnlySpan plaintext, ReadOnlySpan associatedDa /// /// The object has already been disposed. /// + /// + /// Messages must be decrypted by the corresponding recipient context in the same order + /// in which they were encrypted. + /// public byte[] Seal(byte[] plaintext, byte[]? associatedData = null) { ArgumentNullException.ThrowIfNull(plaintext); @@ -132,6 +136,10 @@ public byte[] Seal(byte[] plaintext, byte[]? associatedData = null) /// /// The object has already been disposed. /// + /// + /// Messages must be decrypted by the corresponding recipient context in the same order + /// in which they were encrypted. + /// public void Seal( ReadOnlySpan plaintext, Span ciphertext, @@ -170,11 +178,6 @@ public void Seal( /// /// The sender's message limit has been reached, or an error occurred during encryption. /// - /// - /// The calling method has verified that this instance is not disposed and the ciphertext buffer - /// has the exact required length. Implementations must maintain the sender's message sequence, - /// reject encryption when the message limit is reached, and fill the entire ciphertext buffer on success. - /// protected abstract void SealCore( ReadOnlySpan plaintext, Span ciphertext, @@ -201,11 +204,6 @@ protected abstract void SealCore( /// /// The object has already been disposed. /// - /// - /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. - /// Exporting a secret does not advance the sender's message sequence. - /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. - /// public byte[] Export(ReadOnlySpan exporterContext, int length) { ArgumentOutOfRangeException.ThrowIfNegative(length); @@ -257,10 +255,6 @@ public byte[] Export(ReadOnlySpan exporterContext, int length) /// /// The object has already been disposed. /// - /// - /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. - /// The caller is responsible for protecting the returned secret and clearing it when no longer needed. - /// public byte[] Export(byte[] exporterContext, int length) { ArgumentNullException.ThrowIfNull(exporterContext); @@ -291,11 +285,6 @@ public byte[] Export(byte[] exporterContext, int length) /// /// The object has already been disposed. /// - /// - /// The maximum export length is 255 times the hash output length for HKDF, or 65,535 bytes for SHAKE. - /// Exporting a secret does not advance the sender's message sequence. - /// The caller is responsible for protecting the secret and clearing the buffer when no longer needed. - /// public void Export(ReadOnlySpan exporterContext, Span destination) { int maximumLength = Suite.KdfMetadata.MaximumExportLength; @@ -328,11 +317,6 @@ public void Export(ReadOnlySpan exporterContext, Span destination) /// /// An error occurred while deriving the exported secret. /// - /// - /// The calling method has verified that this instance is not disposed and the destination length - /// does not exceed the KDF's maximum export length. The destination may be empty. - /// Implementations must fill the entire destination on success without advancing the sender's message sequence. - /// protected abstract void ExportCore(ReadOnlySpan exporterContext, Span destination); /// diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs index a028a5ed44f54c..506cff80cd889e 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs @@ -87,9 +87,6 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// /// The size of the decapsulation key for the cipher suite, in bytes. /// - /// - /// For ML-KEM and hybrid ML-KEM cipher suites, this is the size of the private seed. - /// public int DecapsulationKeySizeInBytes => KemMetadata.Nsk; /// @@ -112,7 +109,7 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// Gets the name of the cipher suite. /// /// - /// A string containing the KEM, KDF, and AEAD names, separated by spaces. + /// The name of the cipher suite. /// public string Name => field ??= $"{KemMetadata.Name} {KdfMetadata.Name} {AeadMetadata.Name}"; @@ -129,9 +126,6 @@ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) /// is negative or the resulting ciphertext length cannot be /// represented as a signed 32-bit integer. /// - /// - /// The returned length includes the authentication tag, but does not include the encapsulated secret. - /// public int GetCiphertextLength(int plaintextLength) { int tagSize = AeadTagSizeInBytes; From 627779e771da181a2e055155b6c89be1bff8e573 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 12 Sep 2026 16:04:37 -0400 Subject: [PATCH 40/42] Separate HPKE static validation from instance contracts Move static validation and unsupported-factory checks into shared fixtures, split null-argument and import-size tests, and share KEM test data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeContractTests.cs | 76 +---------- .../Cryptography/HpkeNotSupportedTests.cs} | 8 +- .../Security/Cryptography/HpkeTestData.cs | 8 ++ .../System/Security/Cryptography/HpkeTests.cs | 118 ++++++++++++++++++ .../Microsoft.Bcl.Cryptography.Tests.csproj | 4 + .../System.Security.Cryptography.Tests.csproj | 5 +- 6 files changed, 144 insertions(+), 75 deletions(-) rename src/libraries/{System.Security.Cryptography/tests/HpkeTests.cs => Common/tests/System/Security/Cryptography/HpkeNotSupportedTests.cs} (85%) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/HpkeTests.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs index bfd0316e78197b..f27a686e1715c6 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs @@ -22,14 +22,6 @@ public static IEnumerable Suites() } } - public static IEnumerable KemAlgorithms() - { - foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) - { - yield return new object[] { kem }; - } - } - [Fact] public static void Constructor_NullSuite() { @@ -48,62 +40,6 @@ public static void Constructor_SetsSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead } } - [Fact] - public static void StaticMethods_NullArguments() - { - AssertExtensions.Throws("suite", () => Hpke.IsSupported(null)); - AssertExtensions.Throws("suite", () => Hpke.GenerateKey(null)); - AssertExtensions.Throws("suite", () => Hpke.DeriveKey(null, Array.Empty())); - AssertExtensions.Throws("suite", - () => Hpke.DeriveKey(null, ReadOnlySpan.Empty)); - AssertExtensions.Throws("ikm", () => Hpke.DeriveKey(s_suite, (byte[])null)); - AssertExtensions.Throws("suite", - () => Hpke.ImportDecapsulationKey(null, Array.Empty())); - AssertExtensions.Throws("suite", - () => Hpke.ImportDecapsulationKey(null, ReadOnlySpan.Empty)); - AssertExtensions.Throws("source", - () => Hpke.ImportDecapsulationKey(s_suite, (byte[])null)); - AssertExtensions.Throws("suite", - () => Hpke.ImportEncapsulationKey(null, Array.Empty())); - AssertExtensions.Throws("suite", - () => Hpke.ImportEncapsulationKey(null, ReadOnlySpan.Empty)); - AssertExtensions.Throws("source", - () => Hpke.ImportEncapsulationKey(s_suite, (byte[])null)); - } - - [Theory] - [MemberData(nameof(KemAlgorithms))] - public static void ImportKeys_InvalidSize(HpkeKem kem) - { - HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); - - foreach (int length in new[] - { - 0, - suite.DecapsulationKeySizeInBytes - 1, - suite.DecapsulationKeySizeInBytes + 1 - }) - { - byte[] source = new byte[length]; - AssertExtensions.Throws("source", () => Hpke.ImportDecapsulationKey(suite, source)); - AssertExtensions.Throws("source", - () => Hpke.ImportDecapsulationKey(suite, source.AsSpan())); - } - - foreach (int length in new[] - { - 0, - suite.EncapsulationKeySizeInBytes - 1, - suite.EncapsulationKeySizeInBytes + 1 - }) - { - byte[] source = new byte[length]; - AssertExtensions.Throws("source", () => Hpke.ImportEncapsulationKey(suite, source)); - AssertExtensions.Throws("source", - () => Hpke.ImportEncapsulationKey(suite, source.AsSpan())); - } - } - [Theory] [InlineData(1)] [InlineData(7)] @@ -167,7 +103,7 @@ public static void Disposed_InstanceOperationsDoNotCallCore() } [Theory] - [MemberData(nameof(KemAlgorithms))] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] public static void ExportKeys_Allocated(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); @@ -190,7 +126,7 @@ public static void ExportKeys_Allocated(HpkeKem kem) } [Theory] - [MemberData(nameof(KemAlgorithms))] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] public static void ExportKeys_Exact(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); @@ -223,7 +159,7 @@ public static void ExportKeys_Exact(HpkeKem kem) } [Theory] - [MemberData(nameof(KemAlgorithms))] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] public static void ExportKeys_InvalidSizeBeforeDisposal(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); @@ -356,7 +292,7 @@ public static void Seal_Exact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) } [Theory] - [MemberData(nameof(KemAlgorithms))] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] public static void Seal_InvalidOutputSizesBeforeDisposal(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); @@ -468,7 +404,7 @@ public static void Open_Exact(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) } [Theory] - [MemberData(nameof(KemAlgorithms))] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] public static void Open_InvalidSizesBeforeDisposal(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); @@ -666,7 +602,7 @@ public static void CreatePskRecipient_ArrayAndSpan(HpkeKem kem, HpkeKdf kdf, Hpk } [Theory] - [MemberData(nameof(KemAlgorithms))] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] public static void ContextFactories_InvalidEncapsulationSizeBeforeDisposal(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); diff --git a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeNotSupportedTests.cs similarity index 85% rename from src/libraries/System.Security.Cryptography/tests/HpkeTests.cs rename to src/libraries/Common/tests/System/Security/Cryptography/HpkeNotSupportedTests.cs index 598e6ba29e4a5c..fb8d7817063a05 100644 --- a/src/libraries/System.Security.Cryptography/tests/HpkeTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeNotSupportedTests.cs @@ -5,14 +5,14 @@ namespace System.Security.Cryptography.Tests { - public static class HpkeTests + public static class HpkeNotSupportedTests { [Fact] public static void KeyFactories_NotSupported() { - foreach (HpkeKem kem in Enum.GetValues()) - foreach (HpkeKdf kdf in Enum.GetValues()) - foreach (HpkeAead aead in Enum.GetValues()) + foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) + foreach (HpkeKdf kdf in Enum.GetValues(typeof(HpkeKdf))) + foreach (HpkeAead aead in Enum.GetValues(typeof(HpkeAead))) { HpkeSuite suite = new(kem, kdf, aead); diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs index 7bab0b14e848a9..09c677f4b7309c 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs @@ -28,6 +28,14 @@ public static partial class HpkeTestData [HpkeKdf.SHAKE256, 65535], ]; + public static IEnumerable KemAlgorithms() + { + foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) + { + yield return [kem]; + } + } + public static IEnumerable RepresentativeSuites { get diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTests.cs new file mode 100644 index 00000000000000..713041151dffa7 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTests.cs @@ -0,0 +1,118 @@ +// 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.Tests +{ + [ConditionalClass(typeof(PlatformDetection), + nameof(PlatformDetection.IsNotBrowser), + nameof(PlatformDetection.IsNotWasi), + nameof(PlatformDetection.IsNotNetFramework))] + public static class HpkeTests + { + private static readonly HpkeSuite s_suite = new(HpkeKem.MLKEM_768, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + [Fact] + public static void IsSupported_NullSuite() + { + AssertExtensions.Throws("suite", () => Hpke.IsSupported(null)); + } + + [Fact] + public static void GenerateKey_NullSuite() + { + AssertExtensions.Throws("suite", () => Hpke.GenerateKey(null)); + } + + [Fact] + public static void DeriveKey_NullSuite() + { + AssertExtensions.Throws("suite", () => Hpke.DeriveKey(null, Array.Empty())); + AssertExtensions.Throws("suite", + () => Hpke.DeriveKey(null, ReadOnlySpan.Empty)); + } + + [Fact] + public static void DeriveKey_NullIkm() + { + AssertExtensions.Throws("ikm", () => Hpke.DeriveKey(s_suite, (byte[])null)); + } + + [Fact] + public static void ImportDecapsulationKey_NullSuite() + { + AssertExtensions.Throws("suite", + () => Hpke.ImportDecapsulationKey(null, Array.Empty())); + AssertExtensions.Throws("suite", + () => Hpke.ImportDecapsulationKey(null, ReadOnlySpan.Empty)); + } + + [Fact] + public static void ImportDecapsulationKey_NullSource() + { + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(s_suite, (byte[])null)); + } + + [Fact] + public static void ImportEncapsulationKey_NullSuite() + { + AssertExtensions.Throws("suite", + () => Hpke.ImportEncapsulationKey(null, Array.Empty())); + AssertExtensions.Throws("suite", + () => Hpke.ImportEncapsulationKey(null, ReadOnlySpan.Empty)); + } + + [Fact] + public static void ImportEncapsulationKey_NullSource() + { + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(s_suite, (byte[])null)); + } + + [Theory] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + public static void ImportDecapsulationKey_InvalidSize(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + byte[] shortPrivateKey = new byte[suite.DecapsulationKeySizeInBytes - 1]; + byte[] longPrivateKey = new byte[suite.DecapsulationKeySizeInBytes + 1]; + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(suite, Array.Empty())); + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(suite, ReadOnlySpan.Empty)); + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(suite, shortPrivateKey)); + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(suite, shortPrivateKey.AsSpan())); + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(suite, longPrivateKey)); + AssertExtensions.Throws("source", + () => Hpke.ImportDecapsulationKey(suite, longPrivateKey.AsSpan())); + } + + [Theory] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + public static void ImportEncapsulationKey_InvalidSize(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + byte[] shortPublicKey = new byte[suite.EncapsulationKeySizeInBytes - 1]; + byte[] longPublicKey = new byte[suite.EncapsulationKeySizeInBytes + 1]; + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(suite, Array.Empty())); + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(suite, ReadOnlySpan.Empty)); + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(suite, shortPublicKey)); + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(suite, shortPublicKey.AsSpan())); + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(suite, longPublicKey)); + AssertExtensions.Throws("source", + () => Hpke.ImportEncapsulationKey(suite, longPublicKey.AsSpan())); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index c776c8ae672464..635c24704379ed 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -133,6 +133,8 @@ Link="CommonTest\System\Security\Cryptography\HpkeImplementationTests.cs" /> + + + + - From 2d362f95fe7f7f851a12243281a9ba28bec13468 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 12 Sep 2026 17:36:46 -0400 Subject: [PATCH 41/42] Refine HPKE export tests and browser build exclusions Split export contract tests by key type and unroll invalid buffer-size checks. Exclude managed KEM adapters and AEAD support metadata from browser compilation, remove obsolete ECDH suppressions, and narrow the remaining AEAD pragmas. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5 --- .../Cryptography/HpkeContractTests.cs | 85 +++++++++++-------- .../src/System.Security.Cryptography.csproj | 8 +- .../Cryptography/HpkeAeadMetadata.Managed.cs | 4 +- .../HpkeECDiffieHellmanKemAdapter.cs | 6 -- .../Cryptography/HpkeManagedAesAeadAdapter.cs | 10 ++- .../HpkeManagedChaCha20Poly1305AeadAdapter.cs | 4 +- 6 files changed, 67 insertions(+), 50 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs index f27a686e1715c6..9b82d2ddd6bbee 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs @@ -104,36 +104,47 @@ public static void Disposed_InstanceOperationsDoNotCallCore() [Theory] [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] - public static void ExportKeys_Allocated(HpkeKem kem) + public static void ExportDecapsulationKey_Allocated(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); using (HpkeContract hpke = new(suite) { OnExportDecapsulationKeyCore = destination => destination.Fill(0x42), - OnExportEncapsulationKeyCore = destination => destination.Fill(0xE7), }) { byte[] privateKey = hpke.ExportDecapsulationKey(); - byte[] publicKey = hpke.ExportEncapsulationKey(); Assert.Equal(suite.DecapsulationKeySizeInBytes, privateKey.Length); - Assert.Equal(suite.EncapsulationKeySizeInBytes, publicKey.Length); AssertExtensions.FilledWith(0x42, privateKey); - AssertExtensions.FilledWith(0xE7, publicKey); Assert.Equal(1, hpke.ExportDecapsulationKeyCoreCount); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + public static void ExportEncapsulationKey_Allocated(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = new(suite) + { + OnExportEncapsulationKeyCore = destination => destination.Fill(0xE7), + }) + { + byte[] publicKey = hpke.ExportEncapsulationKey(); + Assert.Equal(suite.EncapsulationKeySizeInBytes, publicKey.Length); + AssertExtensions.FilledWith(0xE7, publicKey); Assert.Equal(1, hpke.ExportEncapsulationKeyCoreCount); } } [Theory] [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] - public static void ExportKeys_Exact(HpkeKem kem) + public static void ExportDecapsulationKey_Exact(HpkeKem kem) { HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); byte[] privateBuffer = Filled(suite.DecapsulationKeySizeInBytes + 2, 0xA5); - byte[] publicBuffer = Filled(suite.EncapsulationKeySizeInBytes + 2, 0xA5); Memory privateKey = privateBuffer.AsMemory(1, suite.DecapsulationKeySizeInBytes); - Memory publicKey = publicBuffer.AsMemory(1, suite.EncapsulationKeySizeInBytes); using (HpkeContract hpke = new(suite) { @@ -142,6 +153,24 @@ public static void ExportKeys_Exact(HpkeKem kem) AssertExtensions.Same(privateKey.Span, destination); destination.Fill(0x42); }, + }) + { + hpke.ExportDecapsulationKey(privateKey.Span); + AssertGuardedOutput(privateBuffer, 0x42); + Assert.Equal(1, hpke.ExportDecapsulationKeyCoreCount); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + public static void ExportEncapsulationKey_Exact(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + byte[] publicBuffer = Filled(suite.EncapsulationKeySizeInBytes + 2, 0xA5); + Memory publicKey = publicBuffer.AsMemory(1, suite.EncapsulationKeySizeInBytes); + + using (HpkeContract hpke = new(suite) + { OnExportEncapsulationKeyCore = destination => { AssertExtensions.Same(publicKey.Span, destination); @@ -149,11 +178,8 @@ public static void ExportKeys_Exact(HpkeKem kem) }, }) { - hpke.ExportDecapsulationKey(privateKey.Span); hpke.ExportEncapsulationKey(publicKey.Span); - AssertGuardedOutput(privateBuffer, 0x42); AssertGuardedOutput(publicBuffer, 0xE7); - Assert.Equal(1, hpke.ExportDecapsulationKeyCoreCount); Assert.Equal(1, hpke.ExportEncapsulationKeyCoreCount); } } @@ -173,27 +199,19 @@ public static void ExportKeys_InvalidSizeBeforeDisposal(HpkeKem kem) hpke.Dispose(); } - foreach (int length in new[] - { - 0, - suite.DecapsulationKeySizeInBytes - 1, - suite.DecapsulationKeySizeInBytes + 1 - }) - { - AssertExtensions.Throws("destination", - () => hpke.ExportDecapsulationKey(new byte[length])); - } - - foreach (int length in new[] - { - 0, - suite.EncapsulationKeySizeInBytes - 1, - suite.EncapsulationKeySizeInBytes + 1 - }) - { - AssertExtensions.Throws("destination", - () => hpke.ExportEncapsulationKey(new byte[length])); - } + AssertExtensions.Throws("destination", + () => hpke.ExportDecapsulationKey(Span.Empty)); + AssertExtensions.Throws("destination", + () => hpke.ExportDecapsulationKey(new byte[suite.DecapsulationKeySizeInBytes - 1])); + AssertExtensions.Throws("destination", + () => hpke.ExportDecapsulationKey(new byte[suite.DecapsulationKeySizeInBytes + 1])); + + AssertExtensions.Throws("destination", + () => hpke.ExportEncapsulationKey(Span.Empty)); + AssertExtensions.Throws("destination", + () => hpke.ExportEncapsulationKey(new byte[suite.EncapsulationKeySizeInBytes - 1])); + AssertExtensions.Throws("destination", + () => hpke.ExportEncapsulationKey(new byte[suite.EncapsulationKeySizeInBytes + 1])); } } } @@ -228,8 +246,7 @@ public static void Seal_Allocated(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) if (useSpan) { - hpke.Seal( - plaintext.AsSpan(), + hpke.Seal(plaintext.AsSpan(), out encapsulatedSecret, out ciphertext, associatedData.AsSpan(), 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 5e367b4cd012b2..d1e419b3c51370 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -638,12 +638,8 @@ - - - - @@ -2125,11 +2121,15 @@ + + + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs index 187319b93ca3d0..e79abaa195206f 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs @@ -13,16 +13,16 @@ internal bool IsSupported { switch (Aead) { -#pragma warning disable CA1416 // Not supported on browser +#pragma warning disable CA1416 case HpkeAead.AES_128_GCM: case HpkeAead.AES_256_GCM: return AesGcm.IsSupported; case HpkeAead.ChaCha20Poly1305: return ChaCha20Poly1305.IsSupported; +#pragma warning restore CA1416 default: Debug.Fail($"Aead {Aead}'s support is unknown."); return false; -#pragma warning restore CA1416 // Not supported on browser } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs index 6557503a26c869..7d30b7151bf9cd 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -78,13 +78,11 @@ internal override void ImportDecapsulationKey(ReadOnlySpan decapsulationKe using (PinAndClear.Track(privateKey)) { -#pragma warning disable CA1416 // Not supported on browser _ecdh = ECDiffieHellman.Create(new ECParameters { Curve = _curve, D = privateKey, }); -#pragma warning restore CA1416 // Not supported on browser } } @@ -107,10 +105,8 @@ private ECDiffieHellman CreateFromEncapsulationKey(ReadOnlySpan encapsulat hasPrivateKey: false, out ECParameters parameters); -#pragma warning disable CA1416 // Not supported on browser parameters.Curve = _curve; return ECDiffieHellman.Create(parameters); -#pragma warning restore CA1416 // Not supported on browser } internal override void DeriveKeyPair(ReadOnlySpan ikm) @@ -139,13 +135,11 @@ internal override void DeriveKeyPair(ReadOnlySpan ikm) if (IsValidScalar(privateKey, order)) { -#pragma warning disable CA1416 // Not supported on browser _ecdh = ECDiffieHellman.Create(new ECParameters { Curve = _curve, D = privateKey, }); -#pragma warning restore CA1416 // Not supported on browser return; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs index 9102a4150d572a..d8ad2d6515077d 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable CA1416 // //TODO:HPKE Call is reachable on "unsupported platform" - deal with this messy daignostic later. - namespace System.Security.Cryptography { internal sealed class HpkeManagedAesAeadAdapter : HpkeManagedAeadAdapter @@ -11,7 +9,9 @@ internal sealed class HpkeManagedAesAeadAdapter : HpkeManagedAeadAdapter internal HpkeManagedAesAeadAdapter(HpkeSuite suite, ReadOnlySpan key) { +#pragma warning disable CA1416 _aes = new AesGcm(key, suite.AeadMetadata.Nt); +#pragma warning restore CA1416 } internal override void Encrypt( @@ -21,7 +21,9 @@ internal override void Encrypt( Span ciphertext, Span tag) { +#pragma warning disable CA1416 _aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); +#pragma warning restore CA1416 } internal override void Decrypt( @@ -31,10 +33,14 @@ internal override void Decrypt( ReadOnlySpan tag, Span plaintext) { +#pragma warning disable CA1416 _aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData); +#pragma warning restore CA1416 } +#pragma warning disable CA1416 public override void Dispose() => _aes.Dispose(); +#pragma warning restore CA1416 } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.cs index 102ef8b91f2b9e..0dd7add87d08b3 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedChaCha20Poly1305AeadAdapter.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable CA1416 // //TODO:HPKE Call is reachable on "unsupported platform" - deal with this messy daignostic later. - namespace System.Security.Cryptography { +#pragma warning disable CA1416 internal sealed class HpkeManagedChaCha20Poly1305AeadAdapter : HpkeManagedAeadAdapter { private readonly ChaCha20Poly1305 _chacha; @@ -36,4 +35,5 @@ internal override void Decrypt( public override void Dispose() => _chacha.Dispose(); } +#pragma warning restore CA1416 } From befd9b0084556f4d2d56e56204592aefe513679d Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 12 Sep 2026 19:25:45 -0400 Subject: [PATCH 42/42] Add comment clarifying why OpenCore does not have a concurrency guard --- .../System/Security/Cryptography/HpkeImplementation.Managed.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs index 7542046c9a1291..c9996fbb8b1481 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -451,6 +451,8 @@ protected override void OpenCore( Span plaintext, ReadOnlySpan associatedData) { + // Unlike Seal we do not have a concurrency block here. Seal needs one to prevent nonce repetition. In + // Open a repeated nonce does not result in loss of confidentiality. if (_sequenceNumber == ulong.MaxValue) { throw new CryptographicException(SR.Cryptography_HpkeMessageLimitReached);