Your First SmartPoint Pair
This tutorial walks through creating two identities, forming a SmartPoint pair, sealing a message, transporting it over QUIC, and opening it on the other side.
Overview
The steps:
- Generate two DID identities with post-quantum key material
- Form a SmartPoint pair between them
- Seal a message into a three-layer vector envelope
- Send the sealed envelope over QUIC using
spt-wire - Open the envelope on the receiving side
Step 1: Generate DID Identities
Each participant needs a DID with an ML-KEM key pair (for key exchange) and an ML-DSA key pair (for signing).
use spt_pq_crypto::{MlKem768, MlDsaKeyPair};
// Generate key material for Alice
let (alice_kem_pk, alice_kem_sk) = MlKem768::generate_keypair(&mut rng)?;
let alice_dsa = MlDsaKeyPair::generate(&mut rng)?;
// Generate key material for Bob
let (bob_kem_pk, bob_kem_sk) = MlKem768::generate_keypair(&mut rng)?;
let bob_dsa = MlDsaKeyPair::generate(&mut rng)?;The DIDs follow the SAID format:
did:said:1:sw:blake3_<fingerprint>You can generate a DID from the key material using spt-aid:
use spt_aid::Did;
let alice_did = Did::from_public_key(&alice_kem_pk.as_bytes(), "sw")?;
let bob_did = Did::from_public_key(&bob_kem_pk.as_bytes(), "sw")?;
// e.g. "did:said:1:sw:blake3_a1b2c3d4..."Step 2: Define Terms
Terms define what the pair is allowed to do. They are cryptographically bound to every envelope.
use spt_smartpoint::SmartPointTerms;
use std::collections::BTreeMap;
let mut caps = BTreeMap::new();
caps.insert("messages".into(), vec!["read".into(), "write".into()]);
let terms = SmartPointTerms::with_capabilities(caps)
.with_ttl(3600) // 1 hour
.with_resource_limit("max_message_size", "1048576");Step 3: Form the Pair
Pair formation is a three-step handshake.
use spt_smartpoint::{SmartPointPair, CryptoSuite};
// Alice (initiator) creates a pair offer
let (mut alice_pair, offer) = SmartPointPair::create(
alice_did.to_string(),
bob_did.to_string(),
&bob_kem_pk.as_bytes(),
terms,
CryptoSuite::PqV1,
alice_dsa.secret_key_bytes(),
alice_dsa.public_key_bytes(),
)?;
// Bob (responder) accepts the offer
let (mut bob_pair, response) = SmartPointPair::accept(
&offer,
&bob_kem_sk.as_bytes(),
bob_dsa.secret_key_bytes(),
bob_dsa.public_key_bytes(),
)?;
// Alice finalizes by installing Bob's verification key
alice_pair.finalize(&response);At this point, both alice_pair and bob_pair hold mirrored directional keys. Alice's send key equals Bob's receive key, and vice versa.
Step 4: Seal a Message
Sealing wraps the content in a three-layer vector envelope:
- Layer 1 (Routing): plaintext DIDs and sequence number
- Layer 2 (Payload): ChaCha20-Poly1305 encrypted content
- Layer 3 (Signed): ML-DSA-65 signature + terms hash
use spt_smartpoint::VectorEnvelope;
let content = b"Hello from Alice!";
let sealed = VectorEnvelope::seal(&mut alice_pair, content, vec![])?;
// `sealed` is a Vec<u8> containing the serialized envelopeStep 5: Transport Over QUIC
Use spt-wire to send the sealed envelope over the network.
use spt_wire::{TransportConfig, TransportEndpoint};
// Bob starts a server
let server = TransportEndpoint::bind(TransportConfig::default()).await?;
let server_addr = server.local_addr()?;
// Alice connects and sends
let client = TransportEndpoint::bind(TransportConfig::default()).await?;
let channel = client.connect(server_addr).await?;
channel.send_vector(&sealed).await?;On the server side, Bob receives the vector:
let incoming = server.accept().await?;
let data = incoming.recv_vector().await?;Step 6: Open the Envelope
Bob opens the sealed envelope. This verifies the signature, checks the sequence number for replay protection, validates the terms hash, and decrypts the payload.
let content = VectorEnvelope::open(&mut bob_pair, &data)?;
assert_eq!(b"Hello from Alice!", &content[..]);If any verification fails (tampered payload, wrong terms, replayed sequence), open returns an error.
Using the Wire Bridge
For production use, the spt-wire-bridge crate combines the SmartPoint envelope and QUIC transport into a single interface:
use spt_wire_bridge::WireBridge;
// Alice's side
let alice_bridge = WireBridge::new(channel, alice_pair);
alice_bridge.send_message(b"Hello from Alice!").await?;
// Bob's side
let bob_bridge = WireBridge::new(incoming, bob_pair);
let message = bob_bridge.recv_message().await?;
assert_eq!(b"Hello from Alice!", &message[..]);The bridge automatically seals outbound messages and opens inbound messages.
Complete Example
Here is the full flow in one listing:
use spt_pq_crypto::{MlKem768, MlDsaKeyPair};
use spt_aid::Did;
use spt_smartpoint::{SmartPointPair, SmartPointTerms, CryptoSuite, VectorEnvelope};
use spt_wire::{TransportConfig, TransportEndpoint};
use std::collections::BTreeMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = rand::thread_rng();
// 1. Generate identities
let (alice_kem_pk, alice_kem_sk) = MlKem768::generate_keypair(&mut rng)?;
let alice_dsa = MlDsaKeyPair::generate(&mut rng)?;
let alice_did = Did::from_public_key(&alice_kem_pk.as_bytes(), "sw")?;
let (bob_kem_pk, bob_kem_sk) = MlKem768::generate_keypair(&mut rng)?;
let bob_dsa = MlDsaKeyPair::generate(&mut rng)?;
let bob_did = Did::from_public_key(&bob_kem_pk.as_bytes(), "sw")?;
// 2. Define terms
let mut caps = BTreeMap::new();
caps.insert("messages".into(), vec!["read".into(), "write".into()]);
let terms = SmartPointTerms::with_capabilities(caps).with_ttl(3600);
// 3. Form pair
let (mut alice_pair, offer) = SmartPointPair::create(
alice_did.to_string(), bob_did.to_string(),
&bob_kem_pk.as_bytes(), terms, CryptoSuite::PqV1,
alice_dsa.secret_key_bytes(), alice_dsa.public_key_bytes(),
)?;
let (mut bob_pair, response) = SmartPointPair::accept(
&offer, &bob_kem_sk.as_bytes(),
bob_dsa.secret_key_bytes(), bob_dsa.public_key_bytes(),
)?;
alice_pair.finalize(&response);
// 4. Start transport
let server = TransportEndpoint::bind(TransportConfig::default()).await?;
let server_addr = server.local_addr()?;
let client = TransportEndpoint::bind(TransportConfig::default()).await?;
// 5. Seal and send
let sealed = VectorEnvelope::seal(&mut alice_pair, b"Hello Bob!", vec![])?;
let channel = client.connect(server_addr).await?;
channel.send_vector(&sealed).await?;
// 6. Receive and open
let incoming = server.accept().await?;
let data = incoming.recv_vector().await?;
let content = VectorEnvelope::open(&mut bob_pair, &data)?;
println!("Bob received: {}", String::from_utf8_lossy(&content));
Ok(())
}Next Steps
- Architecture Overview -- understand the full protocol stack
- SmartPoint Pairs -- deep dive into crypto suites and terms
- SPT Wire Protocol -- routing, store-and-forward, wire bridge
- Deploy the Appliance -- run the full network stack