beaconcrypt

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: 0BSD Imports: 5 Imported by: 0

README

!! I am not a cryptographer and this has received no review. Assume this is irreparably broken !!

Overview

Generic C2 PQ-safe cryptographic transport protocol intended to protect against powerful wire attackers, with a rust reference implementation. This repo contains two things:

  • a protocol specification, with an associated threat model
  • a reference implementation

See the doc folder for the specification, threat model and rationale.

What this is not

A C2 transport protocol. This protocol is only concerned with cryptographically protecting the data transmitted between a beacon and its server. It does not know anything about how data should be transported or where. Therefore, the intent is for this protocol to be used in a way that ryhmes with this:

class transport {
    // ...
    std::vector<uint8_t> network_send(const uint8_t* ptr, size_t len);
    // ...
};

bool transport::send(const std::span<const uint8_t> data) {
    uint8_t* encrypted_ptr = nullptr;
    size_t encrypted_len = 0;
    size_t encrypted_capa = 0;
    if (encrypt_to_server(data.data(), data.size(), encrypted_ptr, encrypted_len, encrypted_capa) == 0) {
        auto response = this->network_send(encrypted_ptr, encrypted_len);
        free_vec(encrypted_ptr, encrypted_len, encrypted_capa);
        // ...
    }
    return false;
}

In essence, this only handles crypto, you still get to do whatever you want on the transport side.

Limitations

In short: PQ algorithms take a lot more space than classical ones. This is unfortunately unavoidable. Therfore, the initial registration handshake will be somewhat large (~2.2kb for ML-KEM). However, this does not impact any follow on messages, for which the only overhead is the captn' proto framing.

The reference implementation is large, ~6.5MB for the static lib. It goes down to ~3.5MB if building the stdlib ourselves with a nightly toolchain. This is largely due to the fact that we need to bring a bunch of rust stuff with us. Unfortunately, most crypto libraries aren't really meant to run in 40KB images, so there's always going to be some floor there. It should however be easy to cut the rust-related stuff by implementing this protocol in C or C++, though you'll still have to pay for the libsodium + captn proto libraries.

The C interfaces are probably not thread safe.

TODOs

Test the C interface

Reference implementation

I don't use rust a lot, so the code is probably fairly naive. It provides both a beacon and server implementation with C bindings through cbindgen. Ideally more bindings would be built on top of that so it can be used in the mythic server-side.

The reference implementation expects that all beacons are compiled with the server's public key, and that beaconcrypt is initialized with it.

The server is currently not very usable as it doesn't support saving the state of any individual beacon. This means that if your server goes down, you will not be able to communicate with any previously-registered beacons anymore. The server does support being initialized with an Ed25519 seed (32 random bytes). Users wishing to use the server in practical cases should use this interface to ensure their server keeps its identity across reboots.

Building

You will need Capn'Proto (just the binaries) and a recent version of rust for every build.

For windows, I prefer building with stable-gnu for normal usage, and nightly-gnu for release builds. You can find the exact arguments I use to the the static library as small as possible in release.yml. The MSVC toolchain is expected to work just as well, I just like mingw.

Build and run all tests:

cargo test
cargo build --features gobinds --release --target x86_64-pc-windows-gnu
go test -a -count=1 .
uv run maturin develop --uv
uv run pytest tests

The -a flag is required after rebuilding the Rust static library because Go's build cache does not detect changes to libraries linked through cgo. -count=1 also prevents reuse of a cached successful test result.

Reproducing known-answer test vectors

The fixed cryptographic values used by the Rust known-answer tests can be reproduced independently with Python and Go. Run both generators from the repository root:

uv run python scripts/generate_kat_vectors.py
go run scripts/generate_kat_vectors.go

The Python generator uses PyCryptodome. The Go generator uses the Go team's crypto packages from golang.org/x/crypto. Their output should be identical; any difference indicates that the vectors or one of the generators has diverged.

Wycheproof

The test suite includes pinned C2SP Wycheproof vectors for X25519, Ed25519, ChaCha20-Poly1305-IETF, HKDF-SHA-512 and ML-KEM-768. After cloning, initialize the vector submodule before running the normal test suite:

git submodule update --init --recursive
cargo test

See the Wycheproof test documentation for the coverage matrix, result semantics and update procedure.

Usage

