Verify KDPika payload authenticity in Swift without losing the server's signed numeric tokens.
What you will learn
- Build and run the complete Swift verifier.
- Reject altered, ambiguous and untrusted envelopes.
- Keep signature checking separate from permission to use a license.
Prerequisites
- Read signed responses.
- Install Swift 6+; macOS 13+; built-in CryptoKit.
- 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
mkdir -p build
swiftc swift/verify.swift -o build/swift-verify
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.
This exact sample uses Apple's CryptoKit and was compiled on macOS. It is not a Windows/Linux SwiftCrypto build; do not claim cross-platform execution without porting and testing the crypto import.
Run the positive and negative cases
./build/swift-verify vectors/positive.key.json vectors/positive.envelope.json
./build/swift-verify vectors/tampered.key.json vectors/tampered.envelope.json
./build/swift-verify vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command ./build/swift-verify
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
// KDPika signature authenticity only. Swift 6+, macOS 13+, CryptoKit.
// Run compiled verifier with key.json envelope.json. Output signature_valid=true/false.
// Trust the key separately; verify expiry, installation and product before access.
import Foundation
import CryptoKit
enum Invalid: Error { case json }
indirect enum JSON {
case object([(String, JSON)]), array([JSON]), string(String), number(String), bool(Bool), null
func get(_ key: String) throws -> JSON {
guard case .object(let pairs) = self,
let pair = pairs.first(where: { Array($0.0.utf8) == Array(key.utf8) }) else { throw Invalid.json }
return pair.1
}
func text() throws -> String { guard case .string(let s) = self else { throw Invalid.json }; return s }
}
func quote(_ text: String) -> String {
var out = "\""
for scalar in text.unicodeScalars {
switch scalar.value {
case 34: out += "\\\""
case 92: out += "\\\\"
case 8: out += "\\b"
case 9: out += "\\t"
case 10: out += "\\n"
case 12: out += "\\f"
case 13: out += "\\r"
case 0..<32: out += String(format: "\\u%04x", scalar.value)
case 0x2028: out += "\\u2028"
case 0x2029: out += "\\u2029"
default: out.unicodeScalars.append(scalar)
}
}
return out + "\""
}
struct Parser {
let raw: [UInt8]; var i = 0
init(_ data: Data) throws {
guard data.count <= 1048576, String(data: data, encoding: .utf8) != nil else { throw Invalid.json }
raw = Array(data)
}
mutating func ws() { while i < raw.count && [9,10,13,32].contains(raw[i]) { i += 1 } }
mutating func string() throws -> String {
let start = i; guard i < raw.count && raw[i] == 34 else { throw Invalid.json }; i += 1
while i < raw.count {
let c = raw[i]; i += 1
if c == 92 { guard i < raw.count else { throw Invalid.json }; i += 1 }
else if c == 34 {
let data = Data(raw[start..<i])
guard let result = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) as? String else { throw Invalid.json }
return result
}
}
throw Invalid.json
}
mutating func value(_ depth: Int) throws -> JSON {
guard depth <= 64 else { throw Invalid.json }; ws(); guard i < raw.count else { throw Invalid.json }
let c = raw[i]
if c == 34 { return .string(try string()) }
if c == 123 || c == 91 {
let object = c == 123; let end: UInt8 = object ? 125 : 93; i += 1; ws()
var pairs: [(String, JSON)] = []; var values: [JSON] = []
if i < raw.count && raw[i] == end { i += 1; return object ? .object(pairs) : .array(values) }
while true {
if object {
ws(); let key = try string(); ws()
guard i < raw.count && raw[i] == 58 && !pairs.contains(where: { Array($0.0.utf8) == Array(key.utf8) }) else { throw Invalid.json }
i += 1; pairs.append((key, try value(depth + 1)))
} else { values.append(try value(depth + 1)) }
ws(); guard i < raw.count else { throw Invalid.json }; let next = raw[i]; i += 1
if next == end { return object ? .object(pairs) : .array(values) }
guard next == 44 else { throw Invalid.json }
}
}
for (token, result) in [("true", JSON.bool(true)), ("false", JSON.bool(false)), ("null", JSON.null)] {
let bytes = Array(token.utf8)
if raw.dropFirst(i).starts(with: bytes) { i += bytes.count; return result }
}
let rest = String(decoding: raw[i...], as: UTF8.self)
guard let range = rest.range(of: #"^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?"#, options: .regularExpression) else { throw Invalid.json }
let number = String(rest[range]); i += number.utf8.count; return .number(number)
}
mutating func parse() throws -> JSON { let result = try value(0); ws(); guard i == raw.count else { throw Invalid.json }; return result }
}
func canonical(_ value: JSON) -> String {
switch value {
case .object(let pairs): return "{" + pairs.sorted { Array($0.0.utf8).lexicographicallyPrecedes(Array($1.0.utf8)) }.map { quote($0.0) + ":" + canonical($0.1) }.joined(separator: ",") + "}"
case .array(let items): return "[" + items.map(canonical).joined(separator: ",") + "]"
case .string(let text): return quote(text)
case .number(let raw): return raw
case .bool(let value): return value ? "true" : "false"
case .null: return "null"
}
}
func decode(_ value: JSON, _ size: Int) throws -> Data {
let text = try value.text()
guard text.range(of: #"^[A-Za-z0-9_-]+$"#, options: .regularExpression) != nil else { throw Invalid.json }
let padded = text.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + String(repeating: "=", count: (4 - text.count % 4) % 4)
guard let bytes = Data(base64Encoded: padded), bytes.count == size,
bytes.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") == text else { throw Invalid.json }
return bytes
}
func verifyEnvelope(_ keyRaw: Data, _ envRaw: Data) -> Bool {
do {
var kp = try Parser(keyRaw); var ep = try Parser(envRaw); let key = try kp.parse(); let env = try ep.parse()
let bytes = try decode(key.get("public_key"), 32)
let id = "ed25519_" + SHA256.hash(data: bytes).map { String(format: "%02x", $0) }.joined().prefix(16)
guard try key.get("algorithm").text() == "Ed25519", try key.get("encoding").text() == "base64url", try key.get("key_id").text() == id,
try env.get("algorithm").text() == "Ed25519", try env.get("key_id").text() == id else { return false }
let payload = try env.get("payload"); guard case .object = payload else { return false }
let publicKey = try Curve25519.Signing.PublicKey(rawRepresentation: bytes)
return try publicKey.isValidSignature(decode(env.get("signature"), 64), for: Data(canonical(payload).utf8))
} catch { return false }
}
var valid = false
do { if CommandLine.arguments.count == 3 { valid = verifyEnvelope(try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1])), try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2]))) } }
catch { valid = false }
print("signature_valid=\(valid)"); if !valid { 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.