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:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user