Packages
macula
5.2.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
native/macula_quic/src/cert.rs
//! Self-signed Ed25519 cert generation + pubkey-pin TLS verifier.
//!
//! A station presents a self-signed cert that wraps its macula
//! identity Ed25519 pubkey; a client validates by comparing the
//! cert's SubjectPublicKeyInfo to a pinned pubkey it was given
//! out-of-band. No CA chain, no DNS-anchored trust.
//!
//! Used by station listeners that materialize a cert from their
//! existing identity keypair and hand the PEM bytes to the
//! transport, and by `macula-net` transport bring-up.
use std::sync::Arc;
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, SanType};
use rustls::pki_types::CertificateDer;
use rustler::{Binary, Encoder, Env, NifResult, Term};
use crate::atoms;
// ─────────────────────────────────────────────────────────────────
// Self-signed cert generation
// ─────────────────────────────────────────────────────────────────
/// Generate a self-signed X.509 certificate from an Ed25519 keypair.
///
/// Inputs are raw 32-byte values: `pubkey` is the Ed25519 public
/// key, `privkey` is the Ed25519 secret seed (RFC 8032). Both are
/// what `crypto:generate_key(eddsa, ed25519)` returns on the Erlang
/// side.
///
/// `sans` is the Subject Alternative Names list — typically the
/// identity's hostname (`relay-fi-helsinki.macula.io`) or an
/// IPAddress SAN. We autodetect by trying to parse as IP, falling
/// back to DNS.
///
/// Returns `(cert_pem, key_pem)` — both PEM-encoded text suitable
/// for handing to rustls / Quinn.
pub fn generate_self_signed(
pubkey: &[u8],
privkey: &[u8],
sans: &[String],
) -> Result<(String, String), String> {
if pubkey.len() != 32 {
return Err(format!(
"pubkey must be 32 bytes Ed25519, got {}",
pubkey.len()
));
}
if privkey.len() != 32 {
return Err(format!(
"privkey must be 32 bytes Ed25519 secret seed, got {}",
privkey.len()
));
}
let pkcs8 = wrap_ed25519_pkcs8_v1(privkey);
let key_pair = KeyPair::try_from(pkcs8.as_slice())
.map_err(|e| format!("rcgen KeyPair from PKCS8: {}", e))?;
let _ = pubkey; // pubkey is implicit in the seed; rcgen derives it
let mut params = CertificateParams::default();
params.distinguished_name = DistinguishedName::new();
params
.distinguished_name
.push(DnType::CommonName, "Macula Identity");
let mut san_vec = Vec::with_capacity(sans.len());
for s in sans {
let san = match s.parse::<std::net::IpAddr>() {
Ok(ip) => SanType::IpAddress(ip),
Err(_) => {
let ia5 = s
.clone()
.try_into()
.map_err(|e| format!("invalid DNS SAN {:?}: {:?}", s, e))?;
SanType::DnsName(ia5)
}
};
san_vec.push(san);
}
params.subject_alt_names = san_vec;
// 10-year validity. Identity is the keypair; the cert is just a
// TLS-format wrapper around it.
let not_before = time::OffsetDateTime::now_utc();
params.not_before = not_before;
params.not_after = not_before + time::Duration::days(3650);
let cert = params
.self_signed(&key_pair)
.map_err(|e| format!("rcgen self_signed: {}", e))?;
Ok((cert.pem(), key_pair.serialize_pem()))
}
/// Wrap a raw Ed25519 32-byte secret seed as a PKCS#8 v1 DER blob
/// (RFC 8410). Fixed 48-byte structure that rcgen's
/// `KeyPair::try_from` accepts.
///
/// ```text
/// 30 2e SEQUENCE (46 bytes)
/// 02 01 00 INTEGER 0 (v1)
/// 30 05 SEQUENCE (5 bytes)
/// 06 03 2b 65 70 OID 1.3.101.112 ; id-Ed25519
/// 04 22 OCTET STRING (34 bytes)
/// 04 20 <32-byte seed> inner OCTET STRING wrapping seed
/// ```
fn wrap_ed25519_pkcs8_v1(seed: &[u8]) -> [u8; 48] {
debug_assert_eq!(seed.len(), 32);
let mut out = [0u8; 48];
out[..16].copy_from_slice(&[
0x30, 0x2e, // SEQUENCE 46
0x02, 0x01, 0x00, // INTEGER 0 (v1)
0x30, 0x05, // SEQUENCE 5
0x06, 0x03, 0x2b, 0x65, 0x70, // OID id-Ed25519
0x04, 0x22, // OCTET STRING 34
0x04, 0x20, // inner OCTET STRING 32
]);
out[16..48].copy_from_slice(seed);
out
}
// ─────────────────────────────────────────────────────────────────
// Pubkey-pin TLS verifier
// ─────────────────────────────────────────────────────────────────
/// Custom rustls `ServerCertVerifier` that pins on the leaf cert's
/// SubjectPublicKeyInfo Ed25519 pubkey rather than walking a CA
/// chain.
///
/// Behaviour:
/// - Extract the pubkey from `end_entity` (must be Ed25519).
/// - Compare to the pinned 32-byte pubkey provided at construction.
/// - Accept iff equal; otherwise return a rustls error.
///
/// No expiry check, no SAN check, no CA chain. The pubkey IS the
/// identity. This is the pragmatic equivalent of TLS raw-public-key
/// (RFC 7250) without changing the wire protocol.
#[derive(Debug)]
pub struct PubkeyPinVerifier {
pinned: Vec<u8>,
crypto: Arc<rustls::crypto::CryptoProvider>,
}
impl PubkeyPinVerifier {
pub fn new(pinned_pubkey: Vec<u8>) -> Self {
Self {
pinned: pinned_pubkey,
crypto: Arc::new(rustls::crypto::ring::default_provider()),
}
}
}
impl rustls::client::danger::ServerCertVerifier for PubkeyPinVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
let presented = ed25519_pubkey_from_cert(end_entity.as_ref())
.map_err(|e| rustls::Error::General(format!("pubkey extract: {}", e)))?;
if presented == self.pinned {
Ok(rustls::client::danger::ServerCertVerified::assertion())
} else {
Err(rustls::Error::General(format!(
"pubkey mismatch: pinned={} presented={}",
hex(&self.pinned),
hex(&presented)
)))
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
// Ed25519 leaf is TLS 1.3 only; if a 1.2 path triggers
// signature verification, accept (the cert match is what
// anchors trust).
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
// Defer to the crypto provider — verifies the dss using the
// leaf cert's pubkey, which we already pinned.
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.crypto.signature_verification_algorithms)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.crypto.signature_verification_algorithms.supported_schemes()
}
}
/// Extract the 32-byte Ed25519 pubkey from a DER-encoded X.509 cert's
/// SubjectPublicKeyInfo.
pub fn ed25519_pubkey_from_cert(der: &[u8]) -> Result<Vec<u8>, String> {
let (_, cert) = x509_parser::parse_x509_certificate(der)
.map_err(|e| format!("parse cert: {}", e))?;
let spki = cert.public_key();
// Verify the algorithm is Ed25519 (OID 1.3.101.112).
let alg_oid = &spki.algorithm.algorithm;
if alg_oid.to_id_string() != "1.3.101.112" {
return Err(format!(
"expected Ed25519 SPKI, got OID {}",
alg_oid.to_id_string()
));
}
let pk = spki.subject_public_key.data.to_vec();
if pk.len() != 32 {
return Err(format!("Ed25519 pubkey must be 32 bytes, got {}", pk.len()));
}
Ok(pk)
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
// ─────────────────────────────────────────────────────────────────
// NIF: generate_self_signed_cert(Pubkey, Privkey, Sans) ->
// {ok, {CertPem, KeyPem}} | {error, Reason}
// ─────────────────────────────────────────────────────────────────
/// Sans is passed as a single comma-joined binary on the Erlang
/// side — list-of-binary auto-decode in rustler 0.34 is brittle, a
/// flat string sidesteps it. Pubkey/Privkey are passed as
/// `rustler::Binary` (zero-copy view into the Erlang term) rather
/// than `Vec<u8>` because the latter doesn't decode binaries with
/// arbitrary bytes in this rustler version.
#[rustler::nif(schedule = "DirtyCpu")]
pub fn nif_generate_self_signed_cert<'a>(
env: Env<'a>,
pubkey: Binary<'a>,
privkey: Binary<'a>,
sans_csv: Binary<'a>,
) -> NifResult<Term<'a>> {
let sans: Vec<String> = std::str::from_utf8(sans_csv.as_slice())
.unwrap_or_default()
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
match generate_self_signed(pubkey.as_slice(), privkey.as_slice(), &sans) {
Ok((cert_pem, key_pem)) => Ok((atoms::ok(), (cert_pem, key_pem)).encode(env)),
Err(e) => Ok((atoms::error(), e).encode(env)),
}
}