Storage Interface
Introduction
The high-level account API takes care of publishing updates to an identity and storing secrets securely. It does the latter by using an implementation of the Storage
interface. In this section, we will go into more depth of the interface, and how to implement that interface.
The key idea behind the interface is strongly inspired by the architecture of key management systems (KMS) or secure enclaves: once private keys are entered into the system, they can never be retrieved again. Instead, all operations using the key will have to go through that system. This approach is what allows Storage
implementations to be architected more securely than simply storing and loading private keys from a regular database. Of course, the security is directly dependent on the concrete implementation, which is why we provide one such implementation using Stronghold, and strongly recommend using it. However, there are cases where one cannot use Stronghold
or may want to integrate key management of identities into their own KMS or similar, which is why the Storage
interface is an abstraction over such systems. Any implementation of that interface can then be used by the Account
.
The storage interface has three major categories of functions. A brief overview of those functions:
- DID Operations: Management of identities.
did_create
: Based on a private key, or a generated one, creates a new DID.did_list
: List all DIDs in thisStorage
.did_exists
: Returns whether the given DID exists in thisStorage
.did_purge
: Wipes all data related to the given DID.
- Key Operations: Various functionality to managing cryptographic keys.
key_generate
: Generates a new key for the given DID.key_insert
: Inserts a pre-existing private key for the given DIDlocation
.key_public
: Calculates and returns the public key for the given location to a private key.key_delete
: Removes the key at the given location.key_sign
: Signs the given data with the key at the given location.key_exists
: Returns whether the key at the given location exists.
- Data Operations: Used for keeping state persistent. Storages only need to serialize and store the data.
chain_state_get
: Returns theChainState
data structure for the givenDID
.chain_state_set
: Sets theChainState
data structure for the givenDID
.document_get
: Returns the DID document for the givenDID
.document_set
: Sets the DID document for the givenDID
.
Storage Layout
Identifiers
There are two types of identifiers in the interface, DIDs and key locations. A DID identifies an identity, while a key location identifies a key. An implementation recommendation is to use the DID as a partition key. Everything related to a DID can be stored in a partition identified by that DID. Importantly, the location of a key is only guaranteed to be unique within the DID partition it belongs to. If no partitioning is used, then DID and key location should be combined (e.g. concatenated) to produce a single, globally unique (i.e. across all identities) identifier for a key in storage.
Representations
A KeyLocation
is a compound identifier based on the fragment of a verification method and the hash of a public key. The motivation for this design is that a KeyLocation
can be derived given a DID document and one of its verification methods. Thus, no additional state is necessary.
Canonical string representations of the IotaDID
and KeyLocation
type can be obtained using the string representation of a DID and the canonical
method on KeyLocation
respectively. These representations are intended to be kept stable as much as possible.
Example layout
This illustrates the recommended approach for partitioning the storage layout (where location -> key
is a mapping from location
to key
):
did:iota:Ft3wA8Tv2nF25hij3aegR54Wvqju7t5zqW9xnCB5L3Wu
sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
kex-0:7560300328640998700 -> 0xe494e36164e0a760140f3a9ab7dfdad38edac698f93d5239655dbd7499194760
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep
sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
kex-0:16546298247591944074 -> 0x8e1d037cd343f84276ab737b638da9095bcb6052f7fd9628d21d20f434f9959a
key:8559754420653090937 -> 0x4ef484a54aa16503878aa1ecaa6d73cb8254aefa3f80a569ed33ca685289d01e
Note how fragments (such as kex-0
) can appear more than once, but the hash of the public key - calculated from the stored private key - makes the location unique in general.
Although unlikely in practice, even the same private key can be used across different DIDs, which produces the same key location (here sign-0:16843234495045965331
) and it's important that these are stored independently, so that deleting one does not accidentally delete the other. Hence why a key's full identifier in storage needs to be based on the DID and the key location.
That said, the following flattened structure also satisfies the requirements:
did:iota:Ft3wA8Tv2nF25hij3aegR54Wvqju7t5zqW9xnCB5L3Wu:sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
did:iota:Ft3wA8Tv2nF25hij3aegR54Wvqju7t5zqW9xnCB5L3Wu:kex-0:7560300328640998700 -> 0xe494e36164e0a760140f3a9ab7dfdad38edac698f93d5239655dbd7499194760
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep:sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep:kex-0:16546298247591944074 -> 0x8e1d037cd343f84276ab737b638da9095bcb6052f7fd9628d21d20f434f9959a
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep:key:8559754420653090937 -> 0x4ef484a54aa16503878aa1ecaa6d73cb8254aefa3f80a569ed33ca685289d01e
The primary advantage of the partitioning is that it simplifies the implementation of the did_purge
operation, which wipes all data belonging to a given DID. With partitioning, this operation can simply wipe the partition whereas a storage with a flattened layout will have to do more work.
Indexing
The interface has two methods called did_list
and did_exists
. These return the list of stored DIDs, and whether a DID exists in storage, respectively. Implementations are thus expected to maintain a list or index of stored DIDs. An identity created with did_create
is added to the index, while an identity deleted through did_purge
is removed from the index.
If the storage implementation can be accessed concurrently, then access to the index needs to be synchronized, since it is unique per storage instance.
Implementation
The IOTA Identity framework ships two implementations of Storage
. The MemStore
is an insecure in-memory implementation intended as an example implementation and for testing. The secure and recommended Storage
is Stronghold
. Stronghold
may be interesting for implementers to look at, as it needs to deal with some challenges the in-memory version does not have.
This section will detail some common challenges and embeds the MemStore
implementations in Rust and TypeScript.
Challenges
The did_create
method takes the fragment of the initial verification method, the name of a network in which the DID will eventually exist, and an optional private key. From these inputs, it either generates a key or uses the passed private key to calculate the public key and from that derive the DID. In case a key needs to be generated, the challenge is to obtain the location for the key to be stored at. Since the key location depends on the public key, but key generation likely needs a location for the key to be stored at, there is a circular dependency that needs to be resolved. This can be resolved in at least two ways.
- Generate the key at a random location, then derive the actual location and move the key there
- If moving a key is not possible, then an additional mapping from key location to some storage-internal location identifier can be maintained. Then it's possible to generate the key at some storage-internal location, calculate the key location and store the mapping.
Since this also needs to happen before the DID can be derived from the public key, similar approaches can be used to work around the not-yet available DID partition key. Storages may choose to have one statically identified partition where keys are generated initially, and then moved from there. Storages whose restrictions do not allow for this, may want to use the flattened storage layout described in example layout and use the mapping approach.
Storage Test Suite
The StorageTestSuite
can be used to test the basic functionality of storage implementations. See its documentation (Rust docs, Wasm docs) for more details.
Examples
This section shows the Rust and TypeScript MemStore
implementations, which are thoroughly commented.
- Node.js
- Rust
loading...
loading...