---
title: "Swift signature verification"
description: "Run the complete Swift Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases."
slug: "/docs/v1/security/verify-ed25519/swift"
section: "Signature examples"
order: 208
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 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](/docs/v1/start/glossary#license).

## Prerequisites
- Read [signed responses](/docs/v1/security/signed-responses).
- Install Swift 6+; macOS 13+; built-in CryptoKit.
- 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
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
```bash
./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](/docs/v1/start/glossary#official-sdk).

## Complete source
```swift
// 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](/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)
