Skip to content

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:

  1. Generate two DID identities with post-quantum key material
  2. Form a SmartPoint pair between them
  3. Seal a message into a three-layer vector envelope
  4. Send the sealed envelope over QUIC using spt-wire
  5. 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).

rust,ignore
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:

text
did:said:1:sw:blake3_<fingerprint>

You can generate a DID from the key material using spt-aid:

rust,ignore
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.

rust,ignore
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.

rust,ignore
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
rust,ignore
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 envelope

Step 5: Transport Over QUIC

Use spt-wire to send the sealed envelope over the network.

rust,ignore
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:

rust,ignore
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.

rust,ignore
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:

rust,ignore
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:

rust,ignore
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

Released under the MIT License.