Security

RSA

RSA key pairs as PEM, PKCS#1 v1.5 SHA-256 signatures for partner and bank APIs, and OAEP encryption of short secrets.

Version 1.0.0 · 6 functions · 4 crates

#signing#encryption#keys#api

Functions

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

rsa_keypair(bits: int) → mapor an error

A new key pair of bits (2048 to 4096; 2048 below, 4096 above): { private, public } as PEM (PKCS#8 and SPKI, the BEGIN PRIVATE KEY and BEGIN PUBLIC KEY that OpenSSL writes). 2048 bits takes a fraction of a second; 4096, a few seconds.

rsa_public_key(private_pem: str) → stror an error

The public key (SPKI PEM) of a private key (PKCS#8 or PKCS#1 PEM).

rsa_sign(text: str, private_pem: str) → stror an error

The signature of text with the private key: RSASSA-PKCS1-v1_5 over SHA-256, as base64.

rsa_verify(text: str, signature: str, public_pem: str) → boolor an error

Whether signature (base64) is text signed with the private key of public_pem (SPKI or PKCS#1 PEM, or a certificate's key taken out with the x509 plugin). False for a changed text or a wrong signature.

rsa_encrypt(text: str, public_pem: str) → stror an error

text encrypted to the public key with OAEP (SHA-256), as base64. RSA encrypts short secrets only: up to 190 bytes with a 2048-bit key. For anything longer, encrypt it with a random AES key (the encrypt plugin) and this the key.

rsa_decrypt(encrypted: str, private_pem: str) → stror an error

The text back from rsa_encrypt (or from OpenSSL's pkeyutl -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256). The crate's decryption is not constant-time (RUSTSEC-2023-0071): do not decrypt what strangers send, many times over the network.

Get it

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

Or with curl:

$ curl -O https://quillolang.org/plugins/files/rsa.qwer$ curl -O https://quillolang.org/plugins/files/rsa.qwe

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

main.qwe
use rsa.qweruse rsa_keypair, rsa_public_key from rsa      // Quillo 0.6: only these

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

[dependencies]
rsa = { version = "0.9", features = ["sha2"] }sha2 = { version = "0.10", features = ["oid"] }rand = "0.8"base64 = "0.22"

Example

rsa.qwe
// Signing requests to a partner's API with an RSA key, and a secret encrypted to its public key.////   quillo run rsa.qwe      then open http://localhost:8080/signuse "rsa.qwer"// In a real app the private key is a file or a variable: read_file(env("RSA_KEY_FILE")).let KEYS = rsa_keypair(2048)?route GET /sign {    let body = json.stringify({ payment: "pay_77", amount: "120.50", currency: "EUR" })    let signature = rsa_sign(body, KEYS.private)?    {        signature,        valid: rsa_verify(body, signature, KEYS.public)?,        tampered: rsa_verify(replace(body, "120.50", "999.00"), signature, KEYS.public)?,    }}route GET /secret {    let sealed = rsa_encrypt("db password: hunter22", KEYS.public)?    { sealed, opened: rsa_decrypt(sealed, KEYS.private)?, too_long: rsa_encrypt(repeat("x", 300), KEYS.public).error }}route GET /public.pem = respond(200, rsa_public_key(KEYS.private)?, { "content-type": "application/x-pem-file" })
$ quillo run rsa.qwe$ curl 'localhost:8080/sign'$ curl 'localhost:8080/secret'$ curl 'localhost:8080/public.pem'

Source

rsa.qwer
//! RSA keys as PEM, signatures (PKCS#1 v1.5 with SHA-256, as RS256 and most banks and partner APIs use) and OAEP encryption of short secrets.//! version: 1.0.0//!//! [dependencies]//! rsa = { version = "0.9", features = ["sha2"] }//! sha2 = { version = "0.10", features = ["oid"] }//! rand = "0.8"//! base64 = "0.22"use base64::engine::general_purpose::STANDARD;use base64::Engine;use rand::rngs::OsRng;use rsa::pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey};use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding};use rsa::{Oaep, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};use sha2::{Digest, Sha256};use std::collections::HashMap;fn private(pem: &str) -> Result<RsaPrivateKey, String> {    let pem = pem.trim();    if pem.contains("BEGIN RSA PRIVATE KEY") {        RsaPrivateKey::from_pkcs1_pem(pem).map_err(|e| format!("not an RSA private key: {}", e))    } else {        RsaPrivateKey::from_pkcs8_pem(pem).map_err(|e| format!("not an RSA private key (PKCS#8 PEM): {}", e))    }}fn public(pem: &str) -> Result<RsaPublicKey, String> {    let pem = pem.trim();    if pem.contains("BEGIN RSA PUBLIC KEY") {        RsaPublicKey::from_pkcs1_pem(pem).map_err(|e| format!("not an RSA public key: {}", e))    } else if pem.contains("PRIVATE KEY") {        Ok(private(pem)?.to_public_key())    } else {        RsaPublicKey::from_public_key_pem(pem).map_err(|e| format!("not an RSA public key (SPKI PEM): {}", e))    }}/// A new key pair of `bits` (2048 to 4096; 2048 below, 4096 above): `{ private, public }` as PEM (PKCS#8 and SPKI, the/// `BEGIN PRIVATE KEY` and `BEGIN PUBLIC KEY` that OpenSSL writes). 2048 bits takes a fraction of a second; 4096, a few seconds.pub fn rsa_keypair(bits: i64) -> Result<HashMap<String, String>, String> {    let key = RsaPrivateKey::new(&mut OsRng, bits.clamp(2048, 4096) as usize).map_err(|e| e.to_string())?;    let private = key.to_pkcs8_pem(LineEnding::LF).map_err(|e| e.to_string())?.to_string();    let public = key.to_public_key().to_public_key_pem(LineEnding::LF).map_err(|e| e.to_string())?;    Ok(HashMap::from([("private".to_string(), private), ("public".to_string(), public)]))}/// The public key (SPKI PEM) of a private key (PKCS#8 or PKCS#1 PEM).pub fn rsa_public_key(private_pem: &str) -> Result<String, String> {    private(private_pem)?.to_public_key().to_public_key_pem(LineEnding::LF).map_err(|e| e.to_string())}/// The signature of `text` with the private key: RSASSA-PKCS1-v1_5 over SHA-256, as base64.pub fn rsa_sign(text: &str, private_pem: &str) -> Result<String, String> {    let sig = private(private_pem)?.sign(Pkcs1v15Sign::new::<Sha256>(), &Sha256::digest(text.as_bytes())).map_err(|e| e.to_string())?;    Ok(STANDARD.encode(sig))}/// Whether `signature` (base64) is `text` signed with the private key of `public_pem` (SPKI or PKCS#1 PEM, or a certificate's key/// taken out with the x509 plugin). False for a changed text or a wrong signature.pub fn rsa_verify(text: &str, signature: &str, public_pem: &str) -> Result<bool, String> {    let key = public(public_pem)?;    let Ok(sig) = STANDARD.decode(signature.trim()) else { return Ok(false) };    Ok(key.verify(Pkcs1v15Sign::new::<Sha256>(), &Sha256::digest(text.as_bytes()), &sig).is_ok())}/// `text` encrypted to the public key with OAEP (SHA-256), as base64. RSA encrypts short secrets only: up to 190 bytes with a/// 2048-bit key. For anything longer, encrypt it with a random AES key (the encrypt plugin) and this the key.pub fn rsa_encrypt(text: &str, public_pem: &str) -> Result<String, String> {    let key = public(public_pem)?;    let ct = key.encrypt(&mut OsRng, Oaep::new::<Sha256>(), text.as_bytes()).map_err(|e| format!("{} (RSA takes short secrets only)", e))?;    Ok(STANDARD.encode(ct))}/// The text back from rsa_encrypt (or from OpenSSL's `pkeyutl -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256`)./// The crate's decryption is not constant-time (RUSTSEC-2023-0071): do not decrypt what strangers send, many times over the network.pub fn rsa_decrypt(encrypted: &str, private_pem: &str) -> Result<String, String> {    let ct = STANDARD.decode(encrypted.trim()).map_err(|_| "not base64".to_string())?;    let pt = private(private_pem)?.decrypt(Oaep::new::<Sha256>(), &ct).map_err(|_| "decryption failed: another key, or changed data".to_string())?;    String::from_utf8(pt).map_err(|_| "the decrypted bytes are not text".to_string())}
Biscuit tokens biscuit.qwer · 1.0.0Biscuit authorization tokens: issued with Datalog facts and checks signed by your key, narrowed by their holder without asking the issuer, and authorized offline against each request.authtokensapisigning7 views · 8 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 · 7 downloadsOpenPGP openpgp.qwer · 1.0.0OpenPGP through the pure-Rust pgp crate: keys made and read, messages encrypted to keys or a password and decrypted, cleartext and detached signatures made and verified, signed and encrypted together, and what an armored block holds.encryptionsigningpgpemail3 views · 6 downloadsSignatures signatures.qwer · 1.0.0Ed25519 key pairs, text signed with the private key and checked with the public one: exports, webhooks and messages between servers.signinged25519webhooksapi7 views · 7 downloadsWebhooks webhooks.qwer · 1.0.0Check that a webhook came from Stripe, GitHub, Slack, Shopify or a Standard Webhooks sender, with timestamp tolerance; and sign your own.webhooksapipaymentssigning5 views · 9 downloads
Thread
98Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.