- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
190 lines
8.1 KiB
C#
190 lines
8.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.Concelier.Connector.StellaOpsMirror.Security;
|
|
using StellaOps.Cryptography;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
|
|
|
|
public sealed class MirrorSignatureVerifierTests
|
|
{
|
|
[Fact]
|
|
public async Task VerifyAsync_ValidSignaturePasses()
|
|
{
|
|
var provider = new DefaultCryptoProvider();
|
|
var key = CreateSigningKey("mirror-key");
|
|
provider.UpsertSigningKey(key);
|
|
|
|
var registry = new CryptoProviderRegistry(new[] { provider });
|
|
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
|
|
|
|
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
|
|
var payload = payloadText.ToUtf8Bytes();
|
|
var (signature, _) = await CreateDetachedJwsAsync(provider, key.Reference.KeyId, payload);
|
|
|
|
await verifier.VerifyAsync(payload, signature, CancellationToken.None);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyAsync_InvalidSignatureThrows()
|
|
{
|
|
var provider = new DefaultCryptoProvider();
|
|
var key = CreateSigningKey("mirror-key");
|
|
provider.UpsertSigningKey(key);
|
|
|
|
var registry = new CryptoProviderRegistry(new[] { provider });
|
|
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
|
|
|
|
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
|
|
var payload = payloadText.ToUtf8Bytes();
|
|
var (signature, _) = await CreateDetachedJwsAsync(provider, key.Reference.KeyId, payload);
|
|
|
|
var tampered = signature.Replace('a', 'b');
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(payload, tampered, CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyAsync_KeyMismatchThrows()
|
|
{
|
|
var provider = new DefaultCryptoProvider();
|
|
var key = CreateSigningKey("mirror-key");
|
|
provider.UpsertSigningKey(key);
|
|
|
|
var registry = new CryptoProviderRegistry(new[] { provider });
|
|
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
|
|
|
|
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
|
|
var payload = payloadText.ToUtf8Bytes();
|
|
var (signature, _) = await CreateDetachedJwsAsync(provider, key.Reference.KeyId, payload);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(
|
|
payload,
|
|
signature,
|
|
expectedKeyId: "unexpected-key",
|
|
expectedProvider: null,
|
|
fallbackPublicKeyPath: null,
|
|
cancellationToken: CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyAsync_ThrowsWhenProviderMissingKey()
|
|
{
|
|
var provider = new DefaultCryptoProvider();
|
|
var key = CreateSigningKey("mirror-key");
|
|
provider.UpsertSigningKey(key);
|
|
|
|
var registry = new CryptoProviderRegistry(new[] { provider });
|
|
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
|
|
|
|
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
|
|
var payload = payloadText.ToUtf8Bytes();
|
|
var (signature, _) = await CreateDetachedJwsAsync(provider, key.Reference.KeyId, payload);
|
|
|
|
provider.RemoveSigningKey(key.Reference.KeyId);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(
|
|
payload,
|
|
signature,
|
|
expectedKeyId: key.Reference.KeyId,
|
|
expectedProvider: provider.Name,
|
|
fallbackPublicKeyPath: null,
|
|
cancellationToken: CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyAsync_UsesCachedPublicKeyWhenFileRemoved()
|
|
{
|
|
var provider = new DefaultCryptoProvider();
|
|
var signingKey = CreateSigningKey("mirror-key");
|
|
provider.UpsertSigningKey(signingKey);
|
|
var registry = new CryptoProviderRegistry(new[] { provider });
|
|
var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
|
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, memoryCache);
|
|
|
|
var payload = "{\"advisories\":[]}";
|
|
var (signature, _) = await CreateDetachedJwsAsync(provider, signingKey.Reference.KeyId, payload.ToUtf8Bytes());
|
|
provider.RemoveSigningKey(signingKey.Reference.KeyId);
|
|
var pemPath = WritePublicKeyPem(signingKey);
|
|
|
|
try
|
|
{
|
|
await verifier.VerifyAsync(payload.ToUtf8Bytes(), signature, expectedKeyId: signingKey.Reference.KeyId, expectedProvider: "default", fallbackPublicKeyPath: pemPath, cancellationToken: CancellationToken.None);
|
|
|
|
File.Delete(pemPath);
|
|
|
|
await verifier.VerifyAsync(payload.ToUtf8Bytes(), signature, expectedKeyId: signingKey.Reference.KeyId, expectedProvider: "default", fallbackPublicKeyPath: pemPath, cancellationToken: CancellationToken.None);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(pemPath))
|
|
{
|
|
File.Delete(pemPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static CryptoSigningKey CreateSigningKey(string keyId)
|
|
{
|
|
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
|
|
return new CryptoSigningKey(new CryptoKeyReference(keyId), SignatureAlgorithms.Es256, in parameters, DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
private static string WritePublicKeyPem(CryptoSigningKey signingKey)
|
|
{
|
|
using var ecdsa = ECDsa.Create(signingKey.PublicParameters);
|
|
var info = ecdsa.ExportSubjectPublicKeyInfo();
|
|
var pem = PemEncoding.Write("PUBLIC KEY", info);
|
|
var path = Path.Combine(Path.GetTempPath(), $"stellaops-mirror-{Guid.NewGuid():N}.pem");
|
|
File.WriteAllText(path, pem);
|
|
return path;
|
|
}
|
|
|
|
private static async Task<(string Signature, DateTimeOffset SignedAt)> CreateDetachedJwsAsync(
|
|
DefaultCryptoProvider provider,
|
|
string keyId,
|
|
ReadOnlyMemory<byte> payload)
|
|
{
|
|
var signer = provider.GetSigner(SignatureAlgorithms.Es256, new CryptoKeyReference(keyId));
|
|
var header = new Dictionary<string, object?>
|
|
{
|
|
["alg"] = SignatureAlgorithms.Es256,
|
|
["kid"] = keyId,
|
|
["provider"] = provider.Name,
|
|
["typ"] = "application/vnd.stellaops.concelier.mirror-bundle+jws",
|
|
["b64"] = false,
|
|
["crit"] = new[] { "b64" }
|
|
};
|
|
|
|
var headerJson = System.Text.Json.JsonSerializer.Serialize(header);
|
|
var protectedHeader = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(headerJson);
|
|
|
|
var signingInput = BuildSigningInput(protectedHeader, payload.Span);
|
|
var signatureBytes = await signer.SignAsync(signingInput, CancellationToken.None).ConfigureAwait(false);
|
|
var encodedSignature = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(signatureBytes);
|
|
|
|
return (string.Concat(protectedHeader, "..", encodedSignature), DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
private static ReadOnlyMemory<byte> BuildSigningInput(string encodedHeader, ReadOnlySpan<byte> payload)
|
|
{
|
|
var headerBytes = System.Text.Encoding.ASCII.GetBytes(encodedHeader);
|
|
var buffer = new byte[headerBytes.Length + 1 + payload.Length];
|
|
headerBytes.CopyTo(buffer.AsSpan());
|
|
buffer[headerBytes.Length] = (byte)'.';
|
|
payload.CopyTo(buffer.AsSpan(headerBytes.Length + 1));
|
|
return buffer;
|
|
}
|
|
}
|
|
|
|
file static class Utf8Extensions
|
|
{
|
|
public static ReadOnlyMemory<byte> ToUtf8Bytes(this string value)
|
|
=> System.Text.Encoding.UTF8.GetBytes(value);
|
|
}
|