---
title: "Rust signature verification"
description: "Run the complete Rust Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases."
slug: "/docs/v1/security/verify-ed25519/rust"
section: "Signature examples"
order: 210
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 Rust without losing the server's signed numeric tokens.

## What you will learn
- Build and run the complete Rust 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 Rust 1.81+; exact dependencies in Cargo.toml and included Cargo.lock.
- 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
cargo build --locked --manifest-path rust/Cargo.toml --target-dir build/rust
```
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
./build/rust/debug/kdpika-verifier-sample vectors/positive.key.json vectors/positive.envelope.json
./build/rust/debug/kdpika-verifier-sample vectors/tampered.key.json vectors/tampered.envelope.json
./build/rust/debug/kdpika-verifier-sample vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command ./build/rust/debug/kdpika-verifier-sample
```
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
```toml
[package]
name = "kdpika-verifier-sample"
version = "1.0.0"
edition = "2021"
rust-version = "1.81"

[dependencies]
base64 = "=0.22.1"
# Keep the declared Rust 1.81 toolchain usable when resolving transitive crates.
base64ct = "=1.6.0"
ed25519-dalek = "=2.2.0"
serde = "=1.0.228"
serde_json = { version = "=1.0.145", features = ["raw_value"] }
sha2 = "=0.10.9"
zeroize = "=1.8.1"
```

```rust
// KDPika signature authenticity only. Rust 1.81+, pinned Cargo.toml dependencies.
// Run: cargo run -- key.json envelope.json. Output signature_valid=true/false.
// Trust key.json separately; apply license policy after successful verification.
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use ed25519_dalek::{Signature, VerifyingKey};
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use serde_json::value::RawValue;
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, env, fmt, fs};

struct Object(BTreeMap<String, Box<RawValue>>);
impl<'de> Deserialize<'de> for Object {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        struct ObjectVisitor;
        impl<'de> Visitor<'de> for ObjectVisitor {
            type Value = Object;
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("unique JSON object") }
            fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Object, M::Error> {
                let mut out = BTreeMap::new();
                while let Some((key, value)) = map.next_entry::<String, Box<RawValue>>()? {
                    if out.insert(key, value).is_some() { return Err(de::Error::custom("duplicate JSON property")); }
                }
                Ok(Object(out))
            }
        }
        deserializer.deserialize_map(ObjectVisitor)
    }
}
fn quote(text: &str) -> Result<String, String> {
    Ok(serde_json::to_string(text).map_err(|e| e.to_string())?.replace('\u{2028}', "\\u2028").replace('\u{2029}', "\\u2029"))
}
fn canonical(raw: &RawValue, depth: usize) -> Result<String, String> {
    if depth > 64 { return Err("JSON exceeds 64 levels".into()); }
    let text = raw.get();
    match text.as_bytes().first() {
        Some(b'{') => {
            let map: Object = serde_json::from_str(text).map_err(|e| e.to_string())?;
            let mut parts = Vec::new();
            for (key, value) in map.0 { parts.push(format!("{}:{}", quote(&key)?, canonical(&value, depth + 1)?)); }
            Ok(format!("{{{}}}", parts.join(",")))
        }
        Some(b'[') => {
            let list: Vec<Box<RawValue>> = serde_json::from_str(text).map_err(|e| e.to_string())?;
            let parts: Result<Vec<_>,_> = list.iter().map(|v| canonical(v, depth + 1)).collect();
            Ok(format!("[{}]", parts?.join(",")))
        }
        Some(b'"') => { let value: String = serde_json::from_str(text).map_err(|e| e.to_string())?; quote(&value) }
        _ => Ok(text.to_owned()), // Validated RawValue; numeric lexemes are never converted to f64.
    }
}
fn parse(raw: &[u8]) -> Result<Object, String> {
    if raw.len() > 1048576 { return Err("JSON exceeds 1 MiB".into()); }
    let value: Box<RawValue> = serde_json::from_slice(raw).map_err(|e| e.to_string())?;
    canonical(&value, 0)?;
    serde_json::from_str(value.get()).map_err(|e| e.to_string())
}
fn text(map: &Object, key: &str) -> Result<String, String> {
    serde_json::from_str(map.0.get(key).ok_or("Missing property")?.get()).map_err(|e| e.to_string())
}
fn decode(value: &str, size: usize) -> Result<Vec<u8>, String> {
    let bytes = URL_SAFE_NO_PAD.decode(value).map_err(|e| e.to_string())?;
    if bytes.len() != size || URL_SAFE_NO_PAD.encode(&bytes) != value { return Err("Invalid base64url length".into()); }
    Ok(bytes)
}
pub fn verify_envelope(trusted_key_raw: &[u8], envelope_raw: &[u8]) -> bool {
    fn verify(key_raw: &[u8], env_raw: &[u8]) -> Result<bool,String> {
        let key = parse(key_raw)?; let env = parse(env_raw)?;
        let public = decode(&text(&key,"public_key")?,32)?;
        let id = format!("ed25519_{:x}",Sha256::digest(&public)); let id = &id[..24];
        if text(&key,"algorithm")? != "Ed25519" || text(&key,"encoding")? != "base64url" || text(&key,"key_id")? != id ||
           text(&env,"algorithm")? != "Ed25519" || text(&env,"key_id")? != id { return Ok(false); }
        let payload = env.0.get("payload").ok_or("Missing payload")?;
        if !payload.get().starts_with('{') { return Ok(false); }
        let public_array: [u8;32] = public.try_into().map_err(|_| "Invalid public key")?;
        let verifier = VerifyingKey::from_bytes(&public_array).map_err(|e| e.to_string())?;
        let signature = Signature::from_slice(&decode(&text(&env,"signature")?,64)?).map_err(|e| e.to_string())?;
        Ok(verifier.verify_strict(canonical(payload,0)?.as_bytes(),&signature).is_ok())
    }
    verify(trusted_key_raw,envelope_raw).unwrap_or(false)
}
fn main() {
    let args: Vec<_> = env::args().collect(); let mut valid=false;
    if args.len()==3 { if let (Ok(key),Ok(env))=(fs::read(&args[1]),fs::read(&args[2])) { valid=verify_envelope(&key,&env); } }
    println!("signature_valid={valid}"); if !valid { std::process::exit(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)
