Wire native Dosh auth runtime
ci / test (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-06-13 11:54:24 -04:00
parent 80c7889b46
commit e7f3679fc1
8 changed files with 1086 additions and 11 deletions
+101 -1
View File
@@ -10,6 +10,7 @@ use std::fs;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
pub const HOST_KEY_ALGORITHM: &str = "dosh-ed25519";
pub const NATIVE_PROTOCOL_VERSION: u8 = 1;
@@ -87,9 +88,10 @@ pub struct NativeAuthOk {
pub client_id: [u8; 16],
pub session: String,
pub mode: String,
pub session_key: [u8; 32],
pub session_key_id: [u8; 16],
pub attach_ticket: Vec<u8>,
pub encrypted_attach_ticket_psk: Vec<u8>,
pub attach_ticket_psk: [u8; 32],
pub initial_seq: u64,
pub snapshot: Vec<u8>,
pub policy_flags: Vec<String>,
@@ -331,6 +333,46 @@ pub fn verify_native_user_auth_from_config(
verify_native_user_auth(client, server, auth, &authorized_keys)
}
pub fn generate_native_ephemeral() -> (StaticSecret, [u8; 32]) {
let secret = StaticSecret::random();
let public = X25519PublicKey::from(&secret).to_bytes();
(secret, public)
}
pub fn derive_native_session_key(
local_secret: &StaticSecret,
remote_public: [u8; 32],
client: &NativeClientHello,
server: &NativeServerHello,
) -> Result<[u8; 32]> {
let remote_public = X25519PublicKey::from(remote_public);
let shared = local_secret.diffie_hellman(&remote_public);
anyhow::ensure!(
shared.was_contributory(),
"native key exchange produced non-contributory shared secret"
);
let mut salt = b"dosh/native/session-key/v1".to_vec();
salt.extend(crypto::sha256(&bincode::serialize(client)?));
salt.extend(crypto::sha256(&bincode::serialize(server)?));
crypto::hkdf32(shared.as_bytes(), &salt, b"dosh/native/chacha20poly1305/v1")
}
pub fn load_ed25519_identity(path: &Path) -> Result<SigningKey> {
let private_key = ssh_key::PrivateKey::read_openssh_file(path)
.with_context(|| format!("read OpenSSH identity {}", path.display()))?;
anyhow::ensure!(
!private_key.is_encrypted(),
"OpenSSH identity {} is encrypted; encrypted-key prompts are not wired yet",
path.display()
);
let ed25519 = private_key
.key_data()
.ed25519()
.ok_or_else(|| anyhow::anyhow!("OpenSSH identity {} is not ssh-ed25519", path.display()))?;
ed25519_dalek::SigningKey::try_from(ed25519)
.with_context(|| format!("parse ssh-ed25519 identity {}", path.display()))
}
impl AuthorizedKey {
fn ensure_native_allowed(&self, auth: &NativeUserAuth) -> Result<()> {
if !self.options.unsupported.is_empty() {
@@ -864,6 +906,64 @@ mod tests {
assert!(verify_native_user_auth(&client, &server, &auth, &authorized).is_err());
}
#[test]
fn load_ed25519_identity_reads_openssh_private_key() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("id_ed25519");
let signing = SigningKey::from_bytes(&[11u8; 32]);
let keypair = ssh_key::private::Ed25519Keypair::from(&signing);
let private =
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::from(keypair), "").unwrap();
private
.write_openssh_file(&path, ssh_key::LineEnding::LF)
.unwrap();
let loaded = load_ed25519_identity(&path).unwrap();
assert_eq!(
VerifyingKey::from(&loaded).to_bytes(),
VerifyingKey::from(&signing).to_bytes()
);
}
#[test]
fn native_session_key_derivation_matches_and_binds_transcript() {
let host_signing = SigningKey::from_bytes(&[3u8; 32]);
let (client_secret, client_public) = generate_native_ephemeral();
let (server_secret, server_public) = generate_native_ephemeral();
let mut client = test_client_hello();
client.client_ephemeral_public = client_public;
let mut server = test_server_hello(&host_signing);
server.server_ephemeral_public = server_public;
sign_server_hello(&host_signing, &client, &mut server).unwrap();
let client_key = derive_native_session_key(
&client_secret,
server.server_ephemeral_public,
&client,
&server,
)
.unwrap();
let server_key = derive_native_session_key(
&server_secret,
client.client_ephemeral_public,
&client,
&server,
)
.unwrap();
assert_eq!(client_key, server_key);
let mut tampered = server.clone();
tampered.auth_challenge = [99u8; 32];
let tampered_key = derive_native_session_key(
&client_secret,
tampered.server_ephemeral_public,
&client,
&tampered,
)
.unwrap();
assert_ne!(client_key, tampered_key);
}
#[test]
fn native_user_auth_enforces_no_port_forwarding_and_permitopen() {
let host_signing = SigningKey::from_bytes(&[3u8; 32]);