No; no symetric keys: It's hash based signatures.
So the private key of any single WOTS chain is the seed and the public key is result of the max number of hashing rounds of that seed.
A huge collection of WOTS private keys like that is derived from a single private key using libsodium crypto_kdf_derive_from_key. The matching public key is than derived by taking the
merkle tree root of (small collections of) all those public keys. On signing additional merkle tree node values are added to allow for validation of the signature.
I'll elaborate, but note it's not where the POLA dilemma is about:
Imagine you want a one-time asymetric siging key with what you can sign say 12 bit. Let's use hex values to denote the possible values.
Now imaggine that if I want to sign the value 0x000, I hash the secret once. If I want to sign the value 0x001, I take the result of the first hashing round and hash it one more time.
Same for all other possible values, so if I want to sign the value 0xfff, I go through 2^12+1 hashing round and the result of that would be my signature.
But I still need a one time signing pubkey, so what do I do? I take the privite key and take it through 2^12+2 hashing rounds and the result is my public key.
So if that 12 bits was all I wanted to ever sign, I would share that one time signing-key pubkey, take the 12 bit value V I want to sign, and hash my private key V+1 times.
The receiver of my signature can than take that hash, hash it another 2^12+1-V times, and the value should match my public key.
But there is a bit of a problem here. An attacker now could use this signature and my pubkey to sign any down-stream value. If by chance I signed the value 0x001,
an attacker couldn't sign 0x000, but he could sign any other value. Other algoritms use CRC solutions to solve this, but in my current code I use a second one-time
secret key with what I sign the two's complement of the 12 bit value. This means that after me siging 0x001, an attacker could sign 0x001 upto 0xfff with the UP part
of my signature, he would also be able to sign 0x000 upto 0x001 with the DOWN part of my signature, so using my full two part signature of these 12 bits, he would only
be able to sign the exact value that my signature already signed.
Signing 12 bits like this is nice, but imagine I wan to sign a transaction or a message. I would start off by hashing the message resulting in a secure hash of the data. A 12 bit hash
wont do, so maybe we take a 192 bit hash. That means we need to 16 signatures to sign that many bits. 32 hashes in total as a one time signature using a 192 bit transaction hash.
Also 32 equaly sized secret keys and 32 public keys. On the public key side, I already know I am going to be using all 32 pubkey chunks at once, so I can simply replace the long pubkey
with a short one by hashing it so I only have one single hash as pubkey for the whole thing. On the secret key side, I can use crypto_kdf_derive_from_key to derive all secret keys from a
single master key.
But while the double chain protects me after using my key once, it will protect me less and less if I kept signing with it. If I sign 0x1b3 with it the first time and 0x1af, an attacker could
sign 0x1af, 0x1b0, 0x1b1, 0x1b2 and 0x1b3, not a big hole to squeeze through, but what if I signed 0xfec first and 0x005 second? All of the 12 bit of siging space would be wide open for
attacks. So these keys are single usage, and we need more. So what do we do? We take maybe 1024, 2048 or 4096 such one-time signing keys, and create a new type of key.
On the private key side, we just exend the use of crypto_kdf_derive_from_key to a wider key-index space. On the pubkey size we extend our collection of public keys into a merkle tree
and now we use the merkle tree root as our public key. Now if we sign a transaction, we don't share the specific public key to check against directly, we share merkle tree nodes with
our signature so the validator can reconstruct the merkle tree root and validate it against our public key.
So far so good, but in Web 3.0 a key that can only sign maybe 4096 transactions isn't much good on its own. It will exaust too soon. We want a key to last millions of transactions
at least, billions in some cases. So instead of trying to calculate the pubkey and merkle tree signing nodes for a billion one time keys, we say a billion is avout 1024x1024x1024
and we choose to make our previous merkle tree based multi signature key into a level key. Level 0 is the one that shouldn't run out during your lifetime. In this case, level 2 would
be the key signing the actual transactions. We use the level 0 key to sign the active level 1 key and the level 1 key to signh the active level 2 key. When the level 2 key gets exausted,
and here it gets interesting, we can retire it by creating and publishing a new level 2 key and "revoking" the index space of that key. When eventualy the active level 1 key is exausted,
we do the same for that key. This revoking further reduces the attack surface a tiny bit, but the mechanism will be crucial to later POLA stuff.
Because the same one time signing key can't be re-used in signing algoritms like this, index space is fundamentaly an exaustible resource. From a POLA perspective, I
consider this property to be a feature, not some anoying condition we need to live with.
Now if we consifer our one-billion signatures ready stack of 3 keys considsting of our lifetime key and our two active level keys (level 1 and level 2) as our key, then functionaly,
we realize that the 64 bitof index space that crypto_kdf_derive_from_key grants us (for 20 byte subkeys and up) is much and much bigger than the maybe 35 or 36 bit of index space that our 3 levels of
level keys require. This gives room for using the remaining index space for decomposition of authority and delegation, and to expand on the revocation of chunks of index space as a more general tool.
That is the part I'm looking for feedback on. Consider that my secret key gives me access, either to the full 64 bit of index space for my key and any delegation stuff,
or to a subset of that spece defined by whatever entity created my key. If my key started out with the full 64 bit of index space, and 36 bits would have been enough to hold
all the possible level 1 and 2 keys our original key migh need, then we have quite some space left to work with. But we are going to treat index space as a resource across
delegations, so we need to still be strategic to allow for multiple delegation steps.
One rather weird form of delegation I'm implementing is self-delegation in case we run out of index space with our key. We shouldn't, but things might be misdimensioned
and we might some day approach our 1 billion signatures treshold that would make our level0 key exausted. Before this happens we can delegate all of our authority held
by our key, and all of our furhter remaining index space, to a brand new key "within" our own index space. The last two signatures the 1G-transaction key could sign could be
a transaction delegating all authority and keyspace to its own keyspace, followed by a transaction that revokes the previous key it's entire key index space. Hope this makes sense.
This is a rather weird self-delegation.
Basicly we end up with multiple pubkeys that map to the same privkey. So far this was a hack to make sure that a user never needs to lose his authority because he somehow
signs his way out of signing key-space. But I'm considering expanding on this and make the multi-pubkey idea more common.
But first let me elaborate on the current design for delegation. Imagine a Web 3.0 social blogging blockchain where a user can use his key to administer his public info,
post blog posts, comment and vote on other peoples blog posts, move tokens to other accounts, stake or unstake tokens, send messages to other accounts, etc, etc.
Imagine two users, Alice and Bob. Alice is verry bussy, too busy to read many posts and upvote the good ones, so Alice wants to delegate the authority to vote to Bob.
Maybe Bob wants to send a short comment to a post, so Alice also wants to delegate the right to upvote to Bob.
In the core version of this scenario, three private keys are involved, but only two index-spaces. We have the Allice account key, the Bob account key, and a Bob accept key.
Alice allocates a big chunk of her index-space for the delegation, and creates a transaction that mentiones bob's account pubkey, the exact authority she is delegating
(full voting and commenting), and the amount of index-space she is granting Bob for this authority, and the specific Allice key-space section she is allocating for Bob.
Alice publishes this delegation, but the delegation isn't complete yet, it needs to be accepted. Now Bob needs to anchor this new authority somewhere before using it.
His and Alice her key-space don't directly map, so Bob creats a new private key, dimensions a new key that fits or underfits the keyspace grantes (underfit so he may
re-delegate if desired) , and he publishes an accept transaction that binds this new accept key to the delegated authority. The accept key is constrained to the index space
explicityly allocated for this delegation by Allice, so not the full 64 bit index space. Index-space is a resource that we don't create from thin air by default, at least not for
accept keys.
So this is the design rigt now.
Feedback that I've been given from a dev ion a potential project that could maybe end up using my code (just a single dev without special weight in the project, so it is nice
to be noticed but I'm not considering the project a true potential user yet) sugested to combine the weird hacky multi-pubkey keys with actual delegation.
That option would basicly give Bob the coice to map the allocated index-space to a different region in his own index space and let the bob account key be the accept key,
at least the private key.
This "feature" sounds convenient, but that convenience actualy also is what worries me, because it feels likely that in many cases the convenient choice in this won't be
the POLA choice in this.
So that last part is the actual part I really need POLA insights on.