Security

SSH keys

OpenSSH public keys read: type, size, comment and SHA256 fingerprint as ssh-keygen and GitHub show them, and authorized_keys files.

Version 1.0.0 · 3 functions · 2 crates

#ssh#keys#settings#developer

Functions

What use ssh_keys.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use ssh_key_info, ssh_authorized_keys from ssh_keys (Quillo 0.6).

ssh_key_info(line: str) → anyor an error

One public key, as in id_ed25519.pub: { algorithm, type, bits, comment, fingerprint, md5, weak }. fingerprint is SHA256:... as ssh-keygen -l and GitHub show it; weak is true for DSA and for RSA under 2048 bits. A private key is refused.

ssh_authorized_keys(text: str) → listor an error

The keys of an authorized_keys file (or several .pub lines): each as ssh_key_info gives it, with options (from="...", command="...") or null. Blank lines and # comments are skipped; a line that is not a key is an error naming its number.

ssh_key_same(a: str, b: str) → boolor an error

Whether two lines hold the same key, whatever their comments: to refuse a key that is already on the account.

Get it

$ quillo install ssh_keys               # ssh_keys.qwer into this folder$ quillo install ssh_keys --example     # and its example, ssh_keys.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/ssh_keys.qwer$ curl -O https://quillolang.org/plugins/files/ssh_keys.qwe$ curl -O https://quillolang.org/plugins/files/ssh_keys_sample.pub

Then, in your program, all of it or only what you call:

main.qwe
use ssh_keys.qweruse ssh_key_info, ssh_authorized_keys from ssh_keys      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
ssh-key = { version = "0.6", default-features = false, features = ["std", "ecdsa"] }md-5 = "0.10"

Example

ssh_keys.qwe
// An "SSH keys" settings page: a pasted key checked and shown as GitHub shows it.////   quillo run ssh_keys.qwe      then open http://localhost:8080/keysuse "ssh_keys.qwer"let KEYS = read_file("ssh_keys_sample.pub")?route GET /keys = ssh_authorized_keys(KEYS)? | map((k) => { title: k.comment, type: k.type, bits: k.bits, fingerprint: k.fingerprint })route POST /keys { form: { key: str } } {    let k = ssh_key_info(key)    if is_err(k) { reply 400 k.error }    if k.weak { reply 400 "Use an Ed25519 key, or RSA of 2048 bits or more." }    if any(split(KEYS, "\n") | filter((l) => trim(l) != ""), (l) => ssh_key_same(l, key)?) { reply 409 "That key is already added." }    { added: k.fingerprint }}route GET /check {    let first = split(KEYS, "\n")[0]    {        info: ssh_key_info(first)?,        same_with_other_comment: ssh_key_same(first, replace(first, "ada@laptop", "ada@desktop"))?,        private_refused: ssh_key_info("-----BEGIN OPENSSH PRIVATE KEY-----").error,    }}
$ quillo run ssh_keys.qwe$ curl 'localhost:8080/keys'$ curl 'localhost:8080/check'

Source

