Keys and Signatures (Key)
A signature proves who wrote something, and that it hasn't changed since. Key makes a secret key, gives you its public key to share, signs messages, and checks signatures. Your app has the same functions (StofDoc.sign and friends in JavaScript, Doc.sign in Python, stof::model::seal::sign in Rust), so a signature made in one place checks in any other.
Keys and signatures are plain base64 strings: they travel in JSON, environment variables, and headers. The scheme is Ed25519.
Two rules make signatures hard to misuse:
- What's signed is a message's exact bytes (a string, or a blob). Change one character and the signature no longer checks. To sign an object, sign its export (
stringify('json', obj)) and send that exact text. - Every signature has a context, a short name for what it's for (
'orders/v1','refunds/v1'). It only checks under the same context, so a signature made for one purpose can't be passed off as another. Seal signatures, over a digest, use'stof/seal'.
Key::generate() -> str
A new secret key: 32 bytes from the system's secure random source. Keep it secret. Most of the time your app holds secret keys and passes one in when a document needs to sign; if a document has to keep one, encrypt it with Age.
#[main]
fn main() {
const secret = Key::generate();
pln(secret);
pln(secret == Key::generate()); // false: a new one every time
}Key::public(secret: str) -> str
The public key for a secret key. Share it: anyone with it can check your signatures, but can't make them.
#[main]
fn main() {
const secret = Key::generate();
pln(Key::public(secret));
}Key::sign(secret: str, context: str, message: str | blob) -> str
Sign a message under a context. Returns the signature.
order: { item: 'lamp', qty: 2 }
#[main]
fn main() {
const secret = Key::generate();
const text = stringify('json', self.order); // sign the exact text you send
const signature = Key::sign(secret, 'orders/v1', text);
pln(text);
pln(signature);
}Key::verify(public: str, context: str, message: str | blob, signature: str) -> bool
True only when signature was made over exactly these bytes, under this context, with the secret key behind public. Anything else is false, including a key or signature that isn't one.
#[main]
fn main() {
const secret = Key::generate();
const public = Key::public(secret);
const order = '{"item":"lamp","qty":2}';
const signature = Key::sign(secret, 'orders/v1', order);
pln(Key::verify(public, 'orders/v1', order, signature)); // true
pln(Key::verify(public, 'orders/v1', order.replace('2', '20'), signature)); // false: changed
pln(Key::verify(public, 'refunds/v1', order, signature)); // false: another purpose
pln(Key::verify(Key::public(Key::generate()), 'orders/v1', order, signature)); // false: another key
}Checking Data You Receive
Hold the sender's public key, and check before you use what they sent:
shop: {
key: '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=' // the shop's public key
order: {}
fn accept(order: str, signature: str) -> bool {
if (!Key::verify(self.key, 'orders/v1', order, signature)) return false;
parse(order, self.order, 'json'); // trusted now
true
}
}
#[main]
fn main() {
const order = '{"item":"lamp","qty":2}';
const signature = 'Qr/vFkG4ZLv/M1JwvCdDjSb107rdqjXLjbE044YT2U712OHn2obDLuLns6JNo06QBqCe5ENesDH02ZL4uQqVDQ==';
pln(self.shop.accept(order, signature));
pln(self.shop.order.qty);
}Signing Logic You Ship
A seal signature is a signature over a part's digest, under 'stof/seal'. Your app checks it when it seals the part (doc.seal(path, { publicKey, signature })), which is this check followed by the seal:
rules: {
fn discount(total: float) -> float { total * 0.9 }
}
#[main]
fn main() {
const secret = Key::generate();
const signature = Key::sign(secret, 'stof/seal', digest(self.rules));
pln(Key::verify(Key::public(secret), 'stof/seal', digest(self.rules), signature));
}With Age
Key signs; Age encrypts. They're separate keys for separate jobs. Together: encrypt to the reader so only they can open it, and sign what you encrypted so they know it came from you.
message: { text: 'meet at noon' }
#[main]
fn main() {
const reader = Age::generate(); // the reader's encryption identity
const sender = Key::generate(); // the sender's signing key
const box = Age::blobify(Age::public(reader), 'json', self.message);
const signature = Key::sign(sender, 'messages/v1', box);
if (Key::verify(Key::public(sender), 'messages/v1', box, signature)) {
const opened = new {};
Age::parse(reader, box, opened, 'json');
pln(opened.text);
drop(opened);
}
}In Your App
| JavaScript | Python | Rust | |
|---|---|---|---|
| New secret key | StofDoc.generateKey() | Doc.generate_key() | seal::generate_key() |
| Public key | StofDoc.publicKey(secret) | Doc.public_key(secret) | seal::public_key(secret) |
| Sign | StofDoc.sign(secret, context, message) | Doc.sign(secret, context, message) | seal::sign(secret, context, bytes) |
| Verify | StofDoc.verify(public, context, message, signature) | Doc.verify(public, context, message, signature) | seal::verify(public, context, bytes, signature) |
A message is a string or bytes (Uint8Array in JavaScript, bytes in Python, &[u8] in Rust). In Rust, seal is stof::model::seal.