Expand native auth and benchmark gates
This commit is contained in:
+21
-18
@@ -10,9 +10,10 @@ use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_ser
|
||||
use dosh::crypto;
|
||||
use dosh::native::{
|
||||
EnvVar, ForwardingKind, ForwardingRequest, KnownHostStatus, TrustResult,
|
||||
derive_native_session_key, generate_native_ephemeral, host_fingerprint, load_ed25519_identity,
|
||||
load_ed25519_identity_with_passphrase, parse_host_public_key_line, remove_trusted_host,
|
||||
sign_user_auth, trust_host, verify_known_host, verify_server_hello,
|
||||
derive_native_session_key, generate_native_ephemeral, host_fingerprint, load_native_identity,
|
||||
load_native_identity_with_passphrase, parse_host_public_key_line, remove_trusted_host,
|
||||
sign_user_auth_with_private_key, supported_user_key_algorithms, trust_host, verify_known_host,
|
||||
verify_server_hello,
|
||||
};
|
||||
use dosh::protocol::{
|
||||
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
||||
@@ -1486,7 +1487,7 @@ async fn try_native_auth(
|
||||
requested_mode: mode.to_string(),
|
||||
terminal_size: (cols, rows),
|
||||
supported_aead: vec!["chacha20poly1305".to_string()],
|
||||
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||
supported_user_key_algorithms: supported_user_key_algorithms(),
|
||||
cached_host_key_fingerprint: None,
|
||||
attach_ticket_envelope: None,
|
||||
requested_env,
|
||||
@@ -1642,7 +1643,7 @@ async fn try_native_auth_check(
|
||||
requested_mode: "doctor".to_string(),
|
||||
terminal_size: (80, 24),
|
||||
supported_aead: vec!["chacha20poly1305".to_string()],
|
||||
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||
supported_user_key_algorithms: supported_user_key_algorithms(),
|
||||
cached_host_key_fingerprint: None,
|
||||
attach_ticket_envelope: None,
|
||||
requested_env: Vec::new(),
|
||||
@@ -1766,7 +1767,9 @@ fn sign_native_user_auth(
|
||||
}
|
||||
|
||||
match load_first_native_identity(config, cli_identity, ssh_config) {
|
||||
Ok(identity) => sign_user_auth(&identity, hello, server_hello, requested_forwardings),
|
||||
Ok(identity) => {
|
||||
sign_user_auth_with_private_key(&identity, hello, server_hello, requested_forwardings)
|
||||
}
|
||||
Err(err) => {
|
||||
errors.push(format!("identity files: {err:#}"));
|
||||
Err(anyhow!(
|
||||
@@ -1781,7 +1784,7 @@ fn load_first_native_identity(
|
||||
config: &dosh::config::ClientConfig,
|
||||
cli_identity: Option<&Path>,
|
||||
ssh_config: &SshConfig,
|
||||
) -> Result<ed25519_dalek::SigningKey> {
|
||||
) -> Result<ssh_key::PrivateKey> {
|
||||
load_first_native_identity_with_prompt(
|
||||
config,
|
||||
cli_identity,
|
||||
@@ -1795,7 +1798,7 @@ fn load_first_native_identity_with_prompt<F>(
|
||||
cli_identity: Option<&Path>,
|
||||
ssh_config: &SshConfig,
|
||||
mut prompt: F,
|
||||
) -> Result<ed25519_dalek::SigningKey>
|
||||
) -> Result<ssh_key::PrivateKey>
|
||||
where
|
||||
F: FnMut(&Path) -> Result<String>,
|
||||
{
|
||||
@@ -1817,11 +1820,11 @@ where
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
match load_ed25519_identity(&path) {
|
||||
match load_native_identity(&path) {
|
||||
Ok(identity) => return Ok(identity),
|
||||
Err(err) if err.to_string().contains(" is encrypted") => {
|
||||
match prompt(&path).and_then(|passphrase| {
|
||||
load_ed25519_identity_with_passphrase(&path, Some(&passphrase))
|
||||
load_native_identity_with_passphrase(&path, Some(&passphrase))
|
||||
}) {
|
||||
Ok(identity) => return Ok(identity),
|
||||
Err(err) => errors.push(format!("{}: {err:#}", path.display())),
|
||||
@@ -1831,10 +1834,10 @@ where
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Err(anyhow!("no usable ssh-ed25519 identity found"))
|
||||
Err(anyhow!("no usable native identity found"))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"no usable ssh-ed25519 identity found: {}",
|
||||
"no usable native identity found: {}",
|
||||
errors.join("; ")
|
||||
))
|
||||
}
|
||||
@@ -4222,8 +4225,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
VerifyingKey::from(&loaded).to_bytes(),
|
||||
VerifyingKey::from(&signing).to_bytes()
|
||||
loaded.public_key().key_data().ed25519().unwrap().as_ref(),
|
||||
VerifyingKey::from(&signing).as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4243,7 +4246,7 @@ mod tests {
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("no usable ssh-ed25519 identity"));
|
||||
assert!(err.to_string().contains("no usable native identity"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4263,7 +4266,7 @@ mod tests {
|
||||
})
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("no usable ssh-ed25519 identity"));
|
||||
assert!(err.to_string().contains("no usable native identity"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4285,8 +4288,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
VerifyingKey::from(&loaded).to_bytes(),
|
||||
VerifyingKey::from(&signing).to_bytes()
|
||||
loaded.public_key().key_data().ed25519().unwrap().as_ref(),
|
||||
VerifyingKey::from(&signing).as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+94
-2
@@ -878,9 +878,11 @@ async fn handle_native_client_hello(
|
||||
.hello
|
||||
.supported_user_key_algorithms
|
||||
.iter()
|
||||
.any(|algorithm| algorithm == "ssh-ed25519")
|
||||
.any(|algorithm| dosh::native::is_supported_user_signature_algorithm(algorithm))
|
||||
{
|
||||
Err(anyhow!("native auth requires ssh-ed25519"))
|
||||
Err(anyhow!(
|
||||
"native auth requires a supported user key algorithm"
|
||||
))
|
||||
} else {
|
||||
let current_user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
|
||||
if req.hello.requested_user != current_user {
|
||||
@@ -3266,6 +3268,96 @@ mod tests {
|
||||
assert_eq!(data.bytes, b"hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blocked_stream_data_does_not_block_terminal_frames() {
|
||||
let (pty_tx, _pty_rx) = mpsc::unbounded_channel();
|
||||
let mut state = ServerState::new(ServerConfig::default(), [0u8; 32], pty_tx);
|
||||
let client_id = [8u8; 16];
|
||||
let session_key = [10u8; 32];
|
||||
let receiver = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let sender = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
|
||||
let endpoint = receiver.local_addr().unwrap();
|
||||
state.sessions.insert(
|
||||
"test".to_string(),
|
||||
Session {
|
||||
pty: None,
|
||||
parser: vt100::Parser::new(24, 80, 100),
|
||||
clients: HashMap::from([(
|
||||
client_id,
|
||||
ClientState {
|
||||
endpoint,
|
||||
mode: "read-write".to_string(),
|
||||
session_key,
|
||||
last_acked: 0,
|
||||
replay: ReplayWindow::default(),
|
||||
send_seq: 1,
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
last_seen: Instant::now(),
|
||||
pending: VecDeque::new(),
|
||||
allowed_forwardings: Vec::new(),
|
||||
stream_writers: HashMap::new(),
|
||||
opened_streams: HashSet::from([42]),
|
||||
stream_send_credit: HashMap::from([(42, 0)]),
|
||||
stream_pending_data: HashMap::new(),
|
||||
epoch: 0,
|
||||
epoch_started: Instant::now(),
|
||||
epoch_packets: 0,
|
||||
previous_session_key: None,
|
||||
},
|
||||
)]),
|
||||
output_seq: 0,
|
||||
recent: VecDeque::new(),
|
||||
empty_since: None,
|
||||
holder_control: None,
|
||||
persistent: false,
|
||||
bytes_since_persist: 0,
|
||||
last_persisted_seq: 0,
|
||||
},
|
||||
);
|
||||
state.client_index.insert(client_id, "test".to_string());
|
||||
let state = Arc::new(Mutex::new(state));
|
||||
|
||||
queue_or_send_stream_data_to_client(
|
||||
&state,
|
||||
&sender,
|
||||
client_id,
|
||||
42,
|
||||
b"blocked-forward-bytes".to_vec(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let locked = state.lock().unwrap();
|
||||
let client = locked.sessions["test"].clients.get(&client_id).unwrap();
|
||||
assert_eq!(client.stream_pending_data[&42].len(), 1);
|
||||
assert_eq!(client.stream_send_credit[&42], 0);
|
||||
}
|
||||
|
||||
broadcast_output(
|
||||
&state,
|
||||
&sender,
|
||||
PtyOutput {
|
||||
session: "test".to_string(),
|
||||
bytes: b"terminal-priority".to_vec(),
|
||||
exited: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut buf = [0u8; 2048];
|
||||
let (n, _) = tokio::time::timeout(Duration::from_secs(1), receiver.recv_from(&mut buf))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let packet = protocol::decode(&buf[..n]).unwrap();
|
||||
assert_eq!(packet.header.kind, PacketKind::Frame);
|
||||
let plain = protocol::decrypt_body(&packet, &session_key, SERVER_TO_CLIENT).unwrap();
|
||||
let frame: Frame = protocol::from_body(&plain).unwrap();
|
||||
assert_eq!(frame.bytes, b"terminal-priority");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_non_loopback_bind_requires_explicit_config() {
|
||||
let config = ServerConfig::default();
|
||||
|
||||
+340
-26
@@ -5,7 +5,9 @@ use anyhow::{Context, Result, bail};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use rsa::traits::PublicKeyParts;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use signature::{SignatureEncoding, Signer as SignatureSigner, Verifier as SignatureVerifier};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::net::IpAddr;
|
||||
@@ -339,6 +341,92 @@ pub fn sign_user_auth(
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
pub fn sign_user_auth_with_private_key(
|
||||
private_key: &ssh_key::PrivateKey,
|
||||
client: &NativeClientHello,
|
||||
server: &NativeServerHello,
|
||||
requested_forwardings: Vec<ForwardingRequest>,
|
||||
) -> Result<NativeUserAuth> {
|
||||
anyhow::ensure!(!private_key.is_encrypted(), "OpenSSH identity is encrypted");
|
||||
let public_key = private_key.public_key();
|
||||
let key_algorithm = public_key.algorithm().as_str().to_string();
|
||||
anyhow::ensure!(
|
||||
is_supported_user_key_algorithm(&key_algorithm),
|
||||
"unsupported native identity key algorithm {key_algorithm}"
|
||||
);
|
||||
let public_key_blob = ssh_public_blob_from_public_key(public_key)?;
|
||||
let public_key_for_auth = if key_algorithm == "ssh-ed25519" {
|
||||
parse_ssh_ed25519_public_blob(&public_key_blob)?.to_vec()
|
||||
} else {
|
||||
public_key_blob
|
||||
};
|
||||
let signature_algorithm = signature_algorithm_for_private_key(&key_algorithm)?;
|
||||
let mut auth = NativeUserAuth {
|
||||
public_key_algorithm: signature_algorithm.to_string(),
|
||||
public_key: public_key_for_auth,
|
||||
signature: Vec::new(),
|
||||
requested_forwardings,
|
||||
};
|
||||
let transcript = user_auth_transcript(client, server, &auth)?;
|
||||
auth.signature = if key_algorithm == "ssh-rsa" {
|
||||
sign_rsa_sha512_private_key(private_key, &transcript)?
|
||||
} else {
|
||||
let signature = SignatureSigner::try_sign(private_key, &transcript)
|
||||
.context("sign native user auth with OpenSSH private key")?;
|
||||
anyhow::ensure!(
|
||||
signature.algorithm().as_str() == auth.public_key_algorithm,
|
||||
"private key produced signature algorithm {}, expected {}",
|
||||
signature.algorithm().as_str(),
|
||||
auth.public_key_algorithm
|
||||
);
|
||||
signature.as_bytes().to_vec()
|
||||
};
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
fn sign_rsa_sha512_private_key(
|
||||
private_key: &ssh_key::PrivateKey,
|
||||
transcript: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
let rsa_keypair = private_key
|
||||
.key_data()
|
||||
.rsa()
|
||||
.ok_or_else(|| anyhow::anyhow!("OpenSSH identity is not ssh-rsa"))?;
|
||||
let rsa_private = rsa_private_key_from_ssh_keypair(rsa_keypair)?;
|
||||
let signing_key = rsa::pkcs1v15::SigningKey::<sha2::Sha512>::new(rsa_private);
|
||||
let signature: rsa::pkcs1v15::Signature = SignatureSigner::sign(&signing_key, transcript);
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
|
||||
fn rsa_private_key_from_ssh_keypair(
|
||||
keypair: &ssh_key::private::RsaKeypair,
|
||||
) -> Result<rsa::RsaPrivateKey> {
|
||||
let private = rsa::RsaPrivateKey::from_components(
|
||||
rsa::BigUint::try_from(&keypair.public.n).context("convert RSA modulus")?,
|
||||
rsa::BigUint::try_from(&keypair.public.e).context("convert RSA public exponent")?,
|
||||
rsa::BigUint::try_from(&keypair.private.d).context("convert RSA private exponent")?,
|
||||
vec![
|
||||
rsa::BigUint::try_from(&keypair.private.p).context("convert RSA p prime")?,
|
||||
rsa::BigUint::try_from(&keypair.private.q).context("convert RSA q prime")?,
|
||||
],
|
||||
)
|
||||
.context("convert OpenSSH RSA private key")?;
|
||||
anyhow::ensure!(
|
||||
private.size().saturating_mul(8) >= 2048,
|
||||
"OpenSSH RSA identity is smaller than 2048 bits"
|
||||
);
|
||||
Ok(private)
|
||||
}
|
||||
|
||||
fn signature_algorithm_for_private_key(key_algorithm: &str) -> Result<&'static str> {
|
||||
match key_algorithm {
|
||||
"ssh-ed25519" => Ok("ssh-ed25519"),
|
||||
"ecdsa-sha2-nistp256" => Ok("ecdsa-sha2-nistp256"),
|
||||
"ssh-rsa" => Ok("rsa-sha2-512"),
|
||||
_ => bail!("unsupported native identity key algorithm {key_algorithm}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_native_user_auth(
|
||||
client: &NativeClientHello,
|
||||
server: &NativeServerHello,
|
||||
@@ -347,37 +435,127 @@ pub fn verify_native_user_auth(
|
||||
source_ip: Option<IpAddr>,
|
||||
) -> Result<AuthorizedKey> {
|
||||
anyhow::ensure!(
|
||||
auth.public_key_algorithm == "ssh-ed25519",
|
||||
is_supported_user_signature_algorithm(&auth.public_key_algorithm),
|
||||
"unsupported native user key algorithm {}",
|
||||
auth.public_key_algorithm
|
||||
);
|
||||
anyhow::ensure!(
|
||||
auth.public_key.len() == 32,
|
||||
"ssh-ed25519 public key must be 32 bytes"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
auth.signature.len() == 64,
|
||||
"ssh-ed25519 signature must be 64 bytes"
|
||||
);
|
||||
let authorized_algorithm = authorized_key_algorithm_for_auth(&auth.public_key_algorithm)?;
|
||||
let authorized = authorized_keys
|
||||
.iter()
|
||||
.find(|key| key.algorithm == "ssh-ed25519" && key.key == auth.public_key)
|
||||
.find(|key| key.algorithm == authorized_algorithm && key.key == auth.public_key)
|
||||
.ok_or_else(|| anyhow::anyhow!("native user key is not authorized"))?;
|
||||
authorized.ensure_native_allowed(auth, source_ip)?;
|
||||
|
||||
let mut public_key = [0u8; 32];
|
||||
public_key.copy_from_slice(&auth.public_key);
|
||||
let verifying_key = VerifyingKey::from_bytes(&public_key).context("parse user public key")?;
|
||||
let mut signature = [0u8; 64];
|
||||
signature.copy_from_slice(&auth.signature);
|
||||
let signature = Signature::from_bytes(&signature);
|
||||
let transcript = user_auth_transcript(client, server, auth)?;
|
||||
verifying_key
|
||||
.verify(&transcript, &signature)
|
||||
.context("verify native user signature")?;
|
||||
verify_native_user_signature(auth, &transcript)?;
|
||||
Ok(authorized.clone())
|
||||
}
|
||||
|
||||
pub fn supported_user_key_algorithms() -> Vec<String> {
|
||||
vec![
|
||||
"ssh-ed25519".to_string(),
|
||||
"ecdsa-sha2-nistp256".to_string(),
|
||||
"rsa-sha2-512".to_string(),
|
||||
"rsa-sha2-256".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn is_supported_user_key_algorithm(algorithm: &str) -> bool {
|
||||
matches!(algorithm, "ssh-ed25519" | "ecdsa-sha2-nistp256" | "ssh-rsa")
|
||||
}
|
||||
|
||||
pub fn is_supported_user_signature_algorithm(algorithm: &str) -> bool {
|
||||
matches!(
|
||||
algorithm,
|
||||
"ssh-ed25519" | "ecdsa-sha2-nistp256" | "rsa-sha2-512" | "rsa-sha2-256"
|
||||
)
|
||||
}
|
||||
|
||||
fn authorized_key_algorithm_for_auth(signature_algorithm: &str) -> Result<&'static str> {
|
||||
match signature_algorithm {
|
||||
"ssh-ed25519" => Ok("ssh-ed25519"),
|
||||
"ecdsa-sha2-nistp256" => Ok("ecdsa-sha2-nistp256"),
|
||||
"rsa-sha2-512" | "rsa-sha2-256" => Ok("ssh-rsa"),
|
||||
_ => bail!("unsupported native user key algorithm {signature_algorithm}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_native_user_signature(auth: &NativeUserAuth, transcript: &[u8]) -> Result<()> {
|
||||
match auth.public_key_algorithm.as_str() {
|
||||
"ssh-ed25519" => {
|
||||
anyhow::ensure!(
|
||||
auth.public_key.len() == 32,
|
||||
"ssh-ed25519 public key must be 32 bytes"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
auth.signature.len() == 64,
|
||||
"ssh-ed25519 signature must be 64 bytes"
|
||||
);
|
||||
let mut public_key = [0u8; 32];
|
||||
public_key.copy_from_slice(&auth.public_key);
|
||||
let verifying_key =
|
||||
VerifyingKey::from_bytes(&public_key).context("parse user public key")?;
|
||||
let mut signature = [0u8; 64];
|
||||
signature.copy_from_slice(&auth.signature);
|
||||
let signature = Signature::from_bytes(&signature);
|
||||
verifying_key
|
||||
.verify(transcript, &signature)
|
||||
.context("verify native user signature")?;
|
||||
}
|
||||
"ecdsa-sha2-nistp256" => {
|
||||
let public_key = ssh_public_key_from_blob(&auth.public_key)
|
||||
.context("parse native user SSH public key")?;
|
||||
let algorithm =
|
||||
ssh_key::Algorithm::from_str(&auth.public_key_algorithm).with_context(|| {
|
||||
format!("parse signature algorithm {}", auth.public_key_algorithm)
|
||||
})?;
|
||||
let signature = ssh_key::Signature::new(algorithm, auth.signature.clone())
|
||||
.context("parse native user SSH signature")?;
|
||||
SignatureVerifier::verify(public_key.key_data(), transcript, &signature)
|
||||
.context("verify native user SSH signature")?;
|
||||
}
|
||||
"rsa-sha2-512" | "rsa-sha2-256" => {
|
||||
verify_rsa_sha2_signature(
|
||||
&auth.public_key,
|
||||
&auth.public_key_algorithm,
|
||||
transcript,
|
||||
&auth.signature,
|
||||
)?;
|
||||
}
|
||||
algorithm => bail!("unsupported native user key algorithm {algorithm}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_rsa_sha2_signature(
|
||||
public_key_blob: &[u8],
|
||||
signature_algorithm: &str,
|
||||
transcript: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Result<()> {
|
||||
let (e, n) = parse_ssh_rsa_public_blob(public_key_blob)?;
|
||||
let public = rsa::RsaPublicKey::new(
|
||||
rsa::BigUint::from_bytes_be(n),
|
||||
rsa::BigUint::from_bytes_be(e),
|
||||
)
|
||||
.context("parse RSA public key")?;
|
||||
let signature =
|
||||
rsa::pkcs1v15::Signature::try_from(signature).context("parse RSA PKCS#1v1.5 signature")?;
|
||||
match signature_algorithm {
|
||||
"rsa-sha2-256" => {
|
||||
let key = rsa::pkcs1v15::VerifyingKey::<sha2::Sha256>::new(public);
|
||||
SignatureVerifier::verify(&key, transcript, &signature)
|
||||
.context("verify RSA-SHA256 signature")
|
||||
}
|
||||
"rsa-sha2-512" => {
|
||||
let key = rsa::pkcs1v15::VerifyingKey::<sha2::Sha512>::new(public);
|
||||
SignatureVerifier::verify(&key, transcript, &signature)
|
||||
.context("verify RSA-SHA512 signature")
|
||||
}
|
||||
_ => bail!("legacy ssh-rsa/SHA-1 signatures are not accepted"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_native_user_auth_from_config(
|
||||
config: &ServerConfig,
|
||||
client: &NativeClientHello,
|
||||
@@ -470,6 +648,35 @@ pub fn load_ed25519_identity_with_passphrase(
|
||||
.with_context(|| format!("parse ssh-ed25519 identity {}", path.display()))
|
||||
}
|
||||
|
||||
pub fn load_native_identity(path: &Path) -> Result<ssh_key::PrivateKey> {
|
||||
load_native_identity_with_passphrase(path, None)
|
||||
}
|
||||
|
||||
pub fn load_native_identity_with_passphrase(
|
||||
path: &Path,
|
||||
passphrase: Option<&str>,
|
||||
) -> Result<ssh_key::PrivateKey> {
|
||||
let raw = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
|
||||
let private_key = ssh_key::PrivateKey::from_openssh(&raw)
|
||||
.with_context(|| format!("parse OpenSSH identity {}", path.display()))?;
|
||||
let private_key = if private_key.is_encrypted() {
|
||||
let passphrase = passphrase
|
||||
.ok_or_else(|| anyhow::anyhow!("OpenSSH identity {} is encrypted", path.display()))?;
|
||||
private_key
|
||||
.decrypt(passphrase)
|
||||
.with_context(|| format!("decrypt OpenSSH identity {}", path.display()))?
|
||||
} else {
|
||||
private_key
|
||||
};
|
||||
let algorithm = private_key.public_key().algorithm().as_str().to_string();
|
||||
anyhow::ensure!(
|
||||
is_supported_user_key_algorithm(&algorithm),
|
||||
"OpenSSH identity {} has unsupported native key algorithm {algorithm}",
|
||||
path.display()
|
||||
);
|
||||
Ok(private_key)
|
||||
}
|
||||
|
||||
impl AuthorizedKey {
|
||||
fn ensure_native_allowed(
|
||||
&self,
|
||||
@@ -565,7 +772,7 @@ fn parse_authorized_key_line(line_number: usize, line: &str) -> Result<Authorize
|
||||
fields.len() >= 2,
|
||||
"authorized_keys:{line_number}: expected key fields"
|
||||
);
|
||||
let (options, key_type_index) = if fields[0].starts_with("ssh-") {
|
||||
let (options, key_type_index) = if is_supported_user_key_algorithm(&fields[0]) {
|
||||
(AuthorizedKeyOptions::default(), 0)
|
||||
} else {
|
||||
(parse_authorized_key_options(&fields[0])?, 1)
|
||||
@@ -576,15 +783,20 @@ fn parse_authorized_key_line(line_number: usize, line: &str) -> Result<Authorize
|
||||
let key_blob = fields
|
||||
.get(key_type_index + 1)
|
||||
.with_context(|| format!("authorized_keys:{line_number}: missing key blob"))?;
|
||||
anyhow::ensure!(
|
||||
algorithm == "ssh-ed25519",
|
||||
"authorized_keys:{line_number}: unsupported key type {algorithm}"
|
||||
);
|
||||
let decoded = STANDARD
|
||||
.decode(key_blob)
|
||||
.with_context(|| format!("authorized_keys:{line_number}: decode key blob"))?;
|
||||
let key = parse_ssh_ed25519_public_blob(&decoded)
|
||||
.with_context(|| format!("authorized_keys:{line_number}: parse ssh-ed25519 key"))?;
|
||||
anyhow::ensure!(
|
||||
is_supported_user_key_algorithm(algorithm),
|
||||
"authorized_keys:{line_number}: unsupported key type {algorithm}"
|
||||
);
|
||||
validate_supported_public_key_blob(algorithm, &decoded)
|
||||
.with_context(|| format!("authorized_keys:{line_number}: parse {algorithm} key"))?;
|
||||
let key = if algorithm == "ssh-ed25519" {
|
||||
parse_ssh_ed25519_public_blob(&decoded)?.to_vec()
|
||||
} else {
|
||||
decoded
|
||||
};
|
||||
let comment = if fields.len() > key_type_index + 2 {
|
||||
Some(fields[key_type_index + 2..].join(" "))
|
||||
} else {
|
||||
@@ -598,6 +810,51 @@ fn parse_authorized_key_line(line_number: usize, line: &str) -> Result<Authorize
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_supported_public_key_blob(algorithm: &str, blob: &[u8]) -> Result<()> {
|
||||
let blob_algorithm = ssh_public_key_blob_algorithm(blob)?;
|
||||
anyhow::ensure!(
|
||||
blob_algorithm == algorithm,
|
||||
"key blob type {blob_algorithm} does not match authorized_keys type {algorithm}"
|
||||
);
|
||||
match algorithm {
|
||||
"ssh-ed25519" => {
|
||||
parse_ssh_ed25519_public_blob(blob)?;
|
||||
}
|
||||
"ecdsa-sha2-nistp256" | "ssh-rsa" => {
|
||||
let _ = ssh_public_key_from_blob(blob)?;
|
||||
}
|
||||
_ => bail!("unsupported key type {algorithm}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ssh_public_key_blob_algorithm(blob: &[u8]) -> Result<String> {
|
||||
let mut cursor = blob;
|
||||
let algorithm = read_ssh_string(&mut cursor)?;
|
||||
Ok(String::from_utf8_lossy(algorithm).to_string())
|
||||
}
|
||||
|
||||
fn ssh_public_key_from_blob(blob: &[u8]) -> Result<ssh_key::PublicKey> {
|
||||
let algorithm = ssh_public_key_blob_algorithm(blob)?;
|
||||
let encoded = STANDARD.encode(blob);
|
||||
ssh_key::PublicKey::from_openssh(&format!("{algorithm} {encoded}"))
|
||||
.with_context(|| format!("parse OpenSSH public key blob {algorithm}"))
|
||||
}
|
||||
|
||||
fn ssh_public_blob_from_public_key(public_key: &ssh_key::PublicKey) -> Result<Vec<u8>> {
|
||||
let line = public_key
|
||||
.to_openssh()
|
||||
.context("encode OpenSSH public key")?;
|
||||
let mut fields = line.split_whitespace();
|
||||
let _algorithm = fields
|
||||
.next()
|
||||
.context("encoded public key missing algorithm")?;
|
||||
let encoded = fields.next().context("encoded public key missing blob")?;
|
||||
STANDARD
|
||||
.decode(encoded)
|
||||
.context("decode encoded OpenSSH public key blob")
|
||||
}
|
||||
|
||||
fn split_authorized_key_fields(line: &str) -> Vec<String> {
|
||||
line.split_whitespace().map(ToString::to_string).collect()
|
||||
}
|
||||
@@ -765,6 +1022,18 @@ pub fn parse_ssh_ed25519_public_blob(blob: &[u8]) -> Result<[u8; 32]> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_ssh_rsa_public_blob(blob: &[u8]) -> Result<(&[u8], &[u8])> {
|
||||
let mut cursor = blob;
|
||||
let key_type = read_ssh_string(&mut cursor)?;
|
||||
anyhow::ensure!(key_type == b"ssh-rsa", "key blob type mismatch");
|
||||
let e = read_ssh_mpint(&mut cursor)?;
|
||||
let n = read_ssh_mpint(&mut cursor)?;
|
||||
anyhow::ensure!(cursor.is_empty(), "trailing data in ssh-rsa key blob");
|
||||
anyhow::ensure!(!e.is_empty(), "ssh-rsa exponent is empty");
|
||||
anyhow::ensure!(!n.is_empty(), "ssh-rsa modulus is empty");
|
||||
Ok((e, n))
|
||||
}
|
||||
|
||||
pub fn ssh_ed25519_public_blob(public_key: &[u8; 32]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
write_ssh_string(&mut out, b"ssh-ed25519");
|
||||
@@ -772,6 +1041,12 @@ pub fn ssh_ed25519_public_blob(public_key: &[u8; 32]) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
fn read_ssh_mpint<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> {
|
||||
let raw = read_ssh_string(cursor)?;
|
||||
let raw = raw.strip_prefix(&[0]).unwrap_or(raw);
|
||||
Ok(raw)
|
||||
}
|
||||
|
||||
fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> {
|
||||
anyhow::ensure!(cursor.len() >= 4, "truncated SSH string length");
|
||||
let len = u32::from_be_bytes(cursor[..4].try_into().unwrap()) as usize;
|
||||
@@ -1084,6 +1359,45 @@ mod tests {
|
||||
assert!(verify_native_user_auth(&client, &server, &auth, &removed, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_user_auth_accepts_ecdsa_p256_private_key() {
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
let private = ssh_key::PrivateKey::random(
|
||||
&mut rng,
|
||||
ssh_key::Algorithm::Ecdsa {
|
||||
curve: ssh_key::EcdsaCurve::NistP256,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let host_signing = SigningKey::from_bytes(&[10u8; 32]);
|
||||
let client = test_client_hello();
|
||||
let mut server = test_server_hello(&host_signing);
|
||||
sign_server_hello(&host_signing, &client, &mut server).unwrap();
|
||||
let auth = sign_user_auth_with_private_key(&private, &client, &server, Vec::new()).unwrap();
|
||||
let authorized =
|
||||
parse_authorized_keys(&private.public_key().to_openssh().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(auth.public_key_algorithm, "ecdsa-sha2-nistp256");
|
||||
verify_native_user_auth(&client, &server, &auth, &authorized, None).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_user_auth_accepts_rsa_sha2_private_key() {
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
let private =
|
||||
ssh_key::PrivateKey::random(&mut rng, ssh_key::Algorithm::Rsa { hash: None }).unwrap();
|
||||
let host_signing = SigningKey::from_bytes(&[11u8; 32]);
|
||||
let client = test_client_hello();
|
||||
let mut server = test_server_hello(&host_signing);
|
||||
sign_server_hello(&host_signing, &client, &mut server).unwrap();
|
||||
let auth = sign_user_auth_with_private_key(&private, &client, &server, Vec::new()).unwrap();
|
||||
let authorized =
|
||||
parse_authorized_keys(&private.public_key().to_openssh().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(auth.public_key_algorithm, "rsa-sha2-512");
|
||||
verify_native_user_auth(&client, &server, &auth, &authorized, None).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_user_auth_rejects_tampered_transcript() {
|
||||
let host_signing = SigningKey::from_bytes(&[3u8; 32]);
|
||||
|
||||
+59
-21
@@ -1,6 +1,6 @@
|
||||
use crate::native::{
|
||||
ForwardingRequest, NativeClientHello, NativeServerHello, NativeUserAuth,
|
||||
parse_ssh_ed25519_public_blob, user_auth_transcript,
|
||||
is_supported_user_key_algorithm, parse_ssh_ed25519_public_blob, user_auth_transcript,
|
||||
};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use std::io::{Read, Write};
|
||||
@@ -12,12 +12,16 @@ const SSH2_AGENTC_REQUEST_IDENTITIES: u8 = 11;
|
||||
const SSH2_AGENT_IDENTITIES_ANSWER: u8 = 12;
|
||||
const SSH2_AGENTC_SIGN_REQUEST: u8 = 13;
|
||||
const SSH2_AGENT_SIGN_RESPONSE: u8 = 14;
|
||||
const SSH_AGENT_RSA_SHA2_512: u32 = 4;
|
||||
const MAX_AGENT_PACKET: usize = 256 * 1024;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AgentIdentity {
|
||||
pub key_blob: Vec<u8>,
|
||||
pub public_key: [u8; 32],
|
||||
pub public_key_algorithm: String,
|
||||
pub public_key: Vec<u8>,
|
||||
pub sign_algorithm: String,
|
||||
pub sign_flags: u32,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
@@ -38,13 +42,13 @@ pub fn sign_user_auth_with_agent_at(
|
||||
) -> Result<NativeUserAuth> {
|
||||
let mut agent = UnixStream::connect(socket_path.as_ref())
|
||||
.with_context(|| format!("connect ssh-agent {}", socket_path.as_ref().display()))?;
|
||||
let identities = request_ed25519_identities(&mut agent)?;
|
||||
let identities = request_supported_identities(&mut agent)?;
|
||||
let identity = identities
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("ssh-agent has no ssh-ed25519 identities"))?;
|
||||
.ok_or_else(|| anyhow!("ssh-agent has no supported identities"))?;
|
||||
let mut auth = NativeUserAuth {
|
||||
public_key_algorithm: "ssh-ed25519".to_string(),
|
||||
public_key: identity.public_key.to_vec(),
|
||||
public_key_algorithm: identity.sign_algorithm.clone(),
|
||||
public_key: identity.public_key.clone(),
|
||||
signature: Vec::new(),
|
||||
requested_forwardings,
|
||||
};
|
||||
@@ -54,7 +58,7 @@ pub fn sign_user_auth_with_agent_at(
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
fn request_ed25519_identities(agent: &mut UnixStream) -> Result<Vec<AgentIdentity>> {
|
||||
fn request_supported_identities(agent: &mut UnixStream) -> Result<Vec<AgentIdentity>> {
|
||||
write_agent_packet(agent, &[SSH2_AGENTC_REQUEST_IDENTITIES])?;
|
||||
let payload = read_agent_packet(agent)?;
|
||||
let mut cursor = payload.as_slice();
|
||||
@@ -72,18 +76,49 @@ fn request_ed25519_identities(agent: &mut UnixStream) -> Result<Vec<AgentIdentit
|
||||
for _ in 0..count {
|
||||
let key_blob = read_ssh_string(&mut cursor)?.to_vec();
|
||||
let comment = String::from_utf8_lossy(read_ssh_string(&mut cursor)?).to_string();
|
||||
if let Ok(public_key) = parse_ssh_ed25519_public_blob(&key_blob) {
|
||||
identities.push(AgentIdentity {
|
||||
key_blob,
|
||||
public_key,
|
||||
comment,
|
||||
});
|
||||
if let Some(identity) = supported_identity(key_blob, comment)? {
|
||||
identities.push(identity);
|
||||
}
|
||||
}
|
||||
anyhow::ensure!(cursor.is_empty(), "trailing data in ssh-agent identities");
|
||||
Ok(identities)
|
||||
}
|
||||
|
||||
fn supported_identity(key_blob: Vec<u8>, comment: String) -> Result<Option<AgentIdentity>> {
|
||||
let algorithm = key_blob_algorithm(&key_blob)?;
|
||||
if !is_supported_user_key_algorithm(&algorithm) {
|
||||
return Ok(None);
|
||||
}
|
||||
let identity = match algorithm.as_str() {
|
||||
"ssh-ed25519" => AgentIdentity {
|
||||
public_key_algorithm: algorithm.clone(),
|
||||
public_key: parse_ssh_ed25519_public_blob(&key_blob)?.to_vec(),
|
||||
sign_algorithm: "ssh-ed25519".to_string(),
|
||||
sign_flags: 0,
|
||||
key_blob,
|
||||
comment,
|
||||
},
|
||||
"ecdsa-sha2-nistp256" => AgentIdentity {
|
||||
public_key_algorithm: algorithm.clone(),
|
||||
public_key: key_blob.clone(),
|
||||
sign_algorithm: algorithm,
|
||||
sign_flags: 0,
|
||||
key_blob,
|
||||
comment,
|
||||
},
|
||||
"ssh-rsa" => AgentIdentity {
|
||||
public_key_algorithm: algorithm,
|
||||
public_key: key_blob.clone(),
|
||||
sign_algorithm: "rsa-sha2-512".to_string(),
|
||||
sign_flags: SSH_AGENT_RSA_SHA2_512,
|
||||
key_blob,
|
||||
comment,
|
||||
},
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(identity))
|
||||
}
|
||||
|
||||
fn sign_with_agent(
|
||||
agent: &mut UnixStream,
|
||||
identity: &AgentIdentity,
|
||||
@@ -93,7 +128,7 @@ fn sign_with_agent(
|
||||
request.push(SSH2_AGENTC_SIGN_REQUEST);
|
||||
write_ssh_string(&mut request, &identity.key_blob);
|
||||
write_ssh_string(&mut request, transcript);
|
||||
request.extend_from_slice(&0u32.to_be_bytes());
|
||||
request.extend_from_slice(&identity.sign_flags.to_be_bytes());
|
||||
write_agent_packet(agent, &request)?;
|
||||
|
||||
let payload = read_agent_packet(agent)?;
|
||||
@@ -114,20 +149,17 @@ fn sign_with_agent(
|
||||
|
||||
let mut signature_cursor = signature_blob;
|
||||
let algorithm = read_ssh_string(&mut signature_cursor)?;
|
||||
let algorithm = String::from_utf8_lossy(algorithm);
|
||||
anyhow::ensure!(
|
||||
algorithm == b"ssh-ed25519",
|
||||
"ssh-agent returned unsupported signature algorithm {}",
|
||||
String::from_utf8_lossy(algorithm)
|
||||
algorithm == identity.sign_algorithm,
|
||||
"ssh-agent returned signature algorithm {algorithm}, expected {}",
|
||||
identity.sign_algorithm
|
||||
);
|
||||
let signature = read_ssh_string(&mut signature_cursor)?;
|
||||
anyhow::ensure!(
|
||||
signature_cursor.is_empty(),
|
||||
"trailing data in ssh-agent signature blob"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
signature.len() == 64,
|
||||
"ssh-agent Ed25519 signature was not 64 bytes"
|
||||
);
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
|
||||
@@ -182,6 +214,12 @@ fn write_ssh_string(out: &mut Vec<u8>, value: &[u8]) {
|
||||
out.extend_from_slice(value);
|
||||
}
|
||||
|
||||
fn key_blob_algorithm(blob: &[u8]) -> Result<String> {
|
||||
let mut cursor = blob;
|
||||
let algorithm = read_ssh_string(&mut cursor)?;
|
||||
Ok(String::from_utf8_lossy(algorithm).to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user