57 lines
1.9 KiB
C#
57 lines
1.9 KiB
C#
using System.Text.Json;
|
|
|
|
namespace StellaOps.Provenance.Attestation;
|
|
|
|
public sealed record PromotionPredicate(
|
|
string ImageDigest,
|
|
string SbomDigest,
|
|
string VexDigest,
|
|
string PromotionId,
|
|
string? RekorEntry = null,
|
|
IReadOnlyDictionary<string, string>? Metadata = null);
|
|
|
|
public sealed record PromotionAttestation(
|
|
PromotionPredicate Predicate,
|
|
byte[] Payload,
|
|
SignResult Signature);
|
|
|
|
public static class PromotionAttestationBuilder
|
|
{
|
|
public const string PredicateType = "stella.ops/promotion@v1";
|
|
public const string ContentType = "application/vnd.stella.promotion+json";
|
|
|
|
public static byte[] CreateCanonicalJson(PromotionPredicate predicate)
|
|
{
|
|
if (predicate is null) throw new ArgumentNullException(nameof(predicate));
|
|
return CanonicalJson.SerializeToUtf8Bytes(predicate);
|
|
}
|
|
|
|
public static async Task<PromotionAttestation> BuildAsync(
|
|
PromotionPredicate predicate,
|
|
ISigner signer,
|
|
IReadOnlyDictionary<string, string>? claims = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (predicate is null) throw new ArgumentNullException(nameof(predicate));
|
|
if (signer is null) throw new ArgumentNullException(nameof(signer));
|
|
|
|
var payload = CreateCanonicalJson(predicate);
|
|
|
|
// ensure predicate type claim is always present
|
|
var mergedClaims = claims is null
|
|
? new Dictionary<string, string>(StringComparer.Ordinal)
|
|
: new Dictionary<string, string>(claims, StringComparer.Ordinal);
|
|
mergedClaims["predicateType"] = PredicateType;
|
|
|
|
var request = new SignRequest(
|
|
Payload: payload,
|
|
ContentType: ContentType,
|
|
Claims: mergedClaims,
|
|
RequiredClaims: new[] { "predicateType" });
|
|
|
|
var signature = await signer.SignAsync(request, cancellationToken).ConfigureAwait(false);
|
|
|
|
return new PromotionAttestation(predicate, payload, signature);
|
|
}
|
|
}
|