Verify KDPika payload authenticity in C# without losing the server's signed numeric tokens.
What you will learn
- Build and run the complete C# verifier.
- Reject altered, ambiguous and untrusted envelopes.
- Keep signature checking separate from permission to use a license.
Prerequisites
- Read signed responses.
- Install .NET 7+; BouncyCastle.Cryptography 2.7.0.
- Download the complete verification source archive and extract it.
- Open a terminal in the extracted security directory; all paths below are relative to it.
- Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier.
Install and build
dotnet build csharp/Verifier.csproj -o build/csharp --nologo
Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support.
The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting.
Run the positive and negative cases
dotnet build/csharp/Verifier.dll vectors/positive.key.json vectors/positive.envelope.json
dotnet build/csharp/Verifier.dll vectors/tampered.key.json vectors/tampered.envelope.json
dotnet build/csharp/Verifier.dll vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command dotnet build/csharp/Verifier.dll
Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches.
Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the official SDK.
Complete source
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.7.0" />
</ItemGroup>
</Project>
// KDPika signature authenticity only. .NET 7+, BouncyCastle.Cryptography 2.7.0.
// Run: dotnet run --project Verifier.csproj -- key.json envelope.json
// Output signature_valid=true/false. Independently trust the key; check policy next.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
public static class Verifier
{
private static readonly UTF8Encoding Utf8 = new(false, true);
private static string Quote(string value)
{
Utf8.GetBytes(value); // Reject unpaired surrogates instead of replacing them.
var output = new StringBuilder("\"");
foreach (var rune in value.EnumerateRunes())
{
output.Append(rune.Value switch {
34 => "\\\"", 92 => "\\\\", 8 => "\\b", 9 => "\\t",
10 => "\\n", 12 => "\\f", 13 => "\\r",
< 32 => "\\u" + rune.Value.ToString("x4"),
0x2028 => "\\u2028", 0x2029 => "\\u2029", _ => rune.ToString()
});
}
return output.Append('"').ToString();
}
private static int CompareUtf8(string a, string b) =>
Utf8.GetBytes(a).AsSpan().SequenceCompareTo(Utf8.GetBytes(b));
private static string Canonical(JsonElement value)
{
switch (value.ValueKind)
{
case JsonValueKind.Object:
var properties = value.EnumerateObject().ToArray();
if (properties.Select(p => p.Name).Distinct(StringComparer.Ordinal).Count() != properties.Length)
throw new FormatException("Duplicate JSON property");
Array.Sort(properties, (a, b) => CompareUtf8(a.Name, b.Name));
return "{" + string.Join(",", properties.Select(p => Quote(p.Name) + ":" + Canonical(p.Value))) + "}";
case JsonValueKind.Array:
return "[" + string.Join(",", value.EnumerateArray().Select(Canonical)) + "]";
case JsonValueKind.String: return Quote(value.GetString()!);
case JsonValueKind.Number: return value.GetRawText();
case JsonValueKind.True: return "true";
case JsonValueKind.False: return "false";
case JsonValueKind.Null: return "null";
default: throw new FormatException("Invalid JSON value");
}
}
private static byte[] Decode(string value, int size)
{
if (value.Length == 0 || value.Any(c => !(char.IsAsciiLetterOrDigit(c) || c is '-' or '_')))
throw new FormatException("Invalid base64url");
var result = Convert.FromBase64String(value.Replace('-', '+').Replace('_', '/') + new string('=', (4 - value.Length % 4) % 4));
if (result.Length != size || Convert.ToBase64String(result).TrimEnd('=').Replace('+', '-').Replace('/', '_') != value)
throw new FormatException("Invalid base64url length");
return result;
}
private static JsonDocument Parse(byte[] raw)
{
if (raw.Length > 1048576) throw new FormatException("JSON exceeds 1 MiB");
var doc = JsonDocument.Parse(Utf8.GetString(raw), new JsonDocumentOptions { MaxDepth = 64 });
Canonical(doc.RootElement); // Validate duplicates and strings throughout the envelope.
return doc;
}
public static bool VerifyEnvelope(byte[] trustedKeyRaw, byte[] envelopeRaw)
{
try
{
using var keyDoc = Parse(trustedKeyRaw); using var envDoc = Parse(envelopeRaw);
var key = keyDoc.RootElement; var env = envDoc.RootElement;
var bytes = Decode(key.GetProperty("public_key").GetString()!, 32);
var id = "ed25519_" + Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant()[..16];
if (key.GetProperty("algorithm").GetString() != "Ed25519" || key.GetProperty("encoding").GetString() != "base64url" ||
key.GetProperty("key_id").GetString() != id || env.GetProperty("algorithm").GetString() != "Ed25519" ||
env.GetProperty("key_id").GetString() != id || env.GetProperty("payload").ValueKind != JsonValueKind.Object) return false;
var message = Utf8.GetBytes(Canonical(env.GetProperty("payload")));
var verifier = new Ed25519Signer(); verifier.Init(false, new Ed25519PublicKeyParameters(bytes, 0));
verifier.BlockUpdate(message, 0, message.Length);
return verifier.VerifySignature(Decode(env.GetProperty("signature").GetString()!, 64));
}
catch { return false; }
}
public static int Main(string[] args)
{
var valid = false;
try { if (args.Length == 2) valid = VerifyEnvelope(File.ReadAllBytes(args[0]), File.ReadAllBytes(args[1])); }
catch { valid = false; }
Console.WriteLine("signature_valid=" + valid.ToString().ToLowerInvariant());
return valid ? 0 : 1;
}
}
Expected result
All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values.
Common mistakes
- Taking the verification key from the same untrusted message being checked.
- Treating a successful signature as proof of active access, correct installation or recent payment.
- Parsing and reserializing numbers before passing the envelope into this sample.
Security notes
The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified features.
After signature success, check product, environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See offline mode. The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs.
Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build |
| Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification |
| Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer |
| Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths |
Next steps
Run all test vectors and apply the signed-response policy checks.