#![cfg(feature = "alloc")]
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use purecrypto::bignum::BoxedUint;
use purecrypto::cipher::{Aes128, Aes256, Ctr};
use purecrypto::ec::{BoxedEcdsaPrivateKey, BoxedEcdsaPublicKey, CurveId, Ed25519PrivateKey};
use purecrypto::kdf::bcrypt_pbkdf;
use purecrypto::zeroize::{Zeroize, Zeroizing};
use crate::error::{Error, Result};
use crate::format::{Reader, Writer, read_mpint, write_mpint};
pub(crate) mod base64;
mod encrypted;
mod export;
mod pkcs;
mod writer;
pub use writer::EcdsaCurve;
pub(crate) fn base64_decode(input: &[u8]) -> Result<Vec<u8>> {
base64::decode(input)
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AuthorizedKeyOptions {
pub cert_authority: bool,
pub principals: Vec<String>,
pub command: Option<String>,
pub from: Option<String>,
pub environment: Vec<String>,
pub permit_open: Vec<String>,
pub permit_listen: Vec<String>,
pub expiry_time: Option<String>,
pub restrict: bool,
pub pty: Option<bool>,
pub port_forwarding: Option<bool>,
pub agent_forwarding: Option<bool>,
pub x11_forwarding: Option<bool>,
pub user_rc: Option<bool>,
pub tunnel: Option<String>,
pub no_touch_required: bool,
pub verify_required: bool,
}
impl AuthorizedKeyOptions {
pub fn allows_pty(&self) -> bool {
self.pty.unwrap_or(!self.restrict)
}
pub fn allows_user_rc(&self) -> bool {
self.user_rc.unwrap_or(!self.restrict)
}
pub fn allows_port_forwarding(&self) -> bool {
self.port_forwarding.unwrap_or(!self.restrict)
}
pub fn allows_agent_forwarding(&self) -> bool {
self.agent_forwarding.unwrap_or(!self.restrict)
}
pub fn allows_x11_forwarding(&self) -> bool {
self.x11_forwarding.unwrap_or(!self.restrict)
}
pub fn is_unrestricted(&self) -> bool {
self.command.is_none()
&& self.permit_open.is_empty()
&& self.permit_listen.is_empty()
&& self.allows_pty()
&& self.allows_port_forwarding()
&& self.allows_agent_forwarding()
&& self.allows_x11_forwarding()
&& self.allows_user_rc()
&& self.environment.is_empty()
}
pub fn parse(list: &str) -> Result<Self> {
let mut out = AuthorizedKeyOptions::default();
for opt in split_options(list)? {
let opt = opt.trim();
if opt.is_empty() {
continue;
}
let (name, value) = match opt.split_once('=') {
Some((n, v)) => (n.to_ascii_lowercase(), Some(dequote(v.trim())?)),
None => (opt.to_ascii_lowercase(), None),
};
let flag = |v: &Option<String>| {
if v.is_some() {
Err(Error::Format("authorized_keys: option takes no value"))
} else {
Ok(())
}
};
let need =
|v: Option<String>| v.ok_or(Error::Format("authorized_keys: option needs a value"));
match name.as_str() {
"cert-authority" => {
flag(&value)?;
out.cert_authority = true;
}
"principals" => {
out.principals = need(value)?
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
"command" => out.command = Some(need(value)?),
"from" => out.from = Some(need(value)?),
"environment" => {
let v = need(value)?;
if !v.contains('=') {
return Err(Error::Format("authorized_keys: bad environment"));
}
out.environment.push(v);
}
"permitopen" => out.permit_open.push(need(value)?),
"permitlisten" => out.permit_listen.push(need(value)?),
"expiry-time" => out.expiry_time = Some(need(value)?),
"tunnel" => {
let v = need(value)?;
if !v.eq_ignore_ascii_case("any")
&& v.parse::<u32>().ok().filter(|u| *u < 0x7fff_ffff).is_none()
{
return Err(Error::Format("authorized_keys: invalid tun device"));
}
out.tunnel = Some(v);
}
"restrict" => {
flag(&value)?;
out.restrict = true;
}
other => {
flag(&value)?;
let (on, what) = match other.strip_prefix("no-") {
Some(w) => (false, w),
None => (true, other),
};
match what {
"pty" => out.pty = Some(on),
"port-forwarding" => out.port_forwarding = Some(on),
"agent-forwarding" => out.agent_forwarding = Some(on),
"x11-forwarding" => out.x11_forwarding = Some(on),
"user-rc" => out.user_rc = Some(on),
"touch-required" if !on => out.no_touch_required = true,
"verify-required" if on => out.verify_required = true,
_ => {
return Err(Error::Format(
"authorized_keys: unknown option — refusing rather than silently dropping",
));
}
}
}
}
}
Ok(out)
}
}
fn dequote(v: &str) -> Result<String> {
let inner = v
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.ok_or(Error::Format(
"authorized_keys: option value must be quoted",
))?;
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' && chars.peek() == Some(&'"') {
out.push('"');
chars.next();
} else {
out.push(c);
}
}
Ok(out)
}
fn split_options(list: &str) -> Result<Vec<String>> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_quotes = false;
let mut escaped = false;
for ch in list.chars() {
match ch {
'\\' if in_quotes && !escaped => {
escaped = true;
cur.push(ch);
continue;
}
'"' if !escaped => {
in_quotes = !in_quotes;
cur.push(ch);
}
',' if !in_quotes => {
out.push(core::mem::take(&mut cur));
}
_ => cur.push(ch),
}
escaped = false;
}
if in_quotes {
return Err(Error::Format(
"authorized_keys: unterminated quote in options",
));
}
out.push(cur);
Ok(out)
}
fn options_field_len(line: &str) -> usize {
let mut in_quotes = false;
let mut escaped = false;
for (i, ch) in line.char_indices() {
match ch {
'\\' if in_quotes && !escaped => {
escaped = true;
continue;
}
'"' if !escaped => in_quotes = !in_quotes,
c if c.is_whitespace() && !in_quotes => return i,
_ => {}
}
escaped = false;
}
line.len()
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PublicKey {
Ed25519 {
raw: [u8; 32],
comment: String,
},
EcdsaP256 {
point: Vec<u8>,
comment: String,
},
EcdsaP384 {
point: Vec<u8>,
comment: String,
},
EcdsaP521 {
point: Vec<u8>,
comment: String,
},
Rsa {
e: Vec<u8>,
n: Vec<u8>,
comment: String,
},
MlDsa44Ed25519 {
raw: Vec<u8>,
comment: String,
},
}
#[derive(Clone)]
#[non_exhaustive]
pub enum PrivateKey {
Ed25519 {
seed: [u8; 32],
public: [u8; 32],
comment: String,
},
EcdsaP256 {
d: Vec<u8>,
point: Vec<u8>,
comment: String,
},
EcdsaP384 {
d: Vec<u8>,
point: Vec<u8>,
comment: String,
},
EcdsaP521 {
d: Vec<u8>,
point: Vec<u8>,
comment: String,
},
Rsa {
n: Vec<u8>,
e: Vec<u8>,
d: Vec<u8>,
p: Vec<u8>,
q: Vec<u8>,
iqmp: Vec<u8>,
comment: String,
},
MlDsa44Ed25519 {
seeds: [u8; 64],
public: Vec<u8>,
comment: String,
},
}
impl core::fmt::Debug for PrivateKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PrivateKey")
.field("algorithm", &self.algorithm())
.field("comment", &self.comment())
.field("public", &self.public_key())
.field("secret", &"<redacted>")
.finish()
}
}
const ED25519: &str = "ssh-ed25519";
const ECDSA_P256: &str = "ecdsa-sha2-nistp256";
const ECDSA_P384: &str = "ecdsa-sha2-nistp384";
const ECDSA_P521: &str = "ecdsa-sha2-nistp521";
const RSA: &str = "ssh-rsa";
const MLDSA44_ED25519: &str = "ssh-mldsa44-ed25519@openssh.com";
const NISTP256: &str = "nistp256";
const NISTP384: &str = "nistp384";
const NISTP521: &str = "nistp521";
const MAGIC: &[u8] = b"openssh-key-v1\0";
fn is_known_algorithm_name(s: &str) -> bool {
matches!(
s,
ED25519
| ECDSA_P256
| ECDSA_P384
| ECDSA_P521
| RSA
| "rsa-sha2-256"
| "rsa-sha2-512"
| MLDSA44_ED25519
)
}
impl PublicKey {
pub fn algorithm(&self) -> &'static str {
match self {
PublicKey::Ed25519 { .. } => ED25519,
PublicKey::EcdsaP256 { .. } => ECDSA_P256,
PublicKey::EcdsaP384 { .. } => ECDSA_P384,
PublicKey::EcdsaP521 { .. } => ECDSA_P521,
PublicKey::Rsa { .. } => RSA,
PublicKey::MlDsa44Ed25519 { .. } => MLDSA44_ED25519,
}
}
pub fn comment(&self) -> &str {
match self {
PublicKey::Ed25519 { comment, .. }
| PublicKey::EcdsaP256 { comment, .. }
| PublicKey::EcdsaP384 { comment, .. }
| PublicKey::EcdsaP521 { comment, .. }
| PublicKey::Rsa { comment, .. }
| PublicKey::MlDsa44Ed25519 { comment, .. } => comment,
}
}
pub fn wire_blob(&self) -> Vec<u8> {
let mut w = Writer::new();
match self {
PublicKey::Ed25519 { raw, .. } => {
w.write_string(ED25519.as_bytes());
w.write_string(raw);
}
PublicKey::MlDsa44Ed25519 { raw, .. } => {
w.write_string(MLDSA44_ED25519.as_bytes());
w.write_string(raw);
}
PublicKey::EcdsaP256 { point, .. } => {
w.write_string(ECDSA_P256.as_bytes());
w.write_string(NISTP256.as_bytes());
w.write_string(point);
}
PublicKey::EcdsaP384 { point, .. } => {
w.write_string(ECDSA_P384.as_bytes());
w.write_string(NISTP384.as_bytes());
w.write_string(point);
}
PublicKey::EcdsaP521 { point, .. } => {
w.write_string(ECDSA_P521.as_bytes());
w.write_string(NISTP521.as_bytes());
w.write_string(point);
}
PublicKey::Rsa { e, n, comment: _ } => {
w.write_string(RSA.as_bytes());
write_mpint(&mut w, e);
write_mpint(&mut w, n);
}
}
w.into_vec()
}
pub fn parse_authorized_keys_line(s: &str) -> Result<Self> {
let line = s.trim_end_matches(['\n', '\r']).trim();
if line.is_empty() || line.starts_with('#') {
return Err(Error::Format("authorized_keys: empty or comment line"));
}
let mut it = line.split_whitespace();
let first = it
.next()
.ok_or(Error::Format("authorized_keys: empty line"))?;
if !is_known_algorithm_name(first) {
return Err(Error::Format(
"authorized_keys: line begins with options or unknown tag — refusing rather than silently dropping",
));
}
let algo = first;
let b64 = it
.next()
.ok_or(Error::Format("authorized_keys: missing key blob"))?;
let comment_parts: Vec<&str> = it.collect();
let comment = comment_parts.join(" ");
let blob = base64::decode(b64.as_bytes())?;
let mut pk = Self::parse_wire_blob(&blob)?;
match &mut pk {
PublicKey::Ed25519 { comment: c, .. }
| PublicKey::EcdsaP256 { comment: c, .. }
| PublicKey::EcdsaP384 { comment: c, .. }
| PublicKey::EcdsaP521 { comment: c, .. }
| PublicKey::Rsa { comment: c, .. }
| PublicKey::MlDsa44Ed25519 { comment: c, .. } => *c = comment,
}
if pk.algorithm() != algo {
return Err(Error::Format("authorized_keys: algorithm tag mismatch"));
}
Ok(pk)
}
pub fn parse_authorized_keys_line_with_options(
s: &str,
) -> Result<(Vec<u8>, AuthorizedKeyOptions)> {
let line = s.trim_end_matches(['\n', '\r']).trim();
if line.is_empty() || line.starts_with('#') {
return Err(Error::Format("authorized_keys: empty or comment line"));
}
let first_ws = line.find(char::is_whitespace);
let first_tok = match first_ws {
Some(i) => &line[..i],
None => return Err(Error::Format("authorized_keys: missing key blob")),
};
let mut options = AuthorizedKeyOptions::default();
let (algo_tok, after_algo) =
if is_known_algorithm_name(first_tok) || crate::cert::is_cert_name(first_tok) {
(first_tok, line[first_tok.len()..].trim_start())
} else {
let opt_len = options_field_len(line);
options = AuthorizedKeyOptions::parse(&line[..opt_len])?;
let rest = line[opt_len..].trim_start();
let i = rest.find(char::is_whitespace).ok_or(Error::Format(
"authorized_keys: missing key blob after options",
))?;
(&rest[..i], rest[i..].trim_start())
};
if !(is_known_algorithm_name(algo_tok) || crate::cert::is_cert_name(algo_tok)) {
return Err(Error::Format(
"authorized_keys: unknown key/cert algorithm tag",
));
}
let b64 = after_algo
.split_whitespace()
.next()
.ok_or(Error::Format("authorized_keys: missing key blob"))?;
let blob = base64::decode(b64.as_bytes())?;
Ok((blob, options))
}
pub fn to_authorized_keys_line(&self) -> String {
let b64 = base64::encode(&self.wire_blob());
let mut out = String::with_capacity(self.algorithm().len() + 1 + b64.len());
out.push_str(self.algorithm());
out.push(' ');
out.push_str(&b64);
let c = self.comment();
if !c.is_empty() {
out.push(' ');
out.push_str(c);
}
out
}
pub fn parse_wire_blob(blob: &[u8]) -> Result<Self> {
let mut r = Reader::new(blob);
let algo = r.read_string()?;
let key = match algo {
b if b == ED25519.as_bytes() => {
let raw = r.read_string()?;
if raw.len() != 32 {
return Err(Error::Format("ed25519: public key length"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(raw);
PublicKey::Ed25519 {
raw: arr,
comment: String::new(),
}
}
b if b == MLDSA44_ED25519.as_bytes() => {
let raw = r.read_string()?;
crate::hostkey::MlDsa44Ed25519HostKey::from_public(raw)?;
PublicKey::MlDsa44Ed25519 {
raw: raw.to_vec(),
comment: String::new(),
}
}
b if b == ECDSA_P256.as_bytes() => {
parse_ecdsa_public(&mut r, NISTP256, CurveId::P256, |p, c| {
PublicKey::EcdsaP256 {
point: p,
comment: c,
}
})?
}
b if b == ECDSA_P384.as_bytes() => {
parse_ecdsa_public(&mut r, NISTP384, CurveId::P384, |p, c| {
PublicKey::EcdsaP384 {
point: p,
comment: c,
}
})?
}
b if b == ECDSA_P521.as_bytes() => {
parse_ecdsa_public(&mut r, NISTP521, CurveId::P521, |p, c| {
PublicKey::EcdsaP521 {
point: p,
comment: c,
}
})?
}
b if b == RSA.as_bytes() => {
let e = read_mpint(&mut r)?.to_vec();
let n = read_mpint(&mut r)?.to_vec();
PublicKey::Rsa {
e,
n,
comment: String::new(),
}
}
_ => return Err(Error::Unsupported("openssh public key algorithm")),
};
if !r.is_empty() {
return Err(Error::Format("public key: trailing data"));
}
Ok(key)
}
}
fn parse_ecdsa_public<F>(
r: &mut Reader<'_>,
curve_name: &str,
curve_id: CurveId,
ctor: F,
) -> Result<PublicKey>
where
F: FnOnce(Vec<u8>, String) -> PublicKey,
{
let c = r.read_string()?;
if c != curve_name.as_bytes() {
return Err(Error::Format("ecdsa: curve name mismatch"));
}
let point = r.read_string()?.to_vec();
BoxedEcdsaPublicKey::from_sec1(curve_id, &point)
.map_err(|_| Error::Format("ecdsa: invalid SEC1 point"))?;
Ok(ctor(point, String::new()))
}
impl PrivateKey {
pub fn algorithm(&self) -> &'static str {
match self {
PrivateKey::Ed25519 { .. } => ED25519,
PrivateKey::EcdsaP256 { .. } => ECDSA_P256,
PrivateKey::EcdsaP384 { .. } => ECDSA_P384,
PrivateKey::EcdsaP521 { .. } => ECDSA_P521,
PrivateKey::Rsa { .. } => RSA,
PrivateKey::MlDsa44Ed25519 { .. } => MLDSA44_ED25519,
}
}
pub fn set_comment(&mut self, comment: impl Into<String>) {
match self {
PrivateKey::Ed25519 { comment: c, .. }
| PrivateKey::EcdsaP256 { comment: c, .. }
| PrivateKey::EcdsaP384 { comment: c, .. }
| PrivateKey::EcdsaP521 { comment: c, .. }
| PrivateKey::Rsa { comment: c, .. }
| PrivateKey::MlDsa44Ed25519 { comment: c, .. } => *c = comment.into(),
}
}
pub fn comment(&self) -> &str {
match self {
PrivateKey::Ed25519 { comment, .. }
| PrivateKey::EcdsaP256 { comment, .. }
| PrivateKey::EcdsaP384 { comment, .. }
| PrivateKey::EcdsaP521 { comment, .. }
| PrivateKey::Rsa { comment, .. }
| PrivateKey::MlDsa44Ed25519 { comment, .. } => comment,
}
}
pub fn into_host_key(self) -> Result<alloc::boxed::Box<dyn crate::hostkey::HostKey + Send>> {
Ok(self.into_host_key_sync()?)
}
pub fn into_host_key_sync(
mut self,
) -> Result<alloc::boxed::Box<dyn crate::hostkey::HostKey + Send + Sync>> {
let signer = self.host_key();
self.wipe();
signer
}
fn host_key(&self) -> Result<alloc::boxed::Box<dyn crate::hostkey::HostKey + Send + Sync>> {
match self {
PrivateKey::Ed25519 { seed, .. } => Ok(alloc::boxed::Box::new(
crate::hostkey::Ed25519HostKey::from_seed(*seed),
)),
PrivateKey::EcdsaP256 { d, .. } => Ok(alloc::boxed::Box::new(
crate::hostkey::EcdsaP256HostKey::from_scalar(d)?,
)),
PrivateKey::EcdsaP384 { d, .. } => Ok(alloc::boxed::Box::new(
crate::hostkey::EcdsaP384HostKey::from_scalar(d)?,
)),
PrivateKey::EcdsaP521 { d, .. } => Ok(alloc::boxed::Box::new(
crate::hostkey::EcdsaP521HostKey::from_scalar(d)?,
)),
PrivateKey::Rsa { n, e, d, p, q, .. } => {
Ok(alloc::boxed::Box::new(rsa_host_key(n, e, d, p, q)?))
}
PrivateKey::MlDsa44Ed25519 { seeds, .. } => Ok(alloc::boxed::Box::new(
crate::hostkey::MlDsa44Ed25519HostKey::from_seeds(*seeds),
)),
}
}
pub(crate) fn wipe(&mut self) {
match self {
PrivateKey::Ed25519 { seed, .. } => seed.zeroize(),
PrivateKey::EcdsaP256 { d, .. }
| PrivateKey::EcdsaP384 { d, .. }
| PrivateKey::EcdsaP521 { d, .. } => d.zeroize(),
PrivateKey::Rsa { d, p, q, iqmp, .. } => {
d.zeroize();
p.zeroize();
q.zeroize();
iqmp.zeroize();
}
PrivateKey::MlDsa44Ed25519 { seeds, .. } => seeds.zeroize(),
}
}
pub fn check_consistency(&self) -> Result<()> {
match self {
PrivateKey::Ed25519 { seed, public, .. } => {
let derived = Ed25519PrivateKey::from_bytes(*seed).public_key().to_bytes();
if &derived != public {
return Err(Error::Format(
"ed25519: public key does not match private seed",
));
}
}
PrivateKey::EcdsaP256 { d, point, .. } => check_ecdsa(CurveId::P256, d, point)?,
PrivateKey::EcdsaP384 { d, point, .. } => check_ecdsa(CurveId::P384, d, point)?,
PrivateKey::EcdsaP521 { d, point, .. } => check_ecdsa(CurveId::P521, d, point)?,
PrivateKey::Rsa { n, e, d, p, q, .. } => check_rsa_private(n, e, d, p, q)?,
PrivateKey::MlDsa44Ed25519 { seeds, public, .. } => {
if crate::hostkey::mldsa44_ed25519_public(seeds) != *public {
return Err(Error::Format(
"mldsa44-ed25519: public key does not match private seeds",
));
}
}
}
Ok(())
}
pub fn public_key(&self) -> PublicKey {
match self {
PrivateKey::Ed25519 {
public, comment, ..
} => PublicKey::Ed25519 {
raw: *public,
comment: comment.clone(),
},
PrivateKey::EcdsaP256 { point, comment, .. } => PublicKey::EcdsaP256 {
point: point.clone(),
comment: comment.clone(),
},
PrivateKey::EcdsaP384 { point, comment, .. } => PublicKey::EcdsaP384 {
point: point.clone(),
comment: comment.clone(),
},
PrivateKey::EcdsaP521 { point, comment, .. } => PublicKey::EcdsaP521 {
point: point.clone(),
comment: comment.clone(),
},
PrivateKey::Rsa { e, n, comment, .. } => PublicKey::Rsa {
e: e.clone(),
n: n.clone(),
comment: comment.clone(),
},
PrivateKey::MlDsa44Ed25519 {
public, comment, ..
} => PublicKey::MlDsa44Ed25519 {
raw: public.clone(),
comment: comment.clone(),
},
}
}
pub fn public_key_from_pem(pem: &str) -> Result<PublicKey> {
if !pem.contains("-----BEGIN OPENSSH PRIVATE KEY-----") {
return Self::parse_pem(pem, None).map(|k| k.public_key());
}
let body = strip_pem(pem)?;
let raw = base64::decode(body.as_bytes())?;
if raw.len() < MAGIC.len() || &raw[..MAGIC.len()] != MAGIC {
return Err(Error::Format("openssh key: bad magic"));
}
let mut r = Reader::new(&raw[MAGIC.len()..]);
let _ciphername = r.read_string()?;
let _kdfname = r.read_string()?;
let _kdfoptions = r.read_string()?;
if r.read_u32()? != 1 {
return Err(Error::Unsupported("openssh key: multiple keys"));
}
PublicKey::parse_wire_blob(r.read_string()?)
}
pub fn parse_openssh_pem(pem: &str, passphrase: Option<&[u8]>) -> Result<Self> {
let body = strip_pem(pem)?;
let raw: Zeroizing<Vec<u8>> = Zeroizing::new(base64::decode(body.as_bytes())?);
if raw.len() < MAGIC.len() || &raw[..MAGIC.len()] != MAGIC {
return Err(Error::Format("openssh key: bad magic"));
}
let mut r = Reader::new(&raw[MAGIC.len()..]);
let ciphername = r.read_string()?;
let kdfname = r.read_string()?;
let kdfoptions = r.read_string()?;
let nkeys = r.read_u32()?;
if nkeys != 1 {
return Err(Error::Unsupported("openssh key: multiple keys"));
}
let public_blob = r.read_string()?.to_vec();
let encrypted = r.read_string()?;
if !r.is_empty() {
return Err(Error::Format("openssh key: trailing data"));
}
let plain = decrypt_payload(ciphername, kdfname, kdfoptions, encrypted, passphrase)?;
let mut pr = Reader::new(&plain);
let checkint1 = pr.read_u32()?;
let checkint2 = pr.read_u32()?;
if checkint1 != checkint2 {
return Err(Error::Crypto("wrong passphrase"));
}
let pk = parse_private_fields(&mut pr)?;
let comment_bytes = pr.read_string()?;
let comment = core::str::from_utf8(comment_bytes)
.map_err(|_| Error::Format("comment is not utf-8"))?
.to_string();
let mut expected = 1u8;
while !pr.is_empty() {
let b = pr.read_u8()?;
if b != expected {
return Err(Error::Format("openssh key: bad padding"));
}
expected = expected.wrapping_add(1);
}
let pk_with_comment = attach_comment(pk, comment);
let embedded = PublicKey::parse_wire_blob(&public_blob)?;
if !public_parts_match(&pk_with_comment.public_key(), &embedded) {
return Err(Error::Format(
"openssh key: embedded public key does not match private fields",
));
}
pk_with_comment.check_consistency()?;
Ok(pk_with_comment)
}
}
fn check_ecdsa(curve: CurveId, d: &[u8], point: &[u8]) -> Result<()> {
let sk = BoxedEcdsaPrivateKey::from_bytes(curve, trim_leading_zeros(d))
.map_err(|_| Error::Crypto("ecdsa: private scalar out of range"))?;
if sk.public_key().to_sec1() != point {
return Err(Error::Format(
"ecdsa: public point does not match private scalar",
));
}
Ok(())
}
fn rsa_has_primes(p: &[u8], q: &[u8]) -> bool {
!trim_leading_zeros(p).is_empty() && !trim_leading_zeros(q).is_empty()
}
fn check_rsa_private(n: &[u8], e: &[u8], d: &[u8], p: &[u8], q: &[u8]) -> Result<()> {
if !rsa_has_primes(p, q) {
return Ok(());
}
let n_u = BoxedUint::from_be_bytes(trim_leading_zeros(n));
let p_u = BoxedUint::from_be_bytes(trim_leading_zeros(p));
let q_u = BoxedUint::from_be_bytes(trim_leading_zeros(q));
crate::hostkey::rsa::check_rsa_primes(&n_u, &p_u, &q_u)?;
let e_u = BoxedUint::from_be_bytes(trim_leading_zeros(e));
let d_u = BoxedUint::from_be_bytes(trim_leading_zeros(d));
let one = BoxedUint::from_u64(1);
let ed = e_u.mul(&d_u);
if ed.reduce(&p_u.sub(&one)) != one || ed.reduce(&q_u.sub(&one)) != one {
return Err(Error::Format(
"rsa: private exponent does not match public exponent and primes",
));
}
Ok(())
}
impl PrivateKey {
#[cfg(all(feature = "agent", unix))]
pub(crate) fn agent_signer(
&self,
flags: u32,
) -> Result<Box<dyn crate::hostkey::HostKey + Send>> {
let PrivateKey::Rsa { n, e, d, p, q, .. } = self else {
return Ok(self.host_key()?);
};
let num = |b: &[u8]| BoxedUint::from_be_bytes(trim_leading_zeros(b));
let primes = rsa_has_primes(p, q);
macro_rules! build {
($ty:ty) => {
if primes {
<$ty>::from_components_with_primes(num(n), num(e), num(d), num(p), num(q))
} else {
<$ty>::from_components(num(n), num(e), num(d))
}
.map(|k| Box::new(k) as Box<dyn crate::hostkey::HostKey + Send>)
};
}
if flags & 4 != 0 {
build!(crate::hostkey::RsaSha2_512HostKey)
} else if flags & 2 != 0 {
build!(crate::hostkey::RsaSha2_256HostKey)
} else {
build!(crate::hostkey::RsaSha1HostKey)
}
}
}
fn rsa_host_key(
n: &[u8],
e: &[u8],
d: &[u8],
p: &[u8],
q: &[u8],
) -> Result<crate::hostkey::RsaSha2_512HostKey> {
let n_u = BoxedUint::from_be_bytes(trim_leading_zeros(n));
let e_u = BoxedUint::from_be_bytes(trim_leading_zeros(e));
let d_u = BoxedUint::from_be_bytes(trim_leading_zeros(d));
if rsa_has_primes(p, q) {
let p_u = BoxedUint::from_be_bytes(trim_leading_zeros(p));
let q_u = BoxedUint::from_be_bytes(trim_leading_zeros(q));
crate::hostkey::RsaSha2_512HostKey::from_components_with_primes(n_u, e_u, d_u, p_u, q_u)
} else {
crate::hostkey::RsaSha2_512HostKey::from_components(n_u, e_u, d_u)
}
}
fn attach_comment(mut pk: PrivateKey, comment: String) -> PrivateKey {
match &mut pk {
PrivateKey::Ed25519 { comment: c, .. }
| PrivateKey::EcdsaP256 { comment: c, .. }
| PrivateKey::EcdsaP384 { comment: c, .. }
| PrivateKey::EcdsaP521 { comment: c, .. }
| PrivateKey::Rsa { comment: c, .. }
| PrivateKey::MlDsa44Ed25519 { comment: c, .. } => *c = comment,
}
pk
}
fn public_parts_match(a: &PublicKey, b: &PublicKey) -> bool {
match (a, b) {
(PublicKey::Ed25519 { raw: x, .. }, PublicKey::Ed25519 { raw: y, .. }) => x == y,
(PublicKey::MlDsa44Ed25519 { raw: x, .. }, PublicKey::MlDsa44Ed25519 { raw: y, .. }) => {
x == y
}
(PublicKey::EcdsaP256 { point: x, .. }, PublicKey::EcdsaP256 { point: y, .. })
| (PublicKey::EcdsaP384 { point: x, .. }, PublicKey::EcdsaP384 { point: y, .. })
| (PublicKey::EcdsaP521 { point: x, .. }, PublicKey::EcdsaP521 { point: y, .. }) => x == y,
(PublicKey::Rsa { e: e1, n: n1, .. }, PublicKey::Rsa { e: e2, n: n2, .. }) => {
trim_leading_zeros(e1) == trim_leading_zeros(e2)
&& trim_leading_zeros(n1) == trim_leading_zeros(n2)
}
_ => false,
}
}
fn trim_leading_zeros(b: &[u8]) -> &[u8] {
let mut i = 0;
while i < b.len() && b[i] == 0 {
i += 1;
}
&b[i..]
}
impl PrivateKey {
#[cfg(all(feature = "agent", unix))]
pub(crate) fn from_agent_add_body(r: &mut Reader<'_>) -> Result<Self> {
let pk = parse_private_fields(r)?;
let comment = String::from_utf8_lossy(r.read_string()?).into_owned();
let pk = attach_comment(pk, comment);
pk.check_consistency()?;
Ok(pk)
}
#[cfg(all(feature = "agent", unix))]
pub(crate) fn from_agent_add_cert_body(r: &mut Reader<'_>) -> Result<(Self, Vec<u8>)> {
let cert_type = r.read_string()?;
let cert_blob = r.read_string()?.to_vec();
let cert = crate::cert::Certificate::parse(&cert_blob)?;
if cert.key_type.as_bytes() != cert_type {
return Err(Error::Format("agent certificate: type mismatch"));
}
let embedded = cert.embedded_pubkey_blob().to_vec();
let pk = match PublicKey::parse_wire_blob(&embedded)? {
PublicKey::Ed25519 { raw, .. } => {
let pub_raw = r.read_string()?;
let sk = r.read_string()?;
if pub_raw != raw || sk.len() != 64 || sk[32..] != raw {
return Err(Error::Format("ed25519: certificate key mismatch"));
}
let mut seed = [0u8; 32];
seed.copy_from_slice(&sk[..32]);
PrivateKey::Ed25519 {
seed,
public: raw,
comment: String::new(),
}
}
PublicKey::MlDsa44Ed25519 { raw, .. } => {
let sk = r.read_string()?;
if sk.len() != 64 {
return Err(Error::Format("mldsa44-ed25519: secret length"));
}
let mut seeds = [0u8; 64];
seeds.copy_from_slice(sk);
PrivateKey::MlDsa44Ed25519 {
seeds,
public: raw,
comment: String::new(),
}
}
PublicKey::EcdsaP256 { point, .. } => PrivateKey::EcdsaP256 {
d: read_mpint(r)?.to_vec(),
point,
comment: String::new(),
},
PublicKey::EcdsaP384 { point, .. } => PrivateKey::EcdsaP384 {
d: read_mpint(r)?.to_vec(),
point,
comment: String::new(),
},
PublicKey::EcdsaP521 { point, .. } => PrivateKey::EcdsaP521 {
d: read_mpint(r)?.to_vec(),
point,
comment: String::new(),
},
PublicKey::Rsa { n, e, .. } => {
let d = read_mpint(r)?.to_vec();
let iqmp = read_mpint(r)?.to_vec();
let p = read_mpint(r)?.to_vec();
let q = read_mpint(r)?.to_vec();
PrivateKey::Rsa {
n,
e,
d,
p,
q,
iqmp,
comment: String::new(),
}
}
};
let comment = String::from_utf8_lossy(r.read_string()?).into_owned();
let pk = attach_comment(pk, comment);
pk.check_consistency()?;
if pk.public_key().wire_blob() != embedded {
return Err(Error::Format("agent certificate: key mismatch"));
}
Ok((pk, cert_blob))
}
}
fn parse_private_fields(r: &mut Reader<'_>) -> Result<PrivateKey> {
let algo = r.read_string()?;
match algo {
b if b == ED25519.as_bytes() => {
let pub_raw = r.read_string()?;
if pub_raw.len() != 32 {
return Err(Error::Format("ed25519: public length"));
}
let sk = r.read_string()?;
if sk.len() != 64 {
return Err(Error::Format("ed25519: secret length"));
}
let mut seed = [0u8; 32];
let mut pubk = [0u8; 32];
seed.copy_from_slice(&sk[..32]);
pubk.copy_from_slice(&sk[32..]);
if pubk != pub_raw {
return Err(Error::Format(
"ed25519: public key in secret does not match",
));
}
Ok(PrivateKey::Ed25519 {
seed,
public: pubk,
comment: String::new(),
})
}
b if b == MLDSA44_ED25519.as_bytes() => {
let public = r.read_string()?;
crate::hostkey::MlDsa44Ed25519HostKey::from_public(public)?;
let sk = r.read_string()?;
if sk.len() != 64 {
return Err(Error::Format("mldsa44-ed25519: secret length"));
}
let mut seeds = [0u8; 64];
seeds.copy_from_slice(sk);
Ok(PrivateKey::MlDsa44Ed25519 {
seeds,
public: public.to_vec(),
comment: String::new(),
})
}
b if b == ECDSA_P256.as_bytes() => {
parse_ecdsa_private(r, NISTP256, |d, point| PrivateKey::EcdsaP256 {
d,
point,
comment: String::new(),
})
}
b if b == ECDSA_P384.as_bytes() => {
parse_ecdsa_private(r, NISTP384, |d, point| PrivateKey::EcdsaP384 {
d,
point,
comment: String::new(),
})
}
b if b == ECDSA_P521.as_bytes() => {
parse_ecdsa_private(r, NISTP521, |d, point| PrivateKey::EcdsaP521 {
d,
point,
comment: String::new(),
})
}
b if b == RSA.as_bytes() => {
let n = read_mpint(r)?.to_vec();
let e = read_mpint(r)?.to_vec();
let d = read_mpint(r)?.to_vec();
let iqmp = read_mpint(r)?.to_vec();
let p = read_mpint(r)?.to_vec();
let q = read_mpint(r)?.to_vec();
Ok(PrivateKey::Rsa {
n,
e,
d,
p,
q,
iqmp,
comment: String::new(),
})
}
_ => Err(Error::Unsupported("openssh private key algorithm")),
}
}
fn parse_ecdsa_private<F>(r: &mut Reader<'_>, curve: &str, ctor: F) -> Result<PrivateKey>
where
F: FnOnce(Vec<u8>, Vec<u8>) -> PrivateKey,
{
let c = r.read_string()?;
if c != curve.as_bytes() {
return Err(Error::Format("ecdsa: curve mismatch"));
}
let point = r.read_string()?.to_vec();
let d = read_mpint(r)?.to_vec();
Ok(ctor(d, point))
}
fn strip_pem(pem: &str) -> Result<&str> {
const BEGIN: &str = "-----BEGIN OPENSSH PRIVATE KEY-----";
const END: &str = "-----END OPENSSH PRIVATE KEY-----";
let start = pem
.find(BEGIN)
.ok_or(Error::Format("openssh key: missing BEGIN marker"))?
+ BEGIN.len();
let rest = &pem[start..];
let end = rest
.find(END)
.ok_or(Error::Format("openssh key: missing END marker"))?;
Ok(&rest[..end])
}
fn decrypt_payload(
ciphername: &[u8],
kdfname: &[u8],
kdfoptions: &[u8],
encrypted: &[u8],
passphrase: Option<&[u8]>,
) -> Result<Zeroizing<Vec<u8>>> {
if ciphername == b"none" {
if kdfname != b"none" {
return Err(Error::Format(
"openssh key: cipher 'none' with non-none kdf",
));
}
return Ok(Zeroizing::new(encrypted.to_vec()));
}
let pass = match passphrase {
Some(p) if !p.is_empty() => p,
_ => return Err(Error::Crypto("passphrase required")),
};
let (key_len, iv_len) = match ciphername {
b"aes256-ctr" => (32usize, 16usize),
b"aes128-ctr" => (16, 16),
_ => return Err(Error::Unsupported("openssh key cipher")),
};
if kdfname != b"bcrypt" {
return Err(Error::Unsupported("openssh key kdf"));
}
let mut kr = Reader::new(kdfoptions);
let salt = kr.read_string()?;
let rounds = kr.read_u32()?;
if !kr.is_empty() {
return Err(Error::Format("openssh key: trailing kdfoptions"));
}
const MAX_BCRYPT_ROUNDS: u32 = 10_000;
if rounds == 0 {
return Err(Error::Format("openssh key: bcrypt rounds must be > 0"));
}
if rounds > MAX_BCRYPT_ROUNDS {
return Err(Error::Format(
"openssh key: bcrypt rounds exceeds 10000 (DoS guard)",
));
}
let derived: Zeroizing<Vec<u8>> = Zeroizing::new(
bcrypt_pbkdf(pass, salt, rounds, key_len + iv_len)
.map_err(|_| Error::Crypto("bcrypt_pbkdf: invalid parameters"))?,
);
if !encrypted.len().is_multiple_of(16) {
return Err(Error::Format("openssh key: encrypted length not aligned"));
}
let mut iv: Zeroizing<[u8; 16]> = Zeroizing::new([0u8; 16]);
iv.copy_from_slice(&derived[key_len..key_len + iv_len]);
let mut out: Zeroizing<Vec<u8>> = Zeroizing::new(encrypted.to_vec());
match ciphername {
b"aes256-ctr" => {
let mut k: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
k.copy_from_slice(&derived[..32]);
let mut ctr = Ctr::new(Aes256::new(&k), &iv);
ctr.apply_keystream(&mut out);
}
b"aes128-ctr" => {
let mut k: Zeroizing<[u8; 16]> = Zeroizing::new([0u8; 16]);
k.copy_from_slice(&derived[..16]);
let mut ctr = Ctr::new(Aes128::new(&k), &iv);
ctr.apply_keystream(&mut out);
}
_ => unreachable!(),
}
Ok(out)
}
#[cfg(test)]
mod tests;