Add authority bootstrap flows and Concelier ops runbooks
This commit is contained in:
		@@ -1,8 +1,10 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
@@ -16,6 +18,7 @@ using StellaOps.Cli.Services;
 | 
			
		||||
using StellaOps.Cli.Services.Models;
 | 
			
		||||
using StellaOps.Cli.Telemetry;
 | 
			
		||||
using StellaOps.Cli.Tests.Testing;
 | 
			
		||||
using StellaOps.Cryptography;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Cli.Tests.Commands;
 | 
			
		||||
 | 
			
		||||
@@ -208,6 +211,34 @@ public sealed class CommandHandlersTests
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Theory]
 | 
			
		||||
    [InlineData(null)]
 | 
			
		||||
    [InlineData("default")]
 | 
			
		||||
    [InlineData("libsodium")]
 | 
			
		||||
    public async Task HandleAuthRevokeVerifyAsync_VerifiesBundlesUsingProviderRegistry(string? providerHint)
 | 
			
		||||
    {
 | 
			
		||||
        var original = Environment.ExitCode;
 | 
			
		||||
        using var tempDir = new TempDirectory();
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var artifacts = await WriteRevocationArtifactsAsync(tempDir, providerHint);
 | 
			
		||||
 | 
			
		||||
            await CommandHandlers.HandleAuthRevokeVerifyAsync(
 | 
			
		||||
                artifacts.BundlePath,
 | 
			
		||||
                artifacts.SignaturePath,
 | 
			
		||||
                artifacts.KeyPath,
 | 
			
		||||
                verbose: true,
 | 
			
		||||
                cancellationToken: CancellationToken.None);
 | 
			
		||||
 | 
			
		||||
            Assert.Equal(0, Environment.ExitCode);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            Environment.ExitCode = original;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public async Task HandleAuthStatusAsync_ReportsCachedToken()
 | 
			
		||||
    {
 | 
			
		||||
@@ -360,6 +391,79 @@ public sealed class CommandHandlersTests
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async Task<RevocationArtifactPaths> WriteRevocationArtifactsAsync(TempDirectory temp, string? providerHint)
 | 
			
		||||
    {
 | 
			
		||||
        var (bundleBytes, signature, keyPem) = await BuildRevocationArtifactsAsync(providerHint);
 | 
			
		||||
 | 
			
		||||
        var bundlePath = Path.Combine(temp.Path, "revocation-bundle.json");
 | 
			
		||||
        var signaturePath = Path.Combine(temp.Path, "revocation-bundle.json.jws");
 | 
			
		||||
        var keyPath = Path.Combine(temp.Path, "revocation-key.pem");
 | 
			
		||||
 | 
			
		||||
        await File.WriteAllBytesAsync(bundlePath, bundleBytes);
 | 
			
		||||
        await File.WriteAllTextAsync(signaturePath, signature);
 | 
			
		||||
        await File.WriteAllTextAsync(keyPath, keyPem);
 | 
			
		||||
 | 
			
		||||
        return new RevocationArtifactPaths(bundlePath, signaturePath, keyPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async Task<(byte[] Bundle, string Signature, string KeyPem)> BuildRevocationArtifactsAsync(string? providerHint)
 | 
			
		||||
    {
 | 
			
		||||
        var bundleBytes = Encoding.UTF8.GetBytes("{\"revocations\":[]}");
 | 
			
		||||
 | 
			
		||||
        using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
 | 
			
		||||
        var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
 | 
			
		||||
 | 
			
		||||
        var signingKey = new CryptoSigningKey(
 | 
			
		||||
            new CryptoKeyReference("revocation-test"),
 | 
			
		||||
            SignatureAlgorithms.Es256,
 | 
			
		||||
            privateParameters: in parameters,
 | 
			
		||||
            createdAt: DateTimeOffset.UtcNow);
 | 
			
		||||
 | 
			
		||||
        var provider = new DefaultCryptoProvider();
 | 
			
		||||
        provider.UpsertSigningKey(signingKey);
 | 
			
		||||
        var signer = provider.GetSigner(SignatureAlgorithms.Es256, signingKey.Reference);
 | 
			
		||||
 | 
			
		||||
        var header = new Dictionary<string, object>
 | 
			
		||||
        {
 | 
			
		||||
            ["alg"] = SignatureAlgorithms.Es256,
 | 
			
		||||
            ["kid"] = signingKey.Reference.KeyId,
 | 
			
		||||
            ["typ"] = "application/vnd.stellaops.revocation-bundle+jws",
 | 
			
		||||
            ["b64"] = false,
 | 
			
		||||
            ["crit"] = new[] { "b64" }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(providerHint))
 | 
			
		||||
        {
 | 
			
		||||
            header["provider"] = providerHint;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var serializerOptions = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            PropertyNamingPolicy = null,
 | 
			
		||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var headerJson = JsonSerializer.Serialize(header, serializerOptions);
 | 
			
		||||
        var encodedHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(headerJson));
 | 
			
		||||
 | 
			
		||||
        var signingInput = new byte[encodedHeader.Length + 1 + bundleBytes.Length];
 | 
			
		||||
        var headerBytes = Encoding.ASCII.GetBytes(encodedHeader);
 | 
			
		||||
        Buffer.BlockCopy(headerBytes, 0, signingInput, 0, headerBytes.Length);
 | 
			
		||||
        signingInput[headerBytes.Length] = (byte)'.';
 | 
			
		||||
        Buffer.BlockCopy(bundleBytes, 0, signingInput, headerBytes.Length + 1, bundleBytes.Length);
 | 
			
		||||
 | 
			
		||||
        var signatureBytes = await signer.SignAsync(signingInput);
 | 
			
		||||
        var encodedSignature = Base64UrlEncoder.Encode(signatureBytes);
 | 
			
		||||
        var jws = string.Concat(encodedHeader, "..", encodedSignature);
 | 
			
		||||
 | 
			
		||||
        var publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo();
 | 
			
		||||
        var keyPem = new string(PemEncoding.Write("PUBLIC KEY", publicKeyBytes));
 | 
			
		||||
 | 
			
		||||
        return (bundleBytes, jws, keyPem);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private sealed record RevocationArtifactPaths(string BundlePath, string SignaturePath, string KeyPath);
 | 
			
		||||
 | 
			
		||||
    private static IServiceProvider BuildServiceProvider(
 | 
			
		||||
        IBackendOperationsClient backend,
 | 
			
		||||
        IScannerExecutor? executor = null,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user