What is Sanctum?
Sanctum is a small, reviewable, capable, pq-secure and fully privilege separated VPN daemon for OpenBSD, Linux and MacOS.
Sanctum its unique privilege separation design in combination with strong modern sandboxing guarantee that all of its important assets are separated from processes that talk to the internet or handle non-cryptography related things.
Sanctum can establish traditional site-to-site L2 or L3 tunnels, one-way tunnels and even tunnels between peers when both peers are behind NAT using hole-punching and Sanctum's cathedrals for peer discovery.
See The Reliquary, a community driven Sanctum cathedral setup. Note that it is entirely possible to set up your own cathedrals.
Multi-process
Sanctum is built using a multi-process approach where each process is only doing one thing. This allows for more fine-grained sandboxing in relation to permissions or allowed system calls.
Packets flow between these processes in a well-defined manner making it impossible to move a packet straight from the red side to the black side without passing the encryption process and vice-versa.
Encryption
Sanctum is post-quantum secure due to its unique approach to deriving session keys based on a shared symmetrical secret in combination with a hybridized asymmetrical exchange. It combines both classic ECDH (x25519) and the PQ-secure NIST standardized ML-KEM-1024.
Traffic is by default encrypted under AES256-GCM with unique keys in both RX and TX directions using a 96-bit nonce consisting of a 32-bit salt and a 64-bit packet counter (see rfc4106).
For management traffic unique encryption keys are derived from the shared secret per message. In this case because the keys are freshly derived the nonces used in this construction are fixed as there is no risk for (key, nonce) pair re-use in this specific scenario.
Key exchange
Sanctum uses a strong shared symmetrical secret in combination with two asymmetrical secrets (ECDH and ML-KEM-1024) to derive session keys for both RX and TX directions.
The key exchange process looks as follows:
ss = shared symmetrical key, 256-bit, loaded from disk
traffic_base_rx = sanctum_base_key(ss, PURPOSE_RX)
traffic_base_tx = sanctum_base_key(ss, PURPOSE_TX)
sanctum_base_key(key, purpose):
cathedral_flock = flock tunnel belongs too, or 0 if no cathedral in use
cathedral_flock_dst = flock destination tunnels belongs too, or 0 when
talking to a cathedral or no cathedral is in use
if cathedral_flock <= cathedral_flock_dst:
flock_a = cathedral_flock
flock_b = cathedral_flock_dst
else:
flock_a = cathedral_flock_dst
flock_b = cathedral_flock
if purpose == PURPOSE_OFFER:
label = "SANCTUM.OFFER.KDF"
else if purpose == PURPOSE_RX_KEY:
label = "SANCTUM.KEY.TRAFFIC.RX.KDF"
else if purpose == PURPOSE_TX_KEY:
label = "SANCTUM.KEY.TRAFFIC.TX.KDF"
x = len(flock_a) || flock_a || len(flock_b) || flock_b
base_key = KMAC256(key, label, x), 256-bit
return base_key
derive_offer_encryption_key(seed):
x = len(seed) || seed
ss = shared symmetrical secret, 256-bit
key = sanctum_base_key(ss, PURPOSE_OFFER)
wk = KMAC256(key, "SANCTUM.SACRAMENT.KDF", x), 256-bit
return wk
offer_create():
offer.ecdh = X25519-KEYGEN()
offer.kem = ML-KEM-1024-KEYGEN()
offer.now = TIME(WALL_CLOCK), 64-bit
offer.id = PRNG(64-bit), unique sanctum id
offer.salt = PRNG(32-bit), salt for nonce construction
offer.spi = PRNG(32-bit), the spi for this association
offer.internal_seed = unused and set to random data
offer.internal_tag = unused and set to random data
return offer
offer_send_pk(offer):
seed = PRNG(512-bit)
dk = derive_offer_encryption_key(seed)
header = 0x53414352414D4E54 || flock_src || flock_dst || offer.spi || seed
data = SANCTUM_OFFER_TYPE_EXCHANGE || offer.now ||
offer.id || offer.spi || offer.salt ||
SANCTUM_OFFER_STATE_KEM_PK_FRAGMENT ||
offer.ecdh.pub || offer.kem.pk
encdata = AES256-GCM(dk, nonce=1, aad=header, data)
packet.header = header
packet.data = encdata
send(packet)
offer_recv_pk(offer):
packet = recv()
dk = derive_offer_encryption_key(packet.header.seed)
pt = AES256-GCM(dk, nonce=1, aad=packet.header, packet.data)
ecdh_ss = X25519-SCALAR-MULT(pt.ecdh.pub, offer.ecdh.private)
offer.kem.ct, kem_ss = ML-KEM-1024-ENCAP(pt.kem.pk)
if pt.instance < local_id
traffic_key = sanctum_base_key(ss, PURPOSE_RX_KEY)
else
traffic_key = sanctum_base_key(ss, PURPOSE_TX_KEY)
x = len(ecdh_ss) || ecdh_ss || len(kem_ss) || kem_ss ||
len(ecdh.pub) || ecdh.pub || len(pt.ecdh.pub) || pt.ecdh.pub
rx = KMAC256(traffic_key, "SANCTUM.TRAFFIC.KDF", x), 256-bit
return rx
offer_send_ct(offer):
seed = PRNG(512-bit)
dk = derive_offer_encryption_key(seed)
header = 0x53414352414D4E54 || flock_src || flock_dst || offer.spi || seed
data = SANCTUM_OFFER_TYPE_EXCHANGE || offer.now ||
offer.id || offer.spi || offer.salt ||
SANCTUM_OFFER_STATE_KEM_CT_FRAGMENT ||
offer.ecdh.pub || offer.kem.ct
encdata = AES256-GCM(dk, nonce=1, aad=header, pt)
packet.header = header
packet.data = encdata
send(packet)
offer_recv_ct(offer):
packet = recv()
dk = derive_offer_encryption_key(packet.header.seed)
pt = AES256-GCM(dk, nonce=1, aad=packet.header, packet.data)
ecdh_ss = X25519-SCALAR-MULT(pt.ecdh.pub, offer.ecdh.private)
kem_ss = ML-KEM-1024-DECAP(offer.kem, pt.kem.ct)
if pt.instance < local_id
traffic_key = sanctum_base_key(ss, PURPOSE_TX_KEY)
else
traffic_key = sanctum_base_key(ss, PURPOSE_RX_KEY)
x = len(ecdh_ss) || ecdh_ss || len(kem_ss) || kem_ss ||
len(ecdh.pub) || ecdh.pub || len(pt.ecdh.pub) || pt.ecdh.pub
tx = KMAC256(traffic_key, "SANCTUM.TRAFFIC.KDF", x), 256-bit
return tx
key exchange:
my_offer = offer_create()
peer_offer = offer_create()
offer_send_pk(my_offer)
tx = offer_recv_ct(my_offer)
rx = offer_recv_pk(peer_offer)
offer_send_ct(peer_offer)
Keys are expired automatically after a given number of packets have been submitted on them (1 << 34), or after 1-hour.
Why would I want to use this?
Well, you don't have to. I built Sanctum for me and my hacker friends with the many years of experience I have building this type of stuff at very high assurance levels. There are plenty of alternatives out there.
None of them have cool mythology nor provide you with the same type of post-quantum security or privilege separation as Sanctum does though.
Another benefit is the ability to setup your own entire cathedral network allowing you to build a distributed infrastructure so your devices can always discover and talk to each other no matter what.
Talks
Source?
Latest release: sanctum 0.9.34
A mirror of the repository is available on github.
A library that implements the Sanctum protocol can be found here.
How?
A small simple guide is available here.
I want to contribute!
mail diffs to joris snabel-a sanctorum punkt se
