mod base62;
mod convert;
#[cfg(test)]
mod test;
use crc::{CRC_16_IBM_SDLC, Crc};
use num_enum::TryFromPrimitive;
use p256::NistP256;
use p256::elliptic_curve::pkcs8::der::Document;
use p256::elliptic_curve::pkcs8::{DecodePublicKey, SubjectPublicKeyInfoRef};
use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use p384::NistP384;
use p521::NistP521;
use primeorder::elliptic_curve::sec1::{EncodedPoint, ModulusSize};
use primeorder::elliptic_curve::{CurveArithmetic, FieldBytesSize};
use primeorder::{AffinePoint, PrimeCurveParams};
use ssh_key::public::{EcdsaPublicKey, RsaPublicKey};
use ssh_key::{EcdsaCurve, Mpint, PublicKey};
use std::fmt::{Display, Formatter};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ZqluError {
#[error("Failed to parse the provided input as a key: {0}")]
InvalidInput(String),
#[error("Zqlu does not yet support the supplied key type")]
UnsupportedKeyType,
}
#[derive(Error, Debug)]
pub enum ParsePublicKeyError {
#[error("Failed to parse the provided input as OpenSSH, zqlu, or PEM/SPKI public key text")]
InvalidInput {
openssh: ssh_key::Error,
zqlu: ZqluError,
pem: ZqluError,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub enum ZqluKeyType {
Ed25519 = b'A',
Rsa2048 = b'B',
P256Odd = b'C',
P256Even = b'D',
P384Odd = b'E',
P384Even = b'F',
P521Odd = b'G',
P521Even = b'H',
Rsa3072 = b'I',
Rsa4096 = b'J',
RsaExotic = b'K',
EXT = b'X',
}
impl ZqluKeyType {
fn from_curve_and_compressed_y(ecdsa_curve: EcdsaCurve, even: bool) -> Self {
if even {
match ecdsa_curve {
EcdsaCurve::NistP256 => ZqluKeyType::P256Even,
EcdsaCurve::NistP384 => ZqluKeyType::P384Even,
EcdsaCurve::NistP521 => ZqluKeyType::P521Even,
}
} else {
match ecdsa_curve {
EcdsaCurve::NistP256 => ZqluKeyType::P256Odd,
EcdsaCurve::NistP384 => ZqluKeyType::P384Odd,
EcdsaCurve::NistP521 => ZqluKeyType::P521Odd,
}
}
}
fn even(&self) -> bool {
matches!(
self,
ZqluKeyType::P256Even | ZqluKeyType::P384Even | ZqluKeyType::P521Even
)
}
}
#[derive(Debug)]
pub struct Zqlu(String, PublicKey);
impl Zqlu {}
macro_rules! bail_ii {
($msg:expr) => {
return Err(ZqluError::InvalidInput($msg.into()))
};
}
use ZqluError::UnsupportedKeyType;
pub(crate) use bail_ii;
impl Zqlu {
pub fn new(input: impl AsRef<str>) -> Result<Self, ZqluError> {
let input: String = input
.as_ref()
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let input = input.as_str();
if !input.starts_with("zq.lu") {
bail_ii!("Input must start with 'zq.lu'")
}
let key_type = try_get_type(input)?;
for c in input.chars().skip(6) {
if !c.is_ascii_alphanumeric() {
bail_ii!("Invalid character in Zqlu key")
}
}
let key_and_checksum = base62::decode(&input[6..]);
validate_crc(key_type, &key_and_checksum)?;
let key = &key_and_checksum[..key_and_checksum.len() - 2];
Ok(Zqlu(input.to_string(), to_public_key(key, key_type)?))
}
pub fn get_key_type(&self) -> ZqluKeyType {
try_get_type(&self.0).expect("not possible to create an instance with an invalid key type")
}
pub fn from_public_key(public_key: &PublicKey) -> Result<Zqlu, ZqluError> {
convert::from_public_key(public_key)
}
pub fn public_key(&self) -> &PublicKey {
&self.1
}
}
pub fn parse(input: impl AsRef<str>) -> Result<PublicKey, ParsePublicKeyError> {
let input = input.as_ref().trim();
match PublicKey::from_openssh(input) {
Ok(key) => Ok(key),
Err(openssh) => match Zqlu::new(input) {
Ok(zqlu) => Ok(zqlu.public_key().clone()),
Err(zqlu) => match parse_pem_public_key(input) {
Ok(key) => Ok(key),
Err(pem) => Err(ParsePublicKeyError::InvalidInput { openssh, zqlu, pem }),
},
},
}
}
fn parse_pem_public_key(input: &str) -> Result<PublicKey, ZqluError> {
if !input.starts_with("-----BEGIN PUBLIC KEY-----") {
bail_ii!("Input is not a PEM public key")
}
let pem = if input.ends_with('\n') {
input.to_owned()
} else {
format!("{input}\n")
};
let ed25519 = parse_ed25519_pem_public_key(&pem);
if let Ok(key) = ed25519 {
return Ok(key);
}
let p256 = parse_p256_pem_public_key(&pem);
if let Ok(key) = p256 {
return Ok(key);
}
let p384 = parse_p384_pem_public_key(&pem);
if let Ok(key) = p384 {
return Ok(key);
}
let p521 = parse_p521_pem_public_key(&pem);
if let Ok(key) = p521 {
return Ok(key);
}
Err(ZqluError::InvalidInput(format!(
"Failed to parse PEM public key as Ed25519 ({}) P-256 ({}) P-384 ({}) or P-521 ({})",
match ed25519 {
Err(err) => err,
Ok(_) => unreachable!(),
},
match p256 {
Err(err) => err,
Ok(_) => unreachable!(),
},
match p384 {
Err(err) => err,
Ok(_) => unreachable!(),
},
match p521 {
Err(err) => err,
Ok(_) => unreachable!(),
}
)))
}
fn parse_ed25519_pem_public_key(input: &str) -> Result<PublicKey, ZqluError> {
let (_label, doc) = Document::from_pem(input)
.map_err(|err| ZqluError::InvalidInput(format!("Failed to parse PEM public key: {err}")))?;
let spki = SubjectPublicKeyInfoRef::try_from(doc.as_bytes()).map_err(|err| {
ZqluError::InvalidInput(format!("Failed to parse SPKI public key: {err}"))
})?;
if spki.algorithm.oid
!= p256::elliptic_curve::pkcs8::ObjectIdentifier::new_unwrap("1.3.101.112")
{
bail_ii!("PEM public key is not Ed25519")
}
if spki.algorithm.parameters.is_some() {
bail_ii!("Ed25519 PEM public key must not have algorithm parameters")
}
let key_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| {
ZqluError::InvalidInput("Ed25519 PEM public key contains an invalid bit string".into())
})?;
let key = ssh_key::public::Ed25519PublicKey::try_from(key_bytes).map_err(|err| {
ZqluError::InvalidInput(format!("Failed to parse Ed25519 public key: {err}"))
})?;
Ok(PublicKey::from(key))
}
fn parse_p256_pem_public_key(input: &str) -> Result<PublicKey, ZqluError> {
let key = p256::PublicKey::from_public_key_pem(input)
.map_err(|err| ZqluError::InvalidInput(format!("Failed to parse PEM public key: {err}")))?;
let encoded = key.to_encoded_point(false);
let key = EcdsaPublicKey::from_sec1_bytes(encoded.as_bytes()).map_err(|err| {
ZqluError::InvalidInput(format!("Failed to parse SEC1 public key: {err}"))
})?;
Ok(PublicKey::from(key))
}
fn parse_p384_pem_public_key(input: &str) -> Result<PublicKey, ZqluError> {
let key = p384::PublicKey::from_public_key_pem(input)
.map_err(|err| ZqluError::InvalidInput(format!("Failed to parse PEM public key: {err}")))?;
let encoded = key.to_encoded_point(false);
let key = EcdsaPublicKey::from_sec1_bytes(encoded.as_bytes()).map_err(|err| {
ZqluError::InvalidInput(format!("Failed to parse SEC1 public key: {err}"))
})?;
Ok(PublicKey::from(key))
}
fn parse_p521_pem_public_key(input: &str) -> Result<PublicKey, ZqluError> {
let key = p521::PublicKey::from_public_key_pem(input)
.map_err(|err| ZqluError::InvalidInput(format!("Failed to parse PEM public key: {err}")))?;
let encoded = key.to_encoded_point(false);
let key = EcdsaPublicKey::from_sec1_bytes(encoded.as_bytes()).map_err(|err| {
ZqluError::InvalidInput(format!("Failed to parse SEC1 public key: {err}"))
})?;
Ok(PublicKey::from(key))
}
fn to_public_key(key: &[u8], key_type: ZqluKeyType) -> Result<PublicKey, ZqluError> {
let even = key_type.even();
Ok(match key_type {
ZqluKeyType::Ed25519 => {
if key.len() != 32 {
bail_ii!("Invalid Ed25519 key length")
}
let Ok(key) = ssh_key::public::Ed25519PublicKey::try_from(key) else {
bail_ii!("Invalid Ed25519 key")
};
PublicKey::from(key)
}
ZqluKeyType::P256Odd | ZqluKeyType::P256Even => {
if key.len() != 32 {
bail_ii!("Invalid P256 key length")
}
let encoded = decompress::<NistP256>(key, even)?;
PublicKey::from(EcdsaPublicKey::NistP256(encoded))
}
ZqluKeyType::P384Odd | ZqluKeyType::P384Even => {
if key.len() != 48 {
bail_ii!("Invalid P384 key length")
}
let encoded = decompress::<NistP384>(key, even)?;
PublicKey::from(EcdsaPublicKey::NistP384(encoded))
}
ZqluKeyType::P521Odd | ZqluKeyType::P521Even => {
if key.len() != 66 {
bail_ii!("Invalid P521 key length")
}
let encoded = decompress::<NistP521>(key, even)?;
PublicKey::from(EcdsaPublicKey::NistP521(encoded))
}
ZqluKeyType::Rsa2048 => rsa_from_fixed_modulus(key, 256)?,
ZqluKeyType::Rsa3072 => rsa_from_fixed_modulus(key, 384)?,
ZqluKeyType::Rsa4096 => rsa_from_fixed_modulus(key, 512)?,
ZqluKeyType::RsaExotic => rsa_from_length_value(key)?,
_ => return Err(UnsupportedKeyType),
})
}
fn rsa_from_fixed_modulus(modulus: &[u8], expected_len: usize) -> Result<PublicKey, ZqluError> {
if modulus.len() != expected_len {
bail_ii!("Invalid RSA modulus length")
}
let e = Mpint::from_positive_bytes(&[0x01, 0x00, 0x01])
.map_err(|err| ZqluError::InvalidInput(format!("Invalid RSA exponent: {err}")))?;
let n = Mpint::from_positive_bytes(modulus)
.map_err(|err| ZqluError::InvalidInput(format!("Invalid RSA modulus: {err}")))?;
Ok(PublicKey::from(RsaPublicKey { e, n }))
}
fn rsa_from_length_value(mut key: &[u8]) -> Result<PublicKey, ZqluError> {
let exponent = read_length_value(&mut key, "RSA exponent")?;
let modulus = read_length_value(&mut key, "RSA modulus")?;
if !key.is_empty() {
bail_ii!("Trailing data in RSA key")
}
let e = Mpint::from_positive_bytes(exponent)
.map_err(|err| ZqluError::InvalidInput(format!("Invalid RSA exponent: {err}")))?;
let n = Mpint::from_positive_bytes(modulus)
.map_err(|err| ZqluError::InvalidInput(format!("Invalid RSA modulus: {err}")))?;
Ok(PublicKey::from(RsaPublicKey { e, n }))
}
fn read_length_value<'a>(input: &mut &'a [u8], field_name: &str) -> Result<&'a [u8], ZqluError> {
let len = read_varint(input)?;
if len == 0 {
return Err(ZqluError::InvalidInput(format!("{field_name} is empty")));
}
if input.len() < len {
return Err(ZqluError::InvalidInput(format!(
"{field_name} length exceeds remaining RSA key data"
)));
}
let (value, rest) = input.split_at(len);
*input = rest;
if value.first().copied() == Some(0) {
return Err(ZqluError::InvalidInput(format!(
"{field_name} contains a leading zero octet"
)));
}
Ok(value)
}
fn read_varint(input: &mut &[u8]) -> Result<usize, ZqluError> {
let mut value = 0usize;
let mut shift = 0usize;
for (index, byte) in input.iter().copied().enumerate() {
let low_bits = usize::from(byte & 0x7f);
value = value
.checked_add(
low_bits
.checked_shl(shift as u32)
.ok_or_else(|| ZqluError::InvalidInput("RSA length varint overflow".into()))?,
)
.ok_or_else(|| ZqluError::InvalidInput("RSA length varint overflow".into()))?;
if byte & 0x80 == 0 {
*input = &input[index + 1..];
return Ok(value);
}
shift = shift
.checked_add(7)
.ok_or_else(|| ZqluError::InvalidInput("RSA length varint overflow".into()))?;
if shift >= usize::BITS as usize {
bail_ii!("RSA length varint overflow")
}
}
bail_ii!("Truncated RSA length varint")
}
fn decompress<C>(x: &[u8], y_is_even: bool) -> Result<EncodedPoint<C>, ZqluError>
where
C: PrimeCurveParams + CurveArithmetic,
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
FieldBytesSize<C>: ModulusSize,
{
let mut compressed = Vec::with_capacity(x.len() + 1);
compressed.push(if y_is_even { 0x02 } else { 0x03 });
compressed.extend_from_slice(x);
let encoded = EncodedPoint::<C>::from_bytes(compressed)
.map_err(|_| ZqluError::InvalidInput("Invalid key data".into()))?;
let point: AffinePoint<C> = AffinePoint::from_encoded_point(&encoded)
.into_option()
.ok_or(ZqluError::InvalidInput("Invalid key data".into()))?;
Ok(point.to_encoded_point(false))
}
fn validate_crc(zqlu_key_type: ZqluKeyType, key_and_checksum: &[u8]) -> Result<(), ZqluError> {
let crc = Crc::<u16>::new(&CRC_16_IBM_SDLC);
let mut crc = crc.digest();
crc.update(b"zq.lu");
crc.update(&[zqlu_key_type as u8]);
crc.update(&key_and_checksum[..key_and_checksum.len() - 2]);
let checksum: [u8; 2] = key_and_checksum[key_and_checksum.len() - 2..]
.try_into()
.unwrap();
if crc.finalize() != u16::from_be_bytes(checksum) {
bail_ii!("Invalid CRC")
}
Ok(())
}
fn try_get_type(input: &str) -> Result<ZqluKeyType, ZqluError> {
let Some(key_type) = input.chars().nth(5) else {
bail_ii!("Input is too short to contain a key type")
};
ZqluKeyType::try_from_primitive(key_type as u8).map_err(|_| UnsupportedKeyType)
}
impl Display for Zqlu {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq<&str> for Zqlu {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
#[cfg(test)]
mod tests {
use crate::test::str;
use crate::{ParsePublicKeyError, Zqlu, ZqluError, parse};
use anyhow::Result;
fn assert_pem_roundtrip(input: &str, openssh_prefix: &str) -> Result<()> {
let key = parse(input)?;
let openssh = key.to_openssh()?;
assert!(openssh.starts_with(openssh_prefix));
let zqlu = Zqlu::from_public_key(&key)?;
assert_eq!(zqlu.public_key().to_openssh()?, openssh);
Ok(())
}
#[test]
fn test_zqlu_new() -> Result<()> {
Zqlu::new("zq.luDCGME4UXnRi5W6z3rr8tUTR8dINmiDFB3Y32Jb9ivQ3wC4S")?;
assert!(matches!(Zqlu::new("test"), Err(ZqluError::InvalidInput(_))));
assert!(matches!(
Zqlu::new("zq.lu"),
Err(ZqluError::InvalidInput(_))
));
assert!(matches!(
Zqlu::new("zq.luYwhatever"),
Err(ZqluError::UnsupportedKeyType)
));
assert!(matches!(
Zqlu::new("zq.luA??"),
Err(ZqluError::InvalidInput(_))
));
assert!(matches!(
Zqlu::new("zq.luYdeadbeef"),
Err(ZqluError::UnsupportedKeyType)
));
assert!(matches!(
Zqlu::new("zq.luDCGME4UXnRi5W6z3rr8tUTR8dINmiDFB3Y32Jb9ivQ3wC4Y"),
Err(ZqluError::InvalidInput(_))
));
Ok(())
}
#[test]
fn test_parse_openssh() -> Result<()> {
let key = parse(str!("ed25519.openssh"))?;
assert_eq!(key.to_openssh()?, str!("ed25519.openssh"));
Ok(())
}
#[test]
fn test_parse_zqlu() -> Result<()> {
let key = parse(str!("ed25519.zq"))?;
assert_eq!(key.to_openssh()?, str!("ed25519.openssh"));
Ok(())
}
#[test]
fn test_parse_zqlu_with_embedded_whitespace() -> Result<()> {
let input = str!("ed25519.zq");
let embedded = format!(
"{}\n {} \t{}\r{}",
&input[..10],
&input[10..20],
&input[20..30],
&input[30..],
);
let key = parse(embedded)?;
assert_eq!(key.to_openssh()?, str!("ed25519.openssh"));
for position in 0..=4 {
let input = format!("{}\n \t\r{}", &input[..position], &input[position..]);
let key = parse(input)?;
assert_eq!(key.to_openssh()?, str!("ed25519.openssh"));
}
let input = format!("{}\u{00a0}{}", &input[..10], &input[10..]);
let key = parse(input)?;
assert_eq!(key.to_openssh()?, str!("ed25519.openssh"));
Ok(())
}
#[test]
fn test_parse_ed25519_pem() -> Result<()> {
assert_pem_roundtrip(str!("ed25519.pem"), "ssh-ed25519 ")
}
#[test]
fn test_parse_p256_pem() -> Result<()> {
assert_pem_roundtrip(str!("p256.pem"), "ecdsa-sha2-nistp256 ")
}
#[test]
fn test_parse_p384_pem() -> Result<()> {
assert_pem_roundtrip(str!("p384.pem"), "ecdsa-sha2-nistp384 ")
}
#[test]
fn test_parse_p521_pem() -> Result<()> {
assert_pem_roundtrip(str!("p521.pem"), "ecdsa-sha2-nistp521 ")
}
#[test]
fn test_parse_trimmed() -> Result<()> {
let key = parse(format!("\n {}\n", str!("ed25519.zq")))?;
assert_eq!(key.to_openssh()?, str!("ed25519.openssh"));
Ok(())
}
#[test]
fn test_parse_invalid() {
assert!(matches!(
parse("not a key"),
Err(ParsePublicKeyError::InvalidInput { .. })
));
}
#[test]
fn test_zqlu_to_public_key_ed25519() -> Result<()> {
let zqlu = Zqlu::new(str!("ed25519.zq"))?;
let public_key = zqlu.public_key();
assert_eq!(public_key.to_openssh()?, str!("ed25519.openssh"));
Ok(())
}
#[test]
fn test_zqlu_to_public_key_p256() -> Result<()> {
let zqlu = Zqlu::new(str!("p256.zq"))?;
let public_key = zqlu.public_key();
assert_eq!(public_key.to_openssh()?, str!("p256.openssh"));
Ok(())
}
#[test]
fn test_zqlu_to_public_key_p384() -> Result<()> {
let zqlu = Zqlu::new(str!("p384.zq"))?;
let public_key = zqlu.public_key();
assert_eq!(public_key.to_openssh()?, str!("p384.openssh"));
Ok(())
}
#[test]
fn test_zqlu_to_public_key_p521() -> Result<()> {
let zqlu = Zqlu::new(str!("p521.zq"))?;
let public_key = zqlu.public_key();
assert_eq!(public_key.to_openssh()?, str!("p521.openssh"));
Ok(())
}
}