Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f8e9316
Add experimental HPKE cipher suite descriptors
vcsjones Sep 5, 2026
68258cf
Complete HpkeSuite metadata and public API
vcsjones Sep 5, 2026
939cd7d
Add managed HPKE foundation
vcsjones Sep 5, 2026
e87f872
Split managed HPKE KEM adapters
vcsjones Sep 5, 2026
f57271e
Add HPKE key factory APIs
vcsjones Sep 6, 2026
fa21cda
Implement HPKE decapsulation key export
vcsjones Sep 7, 2026
d035fc0
Implement HPKE encapsulation key export
vcsjones Sep 7, 2026
f589d5a
Checkpoint HPKE Seal and managed AEAD adapters
vcsjones Sep 7, 2026
e621f1c
Add HPKE KEM encapsulation and key schedule adapters
vcsjones Sep 8, 2026
ce437ba
Implement HPKE single-shot sealing
vcsjones Sep 8, 2026
7ea28a5
Use stack buffers for fixed-size HPKE intermediates
vcsjones Sep 8, 2026
576af50
Implement HPKE single-shot opening
vcsjones Sep 8, 2026
6e25e20
Add abstract HPKE sender and recipient contexts
vcsjones Sep 8, 2026
1b0907f
Implement stateful HPKE sender creation and sealing
vcsjones Sep 8, 2026
c2ff6cd
Implement stateful HPKE recipient creation and opening
vcsjones Sep 8, 2026
cfc2d3b
Implement HPKE PSK sender and recipient modes
vcsjones Sep 8, 2026
8f4fd22
Implement P-521 DHKEM support for HPKE
vcsjones Sep 8, 2026
a798ae5
Implement HPKE context secret export
vcsjones Sep 9, 2026
4a10e1d
Implement HPKE key import APIs
vcsjones Sep 9, 2026
6f8aa63
Reject concurrent HPKE sender sealing
vcsjones Sep 9, 2026
4152869
Validate HPKE sender buffer overlaps
vcsjones Sep 9, 2026
9d8e8b6
Complete HPKE single-shot API documentation
vcsjones Sep 9, 2026
0961d98
Fix HPKE target wiring in Microsoft.Bcl.Cryptography
vcsjones Sep 9, 2026
6a80474
Validate HPKE Open and Export buffer overlaps
vcsjones Sep 9, 2026
32f473c
Refine HPKE validation order and temporary buffers
vcsjones Sep 9, 2026
850ea5f
Remove redundant HPKE key-schedule output staging
vcsjones Sep 10, 2026
b26bfdd
Stop clearing non-secret HPKE buffers
vcsjones Sep 10, 2026
f0afdec
Derive HPKE KEM suite IDs from enum values
vcsjones Sep 10, 2026
9badba1
Assert the internal HPKE export-length invariant
vcsjones Sep 10, 2026
68535fa
Reuse the KDF adapter across HPKE key operations
vcsjones Sep 10, 2026
60e3a09
Stream HPKE SHAKE inputs through public APIs
vcsjones Sep 10, 2026
52c1bc7
Merge remote-tracking branch 'ms/main' into hpke-impl
vcsjones Sep 10, 2026
6624230
Add shared HPKE contract tests
vcsjones Sep 11, 2026
eac840c
Add a representative HPKE test-vector corpus
vcsjones Sep 11, 2026
7dad5f4
Bound HPKE test exporter contexts to 1024 bytes
vcsjones Sep 11, 2026
b2b0339
Add shared HPKE sender and recipient contract tests
vcsjones Sep 11, 2026
af31501
Add shared HPKE key tests and prune legacy tests
vcsjones Sep 12, 2026
a4318a9
Add shared HPKE implementation tests
vcsjones Sep 12, 2026
c37a857
Simplify HPKE recipient documentation
vcsjones Sep 12, 2026
93cf8e5
Simplify HPKE sender and suite documentation
vcsjones Sep 12, 2026
627779e
Separate HPKE static validation from instance contracts
vcsjones Sep 12, 2026
2d362f9
Refine HPKE export tests and browser build exclusions
vcsjones Sep 12, 2026
befd9b0
Add comment clarifying why OpenCore does not have a concurrency guard
vcsjones Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Validate HPKE sender buffer overlaps
Reject input/output and output/output overlap before sender core operations while permitting read-only input aliasing. Document overlapping-buffer exceptions and cover rejection before mutation and adjacent buffer handling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5
  • Loading branch information