ssh_keys.qwer
//! OpenSSH public keys read: their type, size, comment and SHA256 fingerprint as `ssh-keygen -l` shows it, and authorized_keys files, for "add your SSH key" pages.//! version: 1.0.0//!//! [dependencies]//! ssh-key = { version = "0.6", default-features = false, features = ["std", "ecdsa"] }//! md-5 = "0.10"use md5::{Digest, Md5};use serde_json::{json, Value};use ssh_key::public::KeyData;use ssh_key::{HashAlg, PublicKey};fn bits(k: &KeyData) -> i64 {    match k {        KeyData::Rsa(r) => (r.n.as_positive_bytes().map_or(0, |b| b.len()) * 8) as i64,        KeyData::Ecdsa(e) => match e.curve().as_str() {            "nistp384" => 384,            "nistp521" => 521,            _ => 256,        },        KeyData::Ed25519(_) | KeyData::SkEd25519(_) => 256,        KeyData::SkEcdsaSha2NistP256(_) => 256,        _ => 0,    }}fn describe(key: &PublicKey, options: Option<String>) -> Value {    let md5: Vec<String> = key.to_bytes().map(|b| Md5::digest(&b).iter().map(|x| format!("{:02x}", x)).collect()).unwrap_or_default();    let alg = key.algorithm();    let kind = match alg.as_str() {        "ssh-ed25519" => "ED25519",        "ssh-rsa" => "RSA",        "ssh-dss" => "DSA",        s if s.starts_with("ecdsa-") => "ECDSA",        s if s.starts_with("sk-ssh-ed25519") => "ED25519-SK",        s if s.starts_with("sk-ecdsa") => "ECDSA-SK",        _ => "OTHER",    };    let b = bits(key.key_data());    json!({        "algorithm": alg.as_str(),        "type": kind,        "bits": b,        "comment": key.comment(),        "fingerprint": key.fingerprint(HashAlg::Sha256).to_string(),        "md5": format!("MD5:{}", md5.join(":")),        "options": options,        "weak": kind == "DSA" || (kind == "RSA" && b < 2048),    })}fn parse(line: &str) -> Result<PublicKey, String> {    let line = line.trim();    if line.len() > 16_384 {        return Err("a public key line is at most 16 KB".into());    }    if line.contains("PRIVATE KEY") {        return Err("this is a private key: paste the .pub file, never the private one".into());    }    PublicKey::from_openssh(line).map_err(|e| format!("not an OpenSSH public key (ssh-ed25519 AAAA... comment): {}", e))}/// One public key, as in `id_ed25519.pub`: `{ algorithm, type, bits, comment, fingerprint, md5, weak }`. `fingerprint` is/// `SHA256:...` as `ssh-keygen -l` and GitHub show it; `weak` is true for DSA and for RSA under 2048 bits. A private key is refused.pub fn ssh_key_info(line: &str) -> Result<Value, String> {    Ok(describe(&parse(line)?, None))}/// The keys of an authorized_keys file (or several .pub lines): each as ssh_key_info gives it, with `options` (`from="..."`,/// `command="..."`) or null. Blank lines and `#` comments are skipped; a line that is not a key is an error naming its number.pub fn ssh_authorized_keys(text: &str) -> Result<Vec<Value>, String> {    let mut out = vec![];    for (i, line) in text.lines().enumerate().take(10_000) {        let l = line.trim();        if l.is_empty() || l.starts_with('#') {            continue;        }        // A key line starts with its algorithm; anything before it is options.        let words: Vec<&str> = l.split_whitespace().collect();        let at = words.iter().position(|w| w.starts_with("ssh-") || w.starts_with("ecdsa-") || w.starts_with("sk-"));        let (opts, key) = match at {            Some(0) => (None, l.to_string()),            Some(n) => (Some(words[..n].join(" ")), words[n..].join(" ")),            None => return Err(format!("line {}: no key on it", i + 1)),        };        out.push(describe(&parse(&key).map_err(|e| format!("line {}: {}", i + 1, e))?, opts));    }    Ok(out)}/// Whether two lines hold the same key, whatever their comments: to refuse a key that is already on the account.pub fn ssh_key_same(a: &str, b: &str) -> Result<bool, String> {    Ok(parse(a)?.key_data() == parse(b)?.key_data())}
SSH certificates ssh_certs.qwer · 1.0.0Read OpenSSH certificates with their principals, validity, critical options and CA, check them as sshd would, and sign new ones with a CA key, for short-lived access. Differs from ssh_keys, which reads plain keys.sshcertificateskeysauthentication4 views · 6 downloadsHPKE hpke.qwer · 1.0.0Hybrid Public Key Encryption (RFC 9180): seal to a public key and open with the private one, in base, PSK, auth and auth-PSK modes, on X25519, P-256, P-384 and P-521 with AES-GCM or ChaCha20-Poly1305, with secrets exported, checked against all 28 of the RFC's test vectors.encryptioncryptographykeys3 views · 6 downloadsLicence keys license_keys.qwer · 1.0.0Signed licence keys with a customer, features and an expiry date: made with your private key, checked offline, impossible to forge.licensingsigningpaymentskeys6 views · 6 downloadsPKCS#12 files pkcs12.qwer · 1.0.0Read .p12 and .pfx files in pure Rust, key and chain out in order, MAC and password checked, modern PBES2 with AES, and write ones OpenSSL opens. The old RC2, RC4 and 3DES ciphers are refused by name, with how to convert the file.certificateskeystlsfiles2 views · 8 downloadsRSA rsa.qwer · 1.0.0RSA key pairs as PEM, PKCS#1 v1.5 SHA-256 signatures for partner and bank APIs, and OAEP encryption of short secrets.signingencryptionkeysapi9 views · 8 downloadsShamir shares shamir.qwer · 1.0.0Shamir's secret sharing: a secret split into N shares, any K of which bring it back, for recovery keys held by several people.secretsrecoverykeysbackups7 views · 7 downloads
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On SSH keys: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.