Loading proofofbrain-blog...

coinZdense deep Dive: The coinZdense Wallet

Sub-key Revocation
index
Intro

coinZdense Wallet



In this final post of the coinZdense deep-dive series, we are going to mainly be looking at the coinZdense wallet. And we are going to tie up a last loose end regarding KDF index-space allocation. In fact, tying up that loose end may be the first we ought to do right now, just to get it out of the way.

As we discussed, we have an owner key as root of authority, from what we can derive attenuated-authority sub keys. from the KDF-Index-Space allocation post you may remember the image showing that while in the unreserved part of the KDF index-space stack a signed transaction took up only an SLT chunk, in reserved space, key-derivation space, next to the SLT, and the allocated chunk of heap, there was always also an LNK section for each signed sub-key. Discussion of this LNK chunk is our loose end at the moment, and a loose end tied to our wallets.

link.png

Because we are storing our master key in our wallet file, we need a salt for the wallet we use for storing our owner key in. Both the owner key salt and the master key are created using system entropy, but as discussed in the least authority post, we need to not be dependent of system entropy for any usage of the owner key, and that includes for deriving attenuated priviledge keys that we shall be storing in their own wallet. This means that for these sub keys we use a chunk of two indices out of the KDF index-apace pool instead.

We define three distinct data structures for our wallet. Just as for the signatures, our first MVP will store these as JSON as far as on-disk storage is concenned, but a binary storage format is planned as alternative later on.

  • The on-disk wallet structure containing password encrypted key material
  • The in-memory wallet structure containing the unencrypted wallet data
  • The pubkey-cache structure for quick merkle-tree partial signing-key reconstruction.

Let's start with the simples one, the on-disk wallet structure containing password encrypted key material.

{
   "HIVEISH": {
       "58292c2844b82efe6548dfed379ecd9b5e5dcf49851ecec7": ["...","..."],
       "d7915a4c29b40fba827e7f9c4a8d7c59a66c0288e84b2d8e": ["...","..."]
   }
}

The key "HIVEISH" in this example is a reference to the application RC parameters for this particular coinZdense application. A wallet may contain keys for multiple applications that way. One application object is basically a dictionary mapping from the hexadecimal representation of a public key to an array containing two values:

  • The wallet salt
  • The encrypted master key plus some key and subkey info relevant for the master key in question.

Now, before we look at jus how this data is encrypted, let's first look at the in-memory version of this same wallet:

{
   "HIVEISH": {
       "58292c2844b82efe6548dfed379ecd9b5e5dcf49851ecec7": {
            "wallet_salt": "...",
            "master_key": "....",
            "index_space_offset": 0,
            "index_space_bits": 64,
            "reserved": 10,
            "priv": [
                "b4298580f3f5bc67e16d0bbbfd99de1d61d3f10b5b7c8527"
            ],
            "sub_keys": {
               "ACTIVE-20220122-XTRA": "d7915a4c29b40fba827e7f9c4a8d7c59a66c0288e84b2d8e"
            }
        },
       "d7915a4c29b40fba827e7f9c4a8d7c59a66c0288e84b2d8e": {
            "wallet_salt": "...",
            "master_key": "....",
            "index_space_offset": 274877906944,
            "index_space_bits": 44,
            "reserved": 10,
            "priv": [
                "fc2196b452b4791f590de6b6923056cf9c3525843038d274",
                "f92efe86f31810d73b2a6a38c8da5c0e8f4242b3ece091df"
            ]
            "sub_keys": {}
       }
   }
}

Most of this should look familiar right now. Like before we have our per-application dictionary with pubkeys as keys, but our encrypted field has been replaced by an object that has the wallet salt inside. We how have the unencrypted master key available, allong with a number of important extra bits of data pertaining to the signing key in question. First we have the offset and size (in 2^bits worth of allocation) designating where inside the KDF index-space this key operates. Next we have our array of priviledge id's, designating which parts of the authority tree this key can be used for. And finaly we have a dictionary mapping memorable human readable names to attenuated-access sub-keys.

But how does the encryption of these signing key objects work? Well, for the encryption of these inner objects, we use the libsodium secretbox API. This API, build on the XSalsa20 stream cipher combined with Poly1305 for integrity, allows us to encrypt our serialized object using a key.

