mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 12:51:06 +00:00
[AZ-576] Add e2e test infrastructure (xUnit + jwks-mock + reporting)
ci/woodpecker/push/build-arm Pipeline failed
ci/woodpecker/push/build-arm Pipeline failed
Scaffold the blackbox test project the rest of epic AZ-575 (AZ-577..AZ-586) will build on. Two new csprojs under tests/, plus the TLS materials and TRX->CSV reporting hand-off the existing docker-compose.test.yml already calls for. JWKS mock (tests/Azaion.Missions.JwksMock/): - ASP.NET Core minimal API on .NET 10, no NuGet deps; JWS is hand-rolled to keep the surface tight and avoid version drift with the SUT - KeyStore with one in-memory ECDSA P-256 keypair + retired-key grace window for NFT-RES-07 / NFT-SEC-11 rotation observability - Endpoints: GET /.well-known/jwks.json, POST /sign, POST /rotate-key - Mock-only alg_override / kid_override switches drive NFT-SEC-09/10/11 - TLS keypair committed under tls/; tests/jwks-mock-ca.crt is a copy mounted into both missions and e2e-consumer per docker-compose.test.yml E2E consumer (tests/Azaion.Missions.E2E.Tests/): - xUnit 2.9.2 + Bogus 35.6.1 + Npgsql 10.0.2 + Xunit.SkippableFact 1.4.13 - TestBase / TokenMinter scaffolding for downstream tasks - Fixtures/ for DbReset, DbSeed, ComposeRestart, JwksRotate, JwksMockReverse - Helpers/ for DbAssertions (side-channel), HttpAssertions, FixtureSql - 8 Tests/<category>/Sanity.cs discovery smoke tests (AC-3) - Tests/InfrastructureSanity.cs SkippableFacts for AC-1/2/5/6 - Tests/AaaPatternEnforcement.cs greps source files for AC-7 - Tests/Reporting/TrxToCsvPostProcessorTests.cs covers AC-4 - Reporting/TrxToCsvPostProcessor.cs handles VSTest TRX -> environment.md CSV; xUnit traits are not propagated by the TRX logger so the converter reflects them out of the test DLL via GetCustomAttributesData - Reporting.Cli/ is a separate console csproj that links the converter source files (test project excludes Reporting.Cli/** from compile) - Dockerfile + entrypoint.sh wire dotnet test -> trx -> csv inside the e2e-consumer container the compose file already references Local verification: 13 pass, 3 skip (with explicit reasons), 0 fail. End-to-end TRX->CSV manually verified against environment.md header spec. Docker stack build is handed off to autodev Step 7 (test-run skill). Reports under _docs/03_implementation/. AZ-576 task spec moved to _docs/tasks/done/. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Azaion.Missions.JwksMock</RootNamespace>
|
||||
<AssemblyName>Azaion.Missions.JwksMock</AssemblyName>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="tls\jwks-mock.crt" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="tls\jwks-mock.key" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG TARGETARCH
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN arch=$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH") && \
|
||||
dotnet publish Azaion.Missions.JwksMock.csproj -c Release -o /app --os linux --arch $arch
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app .
|
||||
EXPOSE 8443
|
||||
ENTRYPOINT ["dotnet", "Azaion.Missions.JwksMock.dll"]
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Nodes;
|
||||
using Azaion.Missions.JwksMock.Services;
|
||||
|
||||
namespace Azaion.Missions.JwksMock.Endpoints;
|
||||
|
||||
public static class JwksEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>GET /.well-known/jwks.json</c>. Mirrors the shape the production
|
||||
/// admin issuer publishes — JsonWebKey 'kty=EC, crv=P-256, alg=ES256,
|
||||
/// use=sig' with base64url x/y coordinates.
|
||||
/// </summary>
|
||||
public static IResult Handle(KeyStore keys)
|
||||
{
|
||||
var keysArray = new JsonArray();
|
||||
foreach (var key in keys.PublishedKeys())
|
||||
{
|
||||
var p = key.Ec.ExportParameters(includePrivateParameters: false);
|
||||
keysArray.Add(new JsonObject
|
||||
{
|
||||
["kty"] = "EC",
|
||||
["use"] = "sig",
|
||||
["alg"] = "ES256",
|
||||
["crv"] = "P-256",
|
||||
["kid"] = key.Kid,
|
||||
["x"] = Base64Url.Encode(p.Q.X!),
|
||||
["y"] = Base64Url.Encode(p.Q.Y!)
|
||||
});
|
||||
}
|
||||
|
||||
var doc = new JsonObject { ["keys"] = keysArray };
|
||||
return Results.Json(doc, statusCode: 200, contentType: "application/json")
|
||||
.WithCacheControl("public, max-age=60");
|
||||
}
|
||||
|
||||
private static IResult WithCacheControl(this IResult result, string value) =>
|
||||
new CacheControlResult(result, value);
|
||||
|
||||
private sealed class CacheControlResult(IResult inner, string cacheControl) : IResult
|
||||
{
|
||||
public Task ExecuteAsync(HttpContext httpContext)
|
||||
{
|
||||
httpContext.Response.Headers.CacheControl = cacheControl;
|
||||
return inner.ExecuteAsync(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Azaion.Missions.JwksMock.Services;
|
||||
|
||||
namespace Azaion.Missions.JwksMock.Endpoints;
|
||||
|
||||
public static class RotateKeyEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>POST /rotate-key</c>. Generates a new active ECDSA P-256 keypair,
|
||||
/// retires the previous active key for <c>OldKeyGraceSeconds</c>, returns
|
||||
/// the new <c>kid</c>.
|
||||
/// </summary>
|
||||
public static IResult Handle(KeyStore keys)
|
||||
{
|
||||
var newKey = keys.Rotate();
|
||||
return Results.Json(new { kid = newKey.Kid });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Azaion.Missions.JwksMock.Services;
|
||||
|
||||
namespace Azaion.Missions.JwksMock.Endpoints;
|
||||
|
||||
public static class SignEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>POST /sign</c>. Body is a small JSON object documented in
|
||||
/// <c>_docs/02_document/tests/test-data.md § JWKS mock token-minting contract</c>.
|
||||
/// All fields optional; omitted fields fall back to mock defaults.
|
||||
/// </summary>
|
||||
public static async Task<IResult> Handle(HttpContext ctx, TokenSigner signer)
|
||||
{
|
||||
SignBody? body;
|
||||
try
|
||||
{
|
||||
body = await JsonSerializer.DeserializeAsync(
|
||||
ctx.Request.Body,
|
||||
SignBodyContext.Default.SignBody,
|
||||
ctx.RequestAborted);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = "invalid_json", detail = ex.Message });
|
||||
}
|
||||
body ??= new SignBody();
|
||||
|
||||
try
|
||||
{
|
||||
var result = signer.Sign(new SignRequest(
|
||||
Issuer: body.Iss,
|
||||
Audience: body.Aud,
|
||||
ExpOffsetSeconds: body.ExpOffsetSeconds,
|
||||
Permissions: body.Permissions,
|
||||
Subject: body.Sub,
|
||||
AlgOverride: body.AlgOverride,
|
||||
KidOverride: body.KidOverride));
|
||||
return Results.Json(new SignResponse(result.Token, result.Kid), SignBodyContext.Default.SignResponse);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = "invalid_arg", detail = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record SignBody(
|
||||
[property: JsonPropertyName("iss")] string? Iss = null,
|
||||
[property: JsonPropertyName("aud")] string? Aud = null,
|
||||
[property: JsonPropertyName("sub")] string? Sub = null,
|
||||
[property: JsonPropertyName("exp_offset_seconds")] int? ExpOffsetSeconds = null,
|
||||
[property: JsonPropertyName("permissions")] string? Permissions = null,
|
||||
[property: JsonPropertyName("alg_override")] string? AlgOverride = null,
|
||||
[property: JsonPropertyName("kid_override")] string? KidOverride = null);
|
||||
|
||||
public sealed record SignResponse(
|
||||
[property: JsonPropertyName("token")] string Token,
|
||||
[property: JsonPropertyName("kid")] string Kid);
|
||||
|
||||
[JsonSerializable(typeof(SignBody))]
|
||||
[JsonSerializable(typeof(SignResponse))]
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
|
||||
internal sealed partial class SignBodyContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Azaion.Missions.JwksMock.Endpoints;
|
||||
using Azaion.Missions.JwksMock.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Tests source these from the compose env block (JWT_ISSUER, JWT_AUDIENCE,
|
||||
// OLD_KEY_GRACE_SECONDS); appsettings.json supplies dev defaults.
|
||||
var issuer = builder.Configuration["JWT_ISSUER"]
|
||||
?? builder.Configuration["Jwks:Issuer"]
|
||||
?? throw new InvalidOperationException("JWT_ISSUER not configured");
|
||||
var audience = builder.Configuration["JWT_AUDIENCE"]
|
||||
?? builder.Configuration["Jwks:Audience"]
|
||||
?? throw new InvalidOperationException("JWT_AUDIENCE not configured");
|
||||
var oldKeyGraceSecRaw = builder.Configuration["OLD_KEY_GRACE_SECONDS"]
|
||||
?? builder.Configuration["Jwks:OldKeyGraceSeconds"]
|
||||
?? "5";
|
||||
var oldKeyGrace = TimeSpan.FromSeconds(int.Parse(oldKeyGraceSecRaw, System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddSingleton<KeyStore>(sp => new KeyStore(oldKeyGrace, sp.GetRequiredService<TimeProvider>()));
|
||||
builder.Services.AddSingleton<TokenSigner>(sp => new TokenSigner(
|
||||
sp.GetRequiredService<KeyStore>(),
|
||||
sp.GetRequiredService<TimeProvider>(),
|
||||
issuer,
|
||||
audience));
|
||||
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(8443, listen =>
|
||||
{
|
||||
listen.UseHttps(LoadTlsCert());
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/.well-known/jwks.json", JwksEndpoint.Handle);
|
||||
app.MapPost("/sign", SignEndpoint.Handle);
|
||||
app.MapPost("/rotate-key", RotateKeyEndpoint.Handle);
|
||||
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
|
||||
|
||||
app.Run();
|
||||
|
||||
// Loads the server TLS cert + key from the build context. The same cert is
|
||||
// also published as `tests/jwks-mock-ca.crt` and mounted into the missions +
|
||||
// e2e-consumer containers as a trust anchor.
|
||||
static X509Certificate2 LoadTlsCert()
|
||||
{
|
||||
var basePath = AppContext.BaseDirectory;
|
||||
var crtPath = Path.Combine(basePath, "tls", "jwks-mock.crt");
|
||||
var keyPath = Path.Combine(basePath, "tls", "jwks-mock.key");
|
||||
if (!File.Exists(crtPath) || !File.Exists(keyPath))
|
||||
throw new FileNotFoundException(
|
||||
$"jwks-mock TLS materials not found. Expected:\n {crtPath}\n {keyPath}\n" +
|
||||
"Run tests/Azaion.Missions.JwksMock/regen-cert.sh to regenerate.");
|
||||
return X509Certificate2.CreateFromPemFile(crtPath, keyPath);
|
||||
}
|
||||
|
||||
public partial class Program; // For WebApplicationFactory if a host-process test ever needs it.
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Azaion.Missions.JwksMock.Services;
|
||||
|
||||
/// <summary>RFC 7515 §2 base64url (no padding) helpers.</summary>
|
||||
public static class Base64Url
|
||||
{
|
||||
public static string Encode(ReadOnlySpan<byte> input)
|
||||
{
|
||||
var b64 = Convert.ToBase64String(input);
|
||||
return b64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||
}
|
||||
|
||||
public static byte[] Decode(string input)
|
||||
{
|
||||
var s = input.Replace('-', '+').Replace('_', '/');
|
||||
var pad = s.Length % 4;
|
||||
if (pad > 0) s += new string('=', 4 - pad);
|
||||
return Convert.FromBase64String(s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Azaion.Missions.JwksMock.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the active ECDSA P-256 keypair used to sign test JWTs, plus an
|
||||
/// optional retired keypair retained for <c>OldKeyGraceSeconds</c> after a
|
||||
/// rotation so consumers can still validate in-flight tokens minted under the
|
||||
/// previous kid (NFT-RES-07 / NFT-SEC-11).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Singleton, thread-safe. The private key never leaves the container — only
|
||||
/// public-half exports go out via the JWKS endpoint.
|
||||
/// </remarks>
|
||||
public sealed class KeyStore : IDisposable
|
||||
{
|
||||
private readonly TimeSpan _graceWindow;
|
||||
private readonly TimeProvider _clock;
|
||||
private readonly Lock _gate = new();
|
||||
|
||||
private KeypairEntry _active;
|
||||
private KeypairEntry? _retired;
|
||||
|
||||
public KeyStore(TimeSpan graceWindow, TimeProvider clock)
|
||||
{
|
||||
_graceWindow = graceWindow;
|
||||
_clock = clock;
|
||||
_active = KeypairEntry.New();
|
||||
}
|
||||
|
||||
public KeypairView Active
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate) return _active.View();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<KeypairView> PublishedKeys()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
EvictExpiredRetired();
|
||||
if (_retired is null)
|
||||
return [_active.View()];
|
||||
return [_active.View(), _retired.View()];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate the active keypair. The previous active key is retained as the
|
||||
/// retired key (overwriting any older retired entry) until
|
||||
/// <c>OldKeyGraceSeconds</c> elapses.
|
||||
/// </summary>
|
||||
public KeypairView Rotate()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_retired?.Dispose();
|
||||
_retired = _active.WithRetiredAt(_clock.GetUtcNow().Add(_graceWindow));
|
||||
_active = KeypairEntry.New();
|
||||
return _active.View();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_active.Dispose();
|
||||
_retired?.Dispose();
|
||||
_retired = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EvictExpiredRetired()
|
||||
{
|
||||
if (_retired is null) return;
|
||||
if (_retired.RetiredAtUtc is { } retiredAt && _clock.GetUtcNow() > retiredAt)
|
||||
{
|
||||
_retired.Dispose();
|
||||
_retired = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class KeypairEntry : IDisposable
|
||||
{
|
||||
public ECDsa Ec { get; }
|
||||
public string Kid { get; }
|
||||
public DateTimeOffset? RetiredAtUtc { get; }
|
||||
|
||||
private KeypairEntry(ECDsa ec, string kid, DateTimeOffset? retiredAt)
|
||||
{
|
||||
Ec = ec;
|
||||
Kid = kid;
|
||||
RetiredAtUtc = retiredAt;
|
||||
}
|
||||
|
||||
public static KeypairEntry New()
|
||||
{
|
||||
var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
// kid: SHA-256 of the public key parameters, base64url-truncated to 16 bytes.
|
||||
var pub = ec.ExportSubjectPublicKeyInfo();
|
||||
var hash = SHA256.HashData(pub);
|
||||
var kid = Base64Url.Encode(hash.AsSpan(0, 16));
|
||||
return new KeypairEntry(ec, kid, retiredAt: null);
|
||||
}
|
||||
|
||||
public KeypairEntry WithRetiredAt(DateTimeOffset retiredAtUtc)
|
||||
=> new(Ec, Kid, retiredAtUtc);
|
||||
|
||||
public KeypairView View() => new(Kid, Ec, RetiredAtUtc);
|
||||
|
||||
public void Dispose() => Ec.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct KeypairView(string Kid, ECDsa Ec, DateTimeOffset? RetiredAtUtc);
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Azaion.Missions.JwksMock.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Hand-rolls JWS-compact ES256 tokens for tests. Honors per-call overrides
|
||||
/// the test harness uses to exercise NFT-SEC-* (alg confusion, unknown kid,
|
||||
/// claim mismatch, etc.).
|
||||
/// </summary>
|
||||
public sealed class TokenSigner
|
||||
{
|
||||
private readonly KeyStore _keys;
|
||||
private readonly TimeProvider _clock;
|
||||
private readonly string _defaultIssuer;
|
||||
private readonly string _defaultAudience;
|
||||
|
||||
public TokenSigner(KeyStore keys, TimeProvider clock, string defaultIssuer, string defaultAudience)
|
||||
{
|
||||
_keys = keys;
|
||||
_clock = clock;
|
||||
_defaultIssuer = defaultIssuer;
|
||||
_defaultAudience = defaultAudience;
|
||||
}
|
||||
|
||||
public SignResult Sign(SignRequest request)
|
||||
{
|
||||
var active = _keys.Active;
|
||||
var kid = request.KidOverride ?? active.Kid;
|
||||
var alg = request.AlgOverride ?? "ES256";
|
||||
|
||||
var nowUnix = _clock.GetUtcNow().ToUnixTimeSeconds();
|
||||
var expUnix = nowUnix + (request.ExpOffsetSeconds ?? 3600);
|
||||
|
||||
var header = new JsonObject
|
||||
{
|
||||
["alg"] = alg,
|
||||
["kid"] = kid,
|
||||
["typ"] = "JWT"
|
||||
};
|
||||
|
||||
var payload = new JsonObject
|
||||
{
|
||||
["iss"] = request.Issuer ?? _defaultIssuer,
|
||||
["aud"] = request.Audience ?? _defaultAudience,
|
||||
["iat"] = nowUnix,
|
||||
["exp"] = expUnix
|
||||
};
|
||||
if (request.Permissions is not null)
|
||||
payload["permissions"] = request.Permissions;
|
||||
if (request.Subject is not null)
|
||||
payload["sub"] = request.Subject;
|
||||
|
||||
var headerBytes = JsonSerializer.SerializeToUtf8Bytes(header);
|
||||
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload);
|
||||
var headerSeg = Base64Url.Encode(headerBytes);
|
||||
var payloadSeg = Base64Url.Encode(payloadBytes);
|
||||
|
||||
// Signing input is the literal ASCII string "<header>.<payload>" per RFC 7515 §5.1.
|
||||
var signingInput = Encoding.ASCII.GetBytes($"{headerSeg}.{payloadSeg}");
|
||||
|
||||
byte[] signature;
|
||||
if (alg == "ES256")
|
||||
{
|
||||
signature = active.Ec.SignData(signingInput, HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation);
|
||||
}
|
||||
else if (alg == "HS256")
|
||||
{
|
||||
// alg-confusion attack vector for NFT-SEC-10. We sign with a key derived
|
||||
// from the active public key so a naive validator that fails to enforce
|
||||
// alg pinning would accept the token.
|
||||
var pubKey = active.Ec.ExportSubjectPublicKeyInfo();
|
||||
using var hmac = new HMACSHA256(pubKey);
|
||||
signature = hmac.ComputeHash(signingInput);
|
||||
}
|
||||
else if (alg == "none")
|
||||
{
|
||||
signature = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Unsupported alg_override '{alg}'", nameof(request));
|
||||
}
|
||||
|
||||
var sigSeg = Base64Url.Encode(signature);
|
||||
var token = $"{headerSeg}.{payloadSeg}.{sigSeg}";
|
||||
return new SignResult(token, kid);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record SignRequest(
|
||||
string? Issuer,
|
||||
string? Audience,
|
||||
int? ExpOffsetSeconds,
|
||||
string? Permissions,
|
||||
string? Subject,
|
||||
string? AlgOverride,
|
||||
string? KidOverride);
|
||||
|
||||
public sealed record SignResult(string Token, string Kid);
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Jwks": {
|
||||
"Issuer": "https://admin-test.azaion.local",
|
||||
"Audience": "azaion-edge",
|
||||
"OldKeyGraceSeconds": 5
|
||||
}
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
## Regenerate the jwks-mock TLS keypair + the trust-anchor copy mounted into
|
||||
## consumers. Both files are committed test artifacts (the test runs are
|
||||
## deterministic, so the cert is reused across CI runs unless the keypair is
|
||||
## intentionally rotated).
|
||||
##
|
||||
## Outputs:
|
||||
## tests/Azaion.Missions.JwksMock/tls/jwks-mock.key (private, 0600)
|
||||
## tests/Azaion.Missions.JwksMock/tls/jwks-mock.crt (public, ECDSA P-256, 100y)
|
||||
## tests/jwks-mock-ca.crt (copy of jwks-mock.crt)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TLS_DIR="$SCRIPT_DIR/tls"
|
||||
TESTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
mkdir -p "$TLS_DIR"
|
||||
cd "$TLS_DIR"
|
||||
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out jwks-mock.key
|
||||
openssl req -new -x509 \
|
||||
-key jwks-mock.key \
|
||||
-out jwks-mock.crt \
|
||||
-days 36500 \
|
||||
-sha256 \
|
||||
-subj "/CN=jwks-mock" \
|
||||
-addext "subjectAltName=DNS:jwks-mock,DNS:localhost,IP:127.0.0.1" \
|
||||
-addext "basicConstraints=critical,CA:TRUE" \
|
||||
-addext "keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign" \
|
||||
-addext "extendedKeyUsage=serverAuth"
|
||||
|
||||
chmod 600 jwks-mock.key
|
||||
cp jwks-mock.crt "$TESTS_DIR/jwks-mock-ca.crt"
|
||||
|
||||
echo "[regen-cert] regenerated:"
|
||||
echo " $TLS_DIR/jwks-mock.key"
|
||||
echo " $TLS_DIR/jwks-mock.crt"
|
||||
echo " $TESTS_DIR/jwks-mock-ca.crt"
|
||||
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBzDCCAXOgAwIBAgIUZDltID1GVJuqwUDA+867RVJHYOwwCgYIKoZIzj0EAwIw
|
||||
FDESMBAGA1UEAwwJandrcy1tb2NrMCAXDTI2MDUxNTAzNDAxM1oYDzIxMjYwNDIx
|
||||
MDM0MDEzWjAUMRIwEAYDVQQDDAlqd2tzLW1vY2swWTATBgcqhkjOPQIBBggqhkjO
|
||||
PQMBBwNCAATS59eN3v/CvrfN5OHTqWe/wp/ZsayKsf6g3sfjWaqreCgQWiVdfHas
|
||||
tbny+dwuGdcv8F0uMINEXcmWDKY73dono4GgMIGdMB0GA1UdDgQWBBT8KD5Dt+Da
|
||||
s19QUvSB0kpY6JxiLzAfBgNVHSMEGDAWgBT8KD5Dt+Das19QUvSB0kpY6JxiLzAl
|
||||
BgNVHREEHjAcgglqd2tzLW1vY2uCCWxvY2FsaG9zdIcEfwAAATAPBgNVHRMBAf8E
|
||||
BTADAQH/MA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAKBggq
|
||||
hkjOPQQDAgNHADBEAiBZL20arEn9WnXpbqilOrvOSk1b9tFb2Ad7NIMq8mQoZAIg
|
||||
BD49p5vjFs7lvIlhX/mjs+LbITx1HX7EpztVszNsAfk=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIBIZ9LfWiAeAxoOIYbFoD+tCDoO+5uIyhsPNSrmMCjknoAoGCCqGSM49
|
||||
AwEHoUQDQgAE0ufXjd7/wr63zeTh06lnv8Kf2bGsirH+oN7H41mqq3goEFolXXx2
|
||||
rLW58vncLhnXL/BdLjCDRF3JlgymO93aJw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user