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
Checkpoint HPKE Seal and managed AEAD adapters
Add public Seal scaffolding, KDF info-length validation, an AES-GCM adapter, and unsupported-platform dispatch. This is an intentionally incomplete, non-building checkpoint; SealCore remains unimplemented.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 438fb068-aa8f-4969-9e8c-7065b52b74b5
  • Loading branch information
vcsjones and Copilot committed Sep 7, 2026
commit f589d5a8fe57b4dab7f58a23755561b35321c148
87 changes: 87 additions & 0 deletions src/libraries/Common/src/System/Security/Cryptography/Hpke.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,83 @@ public void ExportEncapsulationKey(Span<byte> destination)
/// </remarks>
protected abstract void ExportEncapsulationKeyCore(Span<byte> destination);

public void Seal(
ReadOnlySpan<byte> plaintext,
out byte[] encapsulatedSecret,
out byte[] ciphertext,
ReadOnlySpan<byte> associatedData = default,
ReadOnlySpan<byte> info = default)
{
ThrowIfInfoExceedsLimit(info);
ThrowIfDisposed();

byte[] ciphertextBuffer = new byte[Suite.GetCiphertextLength(plaintext.Length)];
byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes];

SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info);

encapsulatedSecret = encapsulatedSecretBuffer;
ciphertext = ciphertextBuffer;
}

public void Seal(
byte[] plaintext,
out byte[] encapsulatedSecret,
out byte[] ciphertext,
byte[]? associatedData = null,
byte[]? info = null)
{
ArgumentNullException.ThrowIfNull(plaintext);
ThrowIfInfoExceedsLimit(info);
ThrowIfDisposed();

byte[] ciphertextBuffer = new byte[Suite.GetCiphertextLength(plaintext.Length)];
byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes];

// associatedData and info null's implicity convert to empty span.
SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info);

encapsulatedSecret = encapsulatedSecretBuffer;
ciphertext = ciphertextBuffer;
}

public void Seal(
ReadOnlySpan<byte> plaintext,
Span<byte> encapsulatedSecret,
Span<byte> ciphertext,
ReadOnlySpan<byte> associatedData = default,
ReadOnlySpan<byte> info = default)
{
ThrowIfInfoExceedsLimit(info);
ThrowIfDisposed();

if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes)
{
throw new ArgumentException(
SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes),
nameof(encapsulatedSecret));
}

int expectedCiphertextLength = Suite.GetCiphertextLength(plaintext.Length);

if (ciphertext.Length != expectedCiphertextLength)
{
throw new ArgumentException(
SR.Format(SR.Argument_DestinationImprecise, expectedCiphertextLength),
nameof(ciphertext));
}

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

protected abstract void SealCore(
ReadOnlySpan<byte> plaintext,
Span<byte> encapsulatedSecret,
Span<byte> ciphertext,
ReadOnlySpan<byte> associatedData,
ReadOnlySpan<byte> info);


/// <summary>
/// Releases all resources used by the <see cref="Hpke" /> class.
/// </summary>
Expand Down Expand Up @@ -318,6 +395,16 @@ private static void ThrowIfNotSupported(HpkeSuite suite)
}
}

private void ThrowIfInfoExceedsLimit(ReadOnlySpan<byte> info)
{
if (info.Length > Suite.KdfMetadata.MaximumInfoLength)
{
throw new ArgumentException(
SR.Format(SR.Argument_HpkeKdfInfoLength, Suite.KdfMetadata.MaximumInfoLength),
nameof(info));
}
}

private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,43 @@ internal sealed partial class HpkeKdfMetadata
internal int Nh { get; }
internal bool IsTwoStage { get; }
internal string Name { get; }
internal int? MaximumInfoLength { get; }

private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name)
{
Kdf = kdf;
Nh = nh;
IsTwoStage = isTwoStage;
Name = name;

if (!IsTwoStage)
{
// One stage (SHAKE) uses a 16-bit integer to encode the info length. Practically that means the info is limited
// to 65,535. See CombineSecrets_OneStage. info is described as lengthPrefixed(info).
// > lengthPrefixed(x): The two-byte length of the byte string x, concatenated with x itself.
// > (lengthPrefixed(x) = concat(I2OSP(len(x), 2), x)) It is an error to call this function with an x
// > value that is more than 65535 bytes long.
// https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-5
// We'll track that is the KDF having a maximum info length.
// Other KDFs have a maximum input length however they far exceed 32-bit integers which is limited by a
// Span's input limit.
MaximumInfoLength = ushort.MaxValue;
}
}

internal static HpkeKdfMetadata? Create(HpkeKdf kdf)
{
switch (kdf)
{
// HKDF SHAs have limits on their info size, 2^61 - 91 and 2^125 - 155. Since this is well above
// A Span's possible length we'll treat it as unlimited.
// https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.2
case HpkeKdf.HKDF_SHA256:
return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: true, name: "HKDF-SHA256");
case HpkeKdf.HKDF_SHA384:
return new HpkeKdfMetadata(kdf, nh: 48, isTwoStage: true, name: "HKDF-SHA384");
case HpkeKdf.HKDF_SHA512:
return new HpkeKdfMetadata(kdf, nh: 64, isTwoStage: true, name: "HKDF-SHA512");

// https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-5
case HpkeKdf.SHAKE128:
return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: false, name: "SHAKE128");
case HpkeKdf.SHAKE256:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
<data name="Argument_KemInvalidSeedLength" xml:space="preserve">
<value>The specified private seed is not the correct length for the ML-KEM algorithm.</value>
</data>
<data name="Argument_HpkeKdfInfoLength" xml:space="preserve">
<value>The specified info exceeds the maximum length of {0} bytes.</value>
</data>
<data name="Argument_MLDsaMuInvalidLength" xml:space="preserve">
<value>The specified mu value is not the correct length for the ML-DSA algorithm.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,6 @@
<Compile Include="System\Security\Cryptography\HMACStatic.cs" />
<Compile Include="System\Security\Cryptography\HpkeAeadMetadata.Managed.cs" />
<Compile Include="System\Security\Cryptography\HpkeECDiffieHellmanKemAdapter.cs" />
<Compile Include="System\Security\Cryptography\HpkeImplementation.Managed.cs" />
<Compile Include="System\Security\Cryptography\HpkeKdfMetadata.Managed.cs" />
<Compile Include="System\Security\Cryptography\HpkeKemMetadata.Managed.cs" />
<Compile Include="System\Security\Cryptography\HpkeManagedKemAdapter.cs" />
Expand Down Expand Up @@ -899,6 +898,7 @@
<Compile Include="System\Security\Cryptography\HashProviderDispenser.Browser.cs" />
<Compile Include="System\Security\Cryptography\HKDF.Managed.cs" />
<Compile Include="System\Security\Cryptography\HMACHashProvider.Browser.Managed.cs" />
<Compile Include="System\Security\Cryptography\HpkeImplementation.Unsupported.cs" />
<Compile Include="System\Security\Cryptography\LiteHash.Browser.cs" />
<Compile Include="System\Security\Cryptography\LiteHash.Kmac.Unsupported.cs" />
<Compile Include="System\Security\Cryptography\MLDsaOpenSsl.NotSupported.cs" />
Expand Down Expand Up @@ -2114,6 +2114,10 @@
<Compile Include="System\Security\Cryptography\X509Certificates\X509Pal.Windows.X500DistinguishedName.cs" />
</ItemGroup>

<ItemGroup Condition="'$(GeneratePlatformNotSupportedAssemblyMessage)' == '' and '$(TargetPlatformIdentifier)' != 'browser'">
<Compile Include="System\Security\Cryptography\HpkeImplementation.Managed.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)System.Collections\src\System.Collections.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Collections.Concurrent\src\System.Collections.Concurrent.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,88 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;

#pragma warning disable CA1416 // //TODO:HPKE Call is reachable on "unsupported platform" - deal with this messy daignostic later.