The reference implementation is a library that can currently be used either from rust, through C FFI, go and python bindings. The C interface is currently only tested through the go bindings. Note that 0-length messages are explicitly disallowed by the reference implementation, as my feeling is that such messages have no purpose except testing parser edge cases. The library also doesn't handle chnking of any kind and will try to process entire messages at once in memory. It is expected that the caller should handle chunking itself if that is required.

From Rust, usage is mostly just instantiating CryptoProvider objects. See the example for usage.

From python, you can just use the wheels published to pypi, see the example for usage.

The C interface emulates the class interface, the caller is responsible for providing a valid state object to every function. See the example for usage.

Go is unfortunately the worst off as the bindings use cgo and therefore building your binary requires being able to link to a version of the library built with the gobinds feature. See the example for usage.

This work is dedicated to the public domain.

The C and go bindings, as well as most tests and the GH action code were generated by an LLM. This includes the KAT generation scripts, the ChaCha20-Poly1305 invisible salamanders implementation and its associated documentation.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrClosed    = errors.New("beaconcrypt: handle is closed")
	ErrCrypto    = errors.New("beaconcrypt: cryptographic operation failed")
	ErrSeedSize  = errors.New("beaconcrypt: server seed must be 32 bytes")
	ErrEmptyData = errors.New("beaconcrypt: input must not be empty")
)

Functions

This section is empty.

Types

type Beacon

type Beacon struct {
	// contains filtered or unexported fields
}

Beacon is safe for concurrent use. Calls on one Beacon are serialized because they mutate shared native ratchet state.

func NewBeacon

func NewBeacon(serverKID uint64, serverPK []byte) (*Beacon, error)

func (*Beacon) Close

func (b *Beacon) Close()

func (*Beacon) DecryptServerMessage

func (b *Beacon) DecryptServerMessage(ciphertext []byte) ([]byte, error)

func (*Beacon) EncryptToServer

func (b *Beacon) EncryptToServer(plaintext []byte) ([]byte, error)

func (*Beacon) GenerateRegistration

func (b *Beacon) GenerateRegistration() ([]byte, error)

func (*Beacon) ProcessInitialMessage

func (b *Beacon) ProcessInitialMessage(data []byte) ([]byte, error)

type EncryptState added in v0.4.0

type EncryptState struct {
	Data []byte
	// Key is the current directional KDF state.
	Key []byte
	// State is the complete ratchet state serialized as JSON.
	State string
	KeyID uint64
	Seq   uint64
}

type RegistrationResponse

type RegistrationResponse struct {
	Serialized []byte
	BeaconPK   []byte
	KeyID      uint64
}

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is safe for concurrent use. Calls on one Server are serialized because they mutate shared native ratchet state.

func NewServer

func NewServer(serverKID uint64) (*Server, error)

func NewServerFromSeed

func NewServerFromSeed(serverKID uint64, seed []byte) (*Server, error)

func NewServerFromState added in v0.4.0

func NewServerFromState(serverKID uint64, seed []byte, state string) (*Server, error)

func (*Server) Close

func (s *Server) Close()

func (*Server) DecryptAndUpdate added in v0.4.0

func (s *Server) DecryptAndUpdate(ciphertext []byte) (*EncryptState, error)

func (*Server) DecryptAndUpdateJSON added in v0.4.0

func (s *Server) DecryptAndUpdateJSON(ciphertext []byte) (string, error)

func (*Server) DecryptBeaconMessage

func (s *Server) DecryptBeaconMessage(ciphertext []byte) ([]byte, error)

func (*Server) EncryptAndUpdate added in v0.4.0

func (s *Server) EncryptAndUpdate(keyID uint64, plaintext []byte) (*EncryptState, error)

func (*Server) EncryptAndUpdateJSON added in v0.4.0

func (s *Server) EncryptAndUpdateJSON(keyID uint64, plaintext []byte) (string, error)

func (*Server) EncryptToBeacon

func (s *Server) EncryptToBeacon(keyID uint64, plaintext []byte) ([]byte, error)

func (*Server) ExportState added in v0.4.0

func (s *Server) ExportState() (string, error)

func (*Server) IdentityPK

func (s *Server) IdentityPK() ([]byte, error)

func (*Server) RegisterBeacon

func (s *Server) RegisterBeacon(registration, initialMessage []byte) (*RegistrationResponse, error)

Directories

Path Synopsis
Command generate_kat_vectors reproduces the fixed cryptographic vectors used by the Rust tests independently from the Python generator.
Command generate_kat_vectors reproduces the fixed cryptographic vectors used by the Rust tests independently from the Python generator.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL