osshkeys 0.7.0

A library to read and write OpenSSH public and private keys
Documentation
use crate::error::*;
use crate::keys::{dsa::*, ecdsa::*, ed25519::*, rsa::*, PublicKey, PublicParts};
use crate::sshbuf::{SshReadExt, SshWriteExt};
use base64::prelude::*;
use ed25519_dalek::VerifyingKey as Ed25519PubKey;
use ed25519_dalek::PUBLIC_KEY_LENGTH;
use openssl::bn::BigNumContext;
use openssl::dsa::DsaRef;
use openssl::ec::{EcKeyRef, PointConversionForm};
use openssl::pkey::{HasParams, HasPublic};
use openssl::rsa::RsaRef;
use std::fmt::Write as _;
use std::io;
use std::str::FromStr;

pub fn parse_ossh_pubkey(keystr: &str) -> OsshResult<PublicKey> {
    let key_split: Vec<&str> = keystr.split_ascii_whitespace().collect();
    if key_split.len() < 2 || key_split.len() > 3 {
        return Err(ErrorKind::InvalidKeyFormat.into());
    }
    let blob = BASE64_STANDARD.decode(key_split[1])?;
    let mut pubkey: PublicKey = match key_split[0] {
        RSA_NAME | RSA_SHA256_NAME | RSA_SHA512_NAME => {
            let mut rsa = decode_rsa_pubkey(&blob)?;
            rsa.set_sign_type(RsaSignature::from_name(key_split[0]).unwrap());
            rsa.into()
        }
        DSA_NAME => decode_dsa_pubkey(&blob)?.into(),
        NIST_P256_NAME => decode_ecdsa_pubkey(&blob, Some(EcCurve::Nistp256))?.into(),
        NIST_P384_NAME => decode_ecdsa_pubkey(&blob, Some(EcCurve::Nistp384))?.into(),
        NIST_P521_NAME => decode_ecdsa_pubkey(&blob, Some(EcCurve::Nistp521))?.into(),
        ED25519_NAME => decode_ed25519_pubkey(&blob)?.into(),
        _ => return Err(ErrorKind::UnsupportType.into()),
    };
    if key_split.len() == 3 {
        *pubkey.comment_mut() = key_split[2].to_owned();
    }
    Ok(pubkey)
}

pub(crate) fn decode_rsa_pubkey(keyblob: &[u8]) -> OsshResult<RsaPublicKey> {
    let mut reader = io::Cursor::new(keyblob);
    let keyname = reader.read_utf8()?;
    if keyname != RSA_NAME && keyname != RSA_SHA256_NAME && keyname != RSA_SHA512_NAME {
        return Err(ErrorKind::TypeNotMatch.into());
    }
    let e = reader.read_mpint()?;
    let n = reader.read_mpint()?;

    Ok(RsaPublicKey::new(n, e)?)
}

pub(crate) fn decode_dsa_pubkey(keyblob: &[u8]) -> OsshResult<DsaPublicKey> {
    let mut reader = io::Cursor::new(keyblob);
    if reader.read_utf8()? != DSA_NAME {
        return Err(ErrorKind::TypeNotMatch.into());
    }

    let p = reader.read_mpint()?;
    let q = reader.read_mpint()?;
    let g = reader.read_mpint()?;
    let y = reader.read_mpint()?;

    Ok(DsaPublicKey::new(p, q, g, y)?)
}

pub(crate) fn decode_ecdsa_pubkey(
    keyblob: &[u8],
    curve_hint: Option<EcCurve>,
) -> OsshResult<EcDsaPublicKey> {
    let mut reader = io::Cursor::new(keyblob);
    let curve = if reader.read_utf8()?.starts_with("ecdsa-sha2-") {
        let ident_str = reader.read_utf8()?;
        EcCurve::from_str(&ident_str).map_err(|_| ErrorKind::UnsupportCurve)?
    } else {
        return Err(ErrorKind::TypeNotMatch.into());
    };
    if let Some(curve_hint) = curve_hint {
        if curve != curve_hint {
            return Err(ErrorKind::TypeNotMatch.into());
        }
    }
    let pub_key = reader.read_string()?;
    EcDsaPublicKey::from_bytes(curve, &pub_key)
}