vcsjones and Copilot committed Sep 9, 2026
commit 4152869012e710c22bfa6cfbf6ffea9bb2aa5a74
78 changes: 76 additions & 2 deletions src/libraries/Common/src/System/Security/Cryptography/Hpke.cs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,45 @@ public void Seal(
ciphertext = ciphertextBuffer;
}

/// <summary>
/// Encrypts and authenticates a single message into the provided buffers using Base mode.
/// </summary>
/// <param name="plaintext">
/// The message to encrypt.
/// </param>
/// <param name="encapsulatedSecret">
/// The buffer to receive the encapsulated secret to send to the recipient.
/// </param>
/// <param name="ciphertext">
/// The buffer to receive the ciphertext followed by its authentication tag.
/// </param>
/// <param name="associatedData">
/// The additional data to authenticate without encrypting.
/// </param>
/// <param name="info">
/// The application context, which must match the value used by the recipient.
/// </param>
/// <exception cref="ArgumentException">
/// <paramref name="encapsulatedSecret" /> is not exactly <see cref="HpkeSuite.EncapsulatedSecretSizeInBytes" />
/// bytes long, <paramref name="ciphertext" /> is not exactly the length returned by
/// <see cref="HpkeSuite.GetCiphertextLength" /> for <paramref name="plaintext" />,
/// or <paramref name="info" /> exceeds the cipher suite's KDF length limit.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// The ciphertext length would exceed <see cref="int.MaxValue" />.
/// </exception>
/// <exception cref="CryptographicException">
/// <para>
/// One or more provided buffers overlap.
/// </para>
/// <para> -or- </para>
/// <para>
/// The current instance does not contain an encapsulation key, or an error occurred during encryption.
/// </para>
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The object has already been disposed.
/// </exception>
public void Seal(
ReadOnlySpan<byte> plaintext,
Span<byte> encapsulatedSecret,
Expand All @@ -509,6 +548,17 @@ public void Seal(
nameof(ciphertext));
}

if (encapsulatedSecret.Overlaps(ciphertext) ||
plaintext.Overlaps(encapsulatedSecret) ||
associatedData.Overlaps(encapsulatedSecret) ||
info.Overlaps(encapsulatedSecret) ||
plaintext.Overlaps(ciphertext) ||
associatedData.Overlaps(ciphertext) ||
info.Overlaps(ciphertext))
{
throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
}

SealCore(plaintext, encapsulatedSecret, ciphertext, associatedData, info);
}

Expand Down Expand Up @@ -807,7 +857,13 @@ public HpkeSender CreateSender(out byte[] encapsulatedSecret, ReadOnlySpan<byte>
/// </para>
/// </exception>
/// <exception cref="CryptographicException">
/// The current instance does not contain an encapsulation key, or an error occurred while creating the sender.
/// <para>
/// One or more provided buffers overlap.
/// </para>
/// <para> -or- </para>
/// <para>
/// The current instance does not contain an encapsulation key, or an error occurred while creating the sender.
/// </para>
/// </exception>
/// <exception cref="PlatformNotSupportedException">
/// Creating a sender is not supported on the current platform.
Expand All @@ -827,6 +883,11 @@ public HpkeSender CreateSender(Span<byte> encapsulatedSecret, ReadOnlySpan<byte>
nameof(encapsulatedSecret));
}

if (info.Overlaps(encapsulatedSecret))
{
throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
}

return CreateSenderCore(encapsulatedSecret, info);
}

Expand Down Expand Up @@ -1096,7 +1157,13 @@ public HpkeSender CreatePskSender(
/// <see cref="HpkeSuite.EncapsulatedSecretSizeInBytes" /> bytes long.
/// </exception>
/// <exception cref="CryptographicException">
/// The current instance does not contain an encapsulation key, or an error occurred while creating the sender.
/// <para>
/// One or more provided buffers overlap.
/// </para>
/// <para> -or- </para>
/// <para>
/// The current instance does not contain an encapsulation key, or an error occurred while creating the sender.
/// </para>
/// </exception>
/// <exception cref="PlatformNotSupportedException">
/// Creating a PSK sender is not supported on the current platform.
Expand Down Expand Up @@ -1125,6 +1192,13 @@ public HpkeSender CreatePskSender(
nameof(encapsulatedSecret));
}

