owned this note
owned this note
Published
Linked with GitHub
# DID Comm API Design
We are proposing so called Implicit API.
## Components
The DID Comm consists of the following components:
1. DID Comm core logic **implementation**: JOSE-based message builder, encryption/descryption, signing/verification.
2. Secret resolver (Wallet) **interface**: manages private keys.
3. DID Resolver **interface**: finds a DID DOC for the given DID.
4. DID DOC **interface**: subset of DID DOC needed for the DID Comm.
## Example API
The API below is just a language agnostic pseudocode to show the general principles, not exact API signatures.
```
// 1: Register implementations for interfaces
register_default_did_resolver(...)
register_default_secrets_resolver(...)
...
// 2A: sign and encrypt for sender DID
payload = "..."
msg = MessageBuilder(payload, id, type)
.from(from_did) # optional
.to(to_did) # optional
.created_time(ts) # optional
.expires_time(ts) # optional
.typ(typ) # optional
.finalize()
packed_msg = PackBuiler(msg)
.did_resolver(did_resolver) # optional
.secrets_resolver(secrets_resovler) # optional
.sign(from_did=from_did)
.auth_crypt(
alg=ECDH-1PU+A256KW,
enc=A256CBC-HS512,
from_did=from_did,
to_dids=[to_did_1, to_did_2]
)
.anon_crypt(
alg=ECDH-ES+A256KW,
enc=XC20P,
to_dids=[to_did_1, to_did_2]
)
.pack()
bytes_to_transfer = forward(
packed_msg=packed_msg,
to=to_did,
alg=ECDH-ES+A256KW,
enc=XC20P,
)
// 2B: sign and encrypt for sender key ID
....
.sign(from_sign_key_id=from_sign_key_id)
.auth_crypt(
alg=ECDH-1PU+A256KW,
enc=A256CBC-HS512,
from_key_id=from_key_id,
to_dids=[to_did_1, to_did_2]
)
...
// 3. decrypt and verify signature
(payload, metadata, signed_message) = UnpackBuilder()
.did_resolver(did_resolver) # optional
.secrets_resolver(secrets_resovler) # optional
.is_forward(false) # optional
.mtc( # optional
MtcBuilder()
.expect_signed(false) # optional
.expect_authcrypted(false) # optional
.expect_anoncrypted(false) # optional
.expect_signed_by_encrypter(false) # optional
.expect_decrypted_by_all_keys(false) # optional
.finalize()
)
.unpack(transferred_bytes)
```
## Interfaces
```
interface SecretsResolver:
get_key(kid: string): Promise<JWK>
get_keys(did: string): Promise<List[JWK]>
find_keys(kids: List[string]): Promise<List[JWK]>
interface DIDDOC:
key_agreements(did: string): List[JWK]
key_agreement(kid: string): JWK
authentications(did: string): List[JWK]
authentication(kid: string): JWK
routing_keys(did: string): List[JWK]
interface DIDResolver:
resolve(did: string): Promise<DIDDOC>
```
## Cons and Pros
Pros:
- API is much easier to understand and use
- Can process anoncrypt(authcrypt) automatically
- Same behavior on the caller side regardless of the message structure
Cons:
- API methods may call some network and asynchronous methods internally (DID resolving, accessing wallet). There will be no way to call just a crypto (in a thin secure enclave for example).
- Need to implement a secrets resolver (wallet) interface
It looks like every Cons can be resolved in an acceptable way:
- *API methods may call some network and asynchronous methods internally*:
- if the caller expects fast exsecution of DID Comm API methods, and doesn't wany any (potentially surprising) network or file calls under the hood, the caller should just provide proper implementations of interfaces (DID resolver, secrets resolver). For example, it can be in-memory maps or caches for did doc resolving.
- we can provide both syncronous and asyncronous versions of DID Comm API (depending on the target language)
- *Need to implement a secrets resolver (wallet) interface*:
- the interface wil lbe very simple: `key_id -> ptivate_key in JWK`. And even without the interface ther user will have to do the same on the app level.
- Having the interface helps to hide DID Comm details and call the API easier.
- We will provide a default in-memory secrets resolver implementation for test and demo purposes.
## Details
### Possible nested message combinations
The message to be transferred may have one of the following formats:
- plain
- sign(plain)
- anoncrypt(plain)
- authcrypt(plain)
- anoncrypt(sign(plain))
- authcrypt(sign(plain)) ?????
- anoncrypt(authcrypt(plain))
- anoncrypt(authcrypt(sign(plain))) ?????
All other combinations are invalid. An error will be thrown in another combination is received.
### Sender specific
Sender's API allows to prepare a message only in one of the combinations above.
Routing is not performed automatically, but needs to be done explicitly by calling `forward(to_did)`. `forward` finds all routing keys for the given DID and prepares an onion message by calling `anoncrypt` possibly multiple times.
- `sign`
- calls the secrets resolver interface and finds the sender's private signing key for the given `from_did` present in the wallet. If there are multiple signing keys for the given DID present in the wallet, the first one will be used. Alternatively, a key can be specified by calling `sign(from_sign_key_id=from_sign_key_id)` instead.
- does JWS signing (signing algorithm is detected from the key type)
- `auth_crypt`
- calls the secrets resolver interface and finds the sender's private key for the given `from_did` present in the wallet. If there are multiple keyAgreement keys for the given DID present in the secrets resolver, the first one will be used. Alternatively, a key can be specified by calling `auth_crypt(from_key_id=from_key_id)` instead.
- calls DID resolver interface and finds all key_ids and corresponding public keys for all given target DIDs (`[to_did_1, to_did_2]`). Only the keys with the same type as the sender key are taken.
- does JWE encryption for the specified algorithms
- `anon_crypt`
- calls DID resolver interface and finds all key_ids and corresponding public keys for all given target DIDs (`[to_did_1, to_did_2]`).
- does JWE encryption for the specified algorithms
- `pack`
- performs consistency checks for the message (see below).
### Receiver specific
Regardless of combination, just one common call should be called by receiver: `unpack`.
- `unpack` returns `(payload, metadata, signed_payload)` regardless of nested messages combination.
- `signed_payload` can be null/empty if there is no signature.
- `metadata` can be used for further confirmation with [MTC](https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0029-message-trust-contexts/README.md) where conformation depends on the payload message type (protocol dependent).
- `unpack` verifies the signature if it's present.
- if input is `anoncrypt(authcrypt)` combination, `unpack` performs **both decryptions** at once and returns the final payload (and optionally a signed message).
`unpack` (in case of `anoncrypt(authcrypt(sign(payload)))`):
- performs consistency checks (see below)
- decrypts anoncrypt
- parses the message, and finds target key IDs
- calls the secrets resolver interface and finds a key ID and a correspondiong private key belonging to the given recepient
- does JWE decryption for the found private key
- decrypts authcrypt
- parses the message, and finds sender's key ID used for authcrypt (`skid` field) and target key IDs
- calls DID resolver interface and finds a public key for the sender's key ID used for authcrypt.
- calls the secrets resolver interface and finds a key ID and a correspondiong private key belonging to the given recepient - does JWE decryption for the found private key
- does JWE decryption for the found private key and sender's public key
- checks if payload is signed, and if so calls DID resolver interface and finds a public key for the sender's signing key ID. Does JWS signature verification for the found sender's signing key ID.
### Proposed Consistency checks
If a check is failed, an error is returned (or an exception is raised).
`pack` method perform the following consistency checks.
- check that message format (nested level) is one from the list above. Other combinations are not expected. Actually this is guaranteed by the sender's API itself.
- if the payload has `from` attribute and authcrypt is used, then `skid` must be from the same DID as specified in `from`.
- if anoncrypt(authcrypt) is used, then it must have the same list of recepient key IDs for authcrypt and anoncrypt.
- if authcrpt and sign are used, then JWS `kid` and authcrypt `skid` must belong to the same DID by default (but it's configurable in the unpacker builder).
`unpack` method perform the following consistency checks.
- check that message format (nested level) is one from the list above. Other combinations are not expected.
- if the payload has `from` attribute and authcrypt is used, then `skid` must be from the same DID as specified in `from`.
- if anoncrypt(authcrypt) is used, then it must have the same list of recepient key IDs for authcrypt and anoncrypt.
- if authcrypt and sign are used, then JWS `kid` and authcrypt `skid` must belong to the same DID by default (but it's configurable in the unpacker builder).
- if receiver has multiple keys (in his wallet/secrets resolver) that can decrypt the message, it must be decryptable for at least one of them by default (but it's configurable in the unpacker builder).