pub(crate) fn decode_ed25519_pubkey(keyblob: &[u8]) -> OsshResult<Ed25519PublicKey> {
    let mut reader = io::Cursor::new(keyblob);
    if reader.read_utf8()? != ED25519_NAME {
        return Err(ErrorKind::TypeNotMatch.into());
    }

    let pub_key = reader.read_string()?;
    if pub_key.len() != PUBLIC_KEY_LENGTH {
        return Err(ErrorKind::InvalidKeySize.into());
    }

    Ok(Ed25519PublicKey::new(
        pub_key.as_slice().try_into().unwrap(),
    )?)
}

pub fn serialize_ossh_pubkey(key: &dyn PublicParts, comment: &str) -> OsshResult<String> {
    let mut keystr = String::new();
    write!(
        &mut keystr,
        "{} {}",
        key.keyname(),
        BASE64_STANDARD.encode(key.blob()?)
    )?;
    if !comment.is_empty() {
        write!(&mut keystr, " {}", comment)?;
    }
    Ok(keystr)
}

pub(crate) fn encode_rsa_pubkey<T: HasPublic + HasParams>(key: &RsaRef<T>) -> OsshResult<Vec<u8>> {
    let mut buf = io::Cursor::new(Vec::new());

    buf.write_utf8(RSA_NAME)?;
    buf.write_mpint(key.e())?;
    buf.write_mpint(key.n())?;

    Ok(buf.into_inner())
}

pub(crate) fn encode_dsa_pubkey<T: HasPublic + HasParams>(key: &DsaRef<T>) -> OsshResult<Vec<u8>> {
    let mut buf = io::Cursor::new(Vec::new());

    buf.write_utf8(DSA_NAME)?;
    buf.write_mpint(key.p())?;
    buf.write_mpint(key.q())?;
    buf.write_mpint(key.g())?;
    buf.write_mpint(key.pub_key())?;

    Ok(buf.into_inner())
}

pub(crate) fn encode_ecdsa_pubkey<T: HasPublic + HasParams>(
    curve: EcCurve,
    key: &EcKeyRef<T>,
) -> OsshResult<Vec<u8>> {
    let mut buf = io::Cursor::new(Vec::new());
    let mut bn_ctx = BigNumContext::new()?;

    buf.write_utf8(curve.name())?;
    buf.write_utf8(curve.ident())?;
    buf.write_string(&key.public_key().to_bytes(
        key.group(),
        PointConversionForm::UNCOMPRESSED,
        &mut bn_ctx,
    )?)?;

    Ok(buf.into_inner())
}

pub(crate) fn encode_ed25519_pubkey(pub_key: &Ed25519PubKey) -> OsshResult<Vec<u8>> {
    let mut buf = io::Cursor::new(Vec::new());

    buf.write_utf8(ED25519_NAME)?;
    buf.write_string(pub_key.as_bytes())?;

    Ok(buf.into_inner())
}

#[cfg(test)]
mod test {
    use super::*;

