← All churts

uBix Vault, Beta 8 — Transit, or: Signing Without a Library

uBix Vault, Beta 8 — Transit, or: Signing Without a Library

The Engine That Only Did Half Its Job

Last time, uBix Vault got a complete set of auth methods and its own certificate authority. This post is about an engine that has been in the vault since almost the beginning but only ever did half of what its name promises: Transit.

Transit is “encryption as a service” — named keys that never leave the vault, so an application sends plaintext and gets ciphertext without ever holding the key. That part has worked for a long time: encrypt, decrypt, rotate. But a real transit engine does more than hide bytes. It signs things, it authenticates things, it hands out data keys for bulk encryption. uBix Vault couldn’t do any of that. Beta 8 closes the gap.

Same disclaimer up front, because it’s a secrets manager and overselling maturity is how people get burned: it is a beta, single-node, not production-hardened, and it has not had an external security review. For production, use HashiCorp Vault or OpenBao. I run this on my own internal cluster, deliberately, with that understood.

Signing Without a Signing Library

The headline is signing keys. A Transit key can now be an asymmetric key — ECDSA on P-256/384/521, or Ed25519 — and the vault will sign data with it and verify signatures against it, while the private key stays sealed inside the barrier the same way a symmetric key does. You get back a version-tagged signature, and you can read the key’s public half as PEM so anything, anywhere, can verify a signature without ever talking to the vault.

If beta 7’s refrain was “verify a JWT without a JWT library,” beta 8’s is the same tune a key lower: sign without a signing library. Signing is not mysterious once you stop treating it as a black box:

  • ECDSA — hash the message (SHA-256/384/512), then ecdsa.SignASN1. Verification is ecdsa.VerifyASN1 against the public key. The standard library does the hard part; I just pick the curve and the hash.
  • Ed25519 — even simpler: it signs the message directly, no prehash, one call each way.
  • The private keys are stored as PKCS#8 and generated fresh per version on rotation, so rotating a signing key works exactly like rotating an encryption key — old versions stick around to verify old signatures, new signatures use the latest.

So the whole thing is crypto/ecdsa, crypto/ed25519, crypto/x509, and encoding/pem. Zero new dependencies, again.

The part I paid the most attention to isn’t the signing — it’s the guardrails. A key is now either a symmetric key or a signing key, and the two can’t be confused: you cannot encrypt with a signing key, you cannot sign with a symmetric key, and either mistake gets a clean type-mismatch error instead of some undefined behavior deep in a cipher. And because keys that predate this change simply have no type recorded, they’re treated as the AES-256 keys they always were — every existing key and ciphertext keeps working untouched. I checked that the boring way: the old encrypt/decrypt tests still pass, unchanged.

The Rest of the Toolbox

Signing was the big one, but a crypto-as-a-service engine needs the smaller tools too, and they landed alongside it:

  • HMAC + verify. Keyed message authentication (SHA-256/384/512), compared in constant time. The MAC carries its key version, so it keeps verifying after the key rotates.
  • Data keys. Ask for a fresh random key and get it back two ways at once: the plaintext key to encrypt your bulk data locally, and that same key wrapped under a Transit key to store alongside the data. That’s envelope encryption — the vault never sees your data, only guards the little key that unlocks it. There’s a wrapped-only variant that never returns the plaintext at all.
  • Rewrap. After you rotate a key, old ciphertexts are still on the old version. Rewrap re-encrypts them to the newest version without ever handing the plaintext back to the caller — so you can retire an old key version without a decrypt-and-re-encrypt round trip through the client.

Each one is a few dozen lines of standard library. None of them moved the dependency count.

Handing Over a Secret Without Handing It Over

The other feature in beta 8 has nothing to do with Transit, but it’s cut from the same cloth: response wrapping.

The problem it solves is secure introduction. You need to give a secret to some consumer — a new service, a CI job, a person — but the moment a secret is in a URL, a log line, a chat message, or a config file, it’s leaked. Response wrapping says: don’t hand over the secret, hand over a single-use token that redeems it. Wrap a payload and you get back a short-lived token (default five minutes). Whoever holds it can unwrap it exactly once; the moment they do, the stored copy is destroyed. A leaked wrapping token is either unused — in which case you know it was never redeemed and can rotate safely — or already burned, in which case it’s worthless.

The implementation follows the same discipline as everything else that guards a secret here: the wrapped payload is encrypted under the barrier, and it’s indexed by the hash of the token, never the token itself, so the token value never appears in an on-disk key name. Unwrapping deletes the record before it returns the payload, so there’s no window to redeem it twice. And an expired token and a token that never existed return the same error — a caller holding a bad token learns nothing about which.

Still One Dependency

Beta 8 adds signing, verification, HMAC, data keys, rewrap, and response wrapping. The count of third-party dependencies after all of it: one, the MySQL driver — unchanged since the very first data-backend commit. Every new primitive is standard-library code I wrote and tested myself.

I keep saying this because it keeps being the point. For a tool whose whole job is to hold your secrets, the ability to read the entire thing — including every line of crypto between your data and the disk — in an afternoon is not a limitation. It’s the feature I’d want most from something I was trusting.

Where It Is, and What’s Next

uBix Vault is BSD 3-Clause, on GitHub at github.com/cwolsen7905/uBixVault, tagged v0.2.0-beta.8 and running on my own cluster.

The road to a real 1.0 hasn’t moved, and I’m not going to pretend it has: Raft-based HA so it’s more than a single node, a pluggable cloud-KMS/HSM seal, and the one that actually gates production use — an external security review. Those are the honest blockers, still written down where anyone can see them.

But the vault does a lot now. It keeps secrets, generates them, authenticates five different ways, issues its own certificates, and — as of this beta — signs, verifies, and hands secrets off safely, all behind a Vault-compatible API and all out of the standard library. Beta by beta, the thing I most wanted to be true about it stays true: I can still explain every part of it, because I wrote every part of it.

← All churts