Wire native Dosh auth runtime
This commit is contained in:
+101
-1
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user