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/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs new file mode 100644 index 00000000000000..a113a9fd27317a --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs @@ -0,0 +1,1519 @@ +// 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) key. + /// + [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public abstract class Hpke : IDisposable + { + // These inputs limits are somewhat arbitrary however 256 MB of any IKM, salt, info, etc. is excessively large. + // Keeping them at 256 MB or less allows avoiding overflowing some contiguous buffers. This is not a promise + // that 256 MB is accepted, either. Some HPKE suites have smaller limits, like single-stage keying material + // cannot be larger than 2^16. + internal const int MaximumInputSizeInBytes = 256 * 1024 * 1024; + + 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 . + /// + /// + /// exceeds the maximum length supported by the cipher suite's KEM. + /// + /// + /// is not supported on the current platform. + /// + public static Hpke DeriveKey(HpkeSuite suite, byte[] ikm) + { + 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 . + /// + /// + /// exceeds the maximum length supported by the cipher suite's KEM. + /// + /// + /// is not supported on the current platform. + /// + public static Hpke DeriveKey(HpkeSuite suite, ReadOnlySpan ikm) + { + ArgumentNullException.ThrowIfNull(suite); + + if (ikm.Length > HpkeKemMetadata.MaximumInputKeyingMaterialLength) + { + throw new ArgumentException( + SR.Format( + SR.Argument_HpkeIkmTooLong, + HpkeKemMetadata.MaximumInputKeyingMaterialLength), + nameof(ikm)); + } + + ThrowIfNotSupported(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); + ThrowIfNotSupported(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. + /// + 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. + /// + 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. + /// + 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. + /// + 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. + /// + /// + /// 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. + /// + /// + /// 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 serialized 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. + /// + protected abstract void ExportDecapsulationKeyCore(Span destination); + + /// + /// Exports the encapsulation key. + /// + /// + /// The encapsulation key. + /// + /// + /// 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. + /// + /// + /// 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. + /// + /// + /// An error occurred while exporting the key. + /// + protected abstract void ExportEncapsulationKeyCore(Span destination); + + /// + /// Encrypts 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. + /// + /// + /// When this method returns, contains a new byte array containing the ciphertext. + /// + /// + /// 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 . + /// + /// + /// An error occurred during encryption. + /// + /// + /// The object has already been disposed. + /// + public void Seal( + ReadOnlySpan plaintext, + out byte[] encapsulatedSecret, + out byte[] ciphertext, + ReadOnlySpan associatedData = default, + ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); + ThrowIfDisposed(); + + byte[] ciphertextBuffer = new byte[ciphertextLength]; + byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes]; + + SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info); + + encapsulatedSecret = encapsulatedSecretBuffer; + ciphertext = ciphertextBuffer; + } + + /// + /// Encrypts 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. + /// + /// + /// When this method returns, contains a new byte array containing the ciphertext. + /// + /// + /// 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 . + /// + /// + /// An error occurred during encryption. + /// + /// + /// The object has already been disposed. + /// + public void Seal( + byte[] plaintext, + out byte[] encapsulatedSecret, + out byte[] ciphertext, + byte[]? associatedData = null, + byte[]? info = null) + { + ArgumentNullException.ThrowIfNull(plaintext); + ThrowIfInfoExceedsLimit(info); + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); + ThrowIfDisposed(); + + byte[] ciphertextBuffer = new byte[ciphertextLength]; + 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; + } + + /// + /// Encrypts 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. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// The application context, which must match the value used by the recipient. + /// + /// + /// + /// 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 . + /// + /// + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// An error occurred during encryption. + /// + /// + /// + /// The object has already been disposed. + /// + public void Seal( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData = default, + ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + + 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)); + } + + 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); + } + + ThrowIfDisposed(); + SealCore(plaintext, encapsulatedSecret, ciphertext, associatedData, info); + } + + /// + /// When overridden in a derived class, encrypts a single message using Base mode. + /// + /// + /// The message to encrypt. + /// + /// + /// The buffer to receive the encapsulated secret. + /// + /// + /// The buffer to receive the ciphertext. + /// + /// + /// The additional data to authenticate without encrypting. + /// + /// + /// The application context. + /// + /// + /// An error occurred during encryption. + /// + protected abstract void SealCore( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + ReadOnlySpan associatedData, + ReadOnlySpan info); + + /// + /// Decrypts a single HPKE ciphertext using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext. + /// + /// + /// 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 plaintext. + /// + /// + /// + /// is not exactly + /// bytes long. + /// + /// -or- + /// + /// is shorter than bytes. + /// + /// -or- + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// + /// The ciphertext's contents 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); + ThrowIfDisposed(); + byte[] plaintext = new byte[plaintextLength]; + + try + { + OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info); + return plaintext; + } + catch + { + CryptographicOperations.ZeroMemory(plaintext); + throw; + } + } + + /// + /// Decrypts a single HPKE ciphertext using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext. + /// + /// + /// 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 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 ciphertext's contents 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 a single HPKE ciphertext into the provided buffer using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext. + /// + /// + /// The buffer to receive the 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 ciphertext's contents could not be verified. + /// + /// + /// + /// 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. + /// + 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)); + } + + if (encapsulatedSecret.Overlaps(plaintext) || + ciphertext.Overlaps(plaintext) || + associatedData.Overlaps(plaintext) || + info.Overlaps(plaintext)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + ThrowIfDisposed(); + OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info); + } + + /// + /// When overridden in a derived class, decrypts a single HPKE ciphertext + /// using Base mode. + /// + /// + /// The encapsulated secret produced by the sender. + /// + /// + /// The ciphertext. + /// + /// + /// The buffer to receive the plaintext. + /// + /// + /// The additional authenticated data. + /// + /// + /// The application context. + /// + /// + /// The ciphertext's contents could not be verified. + /// + /// + /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid, + /// or an error occurred during decryption. + /// + protected abstract void OpenCore( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + Span plaintext, + 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. + /// + /// + /// 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. + /// + /// + /// An error occurred while creating the sender. + /// + /// + /// 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. + /// + /// + /// + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// An error occurred while creating the sender. + /// + /// + /// + /// The object has already been disposed. + /// + public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + + if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes), + nameof(encapsulatedSecret)); + } + + if (info.Overlaps(encapsulatedSecret)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + ThrowIfDisposed(); + 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. + /// + /// + /// An error occurred while creating the sender. + /// + 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. + /// + /// + /// The object has already been disposed. + /// + public HpkeRecipient CreateRecipient( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan info = default) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); + ThrowIfDisposed(); + 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. + /// + /// + /// 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. + /// + 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. + /// + /// + /// 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. + /// + /// + /// An error occurred while creating the sender. + /// + /// + /// Creating a PSK sender is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + 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. + /// + /// + /// 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. + /// + /// + /// An error occurred while creating the sender. + /// + /// + /// Creating a PSK sender is not supported on the current platform. + /// + /// + /// The object has already been disposed. + /// + 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. + /// + /// + /// + /// One or more provided buffers overlap. + /// + /// -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. + /// + public HpkeSender CreatePskSender( + ReadOnlySpan psk, + ReadOnlySpan pskId, + Span encapsulatedSecret, + ReadOnlySpan info = default) + { + ThrowIfInvalidPskInputs(psk, pskId); + ThrowIfInfoExceedsLimit(info); + + if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes), + nameof(encapsulatedSecret)); + } + + if (psk.Overlaps(encapsulatedSecret) || + pskId.Overlaps(encapsulatedSecret) || + info.Overlaps(encapsulatedSecret)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + ThrowIfDisposed(); + 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. + /// + /// + /// An error occurred while creating the sender. + /// + /// + /// Creating a PSK sender is not supported on the current platform. + /// + 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); + ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret); + ThrowIfDisposed(); + 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. + /// + 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) + { + } + + private static void ThrowIfNotSupported(HpkeSuite suite) + { + if (!IsSupported(suite)) + { + throw new PlatformNotSupportedException(); + } + } + + 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 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) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeEncapsulatedSecretLength, Suite.EncapsulatedSecretSizeInBytes), + nameof(encapsulatedSecret)); + } + } + + private int ValidateOpenInputs( + ReadOnlySpan encapsulatedSecret, + ReadOnlySpan ciphertext, + ReadOnlySpan info) + { + ThrowIfInfoExceedsLimit(info); + ThrowIfInvalidEncapsulatedSecretLength(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/Common/src/System/Security/Cryptography/HpkeAead.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs new file mode 100644 index 00000000000000..e589c151a42983 --- /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 = 0x0001, + + /// + /// Indicates that authenticated encryption uses AES-GCM with a 256-bit key. + /// + AES_256_GCM = 0x0002, + + /// + /// Indicates that authenticated encryption uses ChaCha20-Poly1305. + /// + ChaCha20Poly1305 = 0x0003, + } +} 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..cd68e18eb518d0 --- /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 partial 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/HpkeKdf.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs new file mode 100644 index 00000000000000..b7e0684736cb19 --- /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 = 0x0001, + + /// + /// Indicates that key derivation uses HKDF with SHA-384. + /// + HKDF_SHA384 = 0x0002, + + /// + /// Indicates that key derivation uses HKDF with SHA-512. + /// + HKDF_SHA512 = 0x0003, + + /// + /// Indicates that key derivation uses SHAKE128. + /// + SHAKE128 = 0x0010, + + /// + /// Indicates that key derivation uses SHAKE256. + /// + SHAKE256 = 0x0011 + } +} 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..0b304bf2764ad7 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.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 partial class HpkeKdfMetadata + { + internal HpkeKdf Kdf { get; } + internal int Nh { get; } + internal bool IsTwoStage { get; } + internal string Name { get; } + internal int MaximumExporterContextLength { 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 + internal int MaximumExportLength => IsTwoStage ? 255 * Nh : ushort.MaxValue; + + private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name) + { + Debug.Assert(nh <= 64, "Nh value is larger than 64."); + + Kdf = kdf; + Nh = nh; + IsTwoStage = isTwoStage; + Name = name; + MaximumExporterContextLength = Hpke.MaximumInputSizeInBytes; + + if (IsTwoStage) + { + MaximumInfoLength = Hpke.MaximumInputSizeInBytes; + MaximumPskLength = Hpke.MaximumInputSizeInBytes; + MaximumPskIdLength = Hpke.MaximumInputSizeInBytes; + } + else + { + // One-stage KDFs length-prefix each of these inputs with a 16-bit length. + // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1 + MaximumInfoLength = ushort.MaxValue; + MaximumPskLength = ushort.MaxValue; + MaximumPskIdLength = ushort.MaxValue; + } + } + + internal static HpkeKdfMetadata? Create(HpkeKdf kdf) + { + switch (kdf) + { + // HKDF's limits exceed the maximum input size supported by the HPKE API. + // 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"); + 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/HpkeKem.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs new file mode 100644 index 00000000000000..1d460e7b90dba3 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.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 = 0x0010, + + /// + /// Indicates that key encapsulation uses DHKEM with the NIST P-384 curve and HKDF-SHA-384. + /// + DHKEM_P384_HKDF_SHA384 = 0x0011, + + /// + /// Indicates that key encapsulation uses DHKEM with the NIST P-521 curve and HKDF-SHA-512. + /// + DHKEM_P521_HKDF_SHA512 = 0x0012, + + /// + /// Indicates that key encapsulation uses DHKEM with X25519 and HKDF-SHA-256. + /// + DHKEM_X25519_HKDF_SHA256 = 0x0020, + + /// + /// Indicates that key encapsulation uses ML-KEM-512. + /// + MLKEM_512 = 0x0040, + + /// + /// Indicates that key encapsulation uses ML-KEM-768. + /// + MLKEM_768 = 0x0041, + + /// + /// Indicates that key encapsulation uses ML-KEM-1024. + /// + MLKEM_1024 = 0x0042, + + /// + /// Indicates that key encapsulation combines ML-KEM-768 with ECDH using the NIST P-256 curve. + /// + MLKEM768_P256 = 0x0050, + + /// + /// Indicates that key encapsulation combines ML-KEM-1024 with ECDH using the NIST P-384 curve. + /// + MLKEM1024_P384 = 0x0051, + } +} 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..51a9e5e0ecfd91 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs @@ -0,0 +1,65 @@ +// 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 partial class HpkeKemMetadata + { + internal const int MaximumInputKeyingMaterialLength = Hpke.MaximumInputSizeInBytes; + + 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; + Setup(); + } + + partial void Setup(); + + 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_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)"); + + // 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/HpkeRecipient.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs new file mode 100644 index 00000000000000..f48c09029eae0c --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs @@ -0,0 +1,404 @@ +// 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) + { + int plaintextLength = GetPlaintextLength(ciphertext); + ThrowIfDisposed(); + byte[] plaintext = new byte[plaintextLength]; + + 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. + /// + /// + /// 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); + 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. + /// + /// + /// + /// 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. + /// + /// + /// Messages must be supplied in the same order in which the corresponding sender context encrypted them. + /// + public void Open( + ReadOnlySpan ciphertext, + Span plaintext, + ReadOnlySpan associatedData = default) + { + int plaintextLength = GetPlaintextLength(ciphertext); + + if (plaintext.Length != plaintextLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, plaintextLength), + nameof(plaintext)); + } + + if (ciphertext.Overlaps(plaintext) || associatedData.Overlaps(plaintext)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + ThrowIfDisposed(); + 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. + /// + 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. + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// 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. + /// + public byte[] Export(ReadOnlySpan exporterContext, int length) + { + ThrowIfExporterContextExceedsLimit(exporterContext); + ArgumentOutOfRangeException.ThrowIfNegative(length); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (length > maximumLength) + { + throw new ArgumentOutOfRangeException( + nameof(length), + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength)); + } + + ThrowIfDisposed(); + 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 . + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// 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. + /// + 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. + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF, + /// or the length of exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// + /// The object has already been disposed. + /// + public void Export(ReadOnlySpan exporterContext, Span destination) + { + ThrowIfExporterContextExceedsLimit(exporterContext); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (destination.Length > maximumLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength), + nameof(destination)); + } + + if (exporterContext.Overlaps(destination)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + ThrowIfDisposed(); + 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. + /// + 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 void ThrowIfExporterContextExceedsLimit(ReadOnlySpan exporterContext) + { + if (exporterContext.Length > Suite.KdfMetadata.MaximumExporterContextLength) + { + throw new ArgumentException( + SR.Format( + SR.Argument_HpkeExporterContextTooLong, + Suite.KdfMetadata.MaximumExporterContextLength), + nameof(exporterContext)); + } + } + + 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..fb202be5403bda --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs @@ -0,0 +1,369 @@ +// 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) + { + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); + ThrowIfDisposed(); + byte[] ciphertext = new byte[ciphertextLength]; + 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. + /// + /// + /// 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); + 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 . + /// + /// + /// + /// 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. + /// + /// + /// 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, + ReadOnlySpan associatedData = default) + { + int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length); + + if (ciphertext.Length != ciphertextLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, ciphertextLength), + nameof(ciphertext)); + } + + if (plaintext.Overlaps(ciphertext) || associatedData.Overlaps(ciphertext)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + ThrowIfDisposed(); + 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. + /// + 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. + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// 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. + /// + public byte[] Export(ReadOnlySpan exporterContext, int length) + { + ThrowIfExporterContextExceedsLimit(exporterContext); + ArgumentOutOfRangeException.ThrowIfNegative(length); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (length > maximumLength) + { + throw new ArgumentOutOfRangeException( + nameof(length), + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength)); + } + + ThrowIfDisposed(); + 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 . + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF. + /// + /// + /// 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. + /// + 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. + /// + /// + /// exceeds the maximum length supported by the cipher suite's KDF, + /// or the length of exceeds the maximum export length supported by the cipher suite's KDF. + /// + /// + /// + /// One or more provided buffers overlap. + /// + /// -or- + /// + /// An error occurred while deriving the exported secret. + /// + /// + /// + /// The object has already been disposed. + /// + public void Export(ReadOnlySpan exporterContext, Span destination) + { + ThrowIfExporterContextExceedsLimit(exporterContext); + int maximumLength = Suite.KdfMetadata.MaximumExportLength; + + if (destination.Length > maximumLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength), + nameof(destination)); + } + + if (exporterContext.Overlaps(destination)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + ThrowIfDisposed(); + 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. + /// + 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 ThrowIfExporterContextExceedsLimit(ReadOnlySpan exporterContext) + { + if (exporterContext.Length > Suite.KdfMetadata.MaximumExporterContextLength) + { + throw new ArgumentException( + SR.Format( + SR.Argument_HpkeExporterContextTooLong, + Suite.KdfMetadata.MaximumExporterContextLength), + nameof(exporterContext)); + } + } + + private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); + } +} 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..506cff80cd889e --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs @@ -0,0 +1,203 @@ +// 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 +{ + /// + /// Represents a Hybrid Public Key Encryption (HPKE) cipher suite. + /// + [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. + /// + /// + /// 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) + { + 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)); + } + + /// + /// 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 => AeadMetadata.Aead; + + /// + /// Gets the key derivation function (KDF) for the cipher suite. + /// + /// + /// The key derivation function (KDF) for the cipher suite. + /// + public HpkeKdf KdfAlgorithm => KdfMetadata.Kdf; + + /// + /// Gets the key encapsulation mechanism (KEM) for the cipher suite. + /// + /// + /// The key encapsulation mechanism (KEM) for the cipher suite. + /// + 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. + /// + 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. + /// + /// + /// The name of the cipher suite. + /// + 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. + /// + 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. + /// + /// + /// 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); + + /// + public override string ToString() => Name; + + /// + /// 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); + } +} 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..b355c398d7d6bd --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs @@ -0,0 +1,1667 @@ +// 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 }; + } + } + + [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); + } + } + + [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(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + public static void ExportDecapsulationKey_Allocated(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = new(suite)) + { + hpke.OnExportDecapsulationKeyCore = destination => destination.Fill(0x42); + + byte[] privateKey = hpke.ExportDecapsulationKey(); + Assert.Equal(suite.DecapsulationKeySizeInBytes, privateKey.Length); + AssertExtensions.FilledWith(0x42, privateKey); + 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)) + { + hpke.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_IndependentBuffers(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = new(suite)) + { + hpke.OnExportDecapsulationKeyCore = static destination => { }; + hpke.OnExportEncapsulationKeyCore = static destination => { }; + + Assert.NotSame(hpke.ExportDecapsulationKey(), hpke.ExportDecapsulationKey()); + Assert.NotSame(hpke.ExportEncapsulationKey(), hpke.ExportEncapsulationKey()); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + public static void ExportDecapsulationKey_Exact(HpkeKem kem) + { + HpkeSuite suite = new(kem, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM); + byte[] privateBuffer = Filled(suite.DecapsulationKeySizeInBytes + 2, 0xA5); + Memory privateKey = privateBuffer.AsMemory(1, suite.DecapsulationKeySizeInBytes); + + using (HpkeContract hpke = new(suite)) + { + hpke.OnExportDecapsulationKeyCore = destination => + { + 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)) + { + hpke.OnExportEncapsulationKeyCore = destination => + { + AssertExtensions.Same(publicKey.Span, destination); + destination.Fill(0xE7); + }; + + hpke.ExportEncapsulationKey(publicKey.Span); + AssertGuardedOutput(publicBuffer, 0xE7); + Assert.Equal(1, hpke.ExportEncapsulationKeyCoreCount); + } + } + + [Theory] + [MemberData(nameof(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + 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(); + } + + 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])); + } + } + } + + [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)) + { + hpke.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); + AssertExtensions.SequenceEqual([0x51, 0x52, 0x53], (ReadOnlySpan)associatedData); + AssertExtensions.SequenceEqual([0x71, 0x72], (ReadOnlySpan)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)) + { + hpke.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(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + 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)) + { + hpke.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); + AssertExtensions.SequenceEqual([0x51, 0x52, 0x53], (ReadOnlySpan)associatedData); + AssertExtensions.SequenceEqual([0x71, 0x72], (ReadOnlySpan)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)) + { + hpke.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(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + 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)) + { + hpke.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)) + { + hpke.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(HpkeTestData.KemAlgorithms), MemberType = typeof(HpkeTestData))] + 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)] + public static void InfoLength_HkdfMaximumInputSize(HpkeKdf kdf) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = new(suite)) + { + hpke.OnCreateSenderCore = (enc, info) => new ReturnedSender(suite); + + hpke.CreateSender(out _, SpanOfLength(HpkeTestData.MaximumInputSizeInBytes)).Dispose(); + AssertExtensions.Throws( + "info", + () => hpke.CreateSender(out _, SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1))); + } + } + + [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)) + { + hpke.OnCreatePskSenderCore = (enc, info, psk, id) => new ReturnedSender(suite); + hpke.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); + } + } + } + } + + [Theory] + [InlineData(HpkeKdf.HKDF_SHA256)] + [InlineData(HpkeKdf.HKDF_SHA384)] + [InlineData(HpkeKdf.HKDF_SHA512)] + public static void PskInputs_HkdfMaximumInputSize(HpkeKdf kdf) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + using (HpkeContract hpke = new(suite)) + { + hpke.OnCreatePskSenderCore = (enc, info, psk, id) => new ReturnedSender(suite); + + hpke.CreatePskSender( + SpanOfLength(HpkeTestData.MaximumInputSizeInBytes), + SpanOfLength(HpkeTestData.MaximumInputSizeInBytes), + out _).Dispose(); + AssertExtensions.Throws( + "psk", + () => hpke.CreatePskSender( + SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1), + new byte[1], + out _)); + AssertExtensions.Throws( + "pskId", + () => hpke.CreatePskSender( + new byte[32], + SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1), + out _)); + } + } + + [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)) + { + hpke.OnSealCore = (p, e, c, aad, info) => + { + Assert.True(p.IsEmpty); + Assert.True(aad.IsEmpty); + Assert.True(info.IsEmpty); + }; + hpke.OnOpenCore = (e, c, p, aad, info) => + { + Assert.True(p.IsEmpty); + Assert.True(aad.IsEmpty); + Assert.True(info.IsEmpty); + }; + hpke.OnCreateSenderCore = (e, info) => + { + Assert.True(info.IsEmpty); + return new ReturnedSender(s_suite); + }; + hpke.OnCreateRecipientCore = (e, info) => + { + Assert.True(info.IsEmpty); + return new ReturnedRecipient(s_suite); + }; + hpke.OnCreatePskSenderCore = (e, info, key, id) => + { + Assert.True(info.IsEmpty); + return new ReturnedSender(s_suite); + }; + hpke.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)) + { + hpke.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)) + { + hpke.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)) + { + hpke.OnCreateSenderCore = (enc, info) => new ReturnedSender(s_suite); + hpke.OnCreateRecipientCore = (enc, info) => new ReturnedRecipient(s_suite); + hpke.OnCreatePskSenderCore = (enc, info, psk, id) => new ReturnedSender(s_suite); + hpke.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)) + { + hpke.OnSealCore = (p, enc, ct, aad, info) => { }; + hpke.OnOpenCore = (enc, ct, p, aad, info) => { }; + hpke.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)) + { + hpke.OnExportDecapsulationKeyCore = destination => throw exception; + hpke.OnExportEncapsulationKeyCore = destination => throw exception; + hpke.OnSealCore = (p, enc, ct, aad, info) => throw exception; + hpke.OnOpenCore = (enc, ct, p, aad, info) => throw exception; + hpke.OnCreateSenderCore = (enc, info) => throw exception; + hpke.OnCreateRecipientCore = (enc, info) => throw exception; + hpke.OnCreatePskSenderCore = (enc, info, psk, id) => throw exception; + hpke.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)) + { + hpke.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 unsafe ReadOnlySpan SpanOfLength(int length) => + new ReadOnlySpan((void*)1, length); + + 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) + { + int maximumLength = HasInputLengthLimit(Suite.KdfAlgorithm) ? + ushort.MaxValue : + HpkeTestData.MaximumInputSizeInBytes; + Assert.InRange(info.Length, 0, maximumLength); + } + + private void AssertPskInputs(ReadOnlySpan psk, ReadOnlySpan pskId) + { + int maximumLength = HasInputLengthLimit(Suite.KdfAlgorithm) ? + ushort.MaxValue : + HpkeTestData.MaximumInputSizeInBytes; + 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/Common/tests/System/Security/Cryptography/HpkeImplementationTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeImplementationTests.cs new file mode 100644 index 00000000000000..a20d20cc41c15d --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeImplementationTests.cs @@ -0,0 +1,669 @@ +// 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())) + { + 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 recipient = CreateRecipient(key, vector, enc)) + { + AssertKnownExports(recipient, vector.Exports); + + foreach (HpkeMessageVector message in vector.Messages) + { + byte[] plaintext = message.Plaintext.HexToByteArray(); + byte[] ciphertext = message.Ciphertext.HexToByteArray(); + byte[] aad = message.AssociatedData.HexToByteArray(); + + byte[] destination = GuardedBuffer(plaintext.Length); + recipient.Open(ciphertext, destination.AsSpan(1, plaintext.Length), aad); + AssertGuardedOutput(plaintext, destination); + } + + AssertKnownExports(recipient, vector.Exports); + } + } + + [Theory] + [MemberData(nameof(SupportedSuites))] + public static void SingleShot_Roundtrip(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + HpkeSuite suite = new(kem, kdf, aead); + byte[] info = new byte[1024]; + info.AsSpan().Fill(0x3C); + + using (Hpke privateKey = Hpke.GenerateKey(suite)) + using (Hpke publicKey = Hpke.ImportEncapsulationKey(suite, privateKey.ExportEncapsulationKey())) + { + foreach (int length in new[] { 0, 1, 257 }) + { + AssertSingleShotRoundtrip( + privateKey, publicKey, length, "associated data"u8, info); + } + } + } + + [Theory] + [MemberData(nameof(SupportedSuites))] + public static void SingleShot_Roundtrip_EmptyAadAndInfo(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())) + { + AssertSingleShotRoundtrip( + privateKey, publicKey, plaintextLength: 32, ReadOnlySpan.Empty, ReadOnlySpan.Empty); + } + } + + [Theory] + [MemberData(nameof(SupportedVectorNames))] + public static void Contexts_Roundtrip(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 = CreateSender(publicKey, vector.UsePsk, psk, pskId, + encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes), info)) + { + AssertGuards(encBuffer); + byte[] enc = encBuffer.AsSpan(1, suite.EncapsulatedSecretSizeInBytes).ToArray(); + + using (HpkeRecipient recipient = CreateRecipient( + privateKey, vector.UsePsk, enc.AsSpan(), psk, pskId, 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_FirstCiphertextByteModified(string name) + { + AssertOpenAuthenticationFailure(name, static inputs => inputs.Ciphertext[0] ^= 1); + } + + [Theory] + [MemberData(nameof(BaseVectorNames))] + public static void Open_AuthenticationFailure_LastCiphertextByteModified(string name) + { + AssertOpenAuthenticationFailure( + name, static inputs => inputs.Ciphertext[inputs.Ciphertext.Length - 1] ^= 1); + } + + [Theory] + [MemberData(nameof(BaseVectorNames))] + public static void Open_AuthenticationFailure_DifferentAssociatedData(string name) + { + AssertOpenAuthenticationFailure( + name, static inputs => inputs.AssociatedData = Different(inputs.AssociatedData)); + } + + [Theory] + [MemberData(nameof(BaseVectorNames))] + public static void Open_AuthenticationFailure_DifferentInfo(string name) + { + AssertOpenAuthenticationFailure(name, static inputs => inputs.Info = Different(inputs.Info)); + } + + [Theory] + [MemberData(nameof(BaseVectorNames))] + public static void Open_AuthenticationFailure_DifferentEncapsulatedSecret(string name) + { + AssertOpenAuthenticationFailure( + name, static inputs => inputs.EncapsulatedSecret = inputs.DifferentEncapsulatedSecret); + } + + [Theory] + [MemberData(nameof(BaseVectorNames))] + public static void Open_AuthenticationFailure_DifferentKey(string name) + { + AssertOpenAuthenticationFailure(name, static inputs => inputs.Recipient = inputs.WrongKey); + } + + [Theory] + [MemberData(nameof(RepresentativeSuiteModes))] + public static void Recipient_AuthenticationFailureAndOrdering( + HpkeKem kem, HpkeKdf kdf, HpkeAead aead, bool usePsk) + { + HpkeSuite suite = new(kem, kdf, aead); + ReadOnlySpan info = "application context"u8; + byte[] aad = "associated data"u8.ToArray(); + byte[] psk = new byte[32]; + ReadOnlySpan pskId = "identifier"u8; + ReadOnlySpan first = "first"u8; + ReadOnlySpan second = "second"u8; + ReadOnlySpan third = "third"u8; + byte[] enc; + + using (Hpke key = Hpke.GenerateKey(suite)) + using (Hpke wrongKey = Hpke.GenerateKey(suite)) + using (HpkeSender sender = CreateSender(key, usePsk, psk, pskId, out enc, info)) + using (HpkeRecipient recipient = CreateRecipient(key, usePsk, enc, psk, pskId, info)) + using (HpkeRecipient badKey = CreateRecipient(wrongKey, usePsk, enc, psk, pskId, info)) + using (HpkeRecipient badInfo = CreateRecipient(key, usePsk, enc, psk, pskId, Different(info))) + using (HpkeRecipient wrongMode = CreateRecipient(key, !usePsk, 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)); + AssertExtensions.SequenceEqual(first, recipient.Open(firstCiphertext, aad)); + AssertAuthenticationFailure(recipient, firstCiphertext, aad); + AssertAuthenticationFailure(recipient, thirdCiphertext, aad); + AssertExtensions.SequenceEqual(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(); + ReadOnlySpan message = "message"u8; + byte[] firstEnc; + byte[] secondEnc; + Hpke key = Hpke.GenerateKey(suite); + HpkeSender first = CreateSender(key, usePsk, psk, pskId, out firstEnc, info); + HpkeRecipient firstRecipient = CreateRecipient(key, usePsk, firstEnc, psk, pskId, info); + HpkeSender second = CreateSender(key, usePsk, psk, pskId, out secondEnc, info); + + using (HpkeRecipient secondRecipient = CreateRecipient(key, usePsk, secondEnc, psk, pskId, 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(); + + AssertExtensions.SequenceEqual(message, firstRecipient.Open(first.Seal(message))); + AssertExtensions.SequenceEqual(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(); + + AssertExtensions.SequenceEqual(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]; + ReadOnlySpan info = "application context"u8; + byte[] psk = new byte[32]; + ReadOnlySpan pskId = "identifier"u8; + ReadOnlySpan context = "exporter context"u8; + byte[] enc; + + using (Hpke key = Hpke.GenerateKey(suite)) + using (HpkeSender sender = CreateSender(key, usePsk, psk, pskId, out enc, info)) + using (HpkeRecipient recipient = CreateRecipient(key, usePsk, enc, psk, pskId, 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, recipient.Export(context, length)); + } + + 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[] { 200, 300 }) // 300 pushes past the 256-byte stack buffer. + { + byte[] longContext = new byte[contextLength]; + longContext.AsSpan().Fill(0x39); + Assert.Equal(sender.Export(longContext, 32), recipient.Export(longContext, 32)); + } + + ReadOnlySpan message = "message"u8; + + 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)); + AssertExtensions.SequenceEqual(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 void AssertOpenAuthenticationFailure(string name, Action tamper) + { + HpkeTestVector vector = HpkeTestData.GetVector(name); + HpkeSuite suite = Suite(vector); + ReadOnlySpan plaintext = "plaintext"u8; + byte[] associatedData = "associated data"u8.ToArray(); + byte[] info = vector.Info.HexToByteArray(); + int plaintextLength = plaintext.Length; + + using (Hpke key = Hpke.ImportDecapsulationKey(suite, vector.DecapsulationKey.HexToByteArray())) + using (Hpke wrongKey = Hpke.GenerateKey(suite)) + using (HpkeSender unrelated = key.CreateSender(out byte[] differentEncapsulatedSecret, info)) + { + key.Seal(plaintext, out byte[] encapsulatedSecret, out byte[] ciphertext, associatedData, info); + OpenFailureInputs inputs = new( + key, + wrongKey, + encapsulatedSecret, + differentEncapsulatedSecret, + ciphertext, + associatedData, + info); + tamper(inputs); + + Assert.Throws(() => + inputs.Recipient.Open( + inputs.EncapsulatedSecret, + inputs.Ciphertext, + associatedData: inputs.AssociatedData, + info: inputs.Info)); + Assert.Throws(() => + inputs.Recipient.Open( + inputs.EncapsulatedSecret.AsSpan(), + inputs.Ciphertext, + associatedData: inputs.AssociatedData, + info: inputs.Info)); + byte[] destination = GuardedBuffer(plaintextLength); + Assert.Throws(() => + inputs.Recipient.Open( + inputs.EncapsulatedSecret, + inputs.Ciphertext, + destination.AsSpan(1, plaintextLength), + inputs.AssociatedData, + inputs.Info)); + AssertGuardedOutput(new byte[plaintextLength], destination); + AssertExtensions.SequenceEqual( + plaintext, + key.Open( + encapsulatedSecret, + ciphertext, + associatedData: associatedData, + info: info)); + } + } + + private static void AssertSingleShotRoundtrip( + Hpke privateKey, + Hpke publicKey, + int plaintextLength, + ReadOnlySpan aad, + ReadOnlySpan info) + { + byte[] plaintext = new byte[plaintextLength]; + plaintext.AsSpan().Fill(0xA7); + byte[] encBuffer = GuardedBuffer(publicKey.Suite.EncapsulatedSecretSizeInBytes); + byte[] ciphertextBuffer = GuardedBuffer(publicKey.Suite.GetCiphertextLength(plaintextLength)); + + 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(); + byte[] destination = GuardedBuffer(plaintextLength); + privateKey.Open(enc, ciphertext, destination.AsSpan(1, plaintextLength), aad, info); + AssertGuardedOutput(plaintext, destination); + + using (HpkeRecipient recipient = privateKey.CreateRecipient(enc.AsSpan(), info)) + { + destination.AsSpan().Fill(0xA5); + recipient.Open(ciphertext, destination.AsSpan(1, plaintextLength), aad); + AssertGuardedOutput(plaintext, destination); + } + } + + private static HpkeSender CreateSender( + Hpke key, + bool usePsk, + ReadOnlySpan psk, + ReadOnlySpan pskId, + out byte[] enc, + ReadOnlySpan info) + { + return usePsk + ? key.CreatePskSender(psk, pskId, out enc, info) + : key.CreateSender(out enc, info); + } + + private static HpkeSender CreateSender( + Hpke key, + bool usePsk, + ReadOnlySpan psk, + ReadOnlySpan pskId, + Span enc, + ReadOnlySpan info) + { + return usePsk + ? key.CreatePskSender(psk, pskId, enc, info) + : key.CreateSender(enc, info); + } + + private static HpkeRecipient CreateRecipient( + Hpke key, + bool usePsk, + ReadOnlySpan enc, + ReadOnlySpan psk, + ReadOnlySpan pskId, + ReadOnlySpan info) + { + return usePsk + ? key.CreatePskRecipient(enc, psk, pskId, info) + : key.CreateRecipient(enc, info); + } + + private static HpkeRecipient CreateRecipient(Hpke key, HpkeTestVector vector, ReadOnlySpan enc) + { + byte[] info = vector.Info.HexToByteArray(); + + if (vector.UsePsk) + { + byte[] psk = vector.Psk.HexToByteArray(); + byte[] pskId = vector.PskId.HexToByteArray(); + return key.CreatePskRecipient(enc, psk, pskId, info); + } + + return 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(); + 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(ReadOnlySpan input) + { + byte[] result = input.IsEmpty ? [1] : input.ToArray(); + result[0] ^= 0x80; + return result; + } + + private sealed class OpenFailureInputs + { + internal Hpke Recipient { get; set; } + internal Hpke WrongKey { get; } + internal byte[] EncapsulatedSecret { get; set; } + internal byte[] DifferentEncapsulatedSecret { get; } + internal byte[] Ciphertext { get; } + internal byte[] AssociatedData { get; set; } + internal byte[] Info { get; set; } + + internal OpenFailureInputs( + Hpke recipient, + Hpke wrongKey, + byte[] encapsulatedSecret, + byte[] differentEncapsulatedSecret, + byte[] ciphertext, + byte[] associatedData, + byte[] info) + { + Recipient = recipient; + WrongKey = wrongKey; + EncapsulatedSecret = encapsulatedSecret; + DifferentEncapsulatedSecret = differentEncapsulatedSecret; + Ciphertext = (byte[])ciphertext.Clone(); + AssociatedData = associatedData; + Info = info; + } + } + + 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/Common/tests/System/Security/Cryptography/HpkeKeyTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeKeyTests.cs new file mode 100644 index 00000000000000..9e0c812a87f4f9 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeKeyTests.cs @@ -0,0 +1,449 @@ +// 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(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/Common/tests/System/Security/Cryptography/HpkeNotSupportedTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeNotSupportedTests.cs new file mode 100644 index 00000000000000..fb8d7817063a05 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeNotSupportedTests.cs @@ -0,0 +1,36 @@ +// 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 +{ + public static class HpkeNotSupportedTests + { + [Fact] + public static void KeyFactories_NotSupported() + { + 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); + + 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.ImportEncapsulationKey(suite, publicKey)); + Assert.Throws( + () => Hpke.ImportEncapsulationKey(suite, publicKey.AsSpan())); + } + } + } + } +} 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..ece113dcc0d999 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeRecipientContractTests.cs @@ -0,0 +1,629 @@ +// 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)) + { + recipient.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); + AssertExtensions.SequenceEqual([0x71, 0x72, 0x73], (ReadOnlySpan)associatedData); + Assert.Equal(1, recipient.OpenCoreCount); + } + } + } + + [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)) + { + recipient.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); + AssertExtensions.SequenceEqual( + [0xA5, 0x71, 0x72, 0x73, 0xA5], + (ReadOnlySpan)aadBuffer); + Assert.Equal(1, recipient.OpenCoreCount); + } + } + } + + [Fact] + public static void Open_OptionalAssociatedDataIsEmpty() + { + byte[] ciphertext = Data(s_suite.GetCiphertextLength(1)); + + using (HpkeRecipientContract recipient = new(s_suite)) + { + recipient.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)) + { + recipient.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)) + { + recipient.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)) + { + recipient.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); + } + } + } + + [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(HpkeKdf.HKDF_SHA256)] + [InlineData(HpkeKdf.HKDF_SHA384)] + [InlineData(HpkeKdf.HKDF_SHA512)] + [InlineData(HpkeKdf.SHAKE128)] + [InlineData(HpkeKdf.SHAKE256)] + public static void Export_ContextMaximumInputSize(HpkeKdf kdf) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + using (HpkeRecipientContract recipient = new(suite)) + { + recipient.OnExportCore = static (context, destination) => { }; + + recipient.Export(SpanOfLength(HpkeTestData.MaximumInputSizeInBytes), 0); + AssertExtensions.Throws( + "exporterContext", + () => recipient.Export(SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1), 0)); + AssertExtensions.Throws( + "exporterContext", + () => recipient.Export(SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1), Span.Empty)); + } + } + + [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)) + { + recipient.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)) + { + recipient.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); + } + } + + [Fact] + public static void Export_CoreFailurePropagatesUnchanged() + { + CryptographicException exception = new(); + + using (HpkeRecipientContract recipient = new(s_suite)) + { + recipient.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); + } + } + + 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 unsafe ReadOnlySpan SpanOfLength(int length) => + new ReadOnlySpan((void*)1, length); + + 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..1a4a9082c28fad --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSenderContractTests.cs @@ -0,0 +1,578 @@ +// 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)) + { + sender.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); + AssertExtensions.SequenceEqual([0x71, 0x72, 0x73], (ReadOnlySpan)associatedData); + Assert.Equal(1, sender.SealCoreCount); + } + } + } + + [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)) + { + sender.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); + AssertExtensions.SequenceEqual( + [0xA5, 0x71, 0x72, 0x73, 0xA5], + (ReadOnlySpan)aadBuffer); + Assert.Equal(1, sender.SealCoreCount); + } + } + } + + [Fact] + public static void Seal_OptionalAssociatedDataIsEmpty() + { + using (HpkeSenderContract sender = new(s_suite)) + { + sender.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)) + { + sender.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)) + { + sender.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)) + { + sender.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); + } + } + } + + [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(HpkeKdf.HKDF_SHA256)] + [InlineData(HpkeKdf.HKDF_SHA384)] + [InlineData(HpkeKdf.HKDF_SHA512)] + [InlineData(HpkeKdf.SHAKE128)] + [InlineData(HpkeKdf.SHAKE256)] + public static void Export_ContextMaximumInputSize(HpkeKdf kdf) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, kdf, HpkeAead.AES_128_GCM); + + using (HpkeSenderContract sender = new(suite)) + { + sender.OnExportCore = static (context, destination) => { }; + + sender.Export(SpanOfLength(HpkeTestData.MaximumInputSizeInBytes), 0); + AssertExtensions.Throws( + "exporterContext", + () => sender.Export(SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1), 0)); + AssertExtensions.Throws( + "exporterContext", + () => sender.Export(SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1), Span.Empty)); + } + } + + [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)) + { + sender.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)) + { + sender.OnSealCore = (p, ct, aad) => throw exception; + sender.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 unsafe ReadOnlySpan SpanOfLength(int length) => + new ReadOnlySpan((void*)1, length); + + 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/HpkeSuiteTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs new file mode 100644 index 00000000000000..cc502c4df1d2f1 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeSuiteTests.cs @@ -0,0 +1,233 @@ +// 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(19)] + [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] + [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)] + [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.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, + "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); + } + + [Fact] + public static void GetCiphertextLength() + { + foreach (HpkeAead aead in Enum.GetValues(typeof(HpkeAead))) + { + HpkeSuite suite = new(HpkeKem.MLKEM_768, HpkeKdf.HKDF_SHA256, aead); + + foreach (int length in new int[] { 0, 1, 15, 16, 17, 1024 }) + { + Assert.Equal(length + suite.AeadTagSizeInBytes, suite.GetCiphertextLength(length)); + } + + Assert.Equal(int.MaxValue, suite.GetCiphertextLength(int.MaxValue - suite.AeadTagSizeInBytes)); + } + } + + [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) + { + 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/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..0a4095d037b609 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTestData.cs @@ -0,0 +1,89 @@ +// 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 + { + internal const int MaximumInputSizeInBytes = 256 * 1024 * 1024; + + // 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( + Rfc9180Vectors().Concat(PqDraftVectors()).Concat(GeneratedVectors()).ToArray()); + + 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 KemAlgorithms() + { + foreach (HpkeKem kem in Enum.GetValues(typeof(HpkeKem))) + { + yield return [kem]; + } + } + + 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..b60ebcfdb4dc1c --- /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) + { + 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); + } + } + + [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/Common/tests/System/Security/Cryptography/HpkeTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTests.cs new file mode 100644 index 00000000000000..5266c630cc4cd9 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeTests.cs @@ -0,0 +1,134 @@ +// 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 DeriveKey_IkmTooLong() + { + HpkeSuite suite = new( + HpkeKem.DHKEM_P256_HKDF_SHA256, + HpkeKdf.HKDF_SHA256, + HpkeAead.AES_128_GCM); + + AssertExtensions.Throws( + "ikm", + () => Hpke.DeriveKey(suite, SpanOfLength(HpkeTestData.MaximumInputSizeInBytes + 1))); + } + + private static unsafe ReadOnlySpan SpanOfLength(int length) => + new ReadOnlySpan((void*)1, length); + + [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/src/Microsoft.Bcl.Cryptography.Forwards.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs index 5a1e4fe23c3ccf..d93ab52526a0f5 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,13 @@ #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.CompositeMLKemCng))] [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 64da18a2cbf03c..b3281bb0f301e0 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 @@ -21,7 +21,9 @@ true true - true + + true + true @@ -35,6 +37,31 @@ + + + + + + + + + + + + + + @@ -652,6 +679,8 @@ + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx index 56275774db18ee..f99af86bff870b 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -69,6 +69,36 @@ 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 exporter context exceeds the maximum length of {0} bytes. + + + The input keying material exceeds the maximum length of {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. 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 567bedbd26023a..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 @@ -4,7 +4,7 @@ $(NetFrameworkCurrent);$(NetCoreAppCurrent) true true - $(NoWarn);SYSLIB5006 + $(NoWarn);SYSLIB5006;SYSLIB5009 ../src/Resources/Strings.resx true true @@ -127,6 +127,32 @@ 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 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); + 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() { } + 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 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; } + 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)) { } + 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 + { + 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_P521_HKDF_SHA512 = 18, + 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 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) { } + public System.Security.Cryptography.HpkeAead AeadAlgorithm { get { throw null; } } + public int AeadTagSizeInBytes { get { throw null; } } + public int DecapsulationKeySizeInBytes { get { throw null; } } + public int EncapsulatedSecretSizeInBytes { get { throw null; } } + public int EncapsulationKeySizeInBytes { get { throw null; } } + public System.Security.Cryptography.HpkeKdf KdfAlgorithm { get { throw null; } } + public System.Security.Cryptography.HpkeKem KemAlgorithm { get { throw null; } } + public string Name { 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 int GetCiphertextLength(int plaintextLength) { 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 override string ToString() { throw null; } + } public partial interface ICryptoTransform : System.IDisposable { bool CanReuseTransform { get; } diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 5594937b25238c..04402cf489a66c 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -147,6 +147,36 @@ 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 exported secret length can be at most {0} bytes. + + + The exporter context exceeds the maximum length of {0} bytes. + + + The input keying material exceeds the maximum length of {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. + The specified mu value is not the correct length for the ML-DSA algorithm. @@ -447,6 +477,12 @@ 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 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.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index cb2e45e1488afc..6721de217889ac 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 @@ -406,6 +406,26 @@ Link="Common\System\Security\Cryptography\DSAKeyFormatHelper.cs" /> + + + + + + + + + + + @@ -617,6 +638,8 @@ + + @@ -877,6 +900,7 @@ + @@ -1437,7 +1461,6 @@ - @@ -2097,6 +2120,18 @@ + + + + + + + + + + + + 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/HpkeAeadMetadata.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeAeadMetadata.Managed.cs new file mode 100644 index 00000000000000..e79abaa195206f --- /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 + 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; + } + } + } + } +} 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..003eacdc26efc4 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeECDiffieHellmanKemAdapter.cs @@ -0,0 +1,270 @@ +// 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 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(), + }; + + 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), + HpkeKem.DHKEM_P521_HKDF_SHA512 => (ECCurve.NamedCurves.nistP521, (byte)0x01), + _ => throw new UnreachableException(), + }; + } + + 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)) + { + _ecdh = ECDiffieHellman.Create(new ECParameters + { + Curve = _curve, + D = privateKey, + }); + } + } + + 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); + } + + // 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; + + // Windows reports an invalid EC point as PlatformNotSupportedException. Suite support has already + // been validated, so normalize this to the documented invalid-key exception. + try + { + return ECDiffieHellman.Create(parameters); + } + catch (PlatformNotSupportedException e) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey, e); + } + } + + 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]; + Span prkBuffer = stackalloc byte[PrkStackBufferSize]; + + using (PinAndClear.Track(privateKey)) + { + try + { + Span prk = prkBuffer.Slice(0, KeyDerivationKdf.Nh); + LabeledExtract(ReadOnlySpan.Empty, "dkp_prk"u8, ikm, prk); + Span counterBytes = stackalloc byte[1]; + + for (int counter = 0; counter <= byte.MaxValue; counter++) + { + 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)) + { + _ecdh = ECDiffieHellman.Create(new ECParameters + { + Curve = _curve, + D = privateKey, + }); + return; + } + } + + throw new CryptographicException(SR.Cryptography_HpkeKeyDerivationFailed); + } + finally + { + CryptographicOperations.ZeroMemory(prkBuffer); + } + } + } + + 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); + } + } + } + + // 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); + 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); + } + } + + 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)] + 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 new file mode 100644 index 00000000000000..713f773564bc68 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.Managed.cs @@ -0,0 +1,496 @@ +// 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 + { + 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) => + suite.KemMetadata.IsSupported && + suite.KdfMetadata.IsSupported && + suite.AeadMetadata.IsSupported; + + internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan ikm) + { + HpkeManagedKemAdapter adapter = HpkeManagedKemAdapter.Create(suite); + + try + { + adapter.DeriveKeyPair(ikm); + return new HpkeImplementation(suite, adapter); + } + catch + { + adapter.Dispose(); + throw; + } + } + + internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite) + { + HpkeManagedKemAdapter adapter = HpkeManagedKemAdapter.Create(suite); + + try + { + adapter.Generate(); + return new HpkeImplementation(suite, adapter); + } + catch + { + adapter.Dispose(); + throw; + } + } + + 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); + + protected override void ExportEncapsulationKeyCore(Span destination) => + _kemAdapter.ExportEncapsulationKey(destination); + + protected override void SealCore( + ReadOnlySpan plaintext, + Span encapsulatedSecret, + Span ciphertext, + 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.Encapsulate(encapsulatedSecret, sharedSecret); + + _kdfAdapter.DeriveSecrets( + mode: 0, + sharedSecret, + info, + psk: default, + pskId: default, + key, + baseNonce, + exporterSecret); + + 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, + associatedData, + ciphertext.Slice(0, plaintext.Length), + ciphertext.Slice(plaintext.Length)); + } + } + finally + { + CryptographicOperations.ZeroMemory(sharedSecretBuffer); + CryptographicOperations.ZeroMemory(keyBuffer); + CryptographicOperations.ZeroMemory(exporterSecretBuffer); + } + } + + 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); + + _kdfAdapter.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(exporterSecretBuffer); + } + } + + 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]; + 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); + + _kdfAdapter.DeriveSecrets( + mode, + sharedSecret, + info, + psk, + pskId, + key, + baseNonce, + exporterSecret); + + HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key); + + try + { + return new HpkeSenderImplementation(Suite, aead, _kdfAdapter, baseNonce, exporterSecret); + } + catch + { + aead.Dispose(); + throw; + } + } + finally + { + CryptographicOperations.ZeroMemory(sharedSecretBuffer); + CryptographicOperations.ZeroMemory(keyBuffer); + CryptographicOperations.ZeroMemory(exporterSecretBuffer); + } + } + + protected override HpkeRecipient CreateRecipientCore( + ReadOnlySpan encapsulatedSecret, + 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]; + 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); + + _kdfAdapter.DeriveSecrets( + mode, + sharedSecret, + info, + psk, + pskId, + key, + baseNonce, + exporterSecret); + + HpkeManagedAeadAdapter aead = HpkeManagedAeadAdapter.Create(Suite, key); + + try + { + return new HpkeRecipientImplementation(Suite, aead, _kdfAdapter, baseNonce, exporterSecret); + } + catch + { + aead.Dispose(); + throw; + } + } + finally + { + CryptographicOperations.ZeroMemory(sharedSecretBuffer); + CryptographicOperations.ZeroMemory(keyBuffer); + CryptographicOperations.ZeroMemory(exporterSecretBuffer); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _kemAdapter.Dispose(); + } + + base.Dispose(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; + private ConcurrencyBlock _block; + + internal HpkeSenderImplementation( + HpkeSuite suite, + HpkeManagedAeadAdapter aeadAdapter, + 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( + ReadOnlySpan plaintext, + Span ciphertext, + ReadOnlySpan associatedData) + { + // While this API is not documented as thread-safe, we block concurrent calls to prevent silent nonce reuse. + using (ConcurrencyBlock.Enter(ref _block)) + { + if (_sequenceNumber == ulong.MaxValue) + { + 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); + + _aeadAdapter.Encrypt( + plaintext, + nonce, + associatedData, + ciphertext.Slice(0, plaintext.Length), + ciphertext.Slice(plaintext.Length)); + _sequenceNumber++; + } + } + + 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) + { + if (disposing) + { + _aeadAdapter.Dispose(); + _exporterSecret.Dispose(); + } + + base.Dispose(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, + 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( + ReadOnlySpan ciphertext, + 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); + } + + 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.Decrypt( + ciphertext.Slice(0, plaintext.Length), + nonce, + associatedData, + ciphertext.Slice(plaintext.Length), + plaintext); + _sequenceNumber++; + } + + 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) + { + if (disposing) + { + _aeadAdapter.Dispose(); + _exporterSecret.Dispose(); + } + + base.Dispose(disposing); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.NotSupported.cs new file mode 100644 index 00000000000000..286dcfc3a4b3f3 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeImplementation.NotSupported.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.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(); + } + + 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; + 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 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 HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info) => + throw new PlatformNotSupportedException(); + + protected override HpkeRecipient CreateRecipientCore( + ReadOnlySpan encapsulatedSecret, + 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."); + throw new CryptographicException(); + } + } +} 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..7caa891de7ecfb --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeKemMetadata.Managed.cs @@ -0,0 +1,94 @@ +// 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 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: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA256); + break; + case HpkeKem.DHKEM_P384_HKDF_SHA384: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA384); + break; + case HpkeKem.DHKEM_P521_HKDF_SHA512: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA512); + break; + case HpkeKem.DHKEM_X25519_HKDF_SHA256: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.HKDF_SHA256); + break; + case HpkeKem.MLKEM_512: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM_768: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM_1024: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM768_P256: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); + break; + case HpkeKem.MLKEM1024_P384: + (KemKdf, SuiteId) = CreateMetadata(Kem, HpkeKdf.SHAKE256); + break; + default: + Debug.Fail($"Missing KEM KDF mapping for {Kem}."); + throw new CryptographicException(); + } + + static (HpkeKdfMetadata Metadata, byte[] SuiteId) CreateMetadata(HpkeKem kem, HpkeKdf kdf) + { + HpkeKdfMetadata? metadata = HpkeKdfMetadata.Create(kdf); + + if (metadata is null) + { + Debug.Fail("KEM depends on unmapped KDF."); + throw new CryptographicException(); + } + + byte[] suiteId = [.."KEM"u8, 0x00, 0x00]; + BinaryPrimitives.WriteUInt16BigEndian(suiteId.AsSpan(^2), checked((ushort)kem)); + return (metadata, suiteId); + } + } + + internal bool IsSupported + { + get + { + switch (Kem) + { + 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; + case HpkeKem.MLKEM_512: + case HpkeKem.MLKEM_768: + case HpkeKem.MLKEM_1024: + case HpkeKem.MLKEM768_P256: + case HpkeKem.MLKEM1024_P384: + 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/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..d8ad2d6515077d --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedAesAeadAdapter.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. + +namespace System.Security.Cryptography +{ + internal sealed class HpkeManagedAesAeadAdapter : HpkeManagedAeadAdapter + { + private readonly AesGcm _aes; + + 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( + ReadOnlySpan plaintext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + Span ciphertext, + Span tag) + { +#pragma warning disable CA1416 + _aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData); +#pragma warning restore CA1416 + } + + internal override void Decrypt( + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + 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 new file mode 100644 index 00000000000000..0dd7add87d08b3 --- /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. + +namespace System.Security.Cryptography +{ +#pragma warning disable CA1416 + 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(); + } +#pragma warning restore CA1416 +} 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..5997275be7c89c --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKdfAdapter.cs @@ -0,0 +1,326 @@ +// 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) + { + 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 + { + CryptographicOperations.ZeroMemory(key); + CryptographicOperations.ZeroMemory(exporterSecret); + throw; + } + } + + // 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); + Debug.Assert(destination.Length <= Suite.KdfMetadata.MaximumExportLength); + + // 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; + // 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]; + + try + { + 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(secretBuffer); + } + } + + 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) + { + ReadOnlySpan suiteId = SuiteId; + int labeledIkmLength = checked(VersionLabel.Length + suiteId.Length + label.Length + ikm.Length); + + using (CryptoPoolLease labeledIkm = CryptoPoolLease.Rent(labeledIkmLength)) + { + Span destination = labeledIkm.Span; + VersionLabel.CopyTo(destination); + int offset = VersionLabel.Length; + suiteId.CopyTo(destination.Slice(offset)); + offset += suiteId.Length; + label.CopyTo(destination.Slice(offset)); + offset += label.Length; + ikm.CopyTo(destination.Slice(offset)); + + int written = HKDF.Extract(_hashAlgorithm, destination, salt, 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; + + 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); + } + } + + internal abstract class HpkeManagedShakeKdfAdapter : HpkeManagedKdfAdapter + where TShake : class, IDisposable + { + 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 outputLength = checked(key.Length + baseNonce.Length + exporterSecret.Length); + const int MaxStackOutputLength = 128; + + // Current suites require at most 32 + 12 + 64 bytes for these outputs. + Debug.Assert(outputLength <= MaxStackOutputLength); + + using (TShake shake = CreateShake()) + { + Span outputBuffer = stackalloc byte[MaxStackOutputLength]; + + try + { + Span output = outputBuffer.Slice(0, outputLength); + + 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); + } + finally + { + CryptographicOperations.ZeroMemory(outputBuffer); + } + } + } + + protected override void ExportSecretCore( + ReadOnlySpan exporterSecret, + ReadOnlySpan exporterContext, + Span destination) + { + using (TShake shake = CreateShake()) + { + Append(shake, exporterSecret); + AppendLabeledDerivePrefix(shake, "sec"u8, destination.Length); + Append(shake, exporterContext); + GetHashAndReset(shake, destination); + } + } + + // "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 void AppendLengthPrefixed(TShake shake, ReadOnlySpan value) + { + Span lengthBytes = stackalloc byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16BigEndian(lengthBytes, checked((ushort)value.Length)); + Append(shake, lengthBytes); + Append(shake, value); + } + + 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 HpkeManagedShake128KdfAdapter(HpkeSuite suite) : base(suite) + { + } + + 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 HpkeManagedShake256KdfAdapter(HpkeSuite suite) : base(suite) + { + } + + 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); + } +} 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..d1d8d2ebe1c13b --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeManagedKemAdapter.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + 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; + + 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: + case HpkeKem.DHKEM_P521_HKDF_SHA512: + return new HpkeECDiffieHellmanKemAdapter(suite); + case HpkeKem.DHKEM_X25519_HKDF_SHA256: + return new HpkeX25519DiffieHellmanKemAdapter(suite); + default: + throw new PlatformNotSupportedException(); + } + } + + internal void Generate() + { + const int MaxStackIkmSize = 128; + Span ikmStack = stackalloc byte[MaxStackIkmSize]; + + try + { + // https://datatracker.ietf.org/doc/draft-ietf-hpke-hpke/ 7.1.3 + // For all of the above instances of DHKEM, the GenerateKeyPair can be + // implemented as DeriveKeyPair(random(Nsk)). + Span ikm = ikmStack.Slice(0, Suite.KemMetadata.Nsk); + RandomNumberGenerator.Fill(ikm); + DeriveKeyPair(ikm); + } + finally + { + CryptographicOperations.ZeroMemory(ikmStack); + } + } + + // 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 + { + const int MaxStackContextLength = 512; + 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 + { + CryptographicOperations.ZeroMemory(prkBuffer); + } + } + + // 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); + + ReadOnlySpan suiteId = Suite.KemMetadata.SuiteId; + int labeledIkmLength = checked(VersionLabel.Length + suiteId.Length + label.Length + ikm.Length); + + using (CryptoPoolLease labeledIkm = CryptoPoolLease.Rent(labeledIkmLength)) + { + Span destination = labeledIkm.Span; + VersionLabel.CopyTo(destination); + int offset = VersionLabel.Length; + suiteId.CopyTo(destination.Slice(offset)); + offset += suiteId.Length; + label.CopyTo(destination.Slice(offset)); + offset += label.Length; + ikm.CopyTo(destination.Slice(offset)); + + int written = HKDF.Extract(KeyDerivationKdf.HkdfHashAlgorithm, destination, salt, 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 = 512; + Span labeledInfoBuffer = stackalloc byte[MaxStackLabeledInfoLength]; + 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); + 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); + 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..fc1528e359a01a --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HpkeX25519DiffieHellmanKemAdapter.cs @@ -0,0 +1,108 @@ +// 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 ImportDecapsulationKey(ReadOnlySpan decapsulationKey) + { + Debug.Assert(_x25519 is null); + _x25519 = X25519DiffieHellman.ImportPrivateKey(decapsulationKey); + } + + 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]; + Span prkBuffer = stackalloc byte[PrkStackBufferSize]; + + try + { + 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); + } + } + + 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); + } + } + } + + // 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); + Debug.Assert(destination.Length == Suite.DecapsulationKeySizeInBytes); + + _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/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index 74cea3f6cacc6a..aa00459d56e14f 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 @@ -4,7 +4,7 @@ true true $(NoWarn);SYSLIB0021;SYSLIB0026;SYSLIB0027;SYSLIB0028;SYSLIB0057 - $(NoWarn);SYSLIB5006 + $(NoWarn);SYSLIB5006;SYSLIB5009 true ../src/Resources/Strings.resx @@ -230,6 +230,32 @@ Link="CommonTest\System\Security\Cryptography\CompositeMLDsaAlgorithmTests.cs" /> + + + + + + + + + + + + +