Files
git.stella-ops.org/src/Attestor/StellaOps.Provenance.Attestation/PromotionAttestation.cs

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);
}
}