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.
Prerequisites
- Read signed responses.
- Install Rust 1.81+; exact dependencies in Cargo.toml and included Cargo.lock.
- 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
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
./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.
Complete source
[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"
// 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.
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.