diff --git a/CRYPTO.md b/CRYPTO.md index b88415e7..cbe7b961 100644 --- a/CRYPTO.md +++ b/CRYPTO.md @@ -94,12 +94,12 @@ To encrypt to a target recipient $i$, knowing $pk_{ecc}^i$ and $pk_{mlkem}^i$: 1. Compute shared secrets and ciphertexts for both KEM: -$$ -\begin{aligned} -(ss_{ecc}^i, ct_{ecc}^i) = \textrm{Encapsulate}_{DHKEM}(pk_{ecc}^i) \\ -(ss_{mlkem}^i, ct_{mlkem}^i) = \textrm{Encapsulate}_{MLKEM}(pk_{mlkem}^i) -\end{aligned} -$$ +```math +\begin{align} +(ss_{ecc}^i, ct_{ecc}^i) &= \textrm{Encapsulate}_{DHKEM}(pk_{ecc}^i) \\ +(ss_{mlkem}^i, ct_{mlkem}^i) &= \textrm{Encapsulate}_{MLKEM}(pk_{mlkem}^i) +\end{align} +``` 2. Combine the shared secrets (implemented in `mla::crypto::hybrid::combine`): @@ -117,20 +117,22 @@ def combine(ss1, ss2, ct1, ct2): return key ``` -$$ +```math ss_{recipient}^i = \textrm{combine}(ss_{ecc}^i, ss_{mlkem}^i, ct_{ecc}^i, ct_{mlkem}^i) -$$ +``` 3. Wrap the recipients' shared secret: -$$ -(key^i, nonce^i) = \textrm{KeySchedule}_{recipient}( +```math +\begin{align} +(key^i, nonce^i) &= \textrm{KeySchedule}_{recipient}( \textrm{shared\ secret}=ss_{recipient}^i, \textrm{info}=\mathtt{"MLA\ Recipient"} )\\ -ct_{wrap}^i = \textrm{Encrypt}_{AES\ 256\ GCM}(\textrm{key}=key^i, \textrm{nonce}=nonce^i, \textrm{data}=ss_{recipients})\\ -ct_{recipient}^i = ct_{wrap}^i\ .\ ct_{ecc}^i\ .\ ct_{mlkem}^i -$$ +ct_{wrap}^i &= \textrm{Encrypt}_{AES\ 256\ GCM}(\textrm{key}=key^i, \textrm{nonce}=nonce^i, \textrm{data}=ss_{recipients})\\ +ct_{recipient}^i &= ct_{wrap}^i\ .\ ct_{ecc}^i\ .\ ct_{mlkem}^i +\end{align} +``` Informally, this process can be viewed as a per-recipient KEM taking a shared secret $ss_{recipients}$, the recipient public key (made of the elliptic curve and the PQC public keys) and returning a ciphertext $ct_{recipient}^i$. @@ -140,21 +142,25 @@ To obtain the shared secret from $ct_{recipient}^i$ for a recipient $i$ knowing 1. Compute the recipient's shared secret: -$$ -ss_{ecc}^i = \textrm{Decapsulate}_{DHKEM}(sk_{ecc}^i, ct_{ecc}^i) \\ -ss_{mlkem}^i = \textrm{Decapsulate}_{MLKEM}(sk_{mlkem}^i, ct_{mlkem}^i)\\ -ss_{recipient}^i = \textrm{combine}(ss_{ecc}^i, ss_{mlkem}^i, ct_{ecc}^i, ct_{mlkem}^i) -$$ +```math +\begin{align} +ss_{ecc}^i &= \textrm{Decapsulate}_{DHKEM}(sk_{ecc}^i, ct_{ecc}^i) \\ +ss_{mlkem}^i &= \textrm{Decapsulate}_{MLKEM}(sk_{mlkem}^i, ct_{mlkem}^i)\\ +ss_{recipient}^i &= \textrm{combine}(ss_{ecc}^i, ss_{mlkem}^i, ct_{ecc}^i, ct_{mlkem}^i) +\end{align} +``` 2. Try to decrypt the secret shared among recipients: -$$ -(key^i, nonce^i) = \textrm{KeySchedule}_{recipient}( +```math +\begin{align} +(key^i, nonce^i) &= \textrm{KeySchedule}_{recipient}( \textrm{shared\ secret}=ss_{recipient}^i, \textrm{info}=\mathtt{"MLA\ Recipient"} )\\ -ss_{recipients} = \textrm{Decrypt}_{AES\ 256\ GCM}(\textrm{key}=key^i, \textrm{nonce}=nonce^i, \textrm{data}=ct_{wrap}^i) -$$ +ss_{recipients} &= \textrm{Decrypt}_{AES\ 256\ GCM}(\textrm{key}=key^i, \textrm{nonce}=nonce^i, \textrm{data}=ct_{wrap}^i) +\end{align} +``` If the decryption is a success, returns $ss_{recipients}$. Otherwise, returns an error. @@ -203,7 +209,7 @@ $\mathrm{CSPRNG(n)}$ is a cryptographically secured RNG producing a n-bytes secr To encapsulate to a list of recipient $[(pk_{ecc}^0, pk_{mlkem}^0), ..., (pk_{ecc}^{n-1}, pk_{mlkem}^{n-1})]$: -$$ +```math \begin{align*} \mathtt{def\ } & \mathrm{HybridKEM.Encapsulate}([(pk_{ecc}^0, pk_{mlkem}^0), ..., (pk_{ecc}^{n-1}, pk_{mlkem}^{n-1})])\\ & ss_{recipients} = \mathrm{CSPRNG(32)}\\ @@ -213,11 +219,12 @@ $$ & ct_{recipients} = ct_{recipient}^0\ .\ \dots\ .\ ct_{recipient}^{n-1}\\ & \mathtt{return}\ ss_{recipients},\ ct_{recipients} \end{align*} -$$ +``` ---- To decapsulate from a ciphertext $ct_{recipients}$, knowing a recipient private key $(sk_{ecc}^i,sk_{mlkem}^i)$: -$$ + +```math \begin{align*} \mathtt{def\ } & \mathrm{HybridKEM.Decapsulate}((sk_{ecc}^i,sk_{mlkem}^i), ct_{recipients})\\ & \begin{align*} @@ -233,7 +240,7 @@ $$ \end{align*}\\ & \mathtt{throw\ KeyNotFoundError} \end{align*} -$$ +``` #### Arguments @@ -266,22 +273,22 @@ To encrypt n-bytes `data` to a list of public keys $[(pk_{ecc}^0, pk_{mlkem}^0), 1. Compute a shared secret and the corresponding ciphertext: -$$ +```math ss_{recipients},\ ct_{recipients} = \mathrm{HybridKEM.Encapsulate}([(pk_{ecc}^0, pk_{mlkem}^0), ..., (pk_{ecc}^{n-1}, pk_{mlkem}^{n-1})]) -$$ +``` 2. Derive the key and base nonce using HPKE -$$ +```math (key, base\_nonce) = \textrm{KeySchedule}_{hybrid}( \textrm{shared\ secret}=ss_{recipients}, \textrm{info}=\mathtt{"MLA\ Encrypt\ Layer"} )\\ -$$ +``` 3. Ensure key-commitment -$$ +```math \begin{align*} key\_commit& = \textrm{Encrypt}_{AES\ 256\ GCM}(\\ &\textrm{key}=key,\\ @@ -289,11 +296,11 @@ key\_commit& = \textrm{Encrypt}_{AES\ 256\ GCM}(\\ &\textrm{data}=\textrm{KeyCommitmentChain}\\ )& \end{align*} -$$ +``` 4. For each 4MB $chunk_j$ of data: -$$ +```math \begin{align*} enc_j& = \textrm{Encrypt}_{AES\ 256\ GCM}(\\ &\textrm{key}=key,\\ @@ -301,7 +308,7 @@ enc_j& = \textrm{Encrypt}_{AES\ 256\ GCM}(\\ &\textrm{data}=chunk_j\\ )& \end{align*} -$$ +``` 5. When the layer is finalized, the last chunk $chunk_n$ of data (with a length lower than or equals to 4MB) is encrypted the same way @@ -316,17 +323,19 @@ To decrypt the data at position $pos$: 1. Once for the whole session, get the cryptographic materials -$$ -ss_{recipients} = \mathrm{HybridKEM.Decapsulate}((sk_{ecc}^i, sk_{mlkem}^i), ct_{recipients})\\ -(key, base\_nonce) = \textrm{KeySchedule}_{hybrid}( +```math +\begin{align} +ss_{recipients} &= \mathrm{HybridKEM.Decapsulate}((sk_{ecc}^i, sk_{mlkem}^i), ct_{recipients})\\ +(key, base\_nonce) &= \textrm{KeySchedule}_{hybrid}( \textrm{shared\ secret}=ss_{recipients}, \textrm{info}=\mathtt{"MLA\ Encrypt\ Layer"} -)\\ -$$ +) +\end{align} +``` 2. Once for the whole session, check the key commitment -$$ +```math \begin{align*} commit& = \textrm{Decrypt}_{AES\ 256\ GCM}(\\ &\textrm{key}=key,\\ @@ -334,19 +343,21 @@ commit& = \textrm{Decrypt}_{AES\ 256\ GCM}(\\ &\textrm{data}=key\_commit\\ )& \end{align*}\\ -$$ -$$ +``` +```math \mathtt{assert\ }commit = \textrm{KeyCommitmentChain} -$$ +``` 3. Retrieve the encrypted chunk of data -$$ -start = pos - \mathtt{sizeof}(key\_commit)\\ -j = pos \div 4M\\ -$$ +```math +\begin{align} +start &= pos - \mathtt{sizeof}(key\_commit)\\ +j &= pos \div 4M\\ +\end{align} +``` Then: -$$ +```math \begin{align*} chunk_j& = \textrm{Decrypt}_{AES\ 256\ GCM}(\\ &\textrm{key}=key,\\ @@ -354,7 +365,7 @@ chunk_j& = \textrm{Decrypt}_{AES\ 256\ GCM}(\\ &\textrm{data}=enc_j\\ )& \end{align*} -$$ +``` #### Arguments @@ -388,14 +399,16 @@ The derivation scheme is based on the same ideas than `mla::crypto::hybrid::comb From a private key ($sk_{ecc}^i$ and $sk_{mlkem}^i$), the secret is derived from the path $path$ through: -$$ -ecc\_rnd = \mathrm{HKDF.Extract_{SHA512}}(\mathrm{salt}=0, \mathrm{ikm}=sk_{ecc}^i)\\ -seed = \mathrm{HKDF_{SHA512}}( +```math +\begin{align} +ecc\_rnd &= \mathrm{HKDF.Extract_{SHA512}}(\mathrm{salt}=0, \mathrm{ikm}=sk_{ecc}^i)\\ +seed &= \mathrm{HKDF_{SHA512}}( \mathrm{salt}=ecc\_rnd, \mathrm{ikm}=sk_{mlkem}^i, \mathrm{info}=\mathtt{"PATH\ DERIVATION"}\ .\ path ) -$$ +\end{align} +``` To derive a key using a `seed`, a `ChaChaRng` is used. If a `seed` is provided, the `ChaChaRng` is seeded with the first 32-bytes of $\mathrm{SHA512}(seed)$. Otherwise, the `ChaChaRng::from_entropy` is used, wrapping OS Cryptographic RNG sources. @@ -515,4 +528,4 @@ In other words, the list of recipient is not public. Still, the number of recipi [^reviewqb]: https://blog.quarkslab.com/security-audit-of-dalek-libraries.html [^reviewcloudflare]: https://blog.cloudflare.com/using-hpke-to-encrypt-request-payloads/ [^issuezeroize]: https://github.com/ANSSI-FR/MLA/issues/46 -[^issueallowunauth]: https://github.com/ANSSI-FR/MLA/issues/167 \ No newline at end of file +[^issueallowunauth]: https://github.com/ANSSI-FR/MLA/issues/167