crypto-iff-core
v0.1.0
Published
Cryptographic challenge-response (IFF) protocol for drone fleet authentication using Ed25519, SHA-256 hash chains, and HKDF
Maintainers
Readme
crypto-iff-core
Cryptographic challenge-response (IFF) protocol for drone fleet authentication using Ed25519 signatures, SHA-256 hash chains, and HKDF key derivation.
Overview
IFF (Identify Friend or Foe) is a real-time authentication protocol originally developed for military radar systems. This library implements a modern cryptographic variant designed for drone fleet management.
The problem: in a fleet of autonomous drones, a verifier (ground station or lead drone) needs to confirm that a responding drone is a legitimate member of the fleet — not a spoofed or compromised unit. This must happen quickly, with minimal bandwidth, and without exposing long-term secrets.
crypto-iff-core solves this with a three-layer approach:
- Ed25519 signatures — identity binding and message authentication
- SHA-256 hash chains — lightweight one-time proof of knowledge (no private key transmission per challenge)
- HKDF key derivation — deterministic per-drone, per-epoch chain seeds from a single fleet master secret
Features
- Ed25519 keypair generation, signing, and verification
- HKDF-SHA256 key derivation for per-drone, per-epoch chain seeds
- SHA-256 hash chain generation and verification with configurable length
- Challenge-response protocol with replay protection (timestamps + nonces)
- Compact binary wire format (92-byte challenges, 124-byte responses)
- Constant-time byte comparison to prevent timing attacks
- Revocation list support
- Zero runtime dependencies beyond
tweetnacland@noble/hashes - Pure ESM, tree-shakeable (
sideEffects: false)
Installation
npm install crypto-iff-coreQuick Start
import {
generateKeypair,
deriveChainSeed,
generateHashChain,
createChallenge,
verifyChallenge,
createResponse,
verifyResponse,
randomBytes,
toHex,
} from 'crypto-iff-core';
// --- Setup (done once per epoch) ---
// Fleet authority keypair (signs challenges)
const fleetAuth = generateKeypair();
// Verifier and drone keypairs
const verifier = generateKeypair();
const drone = generateKeypair();
// Derive a hash chain for this drone in epoch 1
const masterSeed = randomBytes(32);
const epoch = 1;
const chainSeed = deriveChainSeed(masterSeed, epoch, drone.publicKey);
const hashChain = generateHashChain(chainSeed, 100);
// Register the drone (verifier stores the anchor)
const droneRegistry = [
{
dronePub: drone.publicKey,
callsign: 'ALPHA-1',
anchor: hashChain.anchor,
lastKnownHash: hashChain.anchor,
},
];
// --- Protocol (per authentication) ---
// 1. Verifier creates a challenge
const challenge = createChallenge(verifier, epoch, fleetAuth.secretKey);
// 2. Drone verifies the challenge came from a legitimate fleet authority
const isValid = verifyChallenge(challenge, fleetAuth.publicKey);
// 3. Drone responds with the next hash chain value
let chainIndex = hashChain.length - 2; // start from tip-1, count down
const response = createResponse(challenge, drone, hashChain, chainIndex);
// 4. Verifier validates the response
const result = verifyResponse(response, challenge, droneRegistry, new Set());
console.log(result.status); // 'FRIEND'
console.log(result.callsign); // 'ALPHA-1'Core Concepts
Hash Chains
A hash chain is a sequence of values where each is the SHA-256 hash of the previous one:
seed → h₀ = SHA-256(seed) → h₁ = SHA-256(h₀) → ... → h_{n-1} (anchor)The anchor (last value) is published. Values are revealed in reverse order — the drone reveals h_{n-2} first, then h_{n-3}, etc. The verifier checks that hashing the revealed value produces the last known hash. This gives each value one-time-use authenticity without transmitting private keys.
Epochs
Epochs are time periods (e.g., a mission or a day). Each epoch gets a fresh hash chain per drone, derived deterministically via HKDF from a shared master seed. When an epoch rotates, old chains are discarded.
Challenge-Response Flow
Verifier Drone
│ │
│ 1. createChallenge() │
│ ──────── Challenge (92B) ───────► │
│ [verifierId, nonce, │
│ timestamp, epoch, sig] │
│ │ 2. verifyChallenge()
│ │ 3. createResponse()
│ ◄─────── Response (124B) ──────── │
│ [nonce, hashValue, │
│ epoch, droneIdHint, │
│ timestamp, sig] │
│ │
│ 4. verifyResponse() │
│ → FRIEND / FOE / REVOKED │
│ │API Reference
Keypair Operations
generateKeypair(): Ed25519Keypair
Generate a new Ed25519 keypair.
sign(message: Uint8Array, secretKey: Uint8Array): Uint8Array
Create a detached Ed25519 signature (64 bytes).
verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean
Verify a detached Ed25519 signature.
Key Derivation
deriveChainSeed(masterSeed: Uint8Array, epoch: number, dronePub: Uint8Array): Uint8Array
Derive a 32-byte per-drone, per-epoch chain seed using HKDF-SHA256.
masterSeed— 32-byte fleet master secretepoch— epoch number (used as 4-byte big-endian salt)dronePub— 32-byte drone public key (used in info string)
Hash Chain
generateHashChain(seed: Uint8Array, length: number): HashChain
Generate a hash chain of length values from a 32-byte seed. chain[0] = SHA-256(seed), each subsequent value is SHA-256(previous). The anchor is chain[length - 1].
verifyHashLink(hashValue: Uint8Array, expectedNext: Uint8Array): boolean
Check that SHA-256(hashValue) === expectedNext.
findChainDistance(hashValue: Uint8Array, expectedNext: Uint8Array, maxSteps: number): number
Walk the chain forward up to maxSteps hashes to find expectedNext. Returns the number of steps, or -1 if not reachable.
Protocol
createChallenge(verifierKeypair: Ed25519Keypair, epoch: number, fleetAuthKey: Uint8Array): Challenge
Create a signed challenge. The challenge includes a random 16-byte nonce, the current timestamp, the epoch, and a 4-byte verifier ID hint.
fleetAuthKey— the fleet authority's secret key (signs the challenge)
verifyChallenge(challenge: Challenge, fleetAuthPub: Uint8Array, toleranceSec?: number): boolean
Verify a challenge signature and check that the timestamp is within toleranceSec (default: 30) of the current time.
createResponse(challenge: Challenge, droneKeypair: Ed25519Keypair, hashChain: HashChain, chainIndex: number): Response
Create a signed response to a challenge. The response echoes the challenge nonce, reveals the hash chain value at chainIndex, and includes a 4-byte drone ID hint.
chainIndexmust be>= 0and< chain.length - 1(the anchor is never revealed)
verifyResponse(response: Response, challenge: Challenge, droneRegistry: DroneEntry[], revocationList: RevocationList, maxChainGap?: number): VerificationResult
Verify a response against the challenge, drone registry, and revocation list. Returns a VerificationResult with status FRIEND, FOE, or REVOKED.
maxChainGap— maximum hash chain steps to search (default: 10). Limits CPU cost of chain walking.
Serialization
serializeChallenge(challenge: Challenge): Uint8Array
Serialize a challenge to a 92-byte buffer.
deserializeChallenge(data: Uint8Array): Challenge
Deserialize a 92-byte buffer into a Challenge. Throws if length is wrong.
serializeResponse(response: Response): Uint8Array
Serialize a response to a 124-byte buffer.
deserializeResponse(data: Uint8Array): Response
Deserialize a 124-byte buffer into a Response. Throws if length is wrong.
Utilities
toHex(bytes: Uint8Array): string
Convert bytes to a lowercase hex string.
fromHex(hex: string): Uint8Array
Convert a hex string to bytes.
concat(...arrays: Uint8Array[]): Uint8Array
Concatenate multiple Uint8Array values.
equal(a: Uint8Array, b: Uint8Array): boolean
Constant-time byte array comparison. Returns false if lengths differ.
randomBytes(length: number): Uint8Array
Generate cryptographically random bytes (delegates to tweetnacl).
nowSeconds(): number
Current time as Unix seconds (floored).
Types
Ed25519Keypair
| Field | Type | Size |
|---|---|---|
| publicKey | Uint8Array | 32 bytes |
| secretKey | Uint8Array | 64 bytes |
HashChain
| Field | Type | Description |
|---|---|---|
| seed | Uint8Array | 32-byte HKDF-derived seed |
| chain | Uint8Array[] | [h₀, h₁, ..., h_{n-1}] |
| anchor | Uint8Array | chain[length - 1], the published tip |
| length | number | Number of chain values |
Challenge
| Field | Type | Size |
|---|---|---|
| verifierId | Uint8Array | 4 bytes |
| nonce | Uint8Array | 16 bytes |
| timestamp | number | uint32 (Unix seconds) |
| epoch | number | uint32 |
| signature | Uint8Array | 64 bytes |
Response
| Field | Type | Size |
|---|---|---|
| nonce | Uint8Array | 16 bytes |
| hashValue | Uint8Array | 32 bytes |
| epoch | number | uint32 |
| droneIdHint | Uint8Array | 4 bytes |
| timestamp | number | uint32 |
| signature | Uint8Array | 64 bytes |
VerificationResult
| Field | Type | Description |
|---|---|---|
| status | 'FRIEND' \| 'FOE' \| 'REVOKED' | Authentication result |
| droneIdHint | string | Hex string of the 4-byte hint |
| callsign | string? | Resolved callsign if in registry |
| timestamp | number | When verification was performed |
DroneEntry
| Field | Type | Description |
|---|---|---|
| dronePub | Uint8Array | 32-byte full public key |
| callsign | string | Human-readable identifier |
| anchor | Uint8Array | 32-byte published chain tip |
| lastKnownHash | Uint8Array | Advances on each FRIEND verification |
RevocationList
type RevocationList = Set<string>; // hex-encoded 32-byte public keysProtocol Flow
Step-by-step walkthrough of the full challenge-response cycle:
Setup — The fleet authority generates a master seed. For each drone and epoch,
deriveChainSeed()produces a deterministic 32-byte seed.generateHashChain()builds the chain; the anchor is distributed to all verifiers.Challenge — The verifier calls
createChallenge(), which generates a random nonce, records the current timestamp and epoch, and signs the payload with the fleet authority's secret key. The 92-byte challenge is sent to the drone.Validation — The drone calls
verifyChallenge()to confirm the signature is valid and the timestamp is fresh (within tolerance). This proves the challenge came from a legitimate fleet authority.Response — The drone calls
createResponse(), revealing the next unused hash chain value and signing the response with its own secret key. The 124-byte response is sent back.Verification — The verifier calls
verifyResponse(), which:- Checks the nonce and epoch match the original challenge
- Looks up the drone in the registry by its 4-byte ID hint
- Checks the revocation list
- Walks the hash chain forward from the revealed value to
lastKnownHash(up tomaxChainGapsteps) - Verifies the Ed25519 signature against the drone's public key
- On success, advances
lastKnownHashand returnsFRIEND
Wire Format
Challenge (92 bytes)
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | verifierId |
| 4 | 16 | nonce |
| 20 | 4 | timestamp (uint32 BE) |
| 24 | 4 | epoch (uint32 BE) |
| 28 | 64 | signature |
Response (124 bytes)
| Offset | Size | Field |
|---|---|---|
| 0 | 16 | nonce |
| 16 | 32 | hashValue |
| 48 | 4 | epoch (uint32 BE) |
| 52 | 4 | droneIdHint |
| 56 | 4 | timestamp (uint32 BE) |
| 60 | 64 | signature |
All multi-byte integers are big-endian. No padding or alignment.
Security Considerations
- Ed25519 via tweetnacl — well-audited, pure-JS implementation of Curve25519/Ed25519
- SHA-256 and HKDF via @noble/hashes — audited, high-performance pure-JS cryptographic hashes by Paul Miller
- Constant-time comparison —
equal()uses bitwise OR accumulation to avoid timing side-channels - Replay protection — challenges include a random 16-byte nonce and a timestamp checked within a configurable tolerance window (default: 30 seconds)
- Hash chain gap limit —
maxChainGap(default: 10) bounds the CPU cost of chain verification and prevents an attacker from replaying old chain values - One-time hash values — each chain value is used once and
lastKnownHashadvances forward, preventing reuse - No long-term secrets on the wire — only hash chain values and signatures are transmitted; private keys never leave their host
Dependencies
| Package | Purpose |
|---|---|
| tweetnacl | Ed25519 signatures and random bytes |
| @noble/hashes | SHA-256, HKDF-SHA256 |
Both are pure JavaScript with no native dependencies.
Development
git clone https://github.com/tsolman/cryptoiff.git
cd cryptoiff/crypto-iff-core
npm install
npm run build
npm test