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