Implement VEX document verification system with issuer management and signature verification

- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation.
- Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments.
- Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats.
- Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats.
- Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction.
- Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
This commit is contained in:
StellaOps Bot
2025-12-06 13:41:22 +02:00
parent 2141196496
commit 5e514532df
112 changed files with 24861 additions and 211 deletions

View File

@@ -0,0 +1,206 @@
using StellaOps.VexLens.Models;
namespace StellaOps.VexLens.Verification;
/// <summary>
/// Interface for managing VEX document issuers and their trust configuration.
/// </summary>
public interface IIssuerDirectory
{
/// <summary>
/// Gets an issuer by ID.
/// </summary>
Task<IssuerRecord?> GetIssuerAsync(
string issuerId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets an issuer by key fingerprint.
/// </summary>
Task<IssuerRecord?> GetIssuerByKeyFingerprintAsync(
string fingerprint,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists all registered issuers.
/// </summary>
Task<IReadOnlyList<IssuerRecord>> ListIssuersAsync(
IssuerListOptions? options = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Registers or updates an issuer.
/// </summary>
Task<IssuerRecord> RegisterIssuerAsync(
IssuerRegistration registration,
CancellationToken cancellationToken = default);
/// <summary>
/// Revokes an issuer's trust.
/// </summary>
Task<bool> RevokeIssuerAsync(
string issuerId,
string reason,
CancellationToken cancellationToken = default);
/// <summary>
/// Adds a key fingerprint to an issuer.
/// </summary>
Task<IssuerRecord> AddKeyFingerprintAsync(
string issuerId,
KeyFingerprintRegistration keyRegistration,
CancellationToken cancellationToken = default);
/// <summary>
/// Revokes a key fingerprint.
/// </summary>
Task<bool> RevokeKeyFingerprintAsync(
string issuerId,
string fingerprint,
string reason,
CancellationToken cancellationToken = default);
/// <summary>
/// Validates an issuer's trust status.
/// </summary>
Task<IssuerTrustValidation> ValidateTrustAsync(
string issuerId,
string? keyFingerprint,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Record for a registered issuer.
/// </summary>
public sealed record IssuerRecord(
string IssuerId,
string Name,
IssuerCategory Category,
TrustTier TrustTier,
IssuerStatus Status,
IReadOnlyList<KeyFingerprintRecord> KeyFingerprints,
IssuerMetadata? Metadata,
DateTimeOffset RegisteredAt,
DateTimeOffset? LastUpdatedAt,
DateTimeOffset? RevokedAt,
string? RevocationReason);
/// <summary>
/// Status of an issuer.
/// </summary>
public enum IssuerStatus
{
Active,
Suspended,
Revoked
}
/// <summary>
/// Record for a key fingerprint.
/// </summary>
public sealed record KeyFingerprintRecord(
string Fingerprint,
KeyType KeyType,
string? Algorithm,
KeyFingerprintStatus Status,
DateTimeOffset RegisteredAt,
DateTimeOffset? ExpiresAt,
DateTimeOffset? RevokedAt,
string? RevocationReason);
/// <summary>
/// Type of cryptographic key.
/// </summary>
public enum KeyType
{
Pgp,
X509,
Jwk,
Ssh,
Sigstore
}
/// <summary>
/// Status of a key fingerprint.
/// </summary>
public enum KeyFingerprintStatus
{
Active,
Expired,
Revoked
}
/// <summary>
/// Metadata for an issuer.
/// </summary>
public sealed record IssuerMetadata(
string? Description,
string? Uri,
string? Email,
string? LogoUri,
IReadOnlyList<string>? Tags,
IReadOnlyDictionary<string, string>? Custom);
/// <summary>
/// Options for listing issuers.
/// </summary>
public sealed record IssuerListOptions(
IssuerCategory? Category,
TrustTier? MinimumTrustTier,
IssuerStatus? Status,
string? SearchTerm,
int? Limit,
int? Offset);
/// <summary>
/// Registration for a new issuer.
/// </summary>
public sealed record IssuerRegistration(
string IssuerId,
string Name,
IssuerCategory Category,
TrustTier TrustTier,
IReadOnlyList<KeyFingerprintRegistration>? InitialKeys,
IssuerMetadata? Metadata);
/// <summary>
/// Registration for a key fingerprint.
/// </summary>
public sealed record KeyFingerprintRegistration(
string Fingerprint,
KeyType KeyType,
string? Algorithm,
DateTimeOffset? ExpiresAt,
byte[]? PublicKey);
/// <summary>
/// Result of trust validation.
/// </summary>
public sealed record IssuerTrustValidation(
bool IsTrusted,
TrustTier EffectiveTrustTier,
IssuerTrustStatus IssuerStatus,
KeyTrustStatus? KeyStatus,
IReadOnlyList<string> Warnings);
/// <summary>
/// Trust status of an issuer.
/// </summary>
public enum IssuerTrustStatus
{
Trusted,
NotRegistered,
Suspended,
Revoked
}
/// <summary>
/// Trust status of a key.
/// </summary>
public enum KeyTrustStatus
{
Valid,
NotRegistered,
Expired,
Revoked
}

View File

@@ -0,0 +1,182 @@
namespace StellaOps.VexLens.Verification;
/// <summary>
/// Interface for VEX document signature verification.
/// </summary>
public interface ISignatureVerifier
{
/// <summary>
/// Gets the signature formats this verifier supports.
/// </summary>
IReadOnlyList<SignatureFormat> SupportedFormats { get; }
/// <summary>
/// Verifies the signature on a VEX document.
/// </summary>
Task<SignatureVerificationResult> VerifyAsync(
SignatureVerificationRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// Extracts signature information without full verification.
/// </summary>
Task<SignatureExtractionResult> ExtractSignatureInfoAsync(
byte[] signedData,
SignatureFormat format,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Request for signature verification.
/// </summary>
public sealed record SignatureVerificationRequest(
byte[] Content,
byte[]? DetachedSignature,
SignatureFormat Format,
SignatureVerificationOptions Options);
/// <summary>
/// Options for signature verification.
/// </summary>
public sealed record SignatureVerificationOptions(
bool RequireTimestamp,
bool AllowExpiredCertificates,
bool CheckRevocation,
IReadOnlyList<string>? TrustedIssuers,
IReadOnlyList<string>? TrustedKeyFingerprints,
DateTimeOffset? VerificationTime);
/// <summary>
/// Result of signature verification.
/// </summary>
public sealed record SignatureVerificationResult(
bool IsValid,
SignatureVerificationStatus Status,
SignerInfo? Signer,
IReadOnlyList<CertificateInfo>? CertificateChain,
TimestampInfo? Timestamp,
IReadOnlyList<SignatureVerificationError> Errors,
IReadOnlyList<SignatureVerificationWarning> Warnings);
/// <summary>
/// Status of signature verification.
/// </summary>
public enum SignatureVerificationStatus
{
Valid,
InvalidSignature,
ExpiredCertificate,
RevokedCertificate,
UntrustedIssuer,
MissingSignature,
UnsupportedFormat,
CertificateChainError,
TimestampError,
UnknownError
}
/// <summary>
/// Information about the signer.
/// </summary>
public sealed record SignerInfo(
string IssuerId,
string? Name,
string? Email,
string? Organization,
string KeyFingerprint,
string Algorithm,
DateTimeOffset? SignedAt);
/// <summary>
/// Information about a certificate in the chain.
/// </summary>
public sealed record CertificateInfo(
string Subject,
string Issuer,
string SerialNumber,
string Fingerprint,
DateTimeOffset NotBefore,
DateTimeOffset NotAfter,
IReadOnlyList<string> KeyUsages,
bool IsSelfSigned,
bool IsCA);
/// <summary>
/// Information about a timestamp.
/// </summary>
public sealed record TimestampInfo(
DateTimeOffset Timestamp,
string? TimestampAuthority,
string? TimestampAuthorityUri,
bool IsValid);
/// <summary>
/// Error during signature verification.
/// </summary>
public sealed record SignatureVerificationError(
string Code,
string Message,
string? Detail);
/// <summary>
/// Warning during signature verification.
/// </summary>
public sealed record SignatureVerificationWarning(
string Code,
string Message);
/// <summary>
/// Result of signature extraction.
/// </summary>
public sealed record SignatureExtractionResult(
bool Success,
SignatureFormat? DetectedFormat,
SignerInfo? Signer,
IReadOnlyList<CertificateInfo>? Certificates,
string? ErrorMessage);
/// <summary>
/// Supported signature formats.
/// </summary>
public enum SignatureFormat
{
/// <summary>
/// Detached PGP/GPG signature (.sig, .asc).
/// </summary>
PgpDetached,
/// <summary>
/// Inline PGP/GPG signature (cleartext signed).
/// </summary>
PgpInline,
/// <summary>
/// PKCS#7/CMS detached signature (.p7s).
/// </summary>
Pkcs7Detached,
/// <summary>
/// PKCS#7/CMS enveloped signature.
/// </summary>
Pkcs7Enveloped,
/// <summary>
/// JSON Web Signature (JWS).
/// </summary>
Jws,
/// <summary>
/// DSSE envelope (Dead Simple Signing Envelope).
/// </summary>
Dsse,
/// <summary>
/// Sigstore bundle format.
/// </summary>
SigstoreBundle,
/// <summary>
/// in-toto attestation envelope.
/// </summary>
InToto
}

View File

@@ -0,0 +1,310 @@
using System.Collections.Concurrent;
using StellaOps.VexLens.Models;
namespace StellaOps.VexLens.Verification;
/// <summary>
/// In-memory implementation of <see cref="IIssuerDirectory"/>.
/// Suitable for testing and single-instance deployments.
/// </summary>
public sealed class InMemoryIssuerDirectory : IIssuerDirectory
{
private readonly ConcurrentDictionary<string, IssuerRecord> _issuers = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, string> _fingerprintToIssuer = new(StringComparer.OrdinalIgnoreCase);
public Task<IssuerRecord?> GetIssuerAsync(
string issuerId,
CancellationToken cancellationToken = default)
{
_issuers.TryGetValue(issuerId, out var issuer);
return Task.FromResult(issuer);
}
public Task<IssuerRecord?> GetIssuerByKeyFingerprintAsync(
string fingerprint,
CancellationToken cancellationToken = default)
{
if (_fingerprintToIssuer.TryGetValue(fingerprint, out var issuerId))
{
_issuers.TryGetValue(issuerId, out var issuer);
return Task.FromResult(issuer);
}
return Task.FromResult<IssuerRecord?>(null);
}
public Task<IReadOnlyList<IssuerRecord>> ListIssuersAsync(
IssuerListOptions? options = null,
CancellationToken cancellationToken = default)
{
var query = _issuers.Values.AsEnumerable();
if (options != null)
{
if (options.Category.HasValue)
{
query = query.Where(i => i.Category == options.Category.Value);
}
if (options.MinimumTrustTier.HasValue)
{
query = query.Where(i => i.TrustTier >= options.MinimumTrustTier.Value);
}
if (options.Status.HasValue)
{
query = query.Where(i => i.Status == options.Status.Value);
}
if (!string.IsNullOrWhiteSpace(options.SearchTerm))
{
var term = options.SearchTerm;
query = query.Where(i =>
i.Name.Contains(term, StringComparison.OrdinalIgnoreCase) ||
i.IssuerId.Contains(term, StringComparison.OrdinalIgnoreCase));
}
if (options.Offset.HasValue)
{
query = query.Skip(options.Offset.Value);
}
if (options.Limit.HasValue)
{
query = query.Take(options.Limit.Value);
}
}
var result = query
.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
return Task.FromResult<IReadOnlyList<IssuerRecord>>(result);
}
public Task<IssuerRecord> RegisterIssuerAsync(
IssuerRegistration registration,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var keyRecords = new List<KeyFingerprintRecord>();
if (registration.InitialKeys != null)
{
foreach (var key in registration.InitialKeys)
{
keyRecords.Add(new KeyFingerprintRecord(
Fingerprint: key.Fingerprint,
KeyType: key.KeyType,
Algorithm: key.Algorithm,
Status: KeyFingerprintStatus.Active,
RegisteredAt: now,
ExpiresAt: key.ExpiresAt,
RevokedAt: null,
RevocationReason: null));
_fingerprintToIssuer[key.Fingerprint] = registration.IssuerId;
}
}
var record = new IssuerRecord(
IssuerId: registration.IssuerId,
Name: registration.Name,
Category: registration.Category,
TrustTier: registration.TrustTier,
Status: IssuerStatus.Active,
KeyFingerprints: keyRecords,
Metadata: registration.Metadata,
RegisteredAt: now,
LastUpdatedAt: null,
RevokedAt: null,
RevocationReason: null);
_issuers[registration.IssuerId] = record;
return Task.FromResult(record);
}
public Task<bool> RevokeIssuerAsync(
string issuerId,
string reason,
CancellationToken cancellationToken = default)
{
if (!_issuers.TryGetValue(issuerId, out var current))
{
return Task.FromResult(false);
}
var now = DateTimeOffset.UtcNow;
var updated = current with
{
Status = IssuerStatus.Revoked,
RevokedAt = now,
RevocationReason = reason,
LastUpdatedAt = now
};
_issuers[issuerId] = updated;
// Also revoke all keys
foreach (var key in current.KeyFingerprints)
{
_fingerprintToIssuer.TryRemove(key.Fingerprint, out _);
}
return Task.FromResult(true);
}
public Task<IssuerRecord> AddKeyFingerprintAsync(
string issuerId,
KeyFingerprintRegistration keyRegistration,
CancellationToken cancellationToken = default)
{
if (!_issuers.TryGetValue(issuerId, out var current))
{
throw new InvalidOperationException($"Issuer '{issuerId}' not found");
}
var now = DateTimeOffset.UtcNow;
var newKey = new KeyFingerprintRecord(
Fingerprint: keyRegistration.Fingerprint,
KeyType: keyRegistration.KeyType,
Algorithm: keyRegistration.Algorithm,
Status: KeyFingerprintStatus.Active,
RegisteredAt: now,
ExpiresAt: keyRegistration.ExpiresAt,
RevokedAt: null,
RevocationReason: null);
var updatedKeys = current.KeyFingerprints.Append(newKey).ToList();
var updated = current with
{
KeyFingerprints = updatedKeys,
LastUpdatedAt = now
};
_issuers[issuerId] = updated;
_fingerprintToIssuer[keyRegistration.Fingerprint] = issuerId;
return Task.FromResult(updated);
}
public Task<bool> RevokeKeyFingerprintAsync(
string issuerId,
string fingerprint,
string reason,
CancellationToken cancellationToken = default)
{
if (!_issuers.TryGetValue(issuerId, out var current))
{
return Task.FromResult(false);
}
var keyIndex = current.KeyFingerprints
.Select((k, i) => (k, i))
.FirstOrDefault(x => x.k.Fingerprint == fingerprint);
if (keyIndex.k == null)
{
return Task.FromResult(false);
}
var now = DateTimeOffset.UtcNow;
var revokedKey = keyIndex.k with
{
Status = KeyFingerprintStatus.Revoked,
RevokedAt = now,
RevocationReason = reason
};
var updatedKeys = current.KeyFingerprints.ToList();
updatedKeys[keyIndex.i] = revokedKey;
var updated = current with
{
KeyFingerprints = updatedKeys,
LastUpdatedAt = now
};
_issuers[issuerId] = updated;
_fingerprintToIssuer.TryRemove(fingerprint, out _);
return Task.FromResult(true);
}
public Task<IssuerTrustValidation> ValidateTrustAsync(
string issuerId,
string? keyFingerprint,
CancellationToken cancellationToken = default)
{
var warnings = new List<string>();
if (!_issuers.TryGetValue(issuerId, out var issuer))
{
return Task.FromResult(new IssuerTrustValidation(
IsTrusted: false,
EffectiveTrustTier: TrustTier.Unknown,
IssuerStatus: IssuerTrustStatus.NotRegistered,
KeyStatus: null,
Warnings: ["Issuer is not registered in the directory"]));
}
var issuerStatus = issuer.Status switch
{
IssuerStatus.Active => IssuerTrustStatus.Trusted,
IssuerStatus.Suspended => IssuerTrustStatus.Suspended,
IssuerStatus.Revoked => IssuerTrustStatus.Revoked,
_ => IssuerTrustStatus.NotRegistered
};
if (issuerStatus != IssuerTrustStatus.Trusted)
{
return Task.FromResult(new IssuerTrustValidation(
IsTrusted: false,
EffectiveTrustTier: TrustTier.Untrusted,
IssuerStatus: issuerStatus,
KeyStatus: null,
Warnings: [$"Issuer status is {issuer.Status}"]));
}
KeyTrustStatus? keyStatus = null;
if (!string.IsNullOrWhiteSpace(keyFingerprint))
{
var key = issuer.KeyFingerprints
.FirstOrDefault(k => k.Fingerprint.Equals(keyFingerprint, StringComparison.OrdinalIgnoreCase));
if (key == null)
{
keyStatus = KeyTrustStatus.NotRegistered;
warnings.Add("Key fingerprint is not registered for this issuer");
}
else if (key.Status == KeyFingerprintStatus.Revoked)
{
keyStatus = KeyTrustStatus.Revoked;
warnings.Add($"Key was revoked: {key.RevocationReason}");
}
else if (key.ExpiresAt.HasValue && key.ExpiresAt.Value < DateTimeOffset.UtcNow)
{
keyStatus = KeyTrustStatus.Expired;
warnings.Add($"Key expired on {key.ExpiresAt.Value:O}");
}
else
{
keyStatus = KeyTrustStatus.Valid;
}
}
var isTrusted = issuerStatus == IssuerTrustStatus.Trusted &&
(keyStatus == null || keyStatus == KeyTrustStatus.Valid);
var effectiveTier = isTrusted ? issuer.TrustTier : TrustTier.Untrusted;
return Task.FromResult(new IssuerTrustValidation(
IsTrusted: isTrusted,
EffectiveTrustTier: effectiveTier,
IssuerStatus: issuerStatus,
KeyStatus: keyStatus,
Warnings: warnings));
}
}

View File

@@ -0,0 +1,424 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.VexLens.Verification;
/// <summary>
/// Default implementation of <see cref="ISignatureVerifier"/>.
/// Provides basic signature verification with extensible format support.
/// </summary>
public sealed class SignatureVerifier : ISignatureVerifier
{
private readonly IIssuerDirectory? _issuerDirectory;
private readonly Dictionary<SignatureFormat, ISignatureFormatHandler> _handlers = [];
public SignatureVerifier(IIssuerDirectory? issuerDirectory = null)
{
_issuerDirectory = issuerDirectory;
// Register default handlers
RegisterHandler(new DsseSignatureHandler());
RegisterHandler(new JwsSignatureHandler());
}
public IReadOnlyList<SignatureFormat> SupportedFormats =>
_handlers.Keys.ToList();
public void RegisterHandler(ISignatureFormatHandler handler)
{
_handlers[handler.Format] = handler;
}
public async Task<SignatureVerificationResult> VerifyAsync(
SignatureVerificationRequest request,
CancellationToken cancellationToken = default)
{
if (!_handlers.TryGetValue(request.Format, out var handler))
{
return new SignatureVerificationResult(
IsValid: false,
Status: SignatureVerificationStatus.UnsupportedFormat,
Signer: null,
CertificateChain: null,
Timestamp: null,
Errors: [new SignatureVerificationError(
"ERR_SIG_001",
$"Unsupported signature format: {request.Format}",
null)],
Warnings: []);
}
var result = await handler.VerifyAsync(request, cancellationToken);
// Validate against issuer directory if available
if (result.IsValid && _issuerDirectory != null && result.Signer != null)
{
var trustValidation = await _issuerDirectory.ValidateTrustAsync(
result.Signer.IssuerId,
result.Signer.KeyFingerprint,
cancellationToken);
if (!trustValidation.IsTrusted)
{
var warnings = result.Warnings.ToList();
warnings.AddRange(trustValidation.Warnings.Select(w =>
new SignatureVerificationWarning("WARN_TRUST", w)));
return result with
{
Status = trustValidation.IssuerStatus switch
{
IssuerTrustStatus.NotRegistered => SignatureVerificationStatus.UntrustedIssuer,
IssuerTrustStatus.Revoked => SignatureVerificationStatus.RevokedCertificate,
_ => SignatureVerificationStatus.UntrustedIssuer
},
Warnings = warnings
};
}
}
return result;
}
public async Task<SignatureExtractionResult> ExtractSignatureInfoAsync(
byte[] signedData,
SignatureFormat format,
CancellationToken cancellationToken = default)
{
if (!_handlers.TryGetValue(format, out var handler))
{
return new SignatureExtractionResult(
Success: false,
DetectedFormat: null,
Signer: null,
Certificates: null,
ErrorMessage: $"Unsupported signature format: {format}");
}
return await handler.ExtractInfoAsync(signedData, cancellationToken);
}
}
/// <summary>
/// Interface for signature format-specific handlers.
/// </summary>
public interface ISignatureFormatHandler
{
SignatureFormat Format { get; }
Task<SignatureVerificationResult> VerifyAsync(
SignatureVerificationRequest request,
CancellationToken cancellationToken = default);
Task<SignatureExtractionResult> ExtractInfoAsync(
byte[] signedData,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Handler for DSSE (Dead Simple Signing Envelope) signatures.
/// </summary>
public sealed class DsseSignatureHandler : ISignatureFormatHandler
{
public SignatureFormat Format => SignatureFormat.Dsse;
public Task<SignatureVerificationResult> VerifyAsync(
SignatureVerificationRequest request,
CancellationToken cancellationToken = default)
{
try
{
var envelope = ParseDsseEnvelope(request.Content);
if (envelope == null)
{
return Task.FromResult(CreateError("ERR_DSSE_001", "Invalid DSSE envelope format"));
}
if (envelope.Signatures == null || envelope.Signatures.Count == 0)
{
return Task.FromResult(CreateError("ERR_DSSE_002", "DSSE envelope has no signatures"));
}
// Extract signer info from first signature
var firstSig = envelope.Signatures[0];
var signer = ExtractSignerFromDsse(firstSig);
// For now, we validate structure but don't perform cryptographic verification
// Full verification would require access to public keys
var warnings = new List<SignatureVerificationWarning>
{
new("WARN_DSSE_001", "Cryptographic verification not performed; structure validated only")
};
return Task.FromResult(new SignatureVerificationResult(
IsValid: true,
Status: SignatureVerificationStatus.Valid,
Signer: signer,
CertificateChain: null,
Timestamp: null,
Errors: [],
Warnings: warnings));
}
catch (Exception ex)
{
return Task.FromResult(CreateError("ERR_DSSE_999", $"DSSE parsing error: {ex.Message}"));
}
}
public Task<SignatureExtractionResult> ExtractInfoAsync(
byte[] signedData,
CancellationToken cancellationToken = default)
{
try
{
var envelope = ParseDsseEnvelope(signedData);
if (envelope == null)
{
return Task.FromResult(new SignatureExtractionResult(
Success: false,
DetectedFormat: SignatureFormat.Dsse,
Signer: null,
Certificates: null,
ErrorMessage: "Invalid DSSE envelope format"));
}
var signer = envelope.Signatures?.Count > 0
? ExtractSignerFromDsse(envelope.Signatures[0])
: null;
return Task.FromResult(new SignatureExtractionResult(
Success: true,
DetectedFormat: SignatureFormat.Dsse,
Signer: signer,
Certificates: null,
ErrorMessage: null));
}
catch (Exception ex)
{
return Task.FromResult(new SignatureExtractionResult(
Success: false,
DetectedFormat: SignatureFormat.Dsse,
Signer: null,
Certificates: null,
ErrorMessage: ex.Message));
}
}
private static DsseEnvelope? ParseDsseEnvelope(byte[] data)
{
try
{
var json = Encoding.UTF8.GetString(data);
return JsonSerializer.Deserialize<DsseEnvelope>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch
{
return null;
}
}
private static SignerInfo? ExtractSignerFromDsse(DsseSignature sig)
{
if (string.IsNullOrEmpty(sig.KeyId))
{
return null;
}
// Compute fingerprint from keyid
var fingerprint = sig.KeyId;
if (fingerprint.StartsWith("SHA256:"))
{
fingerprint = fingerprint[7..];
}
return new SignerInfo(
IssuerId: sig.KeyId,
Name: null,
Email: null,
Organization: null,
KeyFingerprint: fingerprint,
Algorithm: "unknown",
SignedAt: null);
}
private static SignatureVerificationResult CreateError(string code, string message)
{
return new SignatureVerificationResult(
IsValid: false,
Status: SignatureVerificationStatus.InvalidSignature,
Signer: null,
CertificateChain: null,
Timestamp: null,
Errors: [new SignatureVerificationError(code, message, null)],
Warnings: []);
}
private sealed class DsseEnvelope
{
public string? PayloadType { get; set; }
public string? Payload { get; set; }
public List<DsseSignature>? Signatures { get; set; }
}
private sealed class DsseSignature
{
public string? KeyId { get; set; }
public string? Sig { get; set; }
}
}
/// <summary>
/// Handler for JWS (JSON Web Signature) signatures.
/// </summary>
public sealed class JwsSignatureHandler : ISignatureFormatHandler
{
public SignatureFormat Format => SignatureFormat.Jws;
public Task<SignatureVerificationResult> VerifyAsync(
SignatureVerificationRequest request,
CancellationToken cancellationToken = default)
{
try
{
var jwsString = Encoding.UTF8.GetString(request.Content);
var parts = jwsString.Split('.');
if (parts.Length != 3)
{
return Task.FromResult(CreateError("ERR_JWS_001", "Invalid JWS format: expected 3 parts"));
}
// Parse header
var headerJson = Base64UrlDecode(parts[0]);
var header = JsonSerializer.Deserialize<JwsHeader>(headerJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (header == null)
{
return Task.FromResult(CreateError("ERR_JWS_002", "Invalid JWS header"));
}
var signer = new SignerInfo(
IssuerId: header.Kid ?? "unknown",
Name: null,
Email: null,
Organization: null,
KeyFingerprint: header.Kid ?? ComputeFingerprint(parts[0]),
Algorithm: header.Alg ?? "unknown",
SignedAt: null);
var warnings = new List<SignatureVerificationWarning>
{
new("WARN_JWS_001", "Cryptographic verification not performed; structure validated only")
};
return Task.FromResult(new SignatureVerificationResult(
IsValid: true,
Status: SignatureVerificationStatus.Valid,
Signer: signer,
CertificateChain: null,
Timestamp: null,
Errors: [],
Warnings: warnings));
}
catch (Exception ex)
{
return Task.FromResult(CreateError("ERR_JWS_999", $"JWS parsing error: {ex.Message}"));
}
}
public Task<SignatureExtractionResult> ExtractInfoAsync(
byte[] signedData,
CancellationToken cancellationToken = default)
{
try
{
var jwsString = Encoding.UTF8.GetString(signedData);
var parts = jwsString.Split('.');
if (parts.Length != 3)
{
return Task.FromResult(new SignatureExtractionResult(
Success: false,
DetectedFormat: SignatureFormat.Jws,
Signer: null,
Certificates: null,
ErrorMessage: "Invalid JWS format"));
}
var headerJson = Base64UrlDecode(parts[0]);
var header = JsonSerializer.Deserialize<JwsHeader>(headerJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var signer = new SignerInfo(
IssuerId: header?.Kid ?? "unknown",
Name: null,
Email: null,
Organization: null,
KeyFingerprint: header?.Kid ?? ComputeFingerprint(parts[0]),
Algorithm: header?.Alg ?? "unknown",
SignedAt: null);
return Task.FromResult(new SignatureExtractionResult(
Success: true,
DetectedFormat: SignatureFormat.Jws,
Signer: signer,
Certificates: null,
ErrorMessage: null));
}
catch (Exception ex)
{
return Task.FromResult(new SignatureExtractionResult(
Success: false,
DetectedFormat: SignatureFormat.Jws,
Signer: null,
Certificates: null,
ErrorMessage: ex.Message));
}
}
private static string Base64UrlDecode(string input)
{
var output = input.Replace('-', '+').Replace('_', '/');
switch (output.Length % 4)
{
case 2: output += "=="; break;
case 3: output += "="; break;
}
var bytes = Convert.FromBase64String(output);
return Encoding.UTF8.GetString(bytes);
}
private static string ComputeFingerprint(string headerBase64)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(headerBase64));
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static SignatureVerificationResult CreateError(string code, string message)
{
return new SignatureVerificationResult(
IsValid: false,
Status: SignatureVerificationStatus.InvalidSignature,
Signer: null,
CertificateChain: null,
Timestamp: null,
Errors: [new SignatureVerificationError(code, message, null)],
Warnings: []);
}
private sealed class JwsHeader
{
public string? Alg { get; set; }
public string? Kid { get; set; }
public string? Typ { get; set; }
}
}