SmartPoint Pairs
A SmartPoint pair is the foundational cryptographic primitive of the SmartPoints architecture (ADR-088). It binds two identities in an asymmetric relationship where each party holds directional encryption keys and agreed-upon terms that are cryptographically bound to the pair.
Core Concepts
A SmartPoint pair is not a symmetric tunnel. Each party has:
- A send key (encrypts messages to the peer)
- A recv key (decrypts messages from the peer)
- A signing key (signs outbound envelopes)
- A verify key (verifies inbound envelopes from the peer)
- Terms (capabilities, TTL, resource limits) bound via BLAKE3 hash
The two halves are mirrored: the initiator's send key equals the responder's receive key, and vice versa.
Formation Protocol
Pair formation is a three-step protocol:
Initiator calls
SmartPointPair::createwith their DID, the peer's DID, the peer's KEM public key, desired terms, and crypto suite. This encapsulates a shared secret against the peer's ML-KEM public key and produces aPairOffer.Responder calls
SmartPointPair::acceptwith the offer and their KEM secret key. This decapsulates the shared secret, derives mirrored directional keys, and produces anAcceptResponsecontaining the responder's verification key.Initiator calls
SmartPointPair::finalizewith the response to install the peer's verification key. Both sides can now exchange envelopes.
use spt_smartpoint::{SmartPointPair, SmartPointTerms, CryptoSuite};
// Step 1: Initiator creates a pair offer
let terms = SmartPointTerms::empty().with_ttl(3600);
let (mut init_pair, offer) = SmartPointPair::create(
"did:said:1:sw:blake3_alice".into(),
"did:said:1:sw:blake3_bob".into(),
&bob_kem_pk,
terms,
CryptoSuite::PqV1,
alice_signing_sk,
alice_signing_pk,
)?;
// Step 2: Responder accepts the offer
let (mut resp_pair, response) = SmartPointPair::accept(
&offer, &bob_kem_sk, bob_signing_sk, bob_signing_pk,
)?;
// Step 3: Initiator finalizes
init_pair.finalize(&response);Three-Layer Vector Envelope
Every message exchanged through a SmartPoint pair is wrapped in a VectorEnvelope with three distinct layers:
| Layer | Name | Visibility | Contents |
|---|---|---|---|
| 1 | Routing | Plaintext | Sender/recipient DIDs, crypto suite ID, routing hints, sequence number |
| 2 | Payload | Encrypted | ChaCha20-Poly1305 ciphertext of the actual content |
| 3 | Signed | Verifiable | ML-DSA-65 signature over (ciphertext + terms hash + sequence + timestamp) |
The routing layer is readable by intermediaries for message relay. The encrypted payload is only decryptable by the intended recipient. The signed layer provides tamper detection and terms binding.
use spt_smartpoint::VectorEnvelope;
// Seal a message (initiator -> responder)
let sealed = VectorEnvelope::seal(&mut init_pair, b"hello", vec![])?;
// Open the message (responder side)
let content = VectorEnvelope::open(&mut resp_pair, &sealed)?;
assert_eq!(b"hello", &content[..]);Replay Protection
Each pair maintains a monotonic sequence counter. VectorEnvelope::seal advances the outbound counter automatically. VectorEnvelope::open validates that each inbound sequence is strictly greater than the last seen value.
Terms Binding
The terms hash (BLAKE3 of the canonical JSON) is included in the signed layer. If either party's terms are modified after pair formation, envelope verification fails with TermsHashMismatch.
Crypto Suite Agility
The CryptoSuite enum selects the full algorithm set for a pair. The suite ID is embedded in every envelope so recipients know which algorithms to use.
| Suite | Key Exchange | Encryption | Signature |
|---|---|---|---|
pq-v1 (default) | ML-KEM-768 | ChaCha20-Poly1305 | ML-DSA-65 |
hybrid-v1 | X25519 + ML-KEM-768 | ChaCha20-Poly1305 | Ed25519 + ML-DSA-65 |
classical-v1 | X25519 | ChaCha20-Poly1305 | Ed25519 |
The default suite (pq-v1) provides post-quantum security. The hybrid suite combines classical and post-quantum algorithms for defense-in-depth. The classical suite is available for interoperability with systems that do not support post-quantum cryptography.
use spt_smartpoint::CryptoSuite;
let suite = CryptoSuite::PqV1;
assert_eq!(suite.as_str(), "pq-v1");
assert_eq!(
suite.description(),
"ML-KEM-768 + ChaCha20-Poly1305 + ML-DSA-65"
);SmartPoint Terms
Terms define what each party in the pair is allowed to do. They are serialized as canonical JSON and hashed with BLAKE3 to produce a tamper-evident binding.
use spt_smartpoint::SmartPointTerms;
use std::collections::BTreeMap;
let mut caps = BTreeMap::new();
caps.insert("documents".into(), vec!["read".into(), "write".into()]);
let terms = SmartPointTerms::with_capabilities(caps)
.with_ttl(3600)
.with_resource_limit("max_message_size", "1048576")
.with_resource_limit("rate_limit_per_min", "100");
assert!(terms.has_capability("documents", "read"));
assert!(!terms.has_capability("admin", "read"));Terms fields:
| Field | Type | Description |
|---|---|---|
capabilities | BTreeMap<String, Vec<String>> | Resource name to permitted actions |
ttl_seconds | Option<u64> | Time-to-live; None means no expiration |
resource_limits | BTreeMap<String, String> | Constraint key-value pairs |
custom | BTreeMap<String, String> | Arbitrary extension terms |
Key Types
| Type | Crate | Description |
|---|---|---|
SmartPointPair | spt-smartpoint | One party's view of the pair |
PairOffer | spt-smartpoint | Serializable offer from initiator |
AcceptResponse | spt-smartpoint | Response from responder |
VectorEnvelope | spt-smartpoint | Three-layer sealed message |
RoutingLayer | spt-smartpoint | Layer 1: plaintext routing |
SignedLayer | spt-smartpoint | Layer 3: signature + terms hash |
SmartPointTerms | spt-smartpoint | Capabilities, TTL, limits |
CryptoSuite | spt-smartpoint | Algorithm suite selector |
SymmetricKey | spt-smartpoint | 32-byte key with zeroize-on-drop |