using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; namespace Azaion.Missions.E2E.Helpers; /// /// Test-only ECDSA P-256 signer used by NFT-SEC-02 to mint a token signed by /// a keypair the JWKS endpoint never published. This is the ONE in-test /// signing path allowed by the task spec — every other test mints via the /// jwks-mock POST /sign endpoint. /// /// /// The private key lives entirely in the test process and is disposed with /// the helper. The wire shape mirrors JwksMock.TokenSigner (JWS-compact /// ES256) so the only thing that differs from a "real" mock-minted token is /// the signing key — defeating any IssuerSigningKeyResolver that fails to /// match kid against the published JWKS. /// public sealed class ForeignKeypair : IDisposable { private readonly ECDsa _ec; private readonly string _kid; public ForeignKeypair() { _ec = ECDsa.Create(ECCurve.NamedCurves.nistP256); // Deterministic kid that is clearly NOT what jwks-mock issues // (mock kids are base64url SHA-256 hashes; this label is plain ASCII). _kid = "foreign-keypair-not-in-jwks"; } public string Mint(string issuer, string audience, string permissions, int expOffsetSeconds = 3600) { var nowUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var expUnix = nowUnix + expOffsetSeconds; var header = new JsonObject { ["alg"] = "ES256", ["kid"] = _kid, ["typ"] = "JWT" }; var payload = new JsonObject { ["iss"] = issuer, ["aud"] = audience, ["iat"] = nowUnix, ["exp"] = expUnix, ["permissions"] = permissions }; var headerSeg = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(header)); var payloadSeg = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(payload)); var signingInput = Encoding.ASCII.GetBytes($"{headerSeg}.{payloadSeg}"); var signature = _ec.SignData(signingInput, HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation); var sigSeg = Base64UrlEncode(signature); return $"{headerSeg}.{payloadSeg}.{sigSeg}"; } public void Dispose() => _ec.Dispose(); private static string Base64UrlEncode(ReadOnlySpan bytes) { var b64 = Convert.ToBase64String(bytes); return b64.Replace('+', '-').Replace('/', '_').TrimEnd('='); } }