Security

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

#signing#ed25519#webhooks#api

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() → map

A 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 error

The public key that goes with a private key, as base64.

sig_sign(text: str, private_key: str) → stror an error

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.

sig_verify(text: str, signature: str, public_key: str) → boolor an error

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.

Get it

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

Or with curl:

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

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

main.qwe
use signatures.qweruse sig_keypair, sig_public_key from signatures      // Quillo 0.6: only these

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

[dependencies]
ed25519-dalek = "2"rand = "0.8"base64 = "0.22"

Example

signatures.qwe
// 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

signatures.qwer
//! 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())}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.