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,446 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Policy.RiskProfile.Scope;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing effective policies with subject pattern matching and priority resolution.
|
||||
/// Implements CONTRACT-AUTHORITY-EFFECTIVE-WRITE-008.
|
||||
/// </summary>
|
||||
public sealed class EffectivePolicyService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ConcurrentDictionary<string, EffectivePolicy> _policies;
|
||||
private readonly ConcurrentDictionary<string, AuthorityScopeAttachment> _scopeAttachments;
|
||||
private readonly ConcurrentDictionary<string, List<string>> _policyAttachmentIndex;
|
||||
|
||||
public EffectivePolicyService(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_policies = new ConcurrentDictionary<string, EffectivePolicy>(StringComparer.OrdinalIgnoreCase);
|
||||
_scopeAttachments = new ConcurrentDictionary<string, AuthorityScopeAttachment>(StringComparer.OrdinalIgnoreCase);
|
||||
_policyAttachmentIndex = new ConcurrentDictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new effective policy.
|
||||
/// </summary>
|
||||
public EffectivePolicy Create(CreateEffectivePolicyRequest request, string? createdBy = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.TenantId))
|
||||
{
|
||||
throw new ArgumentException("TenantId is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PolicyId))
|
||||
{
|
||||
throw new ArgumentException("PolicyId is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.SubjectPattern))
|
||||
{
|
||||
throw new ArgumentException("SubjectPattern is required.");
|
||||
}
|
||||
|
||||
if (!IsValidSubjectPattern(request.SubjectPattern))
|
||||
{
|
||||
throw new ArgumentException($"Invalid subject pattern: {request.SubjectPattern}");
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = GeneratePolicyId(request.TenantId, request.PolicyId, request.SubjectPattern, now);
|
||||
|
||||
var policy = new EffectivePolicy(
|
||||
EffectivePolicyId: id,
|
||||
TenantId: request.TenantId,
|
||||
PolicyId: request.PolicyId,
|
||||
PolicyVersion: request.PolicyVersion,
|
||||
SubjectPattern: request.SubjectPattern,
|
||||
Priority: request.Priority,
|
||||
Enabled: request.Enabled,
|
||||
ExpiresAt: request.ExpiresAt,
|
||||
Scopes: request.Scopes?.ToList().AsReadOnly(),
|
||||
CreatedAt: now,
|
||||
CreatedBy: createdBy,
|
||||
UpdatedAt: now);
|
||||
|
||||
_policies[id] = policy;
|
||||
return policy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an effective policy by ID.
|
||||
/// </summary>
|
||||
public EffectivePolicy? Get(string effectivePolicyId)
|
||||
{
|
||||
return _policies.TryGetValue(effectivePolicyId, out var policy) ? policy : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an effective policy.
|
||||
/// </summary>
|
||||
public EffectivePolicy? Update(string effectivePolicyId, UpdateEffectivePolicyRequest request, string? updatedBy = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!_policies.TryGetValue(effectivePolicyId, out var existing))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var updated = existing with
|
||||
{
|
||||
Priority = request.Priority ?? existing.Priority,
|
||||
Enabled = request.Enabled ?? existing.Enabled,
|
||||
ExpiresAt = request.ExpiresAt ?? existing.ExpiresAt,
|
||||
Scopes = request.Scopes?.ToList().AsReadOnly() ?? existing.Scopes,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
_policies[effectivePolicyId] = updated;
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an effective policy.
|
||||
/// </summary>
|
||||
public bool Delete(string effectivePolicyId)
|
||||
{
|
||||
if (_policies.TryRemove(effectivePolicyId, out _))
|
||||
{
|
||||
// Remove associated scope attachments
|
||||
if (_policyAttachmentIndex.TryRemove(effectivePolicyId, out var attachmentIds))
|
||||
{
|
||||
foreach (var attachmentId in attachmentIds)
|
||||
{
|
||||
_scopeAttachments.TryRemove(attachmentId, out _);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists effective policies matching query criteria.
|
||||
/// </summary>
|
||||
public IReadOnlyList<EffectivePolicy> Query(EffectivePolicyQuery query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
IEnumerable<EffectivePolicy> results = _policies.Values;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.TenantId))
|
||||
{
|
||||
results = results.Where(p => p.TenantId.Equals(query.TenantId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.PolicyId))
|
||||
{
|
||||
results = results.Where(p => p.PolicyId.Equals(query.PolicyId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (query.EnabledOnly)
|
||||
{
|
||||
results = results.Where(p => p.Enabled);
|
||||
}
|
||||
|
||||
if (!query.IncludeExpired)
|
||||
{
|
||||
results = results.Where(p => !p.ExpiresAt.HasValue || p.ExpiresAt.Value > now);
|
||||
}
|
||||
|
||||
return results
|
||||
.OrderByDescending(p => p.Priority)
|
||||
.ThenByDescending(p => p.UpdatedAt)
|
||||
.Take(query.Limit)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches an authority scope to an effective policy.
|
||||
/// </summary>
|
||||
public AuthorityScopeAttachment AttachScope(AttachAuthorityScopeRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.EffectivePolicyId))
|
||||
{
|
||||
throw new ArgumentException("EffectivePolicyId is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Scope))
|
||||
{
|
||||
throw new ArgumentException("Scope is required.");
|
||||
}
|
||||
|
||||
if (!_policies.ContainsKey(request.EffectivePolicyId))
|
||||
{
|
||||
throw new ArgumentException($"Effective policy '{request.EffectivePolicyId}' not found.");
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = GenerateAttachmentId(request.EffectivePolicyId, request.Scope, now);
|
||||
|
||||
var attachment = new AuthorityScopeAttachment(
|
||||
AttachmentId: id,
|
||||
EffectivePolicyId: request.EffectivePolicyId,
|
||||
Scope: request.Scope,
|
||||
Conditions: request.Conditions,
|
||||
CreatedAt: now);
|
||||
|
||||
_scopeAttachments[id] = attachment;
|
||||
IndexAttachment(attachment);
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches an authority scope.
|
||||
/// </summary>
|
||||
public bool DetachScope(string attachmentId)
|
||||
{
|
||||
if (_scopeAttachments.TryRemove(attachmentId, out var attachment))
|
||||
{
|
||||
RemoveFromIndex(attachment);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all scope attachments for an effective policy.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AuthorityScopeAttachment> GetScopeAttachments(string effectivePolicyId)
|
||||
{
|
||||
if (_policyAttachmentIndex.TryGetValue(effectivePolicyId, out var attachmentIds))
|
||||
{
|
||||
lock (attachmentIds)
|
||||
{
|
||||
return attachmentIds
|
||||
.Select(id => _scopeAttachments.TryGetValue(id, out var a) ? a : null)
|
||||
.Where(a => a != null)
|
||||
.Cast<AuthorityScopeAttachment>()
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
}
|
||||
return Array.Empty<AuthorityScopeAttachment>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective policy for a subject using priority and specificity rules.
|
||||
/// Priority resolution order:
|
||||
/// 1. Higher priority value wins
|
||||
/// 2. If equal priority, more specific pattern wins
|
||||
/// 3. If equal specificity, most recently updated wins
|
||||
/// </summary>
|
||||
public EffectivePolicyResolutionResult Resolve(string subject, string? tenantId = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subject);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Find all matching policies
|
||||
var matchingPolicies = _policies.Values
|
||||
.Where(p => p.Enabled)
|
||||
.Where(p => string.IsNullOrWhiteSpace(tenantId) || p.TenantId.Equals(tenantId, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(p => !p.ExpiresAt.HasValue || p.ExpiresAt.Value > now)
|
||||
.Where(p => MatchesPattern(subject, p.SubjectPattern))
|
||||
.ToList();
|
||||
|
||||
if (matchingPolicies.Count == 0)
|
||||
{
|
||||
sw.Stop();
|
||||
return new EffectivePolicyResolutionResult(
|
||||
Subject: subject,
|
||||
EffectivePolicy: null,
|
||||
GrantedScopes: Array.Empty<string>(),
|
||||
MatchedPattern: null,
|
||||
ResolutionTimeMs: sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
// Apply priority resolution rules
|
||||
var winner = matchingPolicies
|
||||
.OrderByDescending(p => p.Priority)
|
||||
.ThenByDescending(p => GetPatternSpecificity(p.SubjectPattern))
|
||||
.ThenByDescending(p => p.UpdatedAt)
|
||||
.First();
|
||||
|
||||
// Collect granted scopes from the winning policy and its attachments
|
||||
var grantedScopes = new List<string>();
|
||||
if (winner.Scopes != null)
|
||||
{
|
||||
grantedScopes.AddRange(winner.Scopes);
|
||||
}
|
||||
|
||||
// Add scopes from attachments
|
||||
var attachments = GetScopeAttachments(winner.EffectivePolicyId);
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
if (!grantedScopes.Contains(attachment.Scope, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
grantedScopes.Add(attachment.Scope);
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
return new EffectivePolicyResolutionResult(
|
||||
Subject: subject,
|
||||
EffectivePolicy: winner,
|
||||
GrantedScopes: grantedScopes.AsReadOnly(),
|
||||
MatchedPattern: winner.SubjectPattern,
|
||||
ResolutionTimeMs: sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a subject pattern.
|
||||
/// Valid patterns: *, pkg:*, pkg:npm/*, pkg:npm/@org/*, oci://registry/*
|
||||
/// </summary>
|
||||
public static bool IsValidSubjectPattern(string pattern)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Universal wildcard
|
||||
if (pattern == "*")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must be a valid PURL or OCI pattern with optional wildcards
|
||||
if (pattern.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase) ||
|
||||
pattern.StartsWith("oci://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Pattern should not have consecutive wildcards or invalid chars
|
||||
if (pattern.Contains("**", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a subject matches a glob-style pattern.
|
||||
/// </summary>
|
||||
public static bool MatchesPattern(string subject, string pattern)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subject) || string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Universal wildcard matches everything
|
||||
if (pattern == "*")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert glob pattern to regex
|
||||
var regexPattern = GlobToRegex(pattern);
|
||||
return Regex.IsMatch(subject, regexPattern, RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specificity score of a pattern (higher = more specific).
|
||||
/// Scoring: length of non-wildcard characters * 10, bonus for segment depth
|
||||
/// </summary>
|
||||
public static int GetPatternSpecificity(string pattern)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Universal wildcard is least specific
|
||||
if (pattern == "*")
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Count literal (non-wildcard) characters
|
||||
var literalChars = pattern.Count(c => c != '*');
|
||||
|
||||
// Count path segments (depth bonus)
|
||||
var segmentCount = pattern.Count(c => c == '/') + 1;
|
||||
|
||||
// Base score: literal characters weighted heavily
|
||||
// Segment bonus: more segments = more specific
|
||||
return (literalChars * 10) + (segmentCount * 5);
|
||||
}
|
||||
|
||||
private static string GlobToRegex(string pattern)
|
||||
{
|
||||
// Escape regex special characters except *
|
||||
var escaped = Regex.Escape(pattern);
|
||||
|
||||
// Replace escaped wildcards with regex equivalents
|
||||
// For trailing wildcards, match everything (including /)
|
||||
// For middle wildcards, match single path segment only
|
||||
if (escaped.EndsWith(@"\*", StringComparison.Ordinal))
|
||||
{
|
||||
// Trailing wildcard: match everything remaining
|
||||
escaped = escaped[..^2] + ".*";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-trailing wildcards: match single path segment
|
||||
escaped = escaped.Replace(@"\*", @"[^/]*");
|
||||
}
|
||||
|
||||
return $"^{escaped}$";
|
||||
}
|
||||
|
||||
private void IndexAttachment(AuthorityScopeAttachment attachment)
|
||||
{
|
||||
var list = _policyAttachmentIndex.GetOrAdd(attachment.EffectivePolicyId, _ => new List<string>());
|
||||
lock (list)
|
||||
{
|
||||
if (!list.Contains(attachment.AttachmentId))
|
||||
{
|
||||
list.Add(attachment.AttachmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromIndex(AuthorityScopeAttachment attachment)
|
||||
{
|
||||
if (_policyAttachmentIndex.TryGetValue(attachment.EffectivePolicyId, out var list))
|
||||
{
|
||||
lock (list)
|
||||
{
|
||||
list.Remove(attachment.AttachmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GeneratePolicyId(string tenantId, string policyId, string pattern, DateTimeOffset timestamp)
|
||||
{
|
||||
var seed = $"{tenantId}|{policyId}|{pattern}|{timestamp:O}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(seed));
|
||||
return $"eff-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
|
||||
private static string GenerateAttachmentId(string policyId, string scope, DateTimeOffset timestamp)
|
||||
{
|
||||
var seed = $"{policyId}|{scope}|{timestamp:O}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(seed));
|
||||
return $"att-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
}
|
||||
@@ -107,3 +107,81 @@ public sealed record ScopeResolutionResult(
|
||||
[property: JsonPropertyName("resolved_profile")] ResolvedScopeProfile? ResolvedProfile,
|
||||
[property: JsonPropertyName("applicable_attachments")] IReadOnlyList<ScopeAttachment> ApplicableAttachments,
|
||||
[property: JsonPropertyName("resolution_time_ms")] double ResolutionTimeMs);
|
||||
|
||||
/// <summary>
|
||||
/// Effective policy attachment with subject pattern matching and priority rules.
|
||||
/// Per CONTRACT-AUTHORITY-EFFECTIVE-WRITE-008.
|
||||
/// </summary>
|
||||
public sealed record EffectivePolicy(
|
||||
[property: JsonPropertyName("effective_policy_id")] string EffectivePolicyId,
|
||||
[property: JsonPropertyName("tenant_id")] string TenantId,
|
||||
[property: JsonPropertyName("policy_id")] string PolicyId,
|
||||
[property: JsonPropertyName("policy_version")] string? PolicyVersion,
|
||||
[property: JsonPropertyName("subject_pattern")] string SubjectPattern,
|
||||
[property: JsonPropertyName("priority")] int Priority,
|
||||
[property: JsonPropertyName("enabled")] bool Enabled,
|
||||
[property: JsonPropertyName("expires_at")] DateTimeOffset? ExpiresAt,
|
||||
[property: JsonPropertyName("scopes")] IReadOnlyList<string>? Scopes,
|
||||
[property: JsonPropertyName("created_at")] DateTimeOffset CreatedAt,
|
||||
[property: JsonPropertyName("created_by")] string? CreatedBy,
|
||||
[property: JsonPropertyName("updated_at")] DateTimeOffset UpdatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Request to create an effective policy.
|
||||
/// </summary>
|
||||
public sealed record CreateEffectivePolicyRequest(
|
||||
[property: JsonPropertyName("tenant_id")] string TenantId,
|
||||
[property: JsonPropertyName("policy_id")] string PolicyId,
|
||||
[property: JsonPropertyName("policy_version")] string? PolicyVersion,
|
||||
[property: JsonPropertyName("subject_pattern")] string SubjectPattern,
|
||||
[property: JsonPropertyName("priority")] int Priority,
|
||||
[property: JsonPropertyName("enabled")] bool Enabled = true,
|
||||
[property: JsonPropertyName("expires_at")] DateTimeOffset? ExpiresAt = null,
|
||||
[property: JsonPropertyName("scopes")] IReadOnlyList<string>? Scopes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Request to update an effective policy.
|
||||
/// </summary>
|
||||
public sealed record UpdateEffectivePolicyRequest(
|
||||
[property: JsonPropertyName("priority")] int? Priority = null,
|
||||
[property: JsonPropertyName("enabled")] bool? Enabled = null,
|
||||
[property: JsonPropertyName("expires_at")] DateTimeOffset? ExpiresAt = null,
|
||||
[property: JsonPropertyName("scopes")] IReadOnlyList<string>? Scopes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Authority scope attachment with conditions.
|
||||
/// </summary>
|
||||
public sealed record AuthorityScopeAttachment(
|
||||
[property: JsonPropertyName("attachment_id")] string AttachmentId,
|
||||
[property: JsonPropertyName("effective_policy_id")] string EffectivePolicyId,
|
||||
[property: JsonPropertyName("scope")] string Scope,
|
||||
[property: JsonPropertyName("conditions")] Dictionary<string, string>? Conditions,
|
||||
[property: JsonPropertyName("created_at")] DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Request to attach an authority scope.
|
||||
/// </summary>
|
||||
public sealed record AttachAuthorityScopeRequest(
|
||||
[property: JsonPropertyName("effective_policy_id")] string EffectivePolicyId,
|
||||
[property: JsonPropertyName("scope")] string Scope,
|
||||
[property: JsonPropertyName("conditions")] Dictionary<string, string>? Conditions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Result of resolving the effective policy for a subject.
|
||||
/// </summary>
|
||||
public sealed record EffectivePolicyResolutionResult(
|
||||
[property: JsonPropertyName("subject")] string Subject,
|
||||
[property: JsonPropertyName("effective_policy")] EffectivePolicy? EffectivePolicy,
|
||||
[property: JsonPropertyName("granted_scopes")] IReadOnlyList<string> GrantedScopes,
|
||||
[property: JsonPropertyName("matched_pattern")] string? MatchedPattern,
|
||||
[property: JsonPropertyName("resolution_time_ms")] double ResolutionTimeMs);
|
||||
|
||||
/// <summary>
|
||||
/// Query for listing effective policies.
|
||||
/// </summary>
|
||||
public sealed record EffectivePolicyQuery(
|
||||
[property: JsonPropertyName("tenant_id")] string? TenantId = null,
|
||||
[property: JsonPropertyName("policy_id")] string? PolicyId = null,
|
||||
[property: JsonPropertyName("enabled_only")] bool EnabledOnly = true,
|
||||
[property: JsonPropertyName("include_expired")] bool IncludeExpired = false,
|
||||
[property: JsonPropertyName("limit")] int Limit = 100);
|
||||
|
||||
Reference in New Issue
Block a user