if (psk.Overlaps(encapsulatedSecret) ||
pskId.Overlaps(encapsulatedSecret) ||
info.Overlaps(encapsulatedSecret))
{
throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
}

return CreatePskSenderCore(encapsulatedSecret, info, psk, pskId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ public byte[] Seal(byte[] plaintext, byte[]? associatedData = null)
/// The ciphertext length would exceed <see cref="int.MaxValue" />.
/// </exception>
/// <exception cref="CryptographicException">
/// The sender's message limit has been reached, or an error occurred during encryption.
/// <para>
/// One or more provided buffers overlap.
/// </para>
/// <para> -or- </para>
/// <para>
/// The sender's message limit has been reached, or an error occurred during encryption.
/// </para>
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The object has already been disposed.
Expand All @@ -140,6 +146,11 @@ public void Seal(
nameof(ciphertext));
}

if (plaintext.Overlaps(ciphertext) || associatedData.Overlaps(ciphertext))
{
throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
}

SealCore(plaintext, ciphertext, associatedData);
}

Expand Down
179 changes: 177 additions & 2 deletions src/libraries/System.Security.Cryptography/tests/HpkeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1731,9 +1731,180 @@ public static void Context_Export(HpkeKem kem, HpkeKdf kdf, HpkeAead aead)
}
}

[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(1)]
public static void Seal_RejectsOverlappingBuffers(int offset)
{
HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
// Indices: plaintext, associatedData, info, encapsulatedSecret, ciphertext.
(int First, int Second)[] pairs = [(0, 3), (1, 3), (2, 3), (0, 4), (1, 4), (2, 4), (3, 4)];

foreach ((int first, int second) in pairs)
{
byte[][] buffers = [new byte[128], new byte[128], new byte[128], new byte[128], new byte[128]];
buffers[second] = buffers[first];
buffers[first].AsSpan().Fill(0xA5);
byte[] original = (byte[])buffers[first].Clone();
int[] starts = [16, 16, 16, 16, 16];
starts[second] += offset;

using (RecordingHpke key = new(suite))
{
Assert.Throws<CryptographicException>(() => key.Seal(
buffers[0].AsSpan(starts[0], 32),
buffers[3].AsSpan(starts[3], suite.EncapsulatedSecretSizeInBytes),
buffers[4].AsSpan(starts[4], suite.GetCiphertextLength(32)),
buffers[1].AsSpan(starts[1], 32),
buffers[2].AsSpan(starts[2], 32)));
Assert.Equal(0, key.SealCalls);
Assert.Equal(original, buffers[first]);
}
}
}

[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(1)]
public static void CreateSender_RejectsOverlappingBuffers(int offset)
{
HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
byte[] buffer = new byte[128];
buffer.AsSpan().Fill(0xA5);
byte[] original = (byte[])buffer.Clone();

using (RecordingHpke key = new(suite))
{
Assert.Throws<CryptographicException>(() => key.CreateSender(
buffer.AsSpan(16 + offset, suite.EncapsulatedSecretSizeInBytes),
buffer.AsSpan(16, 32)));
Assert.Equal(0, key.CreateSenderCalls);
Assert.Equal(original, buffer);
}
}

[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(1)]
public static void CreatePskSender_RejectsOverlappingBuffers(int offset)
{
HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);

for (int input = 0; input < 3; input++)
{
byte[][] inputs = [new byte[128], new byte[128], new byte[128]];
byte[] output = inputs[input];
output.AsSpan().Fill(0xA5);
byte[] original = (byte[])output.Clone();

using (RecordingHpke key = new(suite))
{
Assert.Throws<CryptographicException>(() => key.CreatePskSender(
inputs[0].AsSpan(16, 32),
inputs[1].AsSpan(16, 32),
output.AsSpan(16 + offset, suite.EncapsulatedSecretSizeInBytes),
inputs[2].AsSpan(16, 32)));
Assert.Equal(0, key.PskCalls);
Assert.Equal(0, key.CreateSenderCalls);
Assert.Equal(original, output);
}
}
}

