# DIDComm API Options The API below is just a language agnostic pseudocode to show the general principles, not exact API signatures. The examples are given for auth crypt of a signed message. Anoncrypt case works similarly. ## DID Comm workflow 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 Parser **interface** (and possibly implementation): parses the found DID DOC to get the public keys and other information. This can return only the subset of DID DOC needed for the DID Comm. **Note 1**: possible we can combine DID Resolver and DID DOC Parser interfaces into one, but this is rather details not changing the essence of the options. **Note 2**: In a general case DID DOC Parser interface is assumed. But the options that contain that interface may actually have an implementation (probably need to parse just a subset of DID DOC). Whether we have just DID DOC Parser interface or implementation as well doesn't change the essence of the options. ## Options Overview Component 1 (DID Comm core logic) is a must-have part of DID Comm libraries. Other options are different based on whether Components 2-4 should also be part of DID Comm libraries. - Option 1: Implicit API - All four components are part of DID Comm libs - All public API methods operate with DIDs or key IDs, not with keys itself - Option 2: Explicit API - Only Component 1 (DID Comm core logic) is part of DID Comm libs - Secret resolver, DID Resolver and DID DOC parser interfaces are **not** part of DID Comm libs - All public API methods operate with keys itself (in JWK format). DID resolving, DID DOC parsing and getting private keys is out of scope. - Option 3: Explicit API with DID resolver - DID Comm core logic, DID Resoler interface and DID DOC Parser interfaces are part of DID Comm libs - Secret resolver interface is not part of DID Comm libs - All public API methods operate with private keys (in JWK format) and DIDs, so private keys are passed explicity (secret resolver usage is out of scope), but DID resolution is done by DID Comm - Option 4: Implicit and explicit API - All four components are part of DID Comm libs - There are multiple versions of all API calls: - Implicit - as in Option 1 - Explicit - as in Option 2 - [Optionally] Explicit with DID resolution - as in Option 3 The option choice depends on th answers to the following questions: - whether we expect DID DOC parsing implementation be part of DIDComm libs, or a separate library (either existing one or a new one but separate from DIDComm lib) - whether DID Resolving (and DID DOC parsing) must be part of DIDComm libs from the DID Comm concept point of view. In other words, do we understand by DIDComm a JOSE-based message building and corresponding crypto, or DID resolving/parsing is also a conceptual and inseparable part of DID Comm. ## Proposed Option We propose to go with **Option 1**. Pros: - The user doesn't need to know DID Comm details, doesn't need to know how to combine multiple parts (DID resolver, DID Comm core, secrets storage, etc.) - The API is very easy to use and understand. Option 1 has some Cons (see below), but it looks like every Cons can be resolved in an acceptable way: - 1: *DID Comm libs are more complex to implement*: it's our job as software developers to implement it. It's better for us to spend time once for implementation, than every user of DID Comm will have to understand DID Comm and implement the code - 2: *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) - 3: *It's not clear what to do with the case of onion encryption*: it needs to be defined on the DID Comm protocol level how many nested encryptions are possible, to what extend we need to do decryption, and what is behaviour if for example sender key IDs and expected DIDs don't match at some level. - 4: *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. ## Options Cons and Pros - Option 1: Implicit API - Pros - API is much easier to understand and use - Can process nested encryptions automatically - Cons - DID Comm libs are more complex to implement (comparing to Option2) - 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). - It's not clear what to do with the case of onion encryption (encrypt+encrypt+encrypt+...). - Need to implement a secrets resolver (wallet) interface - Option 2: Explicit API - Pros - Every DID Comm component (1-4, see above) is separate and modular, so allows flexible usage - DID Comm libs are compact, thin and easier to implement - DID Comm methods do only message building and crypto, and hence can easily be called from any secure enclave - Cons - The API is more complex (especially decrypt and verify part) - The decrypt/verify part example looks like a boilerplate all apps will have to wrap in almost similar way. - Harder to process nested encryptions - Questionable - The API is actually not related to DID at all. It's rather a wrapper against JOSE implementing algorithms and format specific for DID Comm. So, it has 'Comm' but doesn't have 'DID'. That can be fine if we alrady have other standard libs that can parse DID DOC. - Option 3: Explicit API with DID resolver - Pros - API is quite easy to understand and use (comparing to Option 2) - No need to implement secrets resolver (wallet) interface - Cons - DID Comm libs are more complex (comparing to Option2) - API methods may call some network and asynchronous methods internally (DID resolving). There will be no way to call just a crypto (in a thin secure enclave for example). - Decryption on the receiver side may look quite complex (not as easy as in Option 1, but not as complex as in Option 2). - Harder to process nested encryptions - Option 4: Implicit and explicit API - Pros - It's possible to use the API in a way easy to use and understand (as in Option 1) that hides low-level details - It's possible to use more low-level version of the API if needed (for example, in secure enclaves) - Cons - Multiple versions of the same API may be confusing - More implementation work ## Questions 1. How many levels can be in onion encryption (encrypt+encrypt+encrypt)? Do we support only anoncrypt+authcrypt, or any number of nested encryptions? 2. Does the library need to check that `from` header from the JWM message matches the `skid` in case of authcrypt? 3. Is it possible to have different `skid` values in case of onion (nested) encryption? How (and whether) should we check that `from` header matches the sender in case of onion encryption? 4. Can `skid` and JWS `kid` belong to different DIDs? Should we raise an exception in this case? 5. When should we stop decrypting a message with onion (nested) encryption? Do we expect to always reach the payload (JWM) and raise an exception otherwise? Or we should just decrypt until we have a corressponding target key? 6. What if we can not decrypt a payload in an onion (nested) encryption at some level as our key is not in the list ot target keys? Should we just raise an exception in this case? ## Options Details ### Option 1: Implicit API ``` // 1: Register implementations for interfaces register_did_resolver(...) register_secrets_resolver(...) ... // 2: sign and encrypt payload = "..." bytes_to_transfer = MessageBuiler(payload) .from(from_did) .build() .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] ) .pack() ... // 3A. decrypt and verify signature (payload, signed_message) = unpack(transferredBytes) // 3B. decrypt and verify signature later (payload, signed_message) = unpack(transferredBytes, verify_signature=false) if signed_message != null: verify_signature(signed_message) ``` - `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_key_id=from_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]`). - does JWE encryption for the specified algorithms - `unpack_and_verify` - 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 for the sender's DID - does JWE decryption for the found private 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. ### Option 2: Explicit API All keys (private/public) are expected in JWK format. ``` // 1: find keys (out-of-scope for the DID Comm lib) // set by the caller by calling app-specific secrets resolver (from_sign_key_id, from_sign_private_key) = ... // set by the caller by calling app-specific DID resolver and DID DOC parser [(to_key_id_1, to_public_key_1), (to_key_id_2, to_public_key_2)] = ... // 2: sign and encrypt payload = "..." bytes_to_transfer = MessageBuiler(payload) .from(from_did) .build() .sign( from=(from_sign_key_id, from_sign_private_key) ) .auth_crypt( alg=ECDH-1PU+A256KW, enc=A256CBC-HS512, from=(from_key_id, from_private_key), to_dids=[to_did_1, to_did_2] to=[ (to_key_id_1, to_public_key_1), (to_key_id_2, to_public_key_2), ] ) .pack() ... // 3. decrypt and verify signature (from_key_id, from_signed_key_id, to_key_ids) = parse_packed(msg) while msg.is_jwe(): from_public_key = null from_sign_public_key = null if from_key_id != null: // out-of-scope for the DID Comm lib: // calls app-specific DID resolver for from_key_id from_public_key = ... if to_key_ids != null: // out-of-scope for the DID Comm lib: // calls app-specific secrets resolver for to_key_id belonging to the given receiver to_private_key = ... (msg, from_sign_public_key_id) = decrypt( msg = transferredBytes, to_private_key=to_private_key, from_public_key=from_public_key ) if from_sign_public_key_id != null: // out-of-scope for the DID Comm lib: // calls app-specific DID resolver for from_sign_public_key_id from_sign_public_key = ... msg = verify_signature( msg=msg, from_sign_public_key=from_sign_public_key ) ``` - `sign` - does JWS signing for the given sender's private signing key (signing algorithm is detected from the key type) - `auth_crypt` - does JWE encryption for the specified algorithms and the given keys - `parse_packed` - parses the message, and finds sender's key ID (`from_key_id`) used for authcrypt (`skid` field) and target key IDs - `from_key_id` will null/empty in case of anoncrypt - `decrypt` - does JWE decryption for the given sender and recipient keys - `from_public_key` can be null/empty in case of anoncrypt - returns the decrypted payload (possible signed) and the sender's signing public key ID if it's signed (otherwise signing key is null/empty) - `verify_signature` - does JWS signature verification for the given sender's signing key. ### Option 3: Explicit API with DID Resolver interface Private keys are expected in JWK format. ``` // 1: Register implementations for interfaces register_did_resolver(...) ... // 2: find private keys (out-of-scope for the DID Comm lib) // set by the caller by calling app-specific secrets resolver (from_sign_key_id, from_sign_private_key) = ... // 3: sign and encrypt payload = "..." bytes_to_transfer = MessageBuiler(payload) .from(from_did) .build() .sign( from=(from_sign_key_id, from_sign_private_key) ) .auth_crypt( alg=ECDH-1PU+A256KW, enc=A256CBC-HS512, from=(from_key_id, from_private_key), to_dids=[to_did_1, to_did_2]) .pack() ... // 4. decrypt and verify signature while msg.is_jwe(): [to_key_id_1, to_key_id_2] = get_receipents(msg) // out-of-scope for the DID Comm lib: // calls app-specific secrets resolver for to_key_id belonging to the given receiver to_private_key = ... (msg, signed_msg) = decrypt( msg=transferredBytes, to_private_key=to_private_key ) if signed_msg != null: verify_signatue(signed_msg) ``` - `sign` - does JWS signing for the given sender's private signing key (signing algorithm is detected from the key type) - `auth_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, given sender's private keys and found target key IDs - `get_receipents` - parses the message, and finds target key IDs - `decrypt_and_verify` - parses the message, and finds sender's key ID used for authcrypt (`skid` field) - calls DID resolver interface and finds a public key for the sender's key ID used for authcrypt. - does JWE decryption for the given private key (`to_private_key`) and found 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. ## Option 4: Implicit and explicit API Provides at least two versions of the API - API signatures as in Option 1 (high-level usage) - API signatures as in Option 2 (low-level usage) - [Optionally]: API signatures as in Option 3 ## Possible nested message combinations - 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. ## How to handle nested messages on receiver side We assume Option 1 is chosen (implicit API) - Regardless of combination, just one common call should be called by reciecver: `unpack`. - `unpack` returns a tuple: `(payload, signed_payload)` regardless of combination. `signed_payload` can be null/empty if there is no signature. - by default, `unpack` verifies the signature if it's present. - it should be possible to call unpack without signature verification (either pass a `verify_signature=false` parameter, or have two methods: `unpack` and `unpack_and_verify`). - if input is `anoncrypt(authcrypt)` combination, `unpack` performs both decryptions at once and returns the final payload (and optionally a signed message). ## Proposed Consistency checks We assume Option 1 chosen. `pack` and `unpack` methods perform the following consistency checks. If an issue is found, an exception is raised. - 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 authcrpt and sign are used, then JWS `kid` and authcrypt `skid` may belong to different DIDs. - if anoncrypt(authcrypt) is used, then it must have the same list of recepient key IDs for authcrypt and anoncrypt. - if receiver has multiple keys (in his wallet/secrets resolver) that can decrypt the message, it must be decryptable for all of them.