# An Envelope for Multi-Recipient Encryption of CBOR Objects ## BCR-2021-003 DRAFT **© 2021 Blockchain Commons** Authors: Wolf McNally, Christopher Allen<br/> Date: May 18, 2021 --- ## Introduction This document describes a method for creating a digital "envelope" that encrypts a CBOR object to one or more recipients using public key cryptography. This object has the UR type `crypto-envelope` and the CBOR tag #6.314. A `crypto-envelope` may be used as a top-level UR, or as tagged CBOR within a parent structure. The cryptographic algorithms specified herein are all implemented in [LibSodium](https://libsodium.gitbook.io). Any library that implements the same primitives could be substituted. Appendix A of this document describes an extension to [SSKR](bcr-2020-011-sskr.md) that uses `crypto-envelope` to allow sharding of objects of any size. Appendix B of this document describes an alternate approach to the problem of both signing and encryption using signcryption. Appendix C of this document describes an approach to the problem of public key cryptography that is compatible with Bitcoin Schnorr keys. This approach uses ECHD with secp256k1 and XSalsa20-Poly1305. ## Status This is currently an **early draft**. Currently there are no reference implementations or validated test vectors, and all aspects of this specification are subject to revision. ## Algorithms Used Public key encryption uses the following algorithms, as defined and implemented in the [LibSodium `crypto_box_*` API](https://libsodium.gitbook.io/doc/public-key_cryptography/authenticated_encryption): * Key exchange: [X25519](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x25519/) * Encryption: [XSalsa20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xsalsa20) * Authentication: [Poly1305](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction) MAC Symmetric encryption uses the following algorithms, as defined and implemented in the [LibSodium `crypto_secretbox_*` API](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretbox): * Encryption: [XSalsa20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xsalsa20) * Authentication: [Poly1305](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction) MAC The method used below to support multiple recipients is based on the one implemented by the [Minilock 2 file format](https://45678.github.io/miniLock-file-format/2.html). ## CDDL and Pseudocode **📖 CDDL**: Data structures in this document are specified using [CDDL](https://datatracker.ietf.org/doc/html/rfc8610). These specifications are normative. **🖥 Pseudocode**: Examples in this document are written in pseudocode, and are only informative. ## Cryptographic Keys A `crypto-secret-key` is a random 32-byte string. It may be used as a symmetric key or as the secret key of a key pair. When not used as the top-level object of a UR, it is tagged #6.315. A `crypto-secret-key` may be generated by several methods: * A cryptographically-strong source of randomness. LibSodium provides `randombytes_buf()` for generating such strings. * Taking the SHA-256 digest of a `crypto-seed` concatenated with the UTF-8 representation of a user-chosen passphrase. This [prevents the passphrase from being the sole source of entropy](http://blog.jacobtorrey.com/my-minilock-concerns). The passphrase may be the empty string if the seed is to be the sole basis for the key. * Using the UTF-8 representation of a user-chosen passphrase with a CPU-intensive password hashing algorithm like LibSodium's `crypto_pwhash()`. In this case the passphrase must not be the empty string, and must be of sufficient complexity to resist dictionary attacks. **📖 CDDL** ``` crypto-secret-key = #6.315(bytes .size 32) ; = randombytes_buf(32) ; = sha256(crypto-seed || utf-8-passphrase) ; = crypto_pwhash(utf-8-passphrase) ``` A `crypto-public-key` is a 32-byte string derived from a `crypto-secret-key`. It can be used to encrypt messages that can only be decrypted by the corresponding `crypto-secret-key`. LibSodium provides `crypto_scalarmult_base()` to derive a public key from a secret key. When not used as the top-level object of a UR, it is tagged #6.316. **📖 CDDL** ``` crypto-public-key = #6.316(bytes .size 32) ; = crypto_scalarmult_base(crypto-secret-key) ``` ## Nonces A cryptographic [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) ("number used once") is a random 24-byte string used to ensure that an encryption function always returns unique output even when the same message is encoded more than once. A nonce is not private, and is distributed with the encrypted message. LibSodium provides `randombytes_buf()` for generating such strings. When not used as the top-level object of a UR, it is tagged #6.317. **📖 CDDL** ``` crypto-nonce = #6.317(bytes .size 24) ; = new_nonce() { randombytes_buf(24) } ``` ## Plaintext The term "plaintext" as used in this document MUST be a well-formed CBOR object. ## Ciphertext Ciphertext is the encrypted plaintext. It is stored as a CBOR byte string. **📖 CDDL** ``` ciphertext = bytes ``` ## Multi-Recipient Encryption and Decryption In a multi-recipient scenario as supported by `crypto-envelope`, a message is encrypted to one or more recipients. Below, further structures are defined using CDDL and pseudocode is used to show an example where a message is encrypted to either Alice or Bob. First, generate a symmetric encryption key and using it to encrypt the plaintext: **🖥 Pseudocode** ``` content_nonce = new_nonce() content_key = crypto_secretbox_keygen() content_ciphertext = crypto_secretbox_easy(plaintext, content_nonce, content_key) ``` Generate a content permit that contains the information needed to decrypt the message. **📖 CDDL** ``` permit = { nonce: crypto-nonce, key: crypto-secret-key } nonce = 1 key = 2 ``` **🖥 Pseudocode** ``` content_permit = permit { nonce: content_nonce, key: content_key } ``` Generate an ephemeral key pair used to encrypt the permit to the recipients. **🖥 Pseudocode** ``` (ephemeral_secretkey, ephemeral_publickey) = crypto_box_keypair() ``` Generate a set of encrypted permits containing the information needed to decrypt the message, one for each possible recipient: **📖 CDDL** ``` recipient-permit = { nonce: crypto-nonce permit: ciphertext } nonce = 1 permit = 2 ``` **🖥 Pseudocode** ``` alice_nonce = new_nonce() alice_permit = recipient-permit { nonce: alice_nonce, permit: crypto_box_easy(content_permit, alice_nonce, alice_publickey, ephemeral_secretkey) } bob_nonce = new_nonce() bob_permit = recipient-permit { bob_nonce, encrypted_permit: crypto_box_easy(content_permit, bob_nonce, bob_publickey, ephemeral_secretkey) } ``` Construct the envelope: **📖 CDDL** ``` crypto-envelope = { permits: [recipient-permit], ephemeral: crypto-public-key, content: ciphertext } permits = 1 ephemeral = 2 content = 3 ``` **🖥 Pseudocode** ``` envelope = crypto-envelope { permits: [alice_permit, bob_permit], ephemeral: ephemeral_publickey, content: content_ciphertext } ``` On receiving the `crypto-envelope`, Bob attempts to decrypt each permit, stopping when he finds one he can decrypt: **🖥 Pseudocode** ``` ; fails content_permit = crypto_box_open_easy( alice_permit.permit, alice_permit.nonce, envelope.ephemeral, bob_secretkey ) ; succeeds content_permit = crypto_box_open_easy( bob_permit.permit, bob_permit.nonce, envelope.ephemeral, bob_secretkey ) ``` Bob uses the symmetric key from the permit to decrypt the message. **🖥 Pseudocode** ``` plaintext = crypto_secretbox_open_easy(envelope.content, content_permit.nonce, content_permit.key) ``` ## Appendix A: Extended SSKR Alice wishes to back up an object larger than a seed using SSKR shares distributed to a set of trustees. The Shamir's Secret Sharing algorithm upon which SSKR is based can split secrets up to 32 bytes in length. This is not large enough to split arbitrary binary objects, but is large enough to split a `crypto-secret-key`, which is always exactly 32 bytes long. Alice first generates a key pair for her object: **🖥 Pseudocode** ``` (object_secretkey, object_publickey) = crypto_box_keypair() ``` She then creates a `crypto-envelope` for her object with `object_publickey` as the recipient: **🖥 Pseudocode** ``` object_envelope = crypto-envelope { permits: [object_permit], ephemeral: ephemeral_publickey, content: content_ciphertext } ``` She then generates nine SSKR shares in a "2-of-3 of 2-of-3" split using `object_secretkey` as the sharded secret: **🖥 Pseudocode** ``` [shares] = sskr_split(object_secret_key, 2, (2, 3), (2, 3), (2, 3)) ``` To each trustee she provides one share as `ur:crypto-sskr` and a copy of `object_envelope` as `ur:crypto-envelope`. To reverse the process, she will need to collect a minimum of four shares: 2 from each of 2 of the 3 groups, and at least one copy of the envelope. With this she can reconstruct `object_secretkey`, which she can then use to decrypt the object in the envelope. Currently an SSKR share is defined like this: **📖 CDDL** ``` sskr-share = bytes crypto-sskr = sskr-share ``` Where the first five bytes are the SSKR header and the remainder are the sharded secret. A new type that includes both the share and the envelope is now defined, tagged with #6.318 when not used as the top level object in a UR: **📖 CDDL** ``` crypto-sskr-envelope = { share: sskr-share ; share of `object_secret_key` envelope: crypto-envelope ; envelope encrypting the original object with `object_public_key` as recipient. } share = 1 envelope = 2 ``` Finally, to simplify the use of this Extended SSKR strategy, the `crypto-sskr` specification is extended so the sharded secret and the encrypted envelope are relayed together in the same `ur:crypto-sskr`: **📖 CDDL** ``` crypto-sskr = sskr-share / #6.318(sskr-envelope) ; Extended with new alternative ``` This allows the existing `crypto-sskr` standard to be extended to back up objects of any size while retaining backward compatibility. ## Appendix B: Signcryption One goal not addressed by this document is providing for signing as well as encryption. The proposal described above handles encryption but not signing. One approach we have considered would be to create a separate CBOR structure `crypto-seal` that would carry a content message that may be plaintext or that may be encrypted by the `crypto-envelope` structure. Composing this structure would then support sign-then-encrypt or encrypt-then-sign. A more all-encompassing proposal would provide a unified way of addressing both, that would handle multi-recipient encryption, signed plaintext, or messages that are both signed and encrypted to multiple recipients. Such a proposal would also provide for the attached signature to support single signature, multi-signature (n-of-n) and threshold signature (k-of-n) schemes. One approach to supporting this would be to create signatures using [Ristretto](https://ristretto.group/), which is [supported by LibSodium](https://libsodium.gitbook.io/doc/advanced/point-arithmetic/ristretto), although Ristretto is not integrated into its high-level public key signing algorithms. The author of this document is an engineer, not a cryptographer, so determining a correct approach to solving all of these problems simultaneously is not in his wheelhouse. Nonetheless, his exploration of the topic suggests that an approach using [signcryption](https://en.wikipedia.org/wiki/Signcryption) may satisfy these requirements. The [LibSodium docs](https://libsodium.gitbook.io/doc/quickstart#how-can-i-sign-and-encrypt-using-the-same-key-pair) themselves provide a pointer to a GitHub repo where [a signcryption method](https://github.com/jedisct1/libsodium-signcryption) has been implemented on top of LibSodium that implements the [Toorani-Beheshti signcryption construction](https://arxiv.org/ftp/arxiv/papers/1002/1002.3316.pdf). It seems like it has all the sorts of attributes we'd want, including confidentiality, unforgeability, integrity, non-repudiation, public verifiability, and forward secrecy. This signcryption implementation also integrates and uses LibSodium's Ristretto implementation. If implementing on top of Ristretto ultimately supports multisig/threshold signature schemes, then this should accomplish that goal. Signcryption to multiple recipients can be added by using the same techniques as described above, based on the Minilock 2 file format. Although the first phase of the signcryption implementation generates a key to be used for encryption of the plaintext, it does not automatically encrypt the message, nor does the process actually require the encryption to take place. So if the message is to be sent signed but not encrypted, the encryption step can simply be elided. Support for encrypted but not signed texts can be done by performing the signcryption with an ephemeral private key and including the ephemeral public key with the signcrypted payload. In other words, all the permutations of signing and/or encryption are just degenerate cases of the full signcryption protocol. Questions: * Is this described signcryption approach sound? * Are there other security concerns or factors to be considered in specifying or implementing the approaches described herein? ## Appendix C: LibSecp256k1 + LibSodium The above recommendation of Ristretto notwithstanding, it is desirable that the keys that we use for our encryption/signing object be compatible with Bitcoin Schnorr keys. An alternative approach to public key cryptography that avoids the use of Ristretto/Curve 25519 would be to use [secp256k1 with Elliptic Curve Diffie-Hellman for key agreement](https://asecuritysite.com//encryption/ecdh2) (as implemented in libsecp256k1) and then using the shared key with the XSalsa20-Poly1305 construction (as implemented in LibSodium) for the actual encryption. This could possibly be combined with the above approaches including signcryption, threshold signatures, and multi-recipient encryption. Questions: * What stack of algorithms/methods provides for all the possible requirements above while being compatible with Bitcoin Schnorr keys?