---
title: "C# signature verification"
description: "Run the complete C# Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases."
slug: "/docs/v1/security/verify-ed25519/csharp"
section: "Signature examples"
order: 205
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"]
draft: false
---
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](/docs/v1/start/glossary#license).

## Prerequisites
- Read [signed responses](/docs/v1/security/signed-responses).
- Install .NET 7+; BouncyCastle.Cryptography 2.7.0.
- Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) 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
```bash
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
```bash
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](/docs/v1/start/glossary#official-sdk).

## Complete source
```xml
<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>
```

```csharp
// 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](/docs/v1/start/glossary#feature).

After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/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](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses).

## Related
- [Verification language index](/docs/v1/security/verify-ed25519)
- [Secure token storage](/docs/v1/security/secure-token-storage)
