Signatures
Ed25519 key pairs, text signed with the private key and checked with the public one: exports, webhooks and messages between servers.
Version 1.0.0 · 4 functions · 3 crates
Functions
What use signatures.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use sig_keypair, sig_public_key from signatures (Quillo 0.6).
sig_keypair() → mapA new key pair: { private, public }, each 32 bytes as base64. Keep private secret; hand public to whoever checks.
sig_public_key(private_key: str) → stror an errorThe public key that goes with a private key, as base64.
sig_sign(text: str, private_key: str) → stror an errorThe signature of text (its UTF-8 bytes) with private_key: 64 bytes as base64 (88 characters). The same text and key always give the same signature.
sig_verify(text: str, signature: str, public_key: str) → boolor an errorWhether signature is text signed by the owner of public_key. False for a changed text, another key or a malformed signature; an error only for a public key that is not one.
Get it
$ quillo install signatures # signatures.qwer into this folder$ quillo install signatures --example # and its example, signatures.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/signatures.qwer$ curl -O https://quillolang.org/plugins/files/signatures.qweThen, in your program, all of it or only what you call:
use signatures.qweruse sig_keypair, sig_public_key from signatures // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
ed25519-dalek = "2"rand = "0.8"base64 = "0.22"Example
// A server that signs its exports, and a check that a file came from it unchanged.//// quillo run signatures.qwe then open http://localhost:8080/exportuse "signatures.qwer"// In a real app the private key comes from the environment: SIGNING_KEY=$(sig_keypair().private)let KEYS = sig_keypair()let PRIVATE = env("SIGNING_KEY") ?? KEYS.privatelet PUBLIC = sig_public_key(PRIVATE)?route GET /export { let body = json.stringify({ orders: [{ id: 1, total: 120.5 }], at: "2026-11-02" }) { body, signature: sig_sign(body, PRIVATE)?, public_key: PUBLIC }}route GET /check { let body = "invoice 2026-114: 120.50 EUR" let signature = sig_sign(body, PRIVATE)? { genuine: sig_verify(body, signature, PUBLIC)?, tampered: sig_verify("invoice 2026-114: 920.50 EUR", signature, PUBLIC)?, other_key: sig_verify(body, signature, sig_keypair().public)?, }}$ quillo run signatures.qwe$ curl 'localhost:8080/check'$ curl 'localhost:8080/export'Source
//! Ed25519 signatures: make a key pair, sign text with the private key, and check it with the public one, for webhooks, exports and messages between servers.//! version: 1.0.0//!//! [dependencies]//! ed25519-dalek = "2"//! rand = "0.8"//! base64 = "0.22"use base64::engine::general_purpose::STANDARD;use base64::Engine;use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};use rand::RngCore;use std::collections::HashMap;fn bytes32(text: &str, what: &str) -> Result<[u8; 32], String> { let b = STANDARD.decode(text.trim()).map_err(|_| format!("the {} key is not base64", what))?; b.try_into().map_err(|_| format!("the {} key is 32 bytes (44 base64 characters)", what))}fn signing(private_key: &str) -> Result<SigningKey, String> { Ok(SigningKey::from_bytes(&bytes32(private_key, "private")?))}/// A new key pair: `{ private, public }`, each 32 bytes as base64. Keep `private` secret; hand `public` to whoever checks.pub fn sig_keypair() -> HashMap<String, String> { let mut seed = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut seed); let key = SigningKey::from_bytes(&seed); HashMap::from([ ("private".to_string(), STANDARD.encode(key.to_bytes())), ("public".to_string(), STANDARD.encode(key.verifying_key().to_bytes())), ])}/// The public key that goes with a private key, as base64.pub fn sig_public_key(private_key: &str) -> Result<String, String> { Ok(STANDARD.encode(signing(private_key)?.verifying_key().to_bytes()))}/// The signature of `text` (its UTF-8 bytes) with `private_key`: 64 bytes as base64 (88 characters). The same text and key always give the same signature.pub fn sig_sign(text: &str, private_key: &str) -> Result<String, String> { Ok(STANDARD.encode(signing(private_key)?.sign(text.as_bytes()).to_bytes()))}/// Whether `signature` is `text` signed by the owner of `public_key`. False for a changed text, another key or a malformed signature;/// an error only for a public key that is not one.pub fn sig_verify(text: &str, signature: &str, public_key: &str) -> Result<bool, String> { let key = VerifyingKey::from_bytes(&bytes32(public_key, "public")?).map_err(|_| "not an Ed25519 public key".to_string())?; let Ok(sig) = STANDARD.decode(signature.trim()) else { return Ok(false) }; let Ok(sig) = Signature::from_slice(&sig) else { return Ok(false) }; Ok(key.verify_strict(text.as_bytes(), &sig).is_ok())}Related
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.5 views · 9 downloadsBiscuit 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.7 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.9 views · 8 downloadsCanonical JSON json_canonical.qwer · 1.0.0Canonical JSON (RFC 8785): one way to write a value, keys sorted and numbers as JavaScript writes them, so hashes and signatures of JSON hold however it was spaced or ordered.7 views · 5 downloadsDKIM, SPF and DMARC email_auth.qwer · 1.0.0Messages signed and checked with DKIM (RSA and Ed25519), SPF records read and evaluated for a sending IP, and DMARC policies applied, with DNS records given as a map so it runs offline.6 views · 5 downloadsJWKS jwks.qwer · 1.0.0Verify sign-in tokens from Auth0, Okta, Entra ID, Cognito or Keycloak against the provider's JWKS keys, picked by kid.8 views · 6 downloadsThread 0 posts
On Signatures: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.