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,35 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<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" encoding="utf-8"?>
<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.AspNetCore.Mvc.Testing" 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-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<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>
<PackageReference Include="xunit.v3" Version="3.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
<Compile Remove="OpenApiEndpointTests.cs" />
<Content Include="TestContent/**" CopyToOutputDirectory="PreserveNewest" />
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Notifier.WebService\StellaOps.Notifier.WebService.csproj" />
<ProjectReference Include="..\StellaOps.Notifier.Worker\StellaOps.Notifier.Worker.csproj" />

View File

@@ -1,17 +1,17 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Storage.Mongo/StellaOps.Notify.Storage.Mongo.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Engine/StellaOps.Notify.Engine.csproj" />
<ProjectReference Include="../StellaOps.Notifier.Worker/StellaOps.Notifier.Worker.csproj" />
</ItemGroup>
</Project>
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Storage.Mongo/StellaOps.Notify.Storage.Mongo.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Engine/StellaOps.Notify.Engine.csproj" />
<ProjectReference Include="../StellaOps.Notifier.Worker/StellaOps.Notifier.Worker.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,11 +4,13 @@ using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
namespace StellaOps.Notifier.Worker.Security;
/// <summary>
/// Default implementation of webhook security service using HMAC-SHA256.
/// Default implementation of webhook security service using HMAC.
/// Note: External webhooks always use HMAC-SHA256 for interoperability via HmacPurpose.WebhookInterop.
/// </summary>
public sealed class DefaultWebhookSecurityService : IWebhookSecurityService
{
@@ -16,6 +18,7 @@ public sealed class DefaultWebhookSecurityService : IWebhookSecurityService
private const int TimestampToleranceSeconds = 300; // 5 minutes
private readonly WebhookSecurityOptions _options;
private readonly ICryptoHmac _cryptoHmac;
private readonly TimeProvider _timeProvider;
private readonly ILogger<DefaultWebhookSecurityService> _logger;
@@ -24,10 +27,12 @@ public sealed class DefaultWebhookSecurityService : IWebhookSecurityService
public DefaultWebhookSecurityService(
IOptions<WebhookSecurityOptions> options,
ICryptoHmac cryptoHmac,
TimeProvider timeProvider,
ILogger<DefaultWebhookSecurityService> logger)
{
_options = options?.Value ?? new WebhookSecurityOptions();
_cryptoHmac = cryptoHmac ?? throw new ArgumentNullException(nameof(cryptoHmac));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -43,9 +48,8 @@ public sealed class DefaultWebhookSecurityService : IWebhookSecurityService
// Create signed payload: timestamp.payload
var signedData = CreateSignedData(timestampUnix, payload);
using var hmac = new HMACSHA256(config.SecretBytes);
var signature = hmac.ComputeHash(signedData);
var signatureHex = Convert.ToHexString(signature).ToLowerInvariant();
// WebhookInterop always uses HMAC-SHA256 for external webhook compatibility
var signatureHex = _cryptoHmac.ComputeHmacHexForPurpose(config.SecretBytes, signedData, HmacPurpose.WebhookInterop);
// Format: v1=timestamp,signature
return $"{SignaturePrefix}={timestampUnix},{signatureHex}";
@@ -106,25 +110,21 @@ public sealed class DefaultWebhookSecurityService : IWebhookSecurityService
var config = GetOrCreateConfig(tenantId, channelId);
var signedData = CreateSignedData(timestampUnix, payload);
using var hmac = new HMACSHA256(config.SecretBytes);
var expectedSignature = hmac.ComputeHash(signedData);
// Also check previous secret if within rotation window
if (!CryptographicOperations.FixedTimeEquals(expectedSignature, providedSignature))
// WebhookInterop always uses HMAC-SHA256 for external webhook compatibility
if (_cryptoHmac.VerifyHmacForPurpose(config.SecretBytes, signedData, providedSignature, HmacPurpose.WebhookInterop))
{
if (config.PreviousSecretBytes is not null &&
config.PreviousSecretExpiresAt.HasValue &&
_timeProvider.GetUtcNow() < config.PreviousSecretExpiresAt.Value)
{
using var hmacPrev = new HMACSHA256(config.PreviousSecretBytes);
var prevSignature = hmacPrev.ComputeHash(signedData);
return CryptographicOperations.FixedTimeEquals(prevSignature, providedSignature);
}
return false;
return true;
}
return true;
// Also check previous secret if within rotation window
if (config.PreviousSecretBytes is not null &&
config.PreviousSecretExpiresAt.HasValue &&
_timeProvider.GetUtcNow() < config.PreviousSecretExpiresAt.Value)
{
return _cryptoHmac.VerifyHmacForPurpose(config.PreviousSecretBytes, signedData, providedSignature, HmacPurpose.WebhookInterop);
}
return false;
}
public IpValidationResult ValidateIp(string tenantId, string channelId, IPAddress ipAddress)

View File

@@ -5,29 +5,32 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
namespace StellaOps.Notifier.Worker.Security;
/// <summary>
/// HMAC-SHA256 based implementation of acknowledgement token service.
/// HMAC based implementation of acknowledgement token service.
/// </summary>
public sealed class HmacAckTokenService : IAckTokenService, IDisposable
public sealed class HmacAckTokenService : IAckTokenService
{
private const int CurrentVersion = 1;
private const string TokenPrefix = "soa1"; // StellaOps Ack v1
private readonly AckTokenOptions _options;
private readonly ICryptoHmac _cryptoHmac;
private readonly TimeProvider _timeProvider;
private readonly ILogger<HmacAckTokenService> _logger;
private readonly HMACSHA256 _hmac;
private bool _disposed;
private readonly byte[] _derivedKey;
public HmacAckTokenService(
IOptions<AckTokenOptions> options,
ICryptoHmac cryptoHmac,
TimeProvider timeProvider,
ILogger<HmacAckTokenService> logger)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_cryptoHmac = cryptoHmac ?? throw new ArgumentNullException(nameof(cryptoHmac));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -38,13 +41,11 @@ public sealed class HmacAckTokenService : IAckTokenService, IDisposable
// Derive key using HKDF for proper key derivation
var keyBytes = Encoding.UTF8.GetBytes(_options.SigningKey);
var derivedKey = HKDF.DeriveKey(
_derivedKey = HKDF.DeriveKey(
HashAlgorithmName.SHA256,
keyBytes,
32, // 256 bits
info: Encoding.UTF8.GetBytes("StellaOps.AckToken.v1"));
_hmac = new HMACSHA256(derivedKey);
}
public AckToken CreateToken(
@@ -78,7 +79,7 @@ public sealed class HmacAckTokenService : IAckTokenService, IDisposable
var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
// Sign the payload
var signature = _hmac.ComputeHash(payloadBytes);
var signature = _cryptoHmac.ComputeHmacForPurpose(_derivedKey, payloadBytes, HmacPurpose.Authentication);
// Combine: prefix.payload.signature (all base64url)
var payloadB64 = Base64UrlEncode(payloadBytes);
@@ -147,8 +148,7 @@ public sealed class HmacAckTokenService : IAckTokenService, IDisposable
return AckTokenVerification.Fail(AckTokenFailureReason.InvalidFormat, "Invalid signature encoding");
}
var expectedSignature = _hmac.ComputeHash(payloadBytes);
if (!CryptographicOperations.FixedTimeEquals(expectedSignature, providedSignature))
if (!_cryptoHmac.VerifyHmacForPurpose(_derivedKey, payloadBytes, providedSignature, HmacPurpose.Authentication))
{
_logger.LogWarning("Invalid signature for ack token");
return AckTokenVerification.Fail(AckTokenFailureReason.InvalidSignature);
@@ -208,15 +208,6 @@ public sealed class HmacAckTokenService : IAckTokenService, IDisposable
return $"{baseUrl}/api/v1/ack/{Uri.EscapeDataString(token.TokenString)}";
}
public void Dispose()
{
if (!_disposed)
{
_hmac.Dispose();
_disposed = true;
}
}
private static string Base64UrlEncode(byte[] data)
{
return Convert.ToBase64String(data)

View File

@@ -1,27 +1,28 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<UserSecretsId>dotnet-StellaOps.Notifier.Worker-557c5516-a796-4499-942e-a0668e3e9622</UserSecretsId>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cronos" Version="0.10.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Storage.Mongo/StellaOps.Notify.Storage.Mongo.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Engine/StellaOps.Notify.Engine.csproj" />
<ProjectReference Include="../../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
</ItemGroup>
</Project>
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<UserSecretsId>dotnet-StellaOps.Notifier.Worker-557c5516-a796-4499-942e-a0668e3e9622</UserSecretsId>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cronos" Version="0.10.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Storage.Mongo/StellaOps.Notify.Storage.Mongo.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Engine/StellaOps.Notify.Engine.csproj" />
<ProjectReference Include="../../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>