322 lines
12 KiB
C#
322 lines
12 KiB
C#
using System;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Attestor.Core.Options;
|
|
using StellaOps.Attestor.Core.Submission;
|
|
using StellaOps.Attestor.Core.Observability;
|
|
using StellaOps.Attestor.Infrastructure.Rekor;
|
|
using StellaOps.Attestor.Infrastructure.Storage;
|
|
using StellaOps.Attestor.Infrastructure.Submission;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Attestor.Tests;
|
|
|
|
public sealed class AttestorSubmissionServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task SubmitAsync_ReturnsDeterministicUuid_OnDuplicateBundle()
|
|
{
|
|
var options = Options.Create(new AttestorOptions
|
|
{
|
|
Redis = new AttestorOptions.RedisOptions
|
|
{
|
|
Url = string.Empty
|
|
},
|
|
Rekor = new AttestorOptions.RekorOptions
|
|
{
|
|
Primary = new AttestorOptions.RekorBackendOptions
|
|
{
|
|
Url = "https://rekor.stellaops.test",
|
|
ProofTimeoutMs = 1000,
|
|
PollIntervalMs = 50,
|
|
MaxAttempts = 2
|
|
}
|
|
}
|
|
});
|
|
|
|
var canonicalizer = new DefaultDsseCanonicalizer();
|
|
var validator = new AttestorSubmissionValidator(canonicalizer);
|
|
var repository = new InMemoryAttestorEntryRepository();
|
|
var dedupeStore = new InMemoryAttestorDedupeStore();
|
|
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
|
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
|
var auditSink = new InMemoryAttestorAuditSink();
|
|
var logger = new NullLogger<AttestorSubmissionService>();
|
|
using var metrics = new AttestorMetrics();
|
|
var service = new AttestorSubmissionService(
|
|
validator,
|
|
repository,
|
|
dedupeStore,
|
|
rekorClient,
|
|
archiveStore,
|
|
auditSink,
|
|
options,
|
|
logger,
|
|
TimeProvider.System,
|
|
metrics);
|
|
|
|
var request = CreateValidRequest(canonicalizer);
|
|
var context = new SubmissionContext
|
|
{
|
|
CallerSubject = "urn:stellaops:signer",
|
|
CallerAudience = "attestor",
|
|
CallerClientId = "signer-service",
|
|
CallerTenant = "default",
|
|
ClientCertificate = null,
|
|
MtlsThumbprint = "00"
|
|
};
|
|
|
|
var first = await service.SubmitAsync(request, context);
|
|
var second = await service.SubmitAsync(request, context);
|
|
|
|
Assert.NotNull(first.Uuid);
|
|
Assert.Equal(first.Uuid, second.Uuid);
|
|
|
|
var stored = await repository.GetByBundleShaAsync(request.Meta.BundleSha256);
|
|
Assert.NotNull(stored);
|
|
Assert.Equal(first.Uuid, stored!.RekorUuid);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validator_ThrowsWhenModeNotAllowed()
|
|
{
|
|
var canonicalizer = new DefaultDsseCanonicalizer();
|
|
var validator = new AttestorSubmissionValidator(canonicalizer, new[] { "kms" });
|
|
|
|
var request = CreateValidRequest(canonicalizer);
|
|
request.Bundle.Mode = "keyless";
|
|
|
|
await Assert.ThrowsAsync<AttestorValidationException>(() => validator.ValidateAsync(request));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitAsync_Throws_WhenMirrorDisabledButRequested()
|
|
{
|
|
var options = Options.Create(new AttestorOptions
|
|
{
|
|
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
|
Rekor = new AttestorOptions.RekorOptions
|
|
{
|
|
Primary = new AttestorOptions.RekorBackendOptions
|
|
{
|
|
Url = "https://rekor.primary.test",
|
|
ProofTimeoutMs = 1000,
|
|
PollIntervalMs = 50,
|
|
MaxAttempts = 2
|
|
}
|
|
}
|
|
});
|
|
|
|
var canonicalizer = new DefaultDsseCanonicalizer();
|
|
var validator = new AttestorSubmissionValidator(canonicalizer);
|
|
var repository = new InMemoryAttestorEntryRepository();
|
|
var dedupeStore = new InMemoryAttestorDedupeStore();
|
|
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
|
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
|
var auditSink = new InMemoryAttestorAuditSink();
|
|
var logger = new NullLogger<AttestorSubmissionService>();
|
|
using var metrics = new AttestorMetrics();
|
|
|
|
var service = new AttestorSubmissionService(
|
|
validator,
|
|
repository,
|
|
dedupeStore,
|
|
rekorClient,
|
|
archiveStore,
|
|
auditSink,
|
|
options,
|
|
logger,
|
|
TimeProvider.System,
|
|
metrics);
|
|
|
|
var request = CreateValidRequest(canonicalizer);
|
|
request.Meta.LogPreference = "mirror";
|
|
|
|
var context = new SubmissionContext
|
|
{
|
|
CallerSubject = "urn:stellaops:signer",
|
|
CallerAudience = "attestor",
|
|
CallerClientId = "signer-service",
|
|
CallerTenant = "default"
|
|
};
|
|
|
|
var ex = await Assert.ThrowsAsync<AttestorValidationException>(() => service.SubmitAsync(request, context));
|
|
Assert.Equal("mirror_disabled", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitAsync_ReturnsMirrorMetadata_WhenPreferenceBoth()
|
|
{
|
|
var options = Options.Create(new AttestorOptions
|
|
{
|
|
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
|
Rekor = new AttestorOptions.RekorOptions
|
|
{
|
|
Primary = new AttestorOptions.RekorBackendOptions
|
|
{
|
|
Url = "https://rekor.primary.test",
|
|
ProofTimeoutMs = 1000,
|
|
PollIntervalMs = 50,
|
|
MaxAttempts = 2
|
|
},
|
|
Mirror = new AttestorOptions.RekorMirrorOptions
|
|
{
|
|
Enabled = true,
|
|
Url = "https://rekor.mirror.test",
|
|
ProofTimeoutMs = 1000,
|
|
PollIntervalMs = 50,
|
|
MaxAttempts = 2
|
|
}
|
|
}
|
|
});
|
|
|
|
var canonicalizer = new DefaultDsseCanonicalizer();
|
|
var validator = new AttestorSubmissionValidator(canonicalizer);
|
|
var repository = new InMemoryAttestorEntryRepository();
|
|
var dedupeStore = new InMemoryAttestorDedupeStore();
|
|
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
|
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
|
var auditSink = new InMemoryAttestorAuditSink();
|
|
var logger = new NullLogger<AttestorSubmissionService>();
|
|
using var metrics = new AttestorMetrics();
|
|
|
|
var service = new AttestorSubmissionService(
|
|
validator,
|
|
repository,
|
|
dedupeStore,
|
|
rekorClient,
|
|
archiveStore,
|
|
auditSink,
|
|
options,
|
|
logger,
|
|
TimeProvider.System,
|
|
metrics);
|
|
|
|
var request = CreateValidRequest(canonicalizer);
|
|
request.Meta.LogPreference = "both";
|
|
|
|
var context = new SubmissionContext
|
|
{
|
|
CallerSubject = "urn:stellaops:signer",
|
|
CallerAudience = "attestor",
|
|
CallerClientId = "signer-service",
|
|
CallerTenant = "default"
|
|
};
|
|
|
|
var result = await service.SubmitAsync(request, context);
|
|
|
|
Assert.NotNull(result.Mirror);
|
|
Assert.False(string.IsNullOrEmpty(result.Mirror!.Uuid));
|
|
Assert.Equal("included", result.Mirror.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitAsync_UsesMirrorAsCanonical_WhenPreferenceMirror()
|
|
{
|
|
var options = Options.Create(new AttestorOptions
|
|
{
|
|
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
|
Rekor = new AttestorOptions.RekorOptions
|
|
{
|
|
Primary = new AttestorOptions.RekorBackendOptions
|
|
{
|
|
Url = "https://rekor.primary.test",
|
|
ProofTimeoutMs = 1000,
|
|
PollIntervalMs = 50,
|
|
MaxAttempts = 2
|
|
},
|
|
Mirror = new AttestorOptions.RekorMirrorOptions
|
|
{
|
|
Enabled = true,
|
|
Url = "https://rekor.mirror.test",
|
|
ProofTimeoutMs = 1000,
|
|
PollIntervalMs = 50,
|
|
MaxAttempts = 2
|
|
}
|
|
}
|
|
});
|
|
|
|
var canonicalizer = new DefaultDsseCanonicalizer();
|
|
var validator = new AttestorSubmissionValidator(canonicalizer);
|
|
var repository = new InMemoryAttestorEntryRepository();
|
|
var dedupeStore = new InMemoryAttestorDedupeStore();
|
|
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
|
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
|
var auditSink = new InMemoryAttestorAuditSink();
|
|
var logger = new NullLogger<AttestorSubmissionService>();
|
|
using var metrics = new AttestorMetrics();
|
|
|
|
var service = new AttestorSubmissionService(
|
|
validator,
|
|
repository,
|
|
dedupeStore,
|
|
rekorClient,
|
|
archiveStore,
|
|
auditSink,
|
|
options,
|
|
logger,
|
|
TimeProvider.System,
|
|
metrics);
|
|
|
|
var request = CreateValidRequest(canonicalizer);
|
|
request.Meta.LogPreference = "mirror";
|
|
|
|
var context = new SubmissionContext
|
|
{
|
|
CallerSubject = "urn:stellaops:signer",
|
|
CallerAudience = "attestor",
|
|
CallerClientId = "signer-service",
|
|
CallerTenant = "default"
|
|
};
|
|
|
|
var result = await service.SubmitAsync(request, context);
|
|
|
|
Assert.NotNull(result.Uuid);
|
|
var stored = await repository.GetByBundleShaAsync(request.Meta.BundleSha256);
|
|
Assert.NotNull(stored);
|
|
Assert.Equal("mirror", stored!.Log.Backend);
|
|
Assert.Null(result.Mirror);
|
|
}
|
|
|
|
private static AttestorSubmissionRequest CreateValidRequest(DefaultDsseCanonicalizer canonicalizer)
|
|
{
|
|
var request = new AttestorSubmissionRequest
|
|
{
|
|
Bundle = new AttestorSubmissionRequest.SubmissionBundle
|
|
{
|
|
Mode = "keyless",
|
|
Dsse = new AttestorSubmissionRequest.DsseEnvelope
|
|
{
|
|
PayloadType = "application/vnd.in-toto+json",
|
|
PayloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
|
Signatures =
|
|
{
|
|
new AttestorSubmissionRequest.DsseSignature
|
|
{
|
|
KeyId = "test",
|
|
Signature = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Meta = new AttestorSubmissionRequest.SubmissionMeta
|
|
{
|
|
Artifact = new AttestorSubmissionRequest.ArtifactInfo
|
|
{
|
|
Sha256 = new string('a', 64),
|
|
Kind = "sbom"
|
|
},
|
|
LogPreference = "primary",
|
|
Archive = false
|
|
}
|
|
};
|
|
|
|
var canonical = canonicalizer.CanonicalizeAsync(request).GetAwaiter().GetResult();
|
|
request.Meta.BundleSha256 = Convert.ToHexString(SHA256.HashData(canonical)).ToLowerInvariant();
|
|
return request;
|
|
}
|
|
}
|