This commit is contained in:
parent
44ff1d8781
commit
75f4139dea
17 changed files with 382 additions and 131 deletions
44
Cargo.lock
generated
44
Cargo.lock
generated
|
|
@ -95,9 +95,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.0"
|
||||
version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
|
||||
checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"untrusted 0.7.1",
|
||||
|
|
@ -106,14 +106,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.41.0"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
|
||||
checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -982,6 +983,7 @@ dependencies = [
|
|||
"mtp-codec",
|
||||
"mtp-common",
|
||||
"mtp-crypto",
|
||||
"mtp-files",
|
||||
"mtp-host",
|
||||
"mtp-transport",
|
||||
"mtp-type-map",
|
||||
|
|
@ -1043,6 +1045,14 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mtp-files"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mtp-crypto",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mtp-host"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1117,9 +1127,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
|
|
@ -1263,6 +1273,12 @@ dependencies = [
|
|||
"spki 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
|
|
@ -1501,9 +1517,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
|
|
@ -1553,9 +1569,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
|
|
@ -1896,9 +1912,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.51"
|
||||
version = "0.3.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
|
||||
checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
|
|
@ -1916,9 +1932,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
|||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.30"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
|
||||
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ members = [
|
|||
"host",
|
||||
"client",
|
||||
"wasm",
|
||||
"files",
|
||||
]
|
||||
# `wasm` is a wasm32-only crate: it relies on web-sys unstable APIs
|
||||
# (`--cfg=web_sys_unstable_apis`, set in wasm/.cargo/config.toml) and the
|
||||
|
|
@ -26,6 +27,7 @@ default-members = [
|
|||
"transport",
|
||||
"host",
|
||||
"client",
|
||||
"files",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
|
|
@ -58,6 +60,7 @@ mtp-crypto = { version = "0.1.0", path = "crypto", optional = true, features = [
|
|||
] }
|
||||
mtp-host = { version = "0.1.0", path = "host", optional = true }
|
||||
mtp-client = { version = "0.1.0", path = "client", optional = true }
|
||||
mtp-files = { version = "0.1.0", path = "files", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
@ -80,6 +83,10 @@ host = ["dep:mtp-host", "mtp-codec/registry", "mtp-transport/host"]
|
|||
# MTP client - outgoing QUIC connections to a host.
|
||||
client = ["dep:mtp-client"]
|
||||
|
||||
# On-disk storage for keyrings (`.mk`) and public key bundles (`.mpkb`).
|
||||
# Pulls in `crypto` so the `Keyring` / `PublicKeyBundle` types are in scope.
|
||||
files = ["dep:mtp-files", "crypto"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
rcgen = "0.14"
|
||||
|
|
|
|||
3
example/.gitignore
vendored
3
example/.gitignore
vendored
|
|
@ -10,3 +10,6 @@ web-client/node_modules
|
|||
web-client/public/host_public_key_bundle.hex
|
||||
web-client/public/mtp_dev_cert_hash.txt
|
||||
web-client/dist/
|
||||
|
||||
*.mk
|
||||
*.mpkb
|
||||
|
|
|
|||
11
example/Cargo.lock
generated
11
example/Cargo.lock
generated
|
|
@ -207,9 +207,7 @@ dependencies = [
|
|||
name = "client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"mtp",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
@ -902,6 +900,7 @@ dependencies = [
|
|||
"mtp-codec",
|
||||
"mtp-common",
|
||||
"mtp-crypto",
|
||||
"mtp-files",
|
||||
"mtp-host",
|
||||
"mtp-transport",
|
||||
"mtp-type-map",
|
||||
|
|
@ -959,6 +958,14 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mtp-files"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mtp-crypto",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mtp-host"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,5 @@ name = "client"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
mtp = { version = "0.1.0", path = "../../", features = ["client", "crypto"] }
|
||||
mtp = { version = "0.1.0", path = "../../", features = ["client", "crypto", "files"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_json = "1"
|
||||
hex = "0.4"
|
||||
|
|
|
|||
|
|
@ -1,52 +1,49 @@
|
|||
use std::fs;
|
||||
|
||||
use mtp::client::{ClientConfig, MTPClient, MTPConnection};
|
||||
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner, PublicKeyBundle};
|
||||
use mtp::crypto::{
|
||||
Ed25519Signer, KemPrivateKey, KemPublicKey, Keyring, MlDsaSigner, PublicKeyBundle,
|
||||
};
|
||||
use mtp::files::{load_keyring, save_keyring};
|
||||
|
||||
pub async fn connect_or_register(
|
||||
mut config: ClientConfig,
|
||||
host_public_key: PublicKeyBundle,
|
||||
client_key_path: &str,
|
||||
key_prefix: &str,
|
||||
) -> Result<(MTPConnection, Keyring), Box<dyn std::error::Error>> {
|
||||
if let Ok(data) = fs::read_to_string(client_key_path) {
|
||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||
let client_id = json["client_id"].as_u64().expect("Invalid client_id");
|
||||
let keyring = Keyring::from_bytes(&hex::decode(
|
||||
json["keyring"].as_str().expect("Missing keyring"),
|
||||
)?)?;
|
||||
let keyring_path = format!("{key_prefix}.mk");
|
||||
let id_path = format!("{key_prefix}.id");
|
||||
|
||||
println!("Loaded client keys (ID: {})", client_id);
|
||||
if let (Ok(keyring), Ok(id)) = (load_keyring(&keyring_path), fs::read_to_string(&id_path)) {
|
||||
let client_id: u64 = id.trim().parse()?;
|
||||
println!("Loaded client keys (ID: {client_id})");
|
||||
|
||||
config.client_id = client_id;
|
||||
let conn = MTPClient::auth_connect(config, &keyring, &host_public_key).await?;
|
||||
println!("Authenticated (version {})", conn.version);
|
||||
Ok((conn, keyring))
|
||||
} else {
|
||||
return Ok((conn, keyring));
|
||||
}
|
||||
|
||||
println!("No existing keys found: registering new client");
|
||||
|
||||
/* The client authenticates with signatures only, so the KEM slot is empty. */
|
||||
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||
let keyring = Keyring::new(
|
||||
mtp::crypto::KemPublicKey::new(vec![]),
|
||||
mtp::crypto::KemPrivateKey::new(vec![]),
|
||||
KemPublicKey::new(vec![]),
|
||||
KemPrivateKey::new(vec![]),
|
||||
sig_pq_pk,
|
||||
sig_pq_sk,
|
||||
sig_pk,
|
||||
sig_sk,
|
||||
);
|
||||
|
||||
let keyring_bytes = keyring.to_bytes();
|
||||
let conn = MTPClient::auth_register(config, &keyring, &host_public_key).await?;
|
||||
println!("Registered with ID: {}", conn.client_id);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"client_id": conn.client_id,
|
||||
"keyring": hex::encode(&keyring_bytes),
|
||||
});
|
||||
fs::write(client_key_path, serde_json::to_string_pretty(&json)?)?;
|
||||
println!("Saved client keys -> {client_key_path}");
|
||||
save_keyring(&keyring, &keyring_path)?;
|
||||
fs::write(&id_path, conn.client_id.to_string())?;
|
||||
println!("Saved client keys -> {keyring_path}");
|
||||
|
||||
let keyring = Keyring::from_bytes(&keyring_bytes)?;
|
||||
Ok((conn, keyring))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::fs;
|
|||
use std::path::Path;
|
||||
|
||||
use mtp::client::ClientConfig;
|
||||
use mtp::crypto::{KemPublicKey, PublicKeyBundle, SignaturePqPublicKey, SignaturePublicKey};
|
||||
use mtp::files::load_public_key_bundle;
|
||||
|
||||
fn dev_cert_path() -> String {
|
||||
std::env::var("MTP_DEV_CERT").unwrap_or_else(|_| {
|
||||
|
|
@ -25,19 +25,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
"Missing TLS certificate at {cert_path}: enter the Nix shell first or run the server to generate it: {e}"
|
||||
)
|
||||
});
|
||||
let host_public_key = PublicKeyBundle::new(
|
||||
KemPublicKey::new(
|
||||
fs::read("host_enc_kem_pk.bin")
|
||||
.expect("Missing host_enc_kem_pk.bin: run server first"),
|
||||
),
|
||||
SignaturePqPublicKey::new(
|
||||
fs::read("host_sig_pq_pk.bin")
|
||||
.expect("Missing host_sig_pq_pk.bin: run server first"),
|
||||
),
|
||||
SignaturePublicKey::new(
|
||||
fs::read("host_sig_pk.bin").expect("Missing host_sig_pk.bin: run server first"),
|
||||
),
|
||||
);
|
||||
let host_public_key = load_public_key_bundle("host.mpkb")
|
||||
.expect("Missing host.mpkb: run the server first to export it");
|
||||
|
||||
println!("Connecting to 127.0.0.1:8080 ...");
|
||||
|
||||
|
|
@ -46,8 +35,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.with_description("MTP example client");
|
||||
|
||||
let server_bundle = host_public_key.clone();
|
||||
let (conn, keyring) =
|
||||
auth::connect_or_register(config, host_public_key, "client_keys.json").await?;
|
||||
let (conn, keyring) = auth::connect_or_register(config, host_public_key, "client").await?;
|
||||
messages::send_and_receive(&conn, &keyring, &server_bundle).await?;
|
||||
|
||||
println!("\nDone");
|
||||
|
|
|
|||
|
|
@ -10,20 +10,26 @@ pub fn build_demo_message(
|
|||
) -> CommunicationValue {
|
||||
// Encrypt to the server's KEM public key; the server decrypts with its keyring.
|
||||
let enc_type = EncryptionType::MlKemChaCha20Poly1305;
|
||||
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.expect("Ed25519 signer from keyring");
|
||||
let signer =
|
||||
Ed25519Signer::new(&keyring.sig_cl_secret_key).expect("Ed25519 signer from keyring");
|
||||
|
||||
let tm = TypeMap::latest();
|
||||
|
||||
let inner_enc = DataValue::Container(vec![
|
||||
(DataType::Version.to_id(&tm), DataValue::Str("secret inner data".into())),
|
||||
(
|
||||
DataType::Version.to_id(&tm),
|
||||
DataValue::Str("secret inner data".into()),
|
||||
),
|
||||
(DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)),
|
||||
]);
|
||||
let mut dv_enc = inner_enc;
|
||||
dv_enc.encrypt_container(enc_type, server_bundle, b"demo-aad");
|
||||
|
||||
let inner_sig = DataValue::Container(vec![
|
||||
(DataType::Version.to_id(&tm), DataValue::Str("signed by client".into())),
|
||||
(
|
||||
DataType::Version.to_id(&tm),
|
||||
DataValue::Str("signed by client".into()),
|
||||
),
|
||||
(DataType::Id.to_id(&tm), DataValue::UnsignedNumber(99)),
|
||||
]);
|
||||
let mut dv_sig = inner_sig;
|
||||
|
|
@ -37,7 +43,13 @@ pub fn build_demo_message(
|
|||
(DataType::Id.to_id(&tm), DataValue::UnsignedNumber(7)),
|
||||
]);
|
||||
let mut dv_sec = inner_sec;
|
||||
dv_sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, enc_type, server_bundle, b"demo-aad");
|
||||
dv_sec.sign_and_encrypt_container(
|
||||
SigAlgorithm::ED25519,
|
||||
&signer,
|
||||
enc_type,
|
||||
server_bundle,
|
||||
b"demo-aad",
|
||||
);
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
mtp = { version = "0.1.0", path = "../../", features = ["crypto"] }
|
||||
mtp = { version = "0.1.0", path = "../../", features = ["files"] }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,35 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use mtp::crypto::Keyring;
|
||||
use mtp::files::{
|
||||
self, BUNDLE_EXTENSION, KEYRING_EXTENSION, load_keyring, load_public_key_bundle, save_keyring,
|
||||
save_public_key_bundle,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), files::FileError> {
|
||||
let prefix = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "keyring".to_string());
|
||||
let keyring_path = PathBuf::from(format!("{prefix}.{KEYRING_EXTENSION}"));
|
||||
let bundle_path = PathBuf::from(format!("{prefix}.{BUNDLE_EXTENSION}"));
|
||||
|
||||
fn main() {
|
||||
let keyring = Keyring::generate();
|
||||
let bundle = keyring.public_key_bundle();
|
||||
save_keyring(&keyring, &keyring_path)?;
|
||||
save_public_key_bundle(&keyring.public_key_bundle(), &bundle_path)?;
|
||||
|
||||
// The `Debug` impl redacts private keys by design, so use the encoding
|
||||
// methods to emit the full keyring (public + secret keys) instead.
|
||||
println!("Keyring (hex):\n{}\n", keyring.to_hex());
|
||||
println!("Keyring (base64):\n{}\n", keyring.to_base64());
|
||||
/* Read both back to confirm the files round-trip through the on-disk format. */
|
||||
let loaded_keyring = load_keyring(&keyring_path)?;
|
||||
let loaded_bundle = load_public_key_bundle(&bundle_path)?;
|
||||
assert_eq!(keyring.to_bytes(), loaded_keyring.to_bytes());
|
||||
assert_eq!(
|
||||
keyring.public_key_bundle().as_bytes(),
|
||||
loaded_bundle.as_bytes()
|
||||
);
|
||||
println!("\nPrivateKeyRing (base64):\n{}", keyring.to_base64());
|
||||
|
||||
println!("PublicKeyBundle (base64):\n{}", bundle.to_base64());
|
||||
println!("\nPublicKeyBundle (base64):\n{}", loaded_bundle.to_base64());
|
||||
|
||||
println!("Wrote keyring -> {}", keyring_path.display());
|
||||
println!("Wrote bundle -> {}", bundle_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ name = "server"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
mtp = { version = "0.1.0", path = "../../", features = ["crypto", "host"] }
|
||||
mtp = { version = "0.1.0", path = "../../", features = ["crypto", "host", "files"] }
|
||||
rcgen = "0.14"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_json = { version = "1" }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
||||
use mtp::crypto::{
|
||||
CryptoError, Keyring, SignaturePublicKey, SignatureScheme, verify_ed25519,
|
||||
};
|
||||
use mtp::crypto::{CryptoError, Keyring, SignaturePublicKey, SignatureScheme, verify_ed25519};
|
||||
|
||||
struct Ed25519Verifier(SignaturePublicKey);
|
||||
|
||||
|
|
@ -57,7 +55,10 @@ pub fn process_and_respond(
|
|||
let enc = msg.get_data(DataType::EncryptedPayload);
|
||||
if matches!(enc, DataValue::EncryptedContainer(_)) {
|
||||
let mut dv = enc.clone();
|
||||
if dv.decrypt_into_container(host_keyring, b"demo-aad").is_some() {
|
||||
if dv
|
||||
.decrypt_into_container(host_keyring, b"demo-aad")
|
||||
.is_some()
|
||||
{
|
||||
if let Some(entries) = dv.as_container() {
|
||||
println!(" Decrypted EncryptedPayload: {:?}", entries);
|
||||
enc_status = format!("EncryptedPayload decrypted OK ({} entries)", entries.len());
|
||||
|
|
@ -93,7 +94,9 @@ pub fn process_and_respond(
|
|||
if let Some(pk_bundle) = client_pk {
|
||||
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
|
||||
let mut dv = secure.clone();
|
||||
if dv.decrypt_signed_encrypted_container(host_keyring, b"demo-aad").is_some()
|
||||
if dv
|
||||
.decrypt_signed_encrypted_container(host_keyring, b"demo-aad")
|
||||
.is_some()
|
||||
&& dv.verify_into_container(&verifier).is_some()
|
||||
{
|
||||
if let Some(entries) = dv.as_container() {
|
||||
|
|
|
|||
|
|
@ -1,50 +1,33 @@
|
|||
use std::fs;
|
||||
|
||||
use mtp::crypto::kem::HybridKem;
|
||||
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner};
|
||||
use mtp::crypto::Keyring;
|
||||
use mtp::files::{load_keyring, save_keyring, save_public_key_bundle};
|
||||
|
||||
/* Host id is fixed for the example; only the keyring itself is persisted. */
|
||||
const HOST_ID: u64 = 1;
|
||||
|
||||
pub fn load_or_generate_host_keys(
|
||||
path: &str,
|
||||
keyring_path: &str,
|
||||
) -> Result<(u64, Keyring), Box<dyn std::error::Error>> {
|
||||
if let Ok(data) = fs::read_to_string(path) {
|
||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||
let hid = json["host_id"].as_u64().unwrap_or(1);
|
||||
let keyring = Keyring::from_bytes(&hex::decode(json["keyring"].as_str().unwrap())?)?;
|
||||
println!("Loaded host keys (ID: {})", hid);
|
||||
return Ok((hid, keyring));
|
||||
if let Ok(keyring) = load_keyring(keyring_path) {
|
||||
println!("Loaded host keyring from {keyring_path}");
|
||||
return Ok((HOST_ID, keyring));
|
||||
}
|
||||
|
||||
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||
let (kem_sk, kem_pk) = HybridKem::generate_keypair();
|
||||
let keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"host_id": 1,
|
||||
"keyring": hex::encode(keyring.to_bytes()),
|
||||
});
|
||||
fs::write(path, serde_json::to_string_pretty(&json)?)?;
|
||||
println!("Generated host keys -> {path}");
|
||||
Ok((1u64, keyring))
|
||||
let keyring = Keyring::generate();
|
||||
save_keyring(&keyring, keyring_path)?;
|
||||
println!("Generated host keyring -> {keyring_path}");
|
||||
Ok((HOST_ID, keyring))
|
||||
}
|
||||
|
||||
pub fn export_host_public_keys(host_keyring: &Keyring) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let public_key_bundle_hex = hex::encode(host_keyring.public_key_bundle().as_bytes());
|
||||
let bundle = host_keyring.public_key_bundle();
|
||||
save_public_key_bundle(&bundle, "host.mpkb")?;
|
||||
|
||||
fs::write("host_public_key_bundle.hex", &public_key_bundle_hex)?;
|
||||
/* The web client fetches the bundle as hex over HTTP. */
|
||||
let bundle_hex = hex::encode(bundle.as_bytes());
|
||||
fs::write("host_public_key_bundle.hex", &bundle_hex)?;
|
||||
fs::create_dir_all("web-client/public")?;
|
||||
fs::write(
|
||||
"web-client/public/host_public_key_bundle.hex",
|
||||
&public_key_bundle_hex,
|
||||
)?;
|
||||
fs::write(
|
||||
"host_enc_kem_pk.bin",
|
||||
host_keyring.kem_public_key.as_bytes(),
|
||||
)?;
|
||||
fs::write("host_sig_pk.bin", host_keyring.sig_cl_public_key.as_bytes())?;
|
||||
fs::write(
|
||||
"host_sig_pq_pk.bin",
|
||||
host_keyring.sig_pq_public_key.as_bytes(),
|
||||
)?;
|
||||
fs::write("web-client/public/host_public_key_bundle.hex", &bundle_hex)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
tls::export_webtransport_cert_hash(&cert_hash)?;
|
||||
println!("WebTransport certificate sha256: {cert_hash}");
|
||||
|
||||
let (_host_id, host_keyring) = keys::load_or_generate_host_keys("host_keys.json")?;
|
||||
let (_host_id, host_keyring) = keys::load_or_generate_host_keys("host.mk")?;
|
||||
keys::export_host_public_keys(&host_keyring)?;
|
||||
|
||||
// The keyring is moved into the host config; keep a copy for decrypting the
|
||||
|
|
@ -47,7 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let (clients, next_id) = clients::load_client_db("clients.json")?;
|
||||
|
||||
let clients_for_get = clients.clone();
|
||||
let get_existing_user = move |id: u64| {
|
||||
let get_existing_user = move |id: u64, _description: Option<String>| {
|
||||
let clients = clients_for_get.clone();
|
||||
Box::pin(async move {
|
||||
let result = clients.lock().unwrap().get(&id).cloned();
|
||||
|
|
@ -63,7 +63,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let clients_for_register = clients.clone();
|
||||
let next_id_for_register = next_id.clone();
|
||||
let clients_path = "clients.json".to_string();
|
||||
let complete_register = move |bundle: mtp::crypto::PublicKeyBundle| {
|
||||
let complete_register = move |bundle: mtp::crypto::PublicKeyBundle,
|
||||
_description: Option<String>| {
|
||||
let db_arc = clients_for_register.clone();
|
||||
let nid_arc = next_id_for_register.clone();
|
||||
let path = clients_path.clone();
|
||||
|
|
@ -95,7 +96,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
cert_pem,
|
||||
key_pem,
|
||||
)
|
||||
.with_authentication(host_keyring, get_existing_user, complete_register)
|
||||
.with_authentication(
|
||||
host_keyring,
|
||||
Box::new(get_existing_user),
|
||||
Box::new(complete_register),
|
||||
)
|
||||
.with_authentication_policy(AuthenticationPolicy::ForceAuthentication);
|
||||
|
||||
let mut host = MTPHost::new(config).await?;
|
||||
|
|
|
|||
10
files/Cargo.toml
Normal file
10
files/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "mtp-files"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
# Only the plain key types (`Keyring`, `PublicKeyBundle`, `CryptoError`) are
|
||||
# needed here; those are always compiled, so no crypto features are required.
|
||||
mtp-crypto = { version = "0.1.0", path = "../crypto", default-features = false }
|
||||
thiserror = "1"
|
||||
197
files/src/lib.rs
Normal file
197
files/src/lib.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* On-disk storage for methanium key material.
|
||||
*
|
||||
* `.mk` files hold a full Keyring (public and secret keys) and are written with
|
||||
* owner-only permissions (0600) on Unix. `.mpkb` files hold a PublicKeyBundle
|
||||
* (public keys only) and are safe to share. Each file opens with a 4-byte magic
|
||||
* that doubles as a type tag, so a bundle never loads as a keyring, followed by
|
||||
* a version byte for future format changes.
|
||||
*/
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use mtp_crypto::{CryptoError, Keyring, PublicKeyBundle};
|
||||
|
||||
/// File extension for a stored [`Keyring`].
|
||||
pub const KEYRING_EXTENSION: &str = "mk";
|
||||
/// File extension for a stored [`PublicKeyBundle`].
|
||||
pub const BUNDLE_EXTENSION: &str = "mpkb";
|
||||
|
||||
/* Container layout: magic (4 bytes) || version (1 byte) || payload. */
|
||||
const KEYRING_MAGIC: [u8; 4] = *b"MTMK"; /* Methanium Keyring */
|
||||
const BUNDLE_MAGIC: [u8; 4] = *b"MPKB"; /* Methanium Public Key Bundle */
|
||||
const FORMAT_VERSION: u8 = 1;
|
||||
const HEADER_LEN: usize = 4 + 1;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FileError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("crypto error: {0}")]
|
||||
Crypto(#[from] CryptoError),
|
||||
#[error("not a valid methanium {expected} file (bad magic)")]
|
||||
BadMagic { expected: &'static str },
|
||||
#[error("unsupported {kind} format version {found} (expected {FORMAT_VERSION})")]
|
||||
UnsupportedVersion { kind: &'static str, found: u8 },
|
||||
#[error("file is truncated: {0} bytes, need at least {HEADER_LEN}")]
|
||||
Truncated(usize),
|
||||
}
|
||||
|
||||
fn encode(magic: [u8; 4], payload: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(HEADER_LEN + payload.len());
|
||||
out.extend_from_slice(&magic);
|
||||
out.push(FORMAT_VERSION);
|
||||
out.extend_from_slice(payload);
|
||||
out
|
||||
}
|
||||
|
||||
fn decode<'a>(bytes: &'a [u8], magic: [u8; 4], kind: &'static str) -> Result<&'a [u8], FileError> {
|
||||
if bytes.len() < HEADER_LEN {
|
||||
return Err(FileError::Truncated(bytes.len()));
|
||||
}
|
||||
if bytes[..4] != magic {
|
||||
return Err(FileError::BadMagic { expected: kind });
|
||||
}
|
||||
let found = bytes[4];
|
||||
if found != FORMAT_VERSION {
|
||||
return Err(FileError::UnsupportedVersion { kind, found });
|
||||
}
|
||||
Ok(&bytes[HEADER_LEN..])
|
||||
}
|
||||
|
||||
/*
|
||||
* `OpenOptions::mode` only applies when the file is created, so the mode is
|
||||
* re-set afterwards to also tighten a pre-existing, more-permissive file.
|
||||
*/
|
||||
#[cfg(unix)]
|
||||
fn write_secret(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)?;
|
||||
file.set_permissions(fs::Permissions::from_mode(0o600))?;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn write_secret(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||
fs::write(path, bytes)
|
||||
}
|
||||
|
||||
// Writes secret keys, so the file is created owner-only (0600) on Unix.
|
||||
pub fn save_keyring(keyring: &Keyring, path: impl AsRef<Path>) -> Result<(), FileError> {
|
||||
let bytes = encode(KEYRING_MAGIC, &keyring.to_bytes());
|
||||
write_secret(path.as_ref(), &bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_keyring(path: impl AsRef<Path>) -> Result<Keyring, FileError> {
|
||||
let bytes = fs::read(path)?;
|
||||
let payload = decode(&bytes, KEYRING_MAGIC, "keyring")?;
|
||||
Ok(Keyring::from_bytes(payload)?)
|
||||
}
|
||||
|
||||
pub fn save_public_key_bundle(
|
||||
bundle: &PublicKeyBundle,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(), FileError> {
|
||||
let bytes = encode(BUNDLE_MAGIC, &bundle.as_bytes());
|
||||
fs::write(path, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_public_key_bundle(path: impl AsRef<Path>) -> Result<PublicKeyBundle, FileError> {
|
||||
let bytes = fs::read(path)?;
|
||||
let payload = decode(&bytes, BUNDLE_MAGIC, "public key bundle")?;
|
||||
Ok(PublicKeyBundle::from_bytes(payload)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mtp_crypto::keypair::{
|
||||
KemPrivateKey, KemPublicKey, SignaturePqPrivateKey, SignaturePqPublicKey,
|
||||
SignaturePrivateKey, SignaturePublicKey,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
fn temp_path(ext: &str) -> PathBuf {
|
||||
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("mtp-files-test-{}-{n}.{ext}", std::process::id()));
|
||||
path
|
||||
}
|
||||
|
||||
fn sample_keyring() -> Keyring {
|
||||
Keyring::new(
|
||||
KemPublicKey::new(vec![1u8; 32]),
|
||||
KemPrivateKey::new(vec![2u8; 32]),
|
||||
SignaturePqPublicKey::new(vec![3u8; 64]),
|
||||
SignaturePqPrivateKey::new(vec![4u8; 64]),
|
||||
SignaturePublicKey::new(vec![5u8; 32]),
|
||||
SignaturePrivateKey::new(vec![6u8; 32]),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyring_save_load_roundtrip() {
|
||||
let path = temp_path(KEYRING_EXTENSION);
|
||||
let keyring = sample_keyring();
|
||||
save_keyring(&keyring, &path).unwrap();
|
||||
let loaded = load_keyring(&path).unwrap();
|
||||
assert_eq!(keyring.to_bytes(), loaded.to_bytes());
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_save_load_roundtrip() {
|
||||
let path = temp_path(BUNDLE_EXTENSION);
|
||||
let bundle = sample_keyring().public_key_bundle();
|
||||
save_public_key_bundle(&bundle, &path).unwrap();
|
||||
let loaded = load_public_key_bundle(&path).unwrap();
|
||||
assert_eq!(bundle.as_bytes(), loaded.as_bytes());
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loading_bundle_as_keyring_fails_on_magic() {
|
||||
let path = temp_path(BUNDLE_EXTENSION);
|
||||
save_public_key_bundle(&sample_keyring().public_key_bundle(), &path).unwrap();
|
||||
assert!(matches!(
|
||||
load_keyring(&path),
|
||||
Err(FileError::BadMagic { .. })
|
||||
));
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_file_is_rejected() {
|
||||
let path = temp_path(KEYRING_EXTENSION);
|
||||
fs::write(&path, b"MT").unwrap();
|
||||
assert!(matches!(load_keyring(&path), Err(FileError::Truncated(2))));
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn keyring_file_is_owner_only() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let path = temp_path(KEYRING_EXTENSION);
|
||||
save_keyring(&sample_keyring(), &path).unwrap();
|
||||
let mode = fs::metadata(&path).unwrap().permissions().mode();
|
||||
assert_eq!(mode & 0o777, 0o600);
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,3 +10,6 @@ pub use mtp_host as host;
|
|||
|
||||
#[cfg(feature = "client")]
|
||||
pub use mtp_client as client;
|
||||
|
||||
#[cfg(feature = "files")]
|
||||
pub use mtp_files as files;
|
||||
|
|
|
|||
Loading…
Reference in a new issue