namespace System.Security.Cryptography
{
internal abstract class HpkeManagedAeadAdapter : IDisposable
{
internal static HpkeManagedAeadAdapter Create(HpkeSuite suite, ReadOnlySpan<byte> key)
{
Debug.Assert(suite.AeadMetadata.Nt == 16);

switch (suite.AeadAlgorithm)
{
case HpkeAead.AES_128_GCM:
case HpkeAead.AES_256_GCM:
return new HpkeManagedAesAeadAdapter(suite, key);
case HpkeAead.ChaCha20Poly1305:
throw new NotImplementedException();
default:
Debug.Fail($"Unmapped AEAD adapter algorithm {suite.AeadAlgorithm}.");
throw new CryptographicException();
}
}

internal abstract void Encrypt(
ReadOnlySpan<byte> plaintext,
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> associatedData,
Span<byte> ciphertext,
Span<byte> tag);

internal abstract void Decrypt(
ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> associatedData,
ReadOnlySpan<byte> tag,
Span<byte> plaintext);

public abstract void Dispose();
}

internal sealed class HpkeManagedAesAeadAdapter : HpkeManagedAeadAdapter
{
private readonly AesGcm _aes;

internal HpkeManagedAesAeadAdapter(HpkeSuite suite, ReadOnlySpan<byte> key)
{
_aes = new AesGcm(key, suite.AeadMetadata.Nt);
}

internal override void Encrypt(
ReadOnlySpan<byte> plaintext,
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> associatedData,
Span<byte> ciphertext,
Span<byte> tag)
{
_aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData);
}

internal override void Decrypt(
ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> associatedData,
ReadOnlySpan<byte> tag,
Span<byte> plaintext)
{
_aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData);
}


public override void Dispose() => _aes.Dispose();
}

internal sealed class HpkeImplementation : Hpke
{
private readonly HpkeManagedKemAdapter _adapter;
private readonly HpkeManagedKemAdapter _kemAdapter;

private HpkeImplementation(HpkeManagedKemAdapter adapter) : base(adapter.Suite)
private HpkeImplementation(HpkeSuite suite, HpkeManagedKemAdapter kemAdapter) : base(suite)
{
_adapter = adapter;
_kemAdapter = kemAdapter;
}

internal static bool IsSupportedImpl(HpkeSuite suite) =>
Expand All @@ -24,7 +97,7 @@ internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan<b
try
{
adapter.DeriveKeyPair(ikm);
return new HpkeImplementation(adapter);
return new HpkeImplementation(suite, adapter);
}
catch
{
Expand All @@ -40,7 +113,7 @@ internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite)
try
{
adapter.Generate();
return new HpkeImplementation(adapter);
return new HpkeImplementation(suite, adapter);
}
catch
{
Expand All @@ -50,16 +123,26 @@ internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite)
}

protected override void ExportDecapsulationKeyCore(Span<byte> destination) =>
_adapter.ExportDecapsulationKey(destination);
_kemAdapter.ExportDecapsulationKey(destination);

protected override void ExportEncapsulationKeyCore(Span<byte> destination) =>
_adapter.ExportEncapsulationKey(destination);
_kemAdapter.ExportEncapsulationKey(destination);

protected override void SealCore(
ReadOnlySpan<byte> plaintext,
Span<byte> encapsulatedSecret,
Span<byte> ciphertext,
ReadOnlySpan<byte> associatedData,
ReadOnlySpan<byte> info)
{
throw new NotImplementedException();
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_adapter.Dispose();
_kemAdapter.Dispose();
}

base.Dispose(disposing);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;

namespace System.Security.Cryptography
{
internal sealed class HpkeImplementation : Hpke
{
internal HpkeImplementation(HpkeSuite suite) : base(suite)
{
}

internal static bool IsSupportedImpl(HpkeSuite suite)
{
_ = suite;
return false;
}

internal static HpkeImplementation DeriveKeyImpl(HpkeSuite suite, ReadOnlySpan<byte> ikm)
{
_ = suite;
_ = ikm;
Debug.Fail("Platform validation should not permit this call.");
throw new CryptographicException();
}

internal static HpkeImplementation GenerateKeyImpl(HpkeSuite suite)
{
_ = suite;
Debug.Fail("Platform validation should not permit this call.");
throw new CryptographicException();
}

protected override void ExportDecapsulationKeyCore(Span<byte> destination)
{
_ = destination;
Debug.Fail("Platform validation should not permit this call.");
throw new CryptographicException();
}

protected override void ExportEncapsulationKeyCore(Span<byte> destination)
{
_ = destination;
Debug.Fail("Platform validation should not permit this call.");
throw new CryptographicException();
}

protected override void SealCore(
ReadOnlySpan<byte> plaintext,
Span<byte> encapsulatedSecret,
Span<byte> ciphertext,
ReadOnlySpan<byte> associatedData,
ReadOnlySpan<byte> info)
{
_ = plaintext;
_ = encapsulatedSecret;
_ = ciphertext;
_ = associatedData;
_ = info;
Debug.Fail("Platform validation should not permit this call.");
throw new CryptographicException();
}

protected override void Dispose(bool disposing)
{
Debug.Fail("Platform validation should not permit this call.");
throw new CryptographicException();
}
}
}