audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -6,6 +6,7 @@
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -93,7 +94,7 @@ public sealed class ExcititorVexImportTarget : IVexImportTarget
Content: contentBytes,
Metadata: ImmutableDictionary<string, string>.Empty
.Add("importSource", "airgap-snapshot")
.Add("snapshotAt", data.SnapshotAt.ToString("O")));
.Add("snapshotAt", data.SnapshotAt.ToString("O", CultureInfo.InvariantCulture)));
await _sink.StoreAsync(document, cancellationToken);
created++;

View File

@@ -59,6 +59,9 @@ public static class AirGapSyncServiceCollectionExtensions
// Bundle exporter
services.TryAddSingleton<IAirGapBundleExporter, AirGapBundleExporter>();
// Bundle DSSE signer (OMP-010)
services.TryAddSingleton<IAirGapBundleDsseSigner, AirGapBundleDsseSigner>();
return services;
}

View File

@@ -0,0 +1,275 @@
// <copyright file="AirGapBundleDsseSigner.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Sync.Models;
using StellaOps.Canonical.Json;
namespace StellaOps.AirGap.Sync.Services;
/// <summary>
/// Options for air-gap bundle DSSE signing.
/// </summary>
public sealed class AirGapBundleDsseOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "AirGap:BundleSigning";
/// <summary>
/// Gets or sets the signing mode: "hmac" for HMAC-SHA256, "none" to disable.
/// </summary>
public string Mode { get; set; } = "none";
/// <summary>
/// Gets or sets the HMAC secret key as Base64.
/// Required when Mode is "hmac".
/// </summary>
public string? SecretBase64 { get; set; }
/// <summary>
/// Gets or sets the key identifier for the signature.
/// </summary>
public string KeyId { get; set; } = "airgap-bundle-signer";
/// <summary>
/// Gets or sets the payload type for DSSE envelope.
/// </summary>
public string PayloadType { get; set; } = "application/vnd.stellaops.airgap.bundle+json";
}
/// <summary>
/// Result of a bundle signature operation.
/// </summary>
/// <param name="KeyId">The key ID used for signing.</param>
/// <param name="Signature">The signature bytes.</param>
/// <param name="SignatureBase64">The signature as Base64 string.</param>
public sealed record AirGapBundleSignatureResult(
string KeyId,
byte[] Signature,
string SignatureBase64);
/// <summary>
/// Interface for air-gap bundle DSSE signing.
/// </summary>
public interface IAirGapBundleDsseSigner
{
/// <summary>
/// Signs an air-gap bundle manifest and returns the signature result.
/// </summary>
/// <param name="bundle">The bundle to sign.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Signature result with key ID and signature.</returns>
Task<AirGapBundleSignatureResult?> SignAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default);
/// <summary>
/// Verifies an air-gap bundle signature.
/// </summary>
/// <param name="bundle">The bundle to verify.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if signature is valid or signing is disabled; false if invalid.</returns>
Task<AirGapBundleVerificationResult> VerifyAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets whether signing is enabled.
/// </summary>
bool IsEnabled { get; }
}
/// <summary>
/// Result of bundle signature verification.
/// </summary>
/// <param name="IsValid">Whether the signature is valid.</param>
/// <param name="Reason">The reason for the result.</param>
public sealed record AirGapBundleVerificationResult(bool IsValid, string Reason)
{
/// <summary>
/// Verification succeeded.
/// </summary>
public static AirGapBundleVerificationResult Valid { get; } = new(true, "Signature verified");
/// <summary>
/// Signing is disabled, so verification is skipped.
/// </summary>
public static AirGapBundleVerificationResult SigningDisabled { get; } = new(true, "Signing disabled");
/// <summary>
/// Bundle has no signature but signing is enabled.
/// </summary>
public static AirGapBundleVerificationResult MissingSignature { get; } = new(false, "Bundle is not signed");
/// <summary>
/// Signature verification failed.
/// </summary>
public static AirGapBundleVerificationResult InvalidSignature { get; } = new(false, "Signature verification failed");
}
/// <summary>
/// DSSE signer for air-gap bundles using HMAC-SHA256.
/// </summary>
public sealed class AirGapBundleDsseSigner : IAirGapBundleDsseSigner
{
private const string DssePrefix = "DSSEv1 ";
private readonly IOptions<AirGapBundleDsseOptions> _options;
private readonly ILogger<AirGapBundleDsseSigner> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="AirGapBundleDsseSigner"/> class.
/// </summary>
public AirGapBundleDsseSigner(
IOptions<AirGapBundleDsseOptions> options,
ILogger<AirGapBundleDsseSigner> logger)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc/>
public bool IsEnabled => string.Equals(_options.Value.Mode, "hmac", StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public Task<AirGapBundleSignatureResult?> SignAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(bundle);
cancellationToken.ThrowIfCancellationRequested();
var opts = _options.Value;
if (!IsEnabled)
{
_logger.LogDebug("Air-gap bundle DSSE signing is disabled");
return Task.FromResult<AirGapBundleSignatureResult?>(null);
}
if (string.IsNullOrWhiteSpace(opts.SecretBase64))
{
throw new InvalidOperationException("HMAC signing mode requires SecretBase64 to be configured");
}
byte[] secret;
try
{
secret = Convert.FromBase64String(opts.SecretBase64);
}
catch (FormatException ex)
{
throw new InvalidOperationException("SecretBase64 is not valid Base64", ex);
}
// Compute PAE (Pre-Authentication Encoding) per DSSE spec
var pae = ComputePreAuthenticationEncoding(opts.PayloadType, bundle.ManifestDigest);
var signature = ComputeHmacSha256(secret, pae);
var signatureBase64 = Convert.ToBase64String(signature);
_logger.LogInformation(
"Signed air-gap bundle {BundleId} with key {KeyId}",
bundle.BundleId,
opts.KeyId);
return Task.FromResult<AirGapBundleSignatureResult?>(
new AirGapBundleSignatureResult(opts.KeyId, signature, signatureBase64));
}
/// <inheritdoc/>
public Task<AirGapBundleVerificationResult> VerifyAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(bundle);
cancellationToken.ThrowIfCancellationRequested();
var opts = _options.Value;
if (!IsEnabled)
{
_logger.LogDebug("Air-gap bundle DSSE signing is disabled, skipping verification");
return Task.FromResult(AirGapBundleVerificationResult.SigningDisabled);
}
if (string.IsNullOrWhiteSpace(bundle.Signature))
{
_logger.LogWarning("Air-gap bundle {BundleId} has no signature", bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.MissingSignature);
}
if (string.IsNullOrWhiteSpace(opts.SecretBase64))
{
throw new InvalidOperationException("HMAC signing mode requires SecretBase64 to be configured");
}
byte[] secret;
try
{
secret = Convert.FromBase64String(opts.SecretBase64);
}
catch (FormatException ex)
{
throw new InvalidOperationException("SecretBase64 is not valid Base64", ex);
}
byte[] expectedSignature;
try
{
expectedSignature = Convert.FromBase64String(bundle.Signature);
}
catch (FormatException)
{
_logger.LogWarning("Air-gap bundle {BundleId} has invalid Base64 signature", bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.InvalidSignature);
}
// Compute PAE and expected signature
var pae = ComputePreAuthenticationEncoding(opts.PayloadType, bundle.ManifestDigest);
var computedSignature = ComputeHmacSha256(secret, pae);
if (!CryptographicOperations.FixedTimeEquals(expectedSignature, computedSignature))
{
_logger.LogWarning(
"Air-gap bundle {BundleId} signature verification failed",
bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.InvalidSignature);
}
_logger.LogDebug(
"Air-gap bundle {BundleId} signature verified successfully",
bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.Valid);
}
/// <summary>
/// Computes DSSE Pre-Authentication Encoding (PAE).
/// PAE = "DSSEv1" SP len(payloadType) SP payloadType SP len(payload) SP payload
/// where len() returns ASCII decimal length, and SP is a space character.
/// </summary>
private static byte[] ComputePreAuthenticationEncoding(string payloadType, string manifestDigest)
{
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType);
var manifestDigestBytes = Encoding.UTF8.GetBytes(manifestDigest);
// Format: "DSSEv1 {payloadType.Length} {payloadType} {payload.Length} {payload}"
var paeString = string.Create(
CultureInfo.InvariantCulture,
$"{DssePrefix}{payloadTypeBytes.Length} {payloadType} {manifestDigestBytes.Length} {manifestDigest}");
return Encoding.UTF8.GetBytes(paeString);
}
private static byte[] ComputeHmacSha256(byte[] key, byte[] data)
{
using var hmac = new HMACSHA256(key);
return hmac.ComputeHash(data);
}
}