What happened when we took the signing key out

What it takes to build a system where the thing an attacker wants to steal was never assembled, and the places it still doesn't save you.

There is a video of this. Same argument, six minutes, with the diagrams moving.

I keep coming back to the 2023 Microsoft breach because of how little the attacker had to do. They had a consumer signing key from 2016, seven years old and never rotated, and with it they could mint tokens for accounts the key was never supposed to be valid for. That got them something like 60,000 State Department emails plus mail from 21 other organisations.

There was no exploit anywhere in that. The token validation path worked correctly the whole time, because it was doing its job: validating a real signature from a real key that happened to be in the wrong hands. Microsoft's first public explanation was that the key leaked through a crash dump; they later withdrew that, and the Cyber Safety Review Board's finding is that Microsoft still cannot say how it was taken. The board also called the intrusion preventable, which is the part that stings.

I've spent the last while building something on the premise that the key didn't have to be there at all, and I want to write up both what works and the places where it plainly doesn't.

First, the word

"Keyless" is a word that needs qualifying, so let me do that before anything else. Key material absolutely exists in this system. What doesn't exist is a complete, reusable key or bearer token sitting anywhere a caller, an app, an admin, a backend or the target service can hold it. The key material is real; it is just never whole and never in one place. If your first instinct was that cryptography without keys is nonsense, you're right, and the name is doing the idea a disservice. "Keylets", like droplets, would be closer and cuter.

The construction

Keys get produced by distributed key generation across a set of independent nodes, using nested Shamir. There's no dealer, so no node ever sees a whole key; at no point in the protocol does one exist to be seen. Signing is a threshold signature: each node does its share of the arithmetic over the message and returns a partial, and enough partials combine into a signature.

fig 1 · one signature, no key five independent nodes · each holds one share · none holds the key node 1 node 2 node 3 node 4 node 5 combine partials one ordinary 64-byte Ed25519 signature
Each node computes its partial from the message alone. The verifier at the far end runs verify(pk, msg, sig) and cannot tell any of this happened. The full private key is not anywhere in this diagram, including in the combiner, because there is no step that produces it.

The property that made this practical for me is that what comes out is an ordinary 64-byte Ed25519 signature verifying against an ordinary public key. The verifier calls verify(pk, msg, sig) and has no idea anything unusual happened. All the strangeness stays on the signer side, which meant I could put this in front of software I hadn't written without touching that software.

Production runs 20 nodes with a threshold of 14. A local test network runs 5 and 3. Those numbers are the whole security argument so I'd rather state them than wave at the word distributed.

Compromise behaviour splits into three ranges and I've seen people conflate two of them constantly. Up to 6 compromised nodes, nothing happens. Between 7 and 13, an attacker can deny service but cannot reconstruct the key, and there's a healing protocol that re-randomises shares and makes the stolen ones useless. At 14 the key is reconstructable and it's over. So confidentiality survives thirteen compromised nodes while availability starts degrading at seven. Those are different numbers about different properties, and quoting one when you mean the other either oversells the guarantee or undersells the operational risk depending which way you're leaning.

fig 2 · drag to compromise nodes
Note where the two boundaries sit. Confidentiality survives 13, but availability starts degrading at 7. Those are different properties and quoting one when you mean the other is the most common mistake made about this design.

Why this isn't the vault you already run

Everyone has solved key custody once already, so this is where the conversation usually stalls.

A secrets manager holds the key and guards it, so compromising the guard, or anything holding kms:Sign, gets you the key's power even if you never see its bytes. An HSM keeps it in silicon you can't extract from, but extraction was never the attack; anything that can call the HSM can use it, and Storm-0558 didn't need to extract anything from anywhere. The comparison I care about more is classic Shamir sharing, because on a whiteboard it looks almost identical to what I'm describing and in the threat model it's the opposite. Shamir splits a key across parties and then puts it back together in one process to use it, and that reassembly step is the entire target. Threshold signing never reaches that step.

fig 3 · splitting is not the same as never assembling classic Shamir sharing threshold signing share share share WHOLE KEY ▲ the attack aims here share share share SIGNATURE nothing to aim at
These look almost identical on a whiteboard, which is why the distinction gets lost. The difference is the box on the left: one process, holding the whole key, for as long as the operation takes. On the right there is no equivalent box, because there is no step that builds one.

