feat: Implement DefaultCryptoHmac for compliance-aware HMAC operations

- Added DefaultCryptoHmac class implementing ICryptoHmac interface.
- Introduced purpose-based HMAC computation methods.
- Implemented verification methods for HMACs with constant-time comparison.
- Created HmacAlgorithms and HmacPurpose classes for well-known identifiers.
- Added compliance profile support for HMAC algorithms.
- Included asynchronous methods for HMAC computation from streams.
This commit is contained in:
StellaOps Bot
2025-12-06 00:41:04 +02:00
parent 43c281a8b2
commit f0662dd45f
362 changed files with 8441 additions and 22338 deletions

View File

@@ -1,8 +1,8 @@
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using StellaOps.Cryptography;
namespace StellaOps.ExportCenter.RiskBundles;
@@ -28,11 +28,13 @@ public sealed record RiskBundleManifestDsseSignature(
public sealed class HmacRiskBundleManifestSigner : IRiskBundleManifestSigner, IRiskBundleArchiveSigner
{
private const string DefaultPayloadType = "application/stellaops.risk-bundle.provider-manifest+json";
private readonly ICryptoHmac _cryptoHmac;
private readonly byte[] _key;
private readonly string _keyId;
public HmacRiskBundleManifestSigner(string key, string keyId)
public HmacRiskBundleManifestSigner(ICryptoHmac cryptoHmac, string key, string keyId)
{
_cryptoHmac = cryptoHmac ?? throw new ArgumentNullException(nameof(cryptoHmac));
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("Signing key cannot be empty.", nameof(key));
@@ -48,7 +50,7 @@ public sealed class HmacRiskBundleManifestSigner : IRiskBundleManifestSigner, IR
cancellationToken.ThrowIfCancellationRequested();
var pae = CreatePreAuthenticationEncoding(DefaultPayloadType, manifestJson);
var signature = ComputeHmac(pae, _key);
var signature = _cryptoHmac.ComputeHmacBase64ForPurpose(_key, pae, HmacPurpose.Signing);
var document = new RiskBundleManifestSignatureDocument(
DefaultPayloadType,
@@ -58,7 +60,7 @@ public sealed class HmacRiskBundleManifestSigner : IRiskBundleManifestSigner, IR
return Task.FromResult(document);
}
public Task<string> SignArchiveAsync(Stream archiveStream, CancellationToken cancellationToken = default)
public async Task<string> SignArchiveAsync(Stream archiveStream, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(archiveStream);
cancellationToken.ThrowIfCancellationRequested();
@@ -69,16 +71,8 @@ public sealed class HmacRiskBundleManifestSigner : IRiskBundleManifestSigner, IR
}
archiveStream.Position = 0;
using var hmac = new HMACSHA256(_key);
var signature = hmac.ComputeHash(archiveStream);
var signature = await _cryptoHmac.ComputeHmacForPurposeAsync(_key, archiveStream, HmacPurpose.Signing, cancellationToken);
archiveStream.Position = 0;
return Task.FromResult(Convert.ToBase64String(signature));
}
private static string ComputeHmac(byte[] pae, byte[] key)
{
using var hmac = new HMACSHA256(key);
var signature = hmac.ComputeHash(pae);
return Convert.ToBase64String(signature);
}

View File

@@ -9,9 +9,10 @@
<ItemGroup>
<ProjectReference Include="../StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/StellaOps.ExportCenter.Infrastructure.csproj" />
<ProjectReference Include="../StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -7,6 +7,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Linq;
using StellaOps.Cryptography;
namespace StellaOps.ExportCenter.Core.DevPortalOffline;
@@ -51,9 +52,11 @@ public sealed class DevPortalOfflineBundleBuilder
};
private readonly TimeProvider _timeProvider;
private readonly ICryptoHash _cryptoHash;
public DevPortalOfflineBundleBuilder(TimeProvider? timeProvider = null)
public DevPortalOfflineBundleBuilder(ICryptoHash cryptoHash, TimeProvider? timeProvider = null)
{
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -130,7 +133,7 @@ public sealed class DevPortalOfflineBundleBuilder
entries);
var manifestJson = JsonSerializer.Serialize(manifest, SerializerOptions);
var rootHash = ComputeSha256(manifestJson);
var rootHash = ComputeContentHash(manifestJson);
var checksums = BuildChecksums(rootHash, collected);
var instructions = BuildInstructions(manifest);
var verificationScript = BuildVerificationScript();
@@ -141,7 +144,7 @@ public sealed class DevPortalOfflineBundleBuilder
return new DevPortalOfflineBundleResult(manifest, manifestJson, checksums, rootHash, bundleStream);
}
private static bool CollectDirectory(
private bool CollectDirectory(
string? directory,
string category,
string prefix,
@@ -179,36 +182,17 @@ public sealed class DevPortalOfflineBundleBuilder
return true;
}
private static FileMetadata CreateFileMetadata(string category, string canonicalPath, string sourcePath)
private FileMetadata CreateFileMetadata(string category, string canonicalPath, string sourcePath)
{
using var stream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 128 * 1024, FileOptions.SequentialScan);
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
long totalBytes = 0;
try
{
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
hash.AppendData(buffer, 0, read);
totalBytes += read;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
var sha = Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant();
return new FileMetadata(category, canonicalPath, sourcePath, totalBytes, sha, GetContentType(sourcePath));
var fileBytes = File.ReadAllBytes(sourcePath);
var sha = _cryptoHash.ComputeHashHexForPurpose(fileBytes, HashPurpose.Content);
return new FileMetadata(category, canonicalPath, sourcePath, fileBytes.Length, sha, GetContentType(sourcePath));
}
private static string ComputeSha256(string content)
private string ComputeContentHash(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
return _cryptoHash.ComputeHashHexForPurpose(bytes, HashPurpose.Content);
}
private static string BuildChecksums(string rootHash, IReadOnlyCollection<FileMetadata> files)

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
@@ -12,10 +12,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\TimelineIndexer\StellaOps.TimelineIndexer\StellaOps.TimelineIndexer.Core\StellaOps.TimelineIndexer.Core.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.ExportCenter.Core.DevPortalOffline;
namespace StellaOps.ExportCenter.Infrastructure.DevPortalOffline;
@@ -13,15 +14,18 @@ namespace StellaOps.ExportCenter.Infrastructure.DevPortalOffline;
public sealed class FileSystemDevPortalOfflineObjectStore : IDevPortalOfflineObjectStore
{
private readonly IOptionsMonitor<DevPortalOfflineStorageOptions> _options;
private readonly ICryptoHash _cryptoHash;
private readonly TimeProvider _timeProvider;
private readonly ILogger<FileSystemDevPortalOfflineObjectStore> _logger;
public FileSystemDevPortalOfflineObjectStore(
IOptionsMonitor<DevPortalOfflineStorageOptions> options,
ICryptoHash cryptoHash,
TimeProvider timeProvider,
ILogger<FileSystemDevPortalOfflineObjectStore> logger)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -40,8 +44,9 @@ public sealed class FileSystemDevPortalOfflineObjectStore : IDevPortalOfflineObj
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
content.Seek(0, SeekOrigin.Begin);
// Write the content to file
using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None);
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
long totalBytes = 0;
@@ -51,7 +56,6 @@ public sealed class FileSystemDevPortalOfflineObjectStore : IDevPortalOfflineObj
while ((read = await content.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
hash.AppendData(buffer, 0, read);
totalBytes += read;
}
}
@@ -61,9 +65,11 @@ public sealed class FileSystemDevPortalOfflineObjectStore : IDevPortalOfflineObj
}
await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
content.Seek(0, SeekOrigin.Begin);
var sha = Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant();
// Compute hash from the written file
content.Seek(0, SeekOrigin.Begin);
var sha = await _cryptoHash.ComputeHashHexForPurposeAsync(content, HashPurpose.Content, cancellationToken).ConfigureAwait(false);
content.Seek(0, SeekOrigin.Begin);
var createdAt = _timeProvider.GetUtcNow();
_logger.LogDebug("Stored devportal artefact at {Path} ({Bytes} bytes).", fullPath, totalBytes);

