A frozen spec is only meaningful if it's paired with the corpus that was current at release time — otherwise a v0.1.0 reader follows "see corpus/payloads/ack/" and lands on a file whose test vectors may have changed since v0.1.0 was cut. Three changes: 1. `cut-version` now copies BOTH src/ AND corpus/ into the snapshot directory. A release is the pair, not just the text. 2. The corpus URL rewriting in cut-version now retargets /tree/main/corpus -> /tree/main/versions/<v>/corpus (and the blob variant). This lets the link resolve on GitHub from main alone — no git tag required, since the frozen copy is a real path under main. Downgrades the "tag is REQUIRED" message from last commit back to "optionally tag the commit". 3. The frozen corpus's spec_ref fields get rewritten too: from `src/04-payload-ack.md` to `versions/<v>/04-payload-ack.md`, so a reader following a test vector lands on the spec text the vector was paired with at release time. versions/v0.1.0/ regenerated from scratch via `just cut-version v0.1.0` to pick up the corpus snapshot + the new sed patterns. The release is now self-consistent: v0.1.0 spec links go to v0.1.0 corpus, which links back to v0.1.0 spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.2 KiB
MeshCore Protocol Specification
Section 15: Identity
Overview
Every MeshCore node has an Ed25519 key pair. The 32-byte public key serves as the node's identity. Hashes used in routing are truncated prefixes of the public key. Shared secrets for encryption are computed via ECDH key exchange using X25519 (Ed25519 keys transposed to Curve25519).
Key Types
| Type | Size | Description |
|---|---|---|
| Public Key | 32 bytes (PUB_KEY_SIZE) | Ed25519 public key |
| Private Key | 64 bytes (PRV_KEY_SIZE) | Ed25519 private key (seed + public) |
| Signature | 64 bytes (SIGNATURE_SIZE) | Ed25519 signature |
| Shared Secret | 32 bytes (PUB_KEY_SIZE) | ECDH shared secret |
Hash Derivation
A node's routing hash is simply a prefix of its public key:
hash = pub_key[0..hash_size-1]
Where hash_size is 1, 2, or 3 bytes depending on the protocol version and
path encoding (see Section 3).
For V1, PATH_HASH_SIZE is 1, meaning a node's hash is the first byte of its
public key. This gives only 256 unique hash values, so hash collisions are
common and expected. The protocol handles this by attempting decryption with all
peers matching a given hash.
Hash Matching
To check if a hash matches a node's identity:
match = memcmp(hash, pub_key, hash_size) == 0
Ed25519 Signing
Used for advertisement signatures (see Section 5):
- Construct the message to sign (e.g., pub_key || timestamp || app_data).
- Sign using the Ed25519 private key.
- The 64-byte signature is included in the advertisement payload.
Verification:
- Extract the public key and signature from the payload.
- Reconstruct the message.
- Verify the signature using Ed25519 verification.
- Receivers MUST discard the packet if verification fails.
ECDH Key Exchange (X25519)
To compute a shared secret between two nodes:
- Convert the Ed25519 keys to X25519 (Curve25519) format.
- Perform X25519 Diffie-Hellman:
shared_secret = X25519(my_private, their_public). - The resulting 32-byte shared secret is used for:
- AES-128 key:
shared_secret[0..15] - HMAC-SHA256 key:
shared_secret[0..31]
- AES-128 key:
Identity Serialization
Identities can be serialized for storage:
- Public Identity: 32 bytes (public key only)
- Local Identity: 64 bytes (private key) + 32 bytes (public key) = 96 bytes
Hex representation uses uppercase characters with 2 hex digits per byte.
Cross-References
- Section 3: Path — Hash size in path encoding
- Section 5: Advertisement — Signature usage
- Section 14: Cryptography — Shared secret usage
- Test vectors:
corpus/crypto/ed25519/,corpus/crypto/ecdh/
Reference Implementation
Identityclass insrc/Identity.h— Public key and verificationLocalIdentityclass insrc/Identity.h— Key pair, signing, ECDHIdentity::copyHashTo()— Hash derivation (prefix copy)Identity::isHashMatch()— Hash comparisonLocalIdentity::calcSharedSecret()— ECDH key exchange