use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::format::{Reader, Writer};
use crate::hostkey::host_key_verify_by_name;
pub const MAX_CERT_BLOB: usize = 64 * 1024;
const MAX_LIST_ENTRIES: usize = 256;
const MAX_LIST_ELEM: usize = 8 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertType {
User,
Host,
}
impl CertType {
fn from_u32(v: u32) -> Result<Self> {
match v {
1 => Ok(CertType::User),
2 => Ok(CertType::Host),
_ => Err(Error::Format("cert: unknown certificate type")),
}
}
}
pub const CERT_KEY_NAMES: &[&str] = &[
"ssh-ed25519-cert-v01@openssh.com",
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com",
"rsa-sha2-512-cert-v01@openssh.com",
"rsa-sha2-256-cert-v01@openssh.com",
];
pub fn is_cert_name(name: &str) -> bool {
name.ends_with("-cert-v01@openssh.com")
}
pub fn cert_name_to_plain(name: &str) -> Option<&'static str> {
match name {
"ssh-ed25519-cert-v01@openssh.com" => Some("ssh-ed25519"),
"ecdsa-sha2-nistp256-cert-v01@openssh.com" => Some("ecdsa-sha2-nistp256"),
"ecdsa-sha2-nistp384-cert-v01@openssh.com" => Some("ecdsa-sha2-nistp384"),
"ecdsa-sha2-nistp521-cert-v01@openssh.com" => Some("ecdsa-sha2-nistp521"),
"rsa-sha2-512-cert-v01@openssh.com" => Some("rsa-sha2-512"),
"rsa-sha2-256-cert-v01@openssh.com" => Some("rsa-sha2-256"),
"ssh-rsa-cert-v01@openssh.com" => Some("ssh-rsa"),
_ => None,
}
}
fn blob_type_to_plain(name: &str) -> Option<&'static str> {
match name {
"ssh-ed25519-cert-v01@openssh.com" => Some("ssh-ed25519"),
"ecdsa-sha2-nistp256-cert-v01@openssh.com" => Some("ecdsa-sha2-nistp256"),
"ecdsa-sha2-nistp384-cert-v01@openssh.com" => Some("ecdsa-sha2-nistp384"),
"ecdsa-sha2-nistp521-cert-v01@openssh.com" => Some("ecdsa-sha2-nistp521"),
"ssh-rsa-cert-v01@openssh.com" => Some("ssh-rsa"),
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct Certificate {
pub key_type: String,
pub nonce: Vec<u8>,
pub embedded_pubkey_blob: Vec<u8>,
pub serial: u64,
pub cert_type: CertType,
pub key_id: String,
pub valid_principals: Vec<String>,
pub valid_after: u64,
pub valid_before: u64,
pub critical_options: Vec<(String, Vec<u8>)>,
pub extensions: Vec<(String, Vec<u8>)>,
pub signature_key_blob: Vec<u8>,
pub signature: Vec<u8>,
pub signed_len: usize,
pub raw: Vec<u8>,
}
fn read_capped(r: &mut Reader<'_>, cap: usize, what: &'static str) -> Result<Vec<u8>> {
let s = r.read_string()?;
if s.len() > cap {
return Err(Error::Format(what));
}
Ok(s.to_vec())
}
fn parse_string_list(blob: &[u8]) -> Result<Vec<String>> {
let mut r = Reader::new(blob);
let mut out = Vec::new();
while !r.is_empty() {
if out.len() >= MAX_LIST_ENTRIES {
return Err(Error::Format("cert: too many list entries"));
}
let elem = r.read_string()?;
if elem.len() > MAX_LIST_ELEM {
return Err(Error::Format("cert: list element too long"));
}
let s = core::str::from_utf8(elem)
.map_err(|_| Error::Format("cert: non-utf8 principal"))?
.to_string();
out.push(s);
}
Ok(out)
}
fn parse_pair_list(blob: &[u8]) -> Result<Vec<(String, Vec<u8>)>> {
let mut r = Reader::new(blob);
let mut out: Vec<(String, Vec<u8>)> = Vec::new();
while !r.is_empty() {
if out.len() >= MAX_LIST_ENTRIES {
return Err(Error::Format("cert: too many option entries"));
}
let name = r.read_string()?;
if name.len() > MAX_LIST_ELEM {
return Err(Error::Format("cert: option name too long"));
}
let data = r.read_string()?;
if data.len() > MAX_LIST_ELEM {
return Err(Error::Format("cert: option data too long"));
}
let name = core::str::from_utf8(name)
.map_err(|_| Error::Format("cert: non-utf8 option name"))?
.to_string();
if let Some((last, _)) = out.last()
&& name.as_str() <= last.as_str()
{
return Err(Error::Format(
"cert: option names must be strictly ascending (duplicate or unsorted)",
));
}
out.push((name, data.to_vec()));
}
Ok(out)
}
fn read_embedded_pubkey(key_type: &str, r: &mut Reader<'_>) -> Result<Vec<u8>> {
let mut w = Writer::new();
match key_type {
"ssh-ed25519-cert-v01@openssh.com" => {
let pk = r.read_string()?;
if pk.len() != 32 {
return Err(Error::Format("cert: ed25519 key length"));
}
w.write_string(b"ssh-ed25519");
w.write_string(pk);
}
"ecdsa-sha2-nistp256-cert-v01@openssh.com"
| "ecdsa-sha2-nistp384-cert-v01@openssh.com"
| "ecdsa-sha2-nistp521-cert-v01@openssh.com" => {
let plain = blob_type_to_plain(key_type).expect("matched arm");
let curve = r.read_string()?;
let point = r.read_string()?;
let want_curve: &[u8] = match plain {
"ecdsa-sha2-nistp256" => b"nistp256",
"ecdsa-sha2-nistp384" => b"nistp384",
"ecdsa-sha2-nistp521" => b"nistp521",
_ => unreachable!(),
};
if curve != want_curve {
return Err(Error::Format("cert: ecdsa curve mismatch"));
}
w.write_string(plain.as_bytes());
w.write_string(curve);
w.write_string(point);
}
"ssh-rsa-cert-v01@openssh.com" => {
let e = r.read_string()?;
let n = r.read_string()?;
crate::hostkey::rsa::check_rsa_modulus_mpint(n)?;
w.write_string(b"ssh-rsa");
w.write_string(e);
w.write_string(n);
}
_ => return Err(Error::Format("cert: unsupported certificate key type")),
}
Ok(w.into_vec())
}
impl Certificate {
pub fn parse(blob: &[u8]) -> Result<Self> {
if blob.len() > MAX_CERT_BLOB {
return Err(Error::Format("cert: blob exceeds maximum size"));
}
let mut r = Reader::new(blob);
let key_type_b = r.read_string()?;
let key_type = core::str::from_utf8(key_type_b)
.map_err(|_| Error::Format("cert: non-utf8 key type"))?
.to_string();
if blob_type_to_plain(&key_type).is_none() {
return Err(Error::Format("cert: unsupported certificate key type"));
}
let nonce = read_capped(&mut r, MAX_LIST_ELEM, "cert: nonce too long")?;
let embedded_pubkey_blob = read_embedded_pubkey(&key_type, &mut r)?;
let serial = r.read_u64()?;
let cert_type = CertType::from_u32(r.read_u32()?)?;
let key_id = {
let b = read_capped(&mut r, MAX_LIST_ELEM, "cert: key id too long")?;
String::from_utf8(b).map_err(|_| Error::Format("cert: non-utf8 key id"))?
};
let principals_blob = read_capped(
&mut r,
MAX_LIST_ENTRIES * MAX_LIST_ELEM,
"cert: principals list too long",
)?;
let valid_principals = parse_string_list(&principals_blob)?;
let valid_after = r.read_u64()?;
let valid_before = r.read_u64()?;
if valid_after > valid_before {
return Err(Error::Format("cert: valid_after after valid_before"));
}
let crit_blob = read_capped(
&mut r,
MAX_LIST_ENTRIES * (2 * MAX_LIST_ELEM),
"cert: critical options too long",
)?;
let critical_options = parse_pair_list(&crit_blob)?;
let ext_blob = read_capped(
&mut r,
MAX_LIST_ENTRIES * (2 * MAX_LIST_ELEM),
"cert: extensions too long",
)?;
let extensions = parse_pair_list(&ext_blob)?;
let _reserved = read_capped(&mut r, MAX_LIST_ELEM, "cert: reserved too long")?;
let signature_key_blob =
read_capped(&mut r, MAX_LIST_ELEM, "cert: signature key too long")?;
let signed_len = blob.len() - r.remaining();
let signature = read_capped(&mut r, MAX_LIST_ELEM, "cert: signature too long")?;
if !r.is_empty() {
return Err(Error::Format("cert: trailing data after certificate"));
}
Ok(Certificate {
key_type,
nonce,
embedded_pubkey_blob,
serial,
cert_type,
key_id,
valid_principals,
valid_after,
valid_before,
critical_options,
extensions,
signature_key_blob,
signature,
signed_len,
raw: blob.to_vec(),
})
}
pub fn parse_openssh_line(line: &str) -> Result<Self> {
let line = line.trim();
let mut it = line.split_whitespace();
let name = it.next().ok_or(Error::Format("cert: empty line"))?;
if !CERT_KEY_NAMES.contains(&name) {
return Err(Error::Format("cert: line is not an OpenSSH certificate"));
}
let b64 = it
.next()
.ok_or(Error::Format("cert: missing base64 blob"))?;
let blob = crate::key::base64_decode(b64.as_bytes())?;
let cert = Self::parse(&blob)?;
if cert_name_to_plain(name) != blob_type_to_plain(&cert.key_type) {
return Err(Error::Format("cert: line key-type disagrees with blob"));
}
Ok(cert)
}
pub fn embedded_algorithm(&self) -> &'static str {
blob_type_to_plain(&self.key_type).expect("validated at parse")
}
pub fn embedded_verifier(
&self,
sig_blob: &[u8],
) -> Result<alloc::boxed::Box<dyn crate::hostkey::HostKeyVerify>> {
let mut r = Reader::new(sig_blob);
let sig_algo = r.read_string()?;
let sig_algo =
core::str::from_utf8(sig_algo).map_err(|_| Error::Format("cert: non-utf8 sig algo"))?;
let embedded = self.embedded_algorithm();
let compatible = match embedded {
"ssh-ed25519" => sig_algo == "ssh-ed25519",
"ecdsa-sha2-nistp256" => sig_algo == "ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384" => sig_algo == "ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521" => sig_algo == "ecdsa-sha2-nistp521",
"ssh-rsa" => {
matches!(sig_algo, "ssh-rsa" | "rsa-sha2-256" | "rsa-sha2-512")
}
_ => false,
};
if !compatible {
return Err(Error::Format(
"cert: signature algorithm not compatible with key",
));
}
host_key_verify_by_name(sig_algo, &self.embedded_pubkey_blob)
}
pub fn embedded_pubkey_blob(&self) -> &[u8] {
&self.embedded_pubkey_blob
}
pub fn ca_algorithm(&self) -> Result<&str> {
let mut r = Reader::new(&self.signature);
let name = r.read_string()?;
core::str::from_utf8(name).map_err(|_| Error::Format("cert: non-utf8 CA signature algo"))
}
pub fn verify_ca_signature(&self, allowed_ca_algos: &[&str]) -> Result<()> {
let ca_algo = self.ca_algorithm().map_err(|_| Error::CertBadCaSignature)?;
if !allowed_ca_algos.contains(&ca_algo) {
return Err(Error::CertBadCaSignature);
}
let verifier = host_key_verify_by_name(ca_algo, &self.signature_key_blob)
.map_err(|_| Error::CertBadCaSignature)?;
verifier
.verify(&self.raw[..self.signed_len], &self.signature)
.map_err(|_| Error::CertBadCaSignature)
}
pub fn check_validity(&self, now: u64) -> Result<()> {
if now < self.valid_after {
return Err(Error::CertNotYetValid);
}
if now >= self.valid_before {
return Err(Error::CertExpired);
}
Ok(())
}
pub fn check_principal(&self, name: &str) -> Result<()> {
if self.valid_principals.is_empty() {
return Ok(());
}
let matched = match self.cert_type {
CertType::Host => self
.valid_principals
.iter()
.any(|p| p.eq_ignore_ascii_case(name)),
CertType::User => self.valid_principals.iter().any(|p| p == name),
};
if matched {
Ok(())
} else {
Err(Error::CertPrincipalMismatch)
}
}
pub fn check_type(&self, want: CertType) -> Result<()> {
if self.cert_type == want {
Ok(())
} else {
Err(Error::CertTypeMismatch)
}
}
pub fn unknown_critical_options(&self) -> Vec<&str> {
self.critical_options
.iter()
.filter(|(name, _)| !matches!(name.as_str(), "force-command" | "source-address"))
.map(|(name, _)| name.as_str())
.collect()
}
pub fn require_known_critical_options(&self) -> Result<()> {
if self.unknown_critical_options().is_empty() {
Ok(())
} else {
Err(Error::CertUnknownCriticalOption)
}
}
pub fn critical_option(&self, name: &str) -> Option<&[u8]> {
self.critical_options
.iter()
.find(|(n, _)| n == name)
.map(|(_, d)| d.as_slice())
}
pub fn has_extension(&self, name: &str) -> bool {
self.extensions.iter().any(|(n, _)| n == name)
}
}
#[cfg(test)]
mod tests;