But now it seems we have a chicken and egg problem. We want to encrypt our master key, plus some additional data, and to do so we need another key that we then would have to keep save somewhere. What we want is to use a password rather than a key. The argon2id key derivation function offered by libsodium allows us to do just that, and here is where our wallet salt comes in. Don't confuse our index-space based key derivation function with this one, the index-space based key derivation function uses a master key and a 64 bit index number to generate a sub key, this key derivation function uses a salt and a password to create an key, in this case one we'll use for encryption of our master key.

wallet.png

Apart from the encrypted on disk wallet file and the in-memory unencrypted wallet containing the same but unencrypted data, there is a third part of our wallet that we also need to look at. A part that is there to store and cache the more dynamic part of the signing keys. While the core wallet key isn't all static, after all, other sub-keys may be added to the wallet, the dynamic use of a signing key, operations that should be much more frequent leading to state change are stored unencrypted in a wallet cache file.

So what does that file look like. We'll use JSON again to show this and JSON will be used in our first MVP, but again, other (binary) serialization will be looked at in the future.

{
   "HIVEISH": {
       "58292c2844b82efe6548dfed379ecd9b5e5dcf49851ecec7": {
           "transaction_index": 1697,
           "derivation_index": 1,
           "transaction": [
              ["76529cd61b5cce5b04d7b679c8392cd06bcfa569a6b8cd66",
               "bc22825fbb1b48abfef5a1325cb47f6c5f9536d2739cce14"],
              ["3b2d4b47287dee9471a4884ff649736f40c136c806f342ff",
               "fc2196b452b4791f590de6b6923056cf9c3525843038d274"]
           ],
           "derivation": [
              ["76529cd61b5cce5b04d7b679c8392cd06bcfa569a6b8cd66",
               "bc22825fbb1b48abfef5a1325cb47f6c5f9536d2739cce14"],
              ["388e0730ef2ff66a3779a5291f7d72951a7ecdde175263c0",
               "c4b175d81821c3762c0e3b439bf3518bfe0ce0b21dd911d8"]
           ],
           "lkeys": {
              "58292c2844b82efe6548dfed379ecd9b5e5dcf49851ecec7": [...],
              "76529cd61b5cce5b04d7b679c8392cd06bcfa569a6b8cd66": [...],
              "bc22825fbb1b48abfef5a1325cb47f6c5f9536d2739cce14": [...],
              "3b2d4b47287dee9471a4884ff649736f40c136c806f342ff": [...],
              "fc2196b452b4791f590de6b6923056cf9c3525843038d274": [...],
              "388e0730ef2ff66a3779a5291f7d72951a7ecdde175263c0": [...],
              "c4b175d81821c3762c0e3b439bf3518bfe0ce0b21dd911d8": [...]
           }
       }
       "d7915a4c29b40fba827e7f9c4a8d7c59a66c0288e84b2d8e": {
          ...
       }
   }
}

The higher levels look the same again. One thing to remember is that the public key of a signing key is the same as the public key of the highest level level-key. We see a signing key defines two (or sometimes one) indices. There will always a transaction index, and depending if there are further derivations possible at, a derivation index. There is a transaction array, and possibly a derivation index , that both, for each of the levels below the top levels has an array of one or two values. The first value will be the pubkey of the level key used at that level for transaction signing or key-derivation respectively. The first value will be the current level key. If there is a second one, that is the next version of the level key.

Note that for the first MVP there won't ever be a second value there. The second value is meant for when the coinZdense API allows for background key-generation, a feature that should lead to a great reduction in occasional signing operations taking a disproportionally long time to finish because new level keys need to be calculated before the signature can be made. This feature while important for user experience is planned post MVP at the moment.

As you see, transaction and derivation level keys can be one and the same, and that's why both are references into the lkeys dictionary.

The lkeys dict maps level key pubkeys to a large array of OTS signing key pubkeys that for the top of the merkle tree for our level-key. Keeping these available as cache means we don't need to recreate all of them on startup, a process that would take minutes or longer.

This was the last post in this deep-dive series. I hope it all ads up for you now, assuming you read the whole thing. I'm really interested in feedback on these posts, especially from infosec, capabilities and blockchain people. So far nothing here has had any kind of peer review, and while I can't wait to bring the code back up to date with the latest itteration of the design, zero review does make me nervous a bit. So please drop your two sents as a comment to my posts, or contact me on Discord, e-mail or Twitter about these posts.

Sub-key Revocation
index
Intro

H2
H3
H4
3 columns
2 columns
1 column
1 Comment