If your response is that this is just FROST with extra steps, that's mostly fair, and I'd rather concede it than argue. The threshold signing itself is well-trodden. What I found interesting was what becomes possible once no component can sign alone: password verification with nothing stored anywhere (a peer-reviewed protocol called PRISM, where no node learns the password or anything usable offline), token issuance where every node independently checks the claims before contributing its partial, and admin changes that require a quorum of administrator signatures which each node validates for itself. That last one means a rogue admin can't grant themselves a role because the network won't produce the signature, rather than because someone would notice in the audit log later.

What I actually measured

An idea like this is only worth anything if it survives contact with software nobody rewrote for it, so I put gateways in front of unmodified official images of SeaweedFS 3.97 and Gitea 1.24.7.

Four components sit in that path, and the rest of this section does not make sense unless I name them first.

  • The caller is an ordinary workload, a Node process, holding no credential at all. Not a blanked one, not a scoped one: its environment is built from an allowlist and there is nothing in it.
  • The authority broker turns a request into a canonical description of one exact action. It holds no signing key and cannot mint anything.
  • The policy is a C# contract deployed to the ORK network and executed independently by every node. There is no single place a decision gets made.
  • The gateway sits in front of the target service and refuses anything arriving without a valid receipt.
fig 4 · where the authority comes from caller no credential broker no signing key ORK network policy runs on every node receipt 1 action · 45s gateway target threshold signature — no node can produce one alone nothing in this row holds a reusable credential
The caller asks; it never holds. The only artefact that ever exists is the receipt, and it describes one action, expires in about 45 seconds, and is spent on use.

To do anything, the caller asks the broker for authority for one exact action. That authority is only minted after the policy, running independently on every node, agrees to it.

Against a live 5-node network on 28 August:

requesting storage.put for customer-files/alice/q3-summary.txt (48 bytes)
job job_e8952871...  advisory=AUTO  hash=81d57b3765e3b5...
fetching committed policy storage-auto
signing via ORK network (Policy:1, IMPLICIT)
ORK network returned a 64-byte Ed25519 signature
AuthorityReceipt auth_a1a93589f2c68331 minted
gateway -> 201 STORED IN SEAWEEDFS

The receipt lives about 45 seconds, covers one action, and is single-use. I then threw everything I could at that same real receipt:

attackresult
replay the identical requestAUTHORITY_ALREADY_CONSUMED
same authority, different bytesCONTENT_HASH_MISMATCH
same authority, DELETE instead of PUTACTION_MISMATCH
same authority, another user's objectRESOURCE_MISMATCH
delete the whole bucketACTION_MISMATCH
no authority at allNO_AUTHORITY
a stolen bearer tokenNO_AUTHORITY

The case I care about is a compromised caller, because that is the one that actually happens in the field: somebody gets code execution inside the workload. What that gets them is the ability to ask the broker for authority. They can ask for a 100MB upload, or a $50,000 refund, or a merge of a commit nobody reviewed. Every one of those requests is evaluated by the policy contract, which each ORK runs for itself, and every one is refused. Not by the broker, which could be compromised too, and not by the gateway: by each node in the network independently. Whoever owns that process ends up a petitioner instead of an owner, which is a smaller change than it sounds and also the only one I was after.

One earlier version of this had a real hole in it, which I'll describe because it's the kind of thing that's easy to leave out. Storage prefixes were keyed on the username, and the username came from the broker's say-so, while the network binds a different identifier entirely. A compromised broker could therefore write into someone else's prefix. Object keys are now derived from the network-bound identifier inside the policy contract itself, so the broker has nothing left to lie about on that path. It still chooses which action to describe to a human approver, though, which is a residual I haven't closed and probably can't.

The one credential I could not remove

The deployment ships with a credential inventory: every credential in the system, one per row, coloured green where that path holds no reusable secret and yellow where it holds one. Storage and payments are green end to end. Exactly one row is yellow, and it always will be.

Gitea's API authenticates with its own token and does not accept Ed25519-signed JWTs, so the Git gateway has to hold one. Compromise that gateway and you get push and merge on that repository without going through any of this, with no reviewer, for as long as the token lives. That is row one of the inventory, it is yellow, and it stays yellow, because a status board that only ever shows green is not telling you anything.

