Add Authority Advisory AI and API Lifecycle Configuration
- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings. - Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations. - Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration. - Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options. - Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations. - Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client. - Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Attestor.Envelope;
|
||||
|
||||
public static class DsseEnvelopeSerializer
|
||||
{
|
||||
public static DsseEnvelopeSerializationResult Serialize(DsseEnvelope envelope, DsseEnvelopeSerializationOptions? options = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envelope);
|
||||
|
||||
options ??= new DsseEnvelopeSerializationOptions();
|
||||
|
||||
var originalPayload = envelope.Payload.ToArray();
|
||||
var processedPayload = ApplyCompression(originalPayload, options.CompressionAlgorithm);
|
||||
var payloadSha256 = Convert.ToHexString(SHA256.HashData(originalPayload)).ToLowerInvariant();
|
||||
var payloadBase64 = Convert.ToBase64String(processedPayload);
|
||||
|
||||
byte[]? compactJson = null;
|
||||
if (options.EmitCompactJson)
|
||||
{
|
||||
compactJson = BuildCompactJson(envelope.PayloadType, payloadBase64, envelope.Signatures);
|
||||
}
|
||||
|
||||
byte[]? expandedJson = null;
|
||||
if (options.EmitExpandedJson)
|
||||
{
|
||||
expandedJson = BuildExpandedJson(
|
||||
envelope,
|
||||
payloadBase64,
|
||||
payloadSha256,
|
||||
originalPayload.Length,
|
||||
processedPayload.Length,
|
||||
options,
|
||||
originalPayload);
|
||||
}
|
||||
|
||||
return new DsseEnvelopeSerializationResult(
|
||||
compactJson,
|
||||
expandedJson,
|
||||
payloadSha256,
|
||||
originalPayload.Length,
|
||||
processedPayload.Length,
|
||||
options.CompressionAlgorithm,
|
||||
envelope.DetachedPayload);
|
||||
}
|
||||
|
||||
private static byte[] BuildCompactJson(string payloadType, string payloadBase64, IReadOnlyList<DsseSignature> signatures)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
using var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = false
|
||||
});
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("payloadType", payloadType);
|
||||
writer.WriteString("payload", payloadBase64);
|
||||
writer.WritePropertyName("signatures");
|
||||
writer.WriteStartArray();
|
||||
|
||||
foreach (var signature in EnumerateCanonicalSignatures(signatures))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
if (!string.IsNullOrWhiteSpace(signature.KeyId))
|
||||
{
|
||||
writer.WriteString("keyid", signature.KeyId);
|
||||
}
|
||||
|
||||
writer.WriteString("sig", signature.Signature);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
writer.Flush();
|
||||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
private static byte[]? BuildExpandedJson(
|
||||
DsseEnvelope envelope,
|
||||
string payloadBase64,
|
||||
string payloadSha256,
|
||||
int originalPayloadLength,
|
||||
int embeddedPayloadLength,
|
||||
DsseEnvelopeSerializationOptions options,
|
||||
byte[] originalPayload)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
using var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = options.IndentExpandedJson
|
||||
});
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("payloadType", envelope.PayloadType);
|
||||
writer.WriteString("payload", payloadBase64);
|
||||
writer.WritePropertyName("signatures");
|
||||
writer.WriteStartArray();
|
||||
|
||||
foreach (var signature in EnumerateCanonicalSignatures(envelope.Signatures))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
if (!string.IsNullOrWhiteSpace(signature.KeyId))
|
||||
{
|
||||
writer.WriteString("keyid", signature.KeyId);
|
||||
}
|
||||
|
||||
writer.WriteString("sig", signature.Signature);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("payloadInfo");
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("sha256", payloadSha256);
|
||||
writer.WriteNumber("length", originalPayloadLength);
|
||||
|
||||
if (options.CompressionAlgorithm != DsseCompressionAlgorithm.None)
|
||||
{
|
||||
writer.WritePropertyName("compression");
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("algorithm", GetCompressionName(options.CompressionAlgorithm));
|
||||
writer.WriteNumber("compressedLength", embeddedPayloadLength);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndObject(); // payloadInfo
|
||||
|
||||
if (options.IncludePayloadPreview && TryWritePayloadPreview(envelope.PayloadContentType, originalPayload, writer))
|
||||
{
|
||||
// preview already written inside helper
|
||||
}
|
||||
|
||||
if (envelope.DetachedPayload is not null)
|
||||
{
|
||||
writer.WritePropertyName("detachedPayload");
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("uri", envelope.DetachedPayload.Uri);
|
||||
writer.WriteString("sha256", envelope.DetachedPayload.Sha256);
|
||||
if (envelope.DetachedPayload.Length.HasValue)
|
||||
{
|
||||
writer.WriteNumber("length", envelope.DetachedPayload.Length.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(envelope.DetachedPayload.MediaType))
|
||||
{
|
||||
writer.WriteString("mediaType", envelope.DetachedPayload.MediaType);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
writer.Flush();
|
||||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
private static bool TryWritePayloadPreview(string? contentType, byte[] originalPayload, Utf8JsonWriter writer)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lower = contentType.ToLowerInvariant();
|
||||
if (!lower.Contains("json") && !lower.StartsWith("text/", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
writer.WritePropertyName("payloadPreview");
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("mediaType", contentType);
|
||||
|
||||
if (lower.Contains("json") && TryParseJson(originalPayload, out var jsonDocument))
|
||||
{
|
||||
writer.WritePropertyName("json");
|
||||
jsonDocument.WriteTo(writer);
|
||||
jsonDocument.Dispose();
|
||||
}
|
||||
else if (TryDecodeUtf8(originalPayload, out var text))
|
||||
{
|
||||
writer.WriteString("text", text);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseJson(byte[] payload, out JsonDocument document)
|
||||
{
|
||||
try
|
||||
{
|
||||
document = JsonDocument.Parse(payload);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
document = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryDecodeUtf8(byte[] payload, out string text)
|
||||
{
|
||||
var utf8 = new UTF8Encoding(false, true);
|
||||
try
|
||||
{
|
||||
text = utf8.GetString(payload);
|
||||
return true;
|
||||
}
|
||||
catch (DecoderFallbackException)
|
||||
{
|
||||
text = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ApplyCompression(byte[] payload, DsseCompressionAlgorithm algorithm)
|
||||
{
|
||||
return algorithm switch
|
||||
{
|
||||
DsseCompressionAlgorithm.None => payload,
|
||||
DsseCompressionAlgorithm.Gzip => CompressWithStream(payload, static (stream) => new GZipStream(stream, CompressionLevel.SmallestSize, leaveOpen: true)),
|
||||
DsseCompressionAlgorithm.Brotli => CompressWithStream(payload, static (stream) => new BrotliStream(stream, CompressionLevel.SmallestSize, leaveOpen: true)),
|
||||
_ => throw new NotSupportedException($"Compression algorithm '{algorithm}' is not supported.")
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] CompressWithStream(byte[] payload, Func<Stream, Stream> streamFactory)
|
||||
{
|
||||
if (payload.Length == 0)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
using var output = new MemoryStream();
|
||||
using (var compressionStream = streamFactory(output))
|
||||
{
|
||||
compressionStream.Write(payload);
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static string GetCompressionName(DsseCompressionAlgorithm algorithm)
|
||||
{
|
||||
return algorithm switch
|
||||
{
|
||||
DsseCompressionAlgorithm.Gzip => "gzip",
|
||||
DsseCompressionAlgorithm.Brotli => "brotli",
|
||||
DsseCompressionAlgorithm.None => "none",
|
||||
_ => algorithm.ToString().ToLowerInvariant()
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<DsseSignature> EnumerateCanonicalSignatures(IReadOnlyList<DsseSignature> signatures)
|
||||
{
|
||||
if (signatures.Count <= 1)
|
||||
{
|
||||
return signatures;
|
||||
}
|
||||
|
||||
var comparer = CanonicalSignatureComparer.Instance;
|
||||
var previous = signatures[0];
|
||||
for (var i = 1; i < signatures.Count; i++)
|
||||
{
|
||||
var current = signatures[i];
|
||||
if (comparer.Compare(previous, current) > 0)
|
||||
{
|
||||
var buffer = new List<DsseSignature>(signatures.Count);
|
||||
for (var j = 0; j < signatures.Count; j++)
|
||||
{
|
||||
buffer.Add(signatures[j]);
|
||||
}
|
||||
|
||||
buffer.Sort(comparer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
previous = current;
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
private sealed class CanonicalSignatureComparer : IComparer<DsseSignature>
|
||||
{
|
||||
public static CanonicalSignatureComparer Instance { get; } = new();
|
||||
|
||||
public int Compare(DsseSignature? x, DsseSignature? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(x);
|
||||
ArgumentNullException.ThrowIfNull(y);
|
||||
|
||||
var keyComparison = string.Compare(x.KeyId, y.KeyId, StringComparison.Ordinal);
|
||||
if (keyComparison != 0)
|
||||
{
|
||||
if (x.KeyId is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y.KeyId is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return keyComparison;
|
||||
}
|
||||
|
||||
return string.Compare(x.Signature, y.Signature, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user