* Add standalone Linux bootstrap foundation * Record bootstrap foundation progress * Add single-code Node enrollment * Validate bootstrap release payloads * Add managed WireGuard mesh bootstrap * Exercise repeated systemd installation in CI * Expose systemd smoke failure diagnostics * Fix bootstrap os-release variable collision * Create policy helper directory during install * Capture systemd smoke healthcheck errors * Report Control listener diagnostics in CI * Capture stalled Control process diagnostics * Set service working directories * Run protected CA healthcheck as root * Add local Mesh emergency recovery * Add multi-architecture Linux system matrix * Add provisioning job control plane * Stabilize Debian systemd smoke files * Add node-scoped provisioning job channel * Add provisioning progress state machine * Reconcile expired provisioning jobs * Add provisioning rollback workflow * Add redacted provisioning event history * Add restricted provisioning preflight helper * Persist typed provisioning preflight facts * Add preflight desired state drift detection * Define strict base install schema catalog * Add safe base install plan generation * Add pre-confirmation base install plans * Test base install plan API flow * Authorize confirmed provisioning execution * Execute confirmed timezone provisioning safely * test: stabilize execution grant expiry boundary --------- Co-authored-by: Ochenstarik <ochenstarik@inbox.ru>
122 lines
6.2 KiB
C#
122 lines
6.2 KiB
C#
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Microsoft.Extensions.Options;
|
|
using ServerMonitorManager.Control;
|
|
using ServerMonitorManager.Core;
|
|
using Xunit;
|
|
|
|
namespace ServerMonitorManager.Control.Tests;
|
|
|
|
public sealed class CertificateAuthorityTests : IDisposable
|
|
{
|
|
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"smm-ca-tests-{Guid.NewGuid():N}");
|
|
|
|
[Fact]
|
|
public void IssuedAgentCertificateChainsToControlAuthority()
|
|
{
|
|
Directory.CreateDirectory(_directory);
|
|
var caPath = Path.Combine(_directory, "control-ca.pfx");
|
|
using var caKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
var caRequest = new CertificateRequest("CN=SMM Test CA", caKey, HashAlgorithmName.SHA256);
|
|
caRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
|
caRequest.CertificateExtensions.Add(new X509KeyUsageExtension(
|
|
X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign,
|
|
true));
|
|
using var ca = caRequest.CreateSelfSigned(
|
|
DateTimeOffset.UtcNow.AddMinutes(-1),
|
|
DateTimeOffset.UtcNow.AddYears(2));
|
|
File.WriteAllBytes(caPath, ca.Export(X509ContentType.Pfx));
|
|
|
|
using var authority = new CertificateAuthority(Options.Create(new ControlOptions
|
|
{
|
|
DatabasePath = Path.Combine(_directory, "unused.db"),
|
|
CertificateAuthorityPath = caPath
|
|
}));
|
|
using var agentKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
var agentRequest = new CertificateRequest("CN=home", agentKey, HashAlgorithmName.SHA256);
|
|
var issued = authority.IssueClientCertificate("home", agentRequest.CreateSigningRequestPem());
|
|
using var agentCertificate = X509Certificate2.CreateFromPem(
|
|
issued.CertificatePem,
|
|
agentKey.ExportPkcs8PrivateKeyPem());
|
|
using var chain = new X509Chain();
|
|
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
|
chain.ChainPolicy.CustomTrustStore.Add(authority.PublicCertificate);
|
|
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
|
|
|
Assert.True(chain.Build(agentCertificate));
|
|
Assert.Contains(
|
|
agentCertificate.Extensions.OfType<X509EnhancedKeyUsageExtension>()
|
|
.SelectMany(extension => extension.EnhancedKeyUsages.Cast<Oid>()),
|
|
oid => oid.Value == "1.3.6.1.5.5.7.3.2");
|
|
}
|
|
|
|
[Fact]
|
|
public void ProvisioningExecutionGrantIsBoundToConfirmedJobPlanAndExpiry()
|
|
{
|
|
Directory.CreateDirectory(_directory);
|
|
var caPath = Path.Combine(_directory, "grant-control-ca.pfx");
|
|
using var caKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
var caRequest = new CertificateRequest("CN=SMM Grant Test CA", caKey, HashAlgorithmName.SHA256);
|
|
caRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
|
caRequest.CertificateExtensions.Add(new X509KeyUsageExtension(
|
|
X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.DigitalSignature,
|
|
true));
|
|
using var ca = caRequest.CreateSelfSigned(
|
|
DateTimeOffset.UtcNow.AddMinutes(-1),
|
|
DateTimeOffset.UtcNow.AddYears(2));
|
|
File.WriteAllBytes(caPath, ca.Export(X509ContentType.Pfx));
|
|
using var authority = new CertificateAuthority(Options.Create(new ControlOptions
|
|
{
|
|
DatabasePath = Path.Combine(_directory, "unused-grant.db"),
|
|
CertificateAuthorityPath = caPath
|
|
}));
|
|
|
|
var now = DateTimeOffset.FromUnixTimeSeconds(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
|
var parameters = System.Text.Json.JsonSerializer.SerializeToElement(
|
|
new SystemBaseInstallParameters(
|
|
"UTC", "en_US.UTF-8", true, false, 1, ["core"],
|
|
"disabled", null, 60, true, "never"),
|
|
SmmJsonContext.Default.SystemBaseInstallParameters);
|
|
var job = new ProvisioningJob(
|
|
Guid.NewGuid().ToString("N"), "home", "system.base-install", 1, parameters,
|
|
ProvisioningJobStates.Running, true, "test", "operator",
|
|
now.AddMinutes(-1), now, now.AddMinutes(30), now, null, 4, 25,
|
|
"execute", null);
|
|
var plan = new SystemBaseInstallPlan(
|
|
"UTC", "en_US.UTF-8", true, false,
|
|
SystemBaseInstallCatalogDefinition.ExpandGroups(["core"]),
|
|
"disabled", null, 60, true, "never", []);
|
|
|
|
var grant = authority.SignProvisioningExecutionGrant(
|
|
job, plan, now, TimeSpan.FromMinutes(2));
|
|
|
|
Assert.True(ProvisioningExecutionGrantCodec.Verify(
|
|
grant, authority.PublicCertificate, job.Id, job.NodeId, plan, now));
|
|
Assert.False(ProvisioningExecutionGrantCodec.Verify(
|
|
grant, authority.PublicCertificate, job.Id, "other", plan, now));
|
|
Assert.False(ProvisioningExecutionGrantCodec.Verify(
|
|
grant, authority.PublicCertificate, job.Id, job.NodeId,
|
|
plan with { Packages = [.. plan.Packages, "untrusted-package"] }, now));
|
|
Assert.False(ProvisioningExecutionGrantCodec.Verify(
|
|
grant, authority.PublicCertificate, job.Id, job.NodeId, plan, now.AddMinutes(3)));
|
|
var expiringJob = job with { ExpiresAt = now.AddSeconds(15) };
|
|
var clamped = authority.SignProvisioningExecutionGrant(
|
|
expiringJob, plan, now, TimeSpan.FromMinutes(2));
|
|
Assert.Equal(expiringJob.ExpiresAt.ToUnixTimeSeconds(), clamped.ExpiresAtUnixSeconds);
|
|
Assert.Throws<InvalidOperationException>(() => authority.SignProvisioningExecutionGrant(
|
|
job with { ExpiresAt = now }, plan, now, TimeSpan.FromMinutes(2)));
|
|
Assert.Throws<InvalidOperationException>(() => authority.SignProvisioningExecutionGrant(
|
|
job with { ExpiresAt = now.AddMilliseconds(100) },
|
|
plan, now, TimeSpan.FromMinutes(2)));
|
|
Assert.Throws<InvalidOperationException>(() => authority.SignProvisioningExecutionGrant(
|
|
job with { ConfirmedAt = null }, plan, now, TimeSpan.FromMinutes(2)));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_directory))
|
|
{
|
|
Directory.Delete(_directory, recursive: true);
|
|
}
|
|
}
|
|
}
|