Files
git.stella-ops.org/src/StellaOps.Concelier.Connector.StellaOpsMirror.Tests/MirrorSignatureVerifierTests.cs
master 48f3071e2a Add tests and implement StubBearer authentication for Signer endpoints
- 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.
2025-10-21 09:37:07 +03:00

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