There's a similar gap on the storage side. Objects in SeaweedFS sit there as plaintext in this demo, because what I was demonstrating is authority and not encryption at rest, so compromising the storage host reads everything. On that path, gateway compromise is roughly equivalent to storage compromise. What the design removes is the caller's standing authority and the stealable artifact, not the gateway's position on the network.

What it does not fix

In February 2025 about $1.5 billion left Bybit, which the FBI attributed to North Korean actors. Bybit was using a multisig wallet and several independent people approved the transaction. It made no difference at all, because a compromised front end showed every one of them the same lie. They approved what they saw and signed something else.

Distributing signing authority is completely orthogonal to that attack. If the thing rendering the request is lying, a bigger quorum just collects more signatures on the wrong payload.

It is worth being exact about what would and would not have helped there, because the obvious answer does not work. Subresource integrity on its own would not have prevented Bybit. SRI checks a script against a hash written into the page that loads it, and in that attack the adversary was serving the application. Anyone who can change the script can change the hash sitting next to it in the same commit, and the browser will then verify the tampered file against the tampered hash and run it without complaint. A pin you control both sides of is not a pin.

What makes it worth anything is where the expected hash lives. Tide's enclave ships with an integrity="sha256-..." attribute, and the value it is supposed to be is published separately, away from the page that serves it. That is the part that matters: it turns the check into something you can do without trusting the server that just handed you the page. It is a real control aimed squarely at this failure, and it is still not a guarantee. It rests on browsers behaving correctly, which is a software property rather than a mathematical one, and it cannot make anybody actually look. What it buys is that a tampered approval screen cannot load quietly.

Every cryptosystem rests on assumptions. The difference between a serious one and a marketing claim is whether it names them out loud and tells you which are mathematics and which are not. There are four here, and the sources call them irreducible, which I think is the right word:

  1. No more than 13 of a key's 20 nodes are colluding. This is the only genuinely cryptographic one.
  2. ECDLP, SHA-256 preimage resistance and EdDSA hold.
  3. Browsers enforce SRI, per the above.
  4. The node operators are genuinely independent. This is the one that is operational rather than mathematical, and the design says so itself instead of claiming a Sybil resistance it does not have: one operator quietly running fourteen nodes would make the threshold meaningless, and what stands in the way is that fourteen independently-appearing organisations are expensive to fake and hard to hide. Worth thinking about at deployment time, too, because independence has to include jurisdictional independence. Fourteen shares reachable by a single court order are not fourteen parties.

What it let me build

The construction is the interesting part to write about, but it is not the part that keeps me here. What does is how little it asks of the thing built on top of it.

KeyleSSH is SSH where the private key does not exist. Not on the server, not in a bastion, not in the browser, not in memory. The authorisation is signed across the ORK network, so no single node can let you in, and who can reach which host is a role in TideCloak rather than a file of authorised keys somebody has to remember to prune. It runs in a browser terminal.

Motherlode is a wallet across 16 chains, Solana and Stellar and XRPL and Cardano and Polkadot and Cosmos and Monero among them, from one interface. The threshold network does the signing, so no complete private key is ever assembled on any device, which is the part every other multi-chain wallet has to hand-wave.

Sashlings is my favourite, and it is the least serious of the three. They are cute 3D collectibles, and every one of them is derived from the network's threshold signature over its own mint. That value does not exist until a quorum of independently operated nodes agrees to seal it, so nobody, including the server serving the page, can predict or steer what hatches. Nothing is stored as an image at all: the creature is recomputed from the ledger on every read, so there is no file to lose or quietly substitute. A trade is one event both parties approve at the enclave, so a completely compromised Sashlings server still cannot move a collectible from one person to another.

An SSH replacement, a multi-chain wallet and a gacha toy have nothing in common as products. Underneath, they turned out to be the same question: who has to be trusted here, and can that number be zero. Three times, on three things that look nothing alike, the answer came back the same.

All three are side projects, and I am not a cryptographer. That is the part I keep turning over. The hard maths is finished and it comes out the other side as an ordinary signature, so what is left is not a cryptography problem at all. It is a design one. Look at something you already use, find the key it keeps that it has no business keeping, and take it out. Far more people can answer that question than can write a threshold signing scheme, and right now almost none of them know it is theirs to answer.

So what I want out of writing this is not agreement. It is for one person to read it, think of that one key, and go and build the thing that does not need it. I genuinely cannot wait to see what people make.

← all posts github / sashyo ↗