#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::sync::atomic::{AtomicBool, Ordering};
pub mod ecdsa;
pub mod ed25519;
pub mod rsa;
static ALLOW_RSA_SHA1: AtomicBool = AtomicBool::new(false);
pub fn set_allow_rsa_sha1(allow: bool) {
ALLOW_RSA_SHA1.store(allow, Ordering::Relaxed);
}
pub fn allow_rsa_sha1() -> bool {
ALLOW_RSA_SHA1.load(Ordering::Relaxed)
}
#[cfg(feature = "alloc")]
pub use ecdsa::{EcdsaP256HostKey, EcdsaP384HostKey, EcdsaP521HostKey};
#[cfg(feature = "alloc")]
pub use ed25519::Ed25519HostKey;
#[cfg(feature = "alloc")]
pub use rsa::{RsaSha1HostKey, RsaSha2_256HostKey, RsaSha2_512HostKey};
pub trait HostKeyAlgorithm {
const NAME: &'static str;
}
#[cfg(feature = "alloc")]
pub trait HostKey {
fn algorithm(&self) -> &'static str;
fn public_blob(&self) -> Vec<u8>;
fn sign(&self, msg: &[u8]) -> crate::Result<Vec<u8>>;
}
#[cfg(feature = "alloc")]
pub trait HostKeyVerify {
fn algorithm(&self) -> &'static str;
fn verify(&self, msg: &[u8], sig_blob: &[u8]) -> crate::Result<()>;
fn from_public_blob(blob: &[u8]) -> crate::Result<Self>
where
Self: Sized;
}
#[cfg(feature = "alloc")]
pub fn host_key_verify_by_name(name: &str, blob: &[u8]) -> crate::Result<Box<dyn HostKeyVerify>> {
match name {
"ssh-ed25519" => Ok(Box::new(Ed25519HostKey::from_public_blob(blob)?)),
"ecdsa-sha2-nistp256" => Ok(Box::new(EcdsaP256HostKey::from_public_blob(blob)?)),
"ecdsa-sha2-nistp384" => Ok(Box::new(EcdsaP384HostKey::from_public_blob(blob)?)),
"ecdsa-sha2-nistp521" => Ok(Box::new(EcdsaP521HostKey::from_public_blob(blob)?)),
"ssh-rsa" => {
if !allow_rsa_sha1() {
return Err(crate::Error::Unsupported(
"ssh-rsa (SHA-1) is disabled by default; call hostkey::set_allow_rsa_sha1(true) to opt in",
));
}
Ok(Box::new(RsaSha1HostKey::from_public_blob(blob)?))
}
"rsa-sha2-256" => Ok(Box::new(RsaSha2_256HostKey::from_public_blob(blob)?)),
"rsa-sha2-512" => Ok(Box::new(RsaSha2_512HostKey::from_public_blob(blob)?)),
_ => Err(crate::Error::Unsupported("host-key algorithm")),
}
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
static GATE_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn sample_rsa_public_blob() -> Vec<u8> {
use purecrypto::bignum::BoxedUint;
let mut n_bytes = alloc::vec![0u8; 256];
n_bytes[0] = 0xc0;
for (i, b) in n_bytes.iter_mut().enumerate().skip(1) {
*b = (i as u8).wrapping_mul(31).wrapping_add(7) | 0x01;
}
let n = BoxedUint::from_be_bytes(&n_bytes);
let e = BoxedUint::from_u64(65537);
RsaSha2_256HostKey::from_public_components(n, e)
.unwrap()
.public_blob()
}
#[test]
fn ssh_rsa_sha1_rejected_by_default() {
let _guard = GATE_MUTEX.lock().unwrap();
set_allow_rsa_sha1(false);
let blob = sample_rsa_public_blob();
let result = host_key_verify_by_name("ssh-rsa", &blob);
match result {
Err(crate::Error::Unsupported(_)) => {}
Err(other) => panic!("expected Unsupported, got {other:?}"),
Ok(_) => panic!("expected ssh-rsa to be rejected by default"),
}
}
#[test]
fn ssh_rsa_sha1_allowed_after_opt_in() {
let _guard = GATE_MUTEX.lock().unwrap();
let blob = sample_rsa_public_blob();
set_allow_rsa_sha1(true);
let result = host_key_verify_by_name("ssh-rsa", &blob);
set_allow_rsa_sha1(false);
result.expect("ssh-rsa should parse once explicitly opted in");
}
#[test]
fn rsa_sha2_names_unaffected_by_gate() {
let _guard = GATE_MUTEX.lock().unwrap();
set_allow_rsa_sha1(false);
let blob = sample_rsa_public_blob();
host_key_verify_by_name("rsa-sha2-256", &blob)
.expect("rsa-sha2-256 must remain enabled regardless of the SHA-1 gate");
host_key_verify_by_name("rsa-sha2-512", &blob)
.expect("rsa-sha2-512 must remain enabled regardless of the SHA-1 gate");
}
}