    const DSA_PUBKEY: &str = "ssh-dss AAAAB3NzaC1kc3MAAACBAORLYnYacOdGmSJ99aZ+j2UqtQldYNHvAVVAI42wt/T/GTkg8cXdwwQ8HSJyD6T1e9ebnCXZd/YItX8DCPIP5GLUHVZy5zzKSzwga7zEjKP2j3JZGLAzFIUpStwQ8gur3zmh5DYi7JOdc/kWNpjT86n4fnrP+s8ZxuVDO5bbSasHAAAAFQD62yfFzJxz313aoIVgoMFoz8cF/wAAAIEAj7rvQz2hmuRyFUZIGWpwVHoR3y3SoQjEryX4ZtzwL04ROIXHSKJeOY9cdu2l5fMVYiMBtfWTQTlltFl1H//0hG/g5KBLhhwQ3Y7ul4Q8wsCWZJZeP3jtcO7+p3BLyMa6vvv5ptnMH+jRMgX5wwdszqogk4jCT+7fM2p6brMGccoAAACAD9qfPNxRo+npg+troNZ/FoYJezECqxg0jUyHWClACt7gS0W+r3dJIn9te6Xi7UFGPrLWJtlC++8i27m2FTS0sQUljM2NmRaf6jrCAhwPaJ0ievPJm5kBQmprTqBbdzCNRpI1+hceAnoHbajRwLueFwpoVOy2QjTkvBzd84Oobtw= osshkeys_dsa-test";
    const RSA_PUBKEY: &str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9NCtKoC/4Gk+zS8XGtA5aGC9BeFfcOCg/9C14ph4oHVXzWlR5t3HdHJK6EJGLlC6fj5vI+6cviX7NUbXJXQ/hJe4m4c5AGzubX/jfzNTjBa+hB+5CEqSztA20aHgEWzBwoakhkOd0knT6IvHV/vqTzHVbtfWIiof2SenyHv7yD9RbS9SCmkjISi4wQWzJ1Yu0O1CbH/U1c18WnP46/HBiaJcmV9hk/L3vjSoI7kpjXfSq4d3KLnwsUdrFdhh3eN7K4/ZdnrZC8n1liDXyMAWiaAL8cu8K5wmBmnHTcqIwxYu7g+k46OzcaZxVy0i9hFBM2bzvGvsCJOF3Hh6zF15p osshkeys_rsa-test";
    const RSA256_PUBKEY: &str = "rsa-sha2-256 AAAAB3NzaC1yc2EAAAADAQABAAABAQC9NCtKoC/4Gk+zS8XGtA5aGC9BeFfcOCg/9C14ph4oHVXzWlR5t3HdHJK6EJGLlC6fj5vI+6cviX7NUbXJXQ/hJe4m4c5AGzubX/jfzNTjBa+hB+5CEqSztA20aHgEWzBwoakhkOd0knT6IvHV/vqTzHVbtfWIiof2SenyHv7yD9RbS9SCmkjISi4wQWzJ1Yu0O1CbH/U1c18WnP46/HBiaJcmV9hk/L3vjSoI7kpjXfSq4d3KLnwsUdrFdhh3eN7K4/ZdnrZC8n1liDXyMAWiaAL8cu8K5wmBmnHTcqIwxYu7g+k46OzcaZxVy0i9hFBM2bzvGvsCJOF3Hh6zF15p osshkeys_rsa-test";
    const ECDSA_PUBKEY: &str = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKtcK82cEoqjiXyqPpyQAlkOQYs8LL5dDahPah5dqoaJfVHcKS5CJYBX0Ow+Dlj9xKtSQRCyJXOCEtJx+k4LUV0= osshkeys_ecdsa-test";
    const ED25519_PUBKEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMoWBluPErgKhNja3lHEf7ie6AVzR24mPRd742xEYodC osshkeys_ed25519-test";

    #[test]
    fn dsa_publickey_parse_serialize() {
        let dsa = parse_ossh_pubkey(DSA_PUBKEY).unwrap();
        assert_eq!(dsa.comment(), "osshkeys_dsa-test");
        let dsa_string = serialize_ossh_pubkey(&dsa, dsa.comment()).unwrap();
        assert_eq!(&dsa_string, DSA_PUBKEY);
    }

    #[test]
    fn rsa_publickey_parse_serialize() {
        let rsa = parse_ossh_pubkey(RSA_PUBKEY).unwrap();
        assert_eq!(rsa.comment(), "osshkeys_rsa-test");
        let rsa_string = serialize_ossh_pubkey(&rsa, rsa.comment()).unwrap();
        assert_eq!(&rsa_string, RSA_PUBKEY);
    }

    #[test]
    fn rsa256_publickey_parse_serialize() {
        let rsa = parse_ossh_pubkey(RSA256_PUBKEY).unwrap();
        assert_eq!(rsa.comment(), "osshkeys_rsa-test");
        let rsa_string = serialize_ossh_pubkey(&rsa, rsa.comment()).unwrap();
        assert_eq!(&rsa_string, RSA256_PUBKEY);
    }

    #[test]
    fn ecdsa_publickey_parse_serialize() {
        let ecdsa = parse_ossh_pubkey(ECDSA_PUBKEY).unwrap();
        assert_eq!(ecdsa.comment(), "osshkeys_ecdsa-test");
        let ecdsa_string = serialize_ossh_pubkey(&ecdsa, ecdsa.comment()).unwrap();
        assert_eq!(&ecdsa_string, ECDSA_PUBKEY);
    }

    #[test]
    fn ed25519_publickey_parse_serialize() {
        let ed25519 = parse_ossh_pubkey(ED25519_PUBKEY).unwrap();
        assert_eq!(ed25519.comment(), "osshkeys_ed25519-test");
        let ed25519_string = serialize_ossh_pubkey(&ed25519, ed25519.comment()).unwrap();
        assert_eq!(&ed25519_string, ED25519_PUBKEY);
    }
}