[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(1)]
public static void Sender_SealRejectsOverlappingBuffers(int offset)
{
HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);

for (int input = 0; input < 2; input++)
{
byte[][] inputs = [new byte[128], new byte[128]];
byte[] output = inputs[input];
output.AsSpan().Fill(0xA5);
byte[] original = (byte[])output.Clone();

using (RecordingHpkeSender sender = new(suite))
{
Assert.Throws<CryptographicException>(() => sender.Seal(
inputs[0].AsSpan(16, 32),
output.AsSpan(16 + offset, suite.GetCiphertextLength(32)),
inputs[1].AsSpan(16, 32)));
Assert.Equal(0, sender.SealCalls);
Assert.Equal(original, output);
}
}
}

[Theory]
[InlineData(0)]
[InlineData(32)]
public static void Seal_AllowsReadOnlyOverlapAndAdjacentOutputs(int inputLength)
{
HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
int encLength = suite.EncapsulatedSecretSizeInBytes;
int ciphertextLength = suite.GetCiphertextLength(inputLength);
byte[] buffer = new byte[inputLength + encLength + ciphertextLength];
buffer.AsSpan().Fill(0xA5);
byte[] originalInput = buffer.AsSpan(0, inputLength).ToArray();

using (RecordingHpke key = new(suite))
{
key.Seal(
buffer.AsSpan(0, inputLength),
buffer.AsSpan(inputLength, encLength),
buffer.AsSpan(inputLength + encLength, ciphertextLength),
buffer.AsSpan(0, inputLength),
buffer.AsSpan(0, inputLength));
Assert.Equal(1, key.SealCalls);
AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength));
Assert.Equal(0xD7, buffer[inputLength]);
Assert.Equal(0xC8, buffer[^1]);
}

using (RecordingHpkeSender sender = new(suite))
{
sender.Seal(
buffer.AsSpan(0, inputLength),
buffer.AsSpan(inputLength, ciphertextLength),
buffer.AsSpan(0, inputLength));
Assert.Equal(1, sender.SealCalls);
AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, inputLength));
}
}

[Fact]
public static void CreateSender_AllowsReadOnlyOverlapAndAdjacentOutput()
{
HpkeSuite suite = new(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
byte[] buffer = new byte[32 + suite.EncapsulatedSecretSizeInBytes];
buffer.AsSpan().Fill(0xA5);
byte[] originalInput = buffer.AsSpan(0, 32).ToArray();

using (RecordingHpke key = new(suite))
using (HpkeSender sender = key.CreateSender(buffer.AsSpan(32), buffer.AsSpan(0, 32)))
using (HpkeSender pskSender = key.CreatePskSender(
buffer.AsSpan(0, 32), buffer.AsSpan(0, 32), buffer.AsSpan(32), buffer.AsSpan(0, 32)))
{
Assert.Equal(2, key.CreateSenderCalls);
Assert.Equal(1, key.PskCalls);
Assert.Equal(originalInput, key.LastPsk);
Assert.Equal(originalInput, key.LastPskId);
Assert.Equal(originalInput, key.LastSenderInfo);
AssertExtensions.SequenceEqual(originalInput.AsSpan(), buffer.AsSpan(0, 32));
Assert.Equal(0xD7, buffer[^1]);
}
}

private sealed class RecordingHpke : Hpke
{
internal bool OpenCoreCalled { get; private set; }
internal int SealCalls { get; private set; }
internal int CreateSenderCalls { get; private set; }
internal byte[] LastSenderInfo { get; private set; } = [];
internal bool ThrowOnCreateSender { get; set; }
Expand Down Expand Up @@ -1821,8 +1992,12 @@ protected override void SealCore(
Span<byte> encapsulatedSecret,
Span<byte> ciphertext,
ReadOnlySpan<byte> associatedData,
ReadOnlySpan<byte> info) =>
throw new InvalidOperationException("Unexpected encryption.");
ReadOnlySpan<byte> info)
{
SealCalls++;
encapsulatedSecret.Fill(0xD7);
ciphertext.Fill(0xC8);
}
}

[Theory]
Expand Down