View File

@@ -1,12 +1,12 @@
using System;
using System.Buffers.Binary;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.ExportCenter.Core.DevPortalOffline;
namespace StellaOps.ExportCenter.Infrastructure.DevPortalOffline;
@@ -14,15 +14,18 @@ namespace StellaOps.ExportCenter.Infrastructure.DevPortalOffline;
public sealed class HmacDevPortalOfflineManifestSigner : IDevPortalOfflineManifestSigner
{
private readonly IOptionsMonitor<DevPortalOfflineManifestSigningOptions> _options;
private readonly ICryptoHmac _cryptoHmac;
private readonly TimeProvider _timeProvider;
private readonly ILogger<HmacDevPortalOfflineManifestSigner> _logger;
public HmacDevPortalOfflineManifestSigner(
IOptionsMonitor<DevPortalOfflineManifestSigningOptions> options,
ICryptoHmac cryptoHmac,
TimeProvider timeProvider,
ILogger<HmacDevPortalOfflineManifestSigner> logger)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_cryptoHmac = cryptoHmac ?? throw new ArgumentNullException(nameof(cryptoHmac));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -49,7 +52,7 @@ public sealed class HmacDevPortalOfflineManifestSigner : IDevPortalOfflineManife
var signedAt = _timeProvider.GetUtcNow();
var payloadBytes = Encoding.UTF8.GetBytes(manifestJson);
var pae = BuildPreAuthEncoding(options.PayloadType, payloadBytes);
var signature = ComputeSignature(options, pae);
var signature = ComputeSignature(options, pae, _cryptoHmac);
var payloadBase64 = Convert.ToBase64String(payloadBytes);
_logger.LogDebug("Signed devportal manifest for bundle {BundleId}.", bundleId);
@@ -82,12 +85,10 @@ public sealed class HmacDevPortalOfflineManifestSigner : IDevPortalOfflineManife
}
}
private static string ComputeSignature(DevPortalOfflineManifestSigningOptions options, byte[] pae)
private static string ComputeSignature(DevPortalOfflineManifestSigningOptions options, byte[] pae, ICryptoHmac cryptoHmac)
{
var secretBytes = Convert.FromBase64String(options.Secret);
using var hmac = new HMACSHA256(secretBytes);
var signatureBytes = hmac.ComputeHash(pae);
return Convert.ToBase64String(signatureBytes);
return cryptoHmac.ComputeHmacBase64ForPurpose(secretBytes, pae, HmacPurpose.Signing);
}
private static byte[] BuildPreAuthEncoding(string payloadType, byte[] payloadBytes)

View File

@@ -11,10 +11,11 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj" />
<ProjectReference Include="..\..\..\TimelineIndexer\StellaOps.TimelineIndexer\StellaOps.TimelineIndexer.Core\StellaOps.TimelineIndexer.Core.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,121 +1,121 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit.v3" Version="3.0.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3"/>
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit.v3" Version="3.0.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3"/>
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj"/>
@@ -123,14 +123,14 @@
<ProjectReference Include="..\StellaOps.ExportCenter.Infrastructure\StellaOps.ExportCenter.Infrastructure.csproj"/>
<ProjectReference Include="..\..\StellaOps.ExportCenter.RiskBundles\StellaOps.ExportCenter.RiskBundles.csproj" />
</ItemGroup>
</Project>
</ItemGroup>
</Project>

View File

@@ -9,7 +9,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj" />

View File

@@ -1,33 +1,33 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<UserSecretsId>dotnet-StellaOps.ExportCenter.Worker-d4cfd239-79d1-4d17-91d6-bb7a78770695</UserSecretsId>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107"/>
</ItemGroup>
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<UserSecretsId>dotnet-StellaOps.ExportCenter.Worker-d4cfd239-79d1-4d17-91d6-bb7a78770695</UserSecretsId>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0"/>
</ItemGroup>
<ItemGroup>