KDPika Licensing Sign inStart free
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Advanced

C# signature verification

Run the complete C# Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases.

On this page
Updated 2026-10-17 UTCDownload Markdown

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

bashbash
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

bashbash
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

Verifier.csprojxml
<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>
Program.cscsharp
// 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

SymptomCauseAction
Positive case failsWrong trusted key, changed numeric tokens or missing dependencyRestore the exact archive/vector files and rerun the build
Tampered case returns falseExpected rejectionKeep the refusal; do not bypass verification
Expired case returns trueAuthenticity and expiry are separateReject access in the license policy layer
Cannot open a fileTerminal is outside the extracted security directoryChange to that directory and retry using quoted paths

Next steps

Run all test vectors and apply the signed-response policy checks.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings