├── .gitignore ├── README.md ├── revision1-WithKyberCR ├── Kyber.ocv ├── Makefile ├── PQXDH.m4.ocv └── README.md ├── revision1 ├── cryptoverif │ ├── .gitignore │ ├── Makefile │ ├── PQXDH.m4.ocv │ └── README.md ├── pqxdh-rev1.pdf └── proverif │ ├── Makefile │ ├── README.md │ ├── pqxdh-model.cpp.pv │ └── run.sh ├── revision2 ├── cryptoverif │ ├── .gitignore │ ├── Makefile │ ├── PQXDH.m4.ocv │ └── README.md ├── pqxdh-diff-rev-1-to-2.pdf ├── pqxdh-rev2.pdf └── proverif │ ├── Makefile │ ├── README.md │ ├── pqxdh-model.cpp.pv │ └── run.sh └── revision3 ├── cryptoverif ├── .gitignore ├── Makefile ├── PQXDH.m4.ocv └── README.md └── proverif ├── Makefile ├── README.md ├── pqxdh-model.cpp.pv └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pv 2 | *.cv -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains a formal analysis of PQXDH, using ProVerif and CryptoVerif. 2 | 3 | It is the artifact for the research paper `Formal verification of the PQXDH Post-Quantum key agreement protocol for end-to-end secure messaging`, by Karthikeyan Bhargavan, Charlie Jacomme, Franziskus Kiefer, and Rolfe Schmidt, published at USENIX Security 2024. 4 | 5 | * `revision1` contains the analysis of the specification of revision 1, both with ProVerif and CryptoVerif; 6 | * `revision2` contains the anlysis of the specification of revision 2, both with CryptoVerif and ProVerif; 7 | * `revision3` contains the anlysis of some attemps at coming up with a revision 3, still both with CryptoVerif and ProVerif; 8 | * `revision1-WithKyberCR` contains a complement to the analysis of revision 1 in CryptoVerif, by also considering that the implementation uses Kyber, which we prove to satisfy an additional collision resistance property. 9 | 10 | ## Checking the proof 11 | 12 | Each subfolder contain a dedicated README describing the files. 13 | 14 | We rely on Proverif 2.04 and CryptoVerif 2.08pl1, easily installed with `opam install proverif` and `opam install cryptoverif` if one has the `opam` ocaml package manager, see the respective webpages otherwise for dedicated installation instructions. 15 | 16 | Files may use preprocessors to enable modeling efficiently several variants of a protocol, a Makefile then gives the appropriate commands to verify the file. 17 | 18 | Each file contains it's expected runtime, obtained on a 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz, with 31Gb of RAM. 19 | 20 | # Background on formal methods 21 | 22 | Formal methods in security aim at providing very strong guarantees over the specifications of security protocols. It does so notably by doing computer-aided cryptography, a program is going to either do or verify the security proof. 23 | 24 | Two major tools of the domain or ProVerif and CryptoVerif. 25 | 26 | CryptoVerif formalizes and partially automates classical cryptographic proof, where we obtain security under the usual notions of IND-CCA, UF-CMA,... 27 | 28 | ProVerif works in a more abstract model, the so-called symbolic model, which abstracts away probabilities and cryptographic assumptions. It is very efficient and fully automated, and notably allows considering quickly many compromise scenarios. 29 | 30 | As a summary, CryptoVerif gives guarantees in the classical cryptographic models, but proofs often require manual guidance. ProVerif on its side does not yield the classical cryptographic guarantees, but is highly automated and can help explore many scenarios and notably find attacks. 31 | 32 | # Analysis 33 | 34 | We summarize below the several steps of the analysis, based on the multiple revisions. The detailed results of each step are in dedicated READMEs in the subfolder. 35 | 36 | Overall, our analysis of the revision 1 uncovered several imprecisions within the specification. We in fact identified several theoretical attacks. Importantly, looping with Signal's developers, we saw that the implementation does not suffer from any of those weaknesses. However, it means that a naive implementation of the specification would not meet the classically desired security guarantees for such protocols. 37 | 38 | Through discussion with Signal's developers, we identified several changes to the specification so that it better matches the current implementation and so that our theoretical attacks are fixed. This lead to the current revision 2. We also suggested several updates to make the protocol even more resilient on the long term, which are under consideration for a revision 3, but which would imply implementation changes. 39 | 40 | 41 | ## Analysis of revision 1 42 | 43 | We made models of this first version in both ProVerif and CryptoVerif. 44 | 45 | Both models are for an unbounded number of agents willing to communicate together, and we allow the compromise of long term identity keys. 46 | 47 | Simply by trying to formally write down the models, we gained several insights: 48 | 49 | - F1: PQXDH (as did X3DH) uses the same secret key for X25519 curve computations and XedDSA signatures. No precise security assumption for this joint use case exists. This forced us to make a simplifying assumption in our CryptoVerif model, where we split the identity key is split into two, one for X25519 computations and one for signatures. We noted that this was not explicit in the related work of section 4 of [PQXDH, rev1]. 50 | - F2: No explicit assumption for the AEAD was mentioned in [PQXDH, rev1], while the secrecy of exchanged messages of course depends on the post-quantum security of the AEAD. 51 | 52 | Then, when trying to come up with the proof in CryptoVerif, two suspicious appeared: 53 | 54 | - F3: we could not make the proof without making the assumption that encodeEC is always distinct from encodeKEM, as otherwise, SPK and PQPK can be confused. 55 | - F4: it did not look possible to prove authentication of the KEM public key, that is, even if two parties compute the same key, we could not prove that they agree on the KEM public key which was used. 56 | 57 | Going to ProVerif, we were in fact able to confirm that there are theoretical attacks on the protocol: 58 | - F3': an attacker can send a SPK instead of the PQPK to an initiator. If the length of the KEM public keys and curve public keys are equal, the protocol would proceed. The initiator then tries to compute an encapsulation, but using a curve element. The classical security guarantees of the KEM are only over honestly generated public keys of the KEM, we are thus in a situation where we have no security guarantees, and the shared secret can be weak and guessable by the attacker. 59 | - F3'': it is in fact also possible for an attack to send a PQPK instead of a SPK, maybe making multiple curve based computation go to a weak secret. 60 | - F4: we demonstrated that if we use a secure IND-CCA public key encryption to build an IND-CCA KEM, an attacker can in fact make two parties compute the same key but both believe to have used a distinct public key. The main issue here is that from the responder point of view, we do not have session independance: compromising the PQPK of one session can break the security of another independent session using an unrelated PQPK. This in fact means that the compromise of a single responder's PQPK implies the compromise of all its other present and futur PQPK. 61 | 62 | 63 | Generalizing on F3, we also had a last comment: 64 | - F5: we observed that there is no way for the initiator to know whether it is using a last resort PQPK or a one time one. 65 | 66 | Interestingly, F3'' in fact illustrates how by adding an extra component to a secure protocol, we may in fact lower the guarantees of the protocol. 67 | F4 is delicate to handle, as some KEM designers precisely state that "Application designers are encouraged to assume solely 68 | the standard IND-CCA2 property" [MCR] 69 | 70 | Importantly, from the practical point of view, those two issues are thwarted in the implementation: 71 | - each public key encoding is prefixed by a one byte identifier corresponding to the algorithm, and no confusion is possible; 72 | - Kyber in facts ties the shared secret to the KEM public key. 73 | 74 | Based on those 5 feedbacks, several fixes/improvements were proposed: 75 | - S1: clarify the gap in existing security proofs, and that there is still the need for a deeper study of the X25519 and XedDSA interactions; 76 | - S2: clarify the security assumptions over the AEAD, which is in fact a parameter of the protocol; 77 | - S3: clarify in the spec that the encodings must be disjoint; 78 | - S4: clarify that IND-CCA in general does not tie the shared secret to the public key, but mention that Kyber does things correctly 79 | - S4': add the KEM public key either in the AD 80 | - S4'': add the key derivation hash computations. 81 | - S5: add byte identifiers separating last resort and one time keys. 82 | 83 | 84 | ## Analysis of revision 2 85 | 86 | S1,2,3 and 4, not requiring changes to the implementation, were integrated inside revision 2, as well as S4' in the form of a recommendation. S4' and S5 were kept for later updates. 87 | 88 | The most notable change in term of security in the models was to add the kem public key in the AD, and remove some ProVerif threat model now explicitly forbidden by the spec (notably the public key signature confusions). 89 | 90 | In this new version, we were able to obtain all most desired security properties: 91 | 92 | * In CryptoVerif, we prove over revision 2 that under the gapDH assumption for the X25519 curve, the UF-CMA assumption over EdDSA (with disjoint keys), the ROM for the hash function and the IND-CPA+INT-CTXT assumption for the AEAD, we have secrecy and authentication of any completed key exchange. Moreover, just IND-CCA for the KEM, PRF for the hash function and IND-CPA+INT-CTXT is enough to ensure the future secrecy of keys as long as the signature was still UF-CMA when the key exchange took place. 93 | 94 | * In ProVerif, we prove in the symbolic model both authentication and secrecy, enumerating precisely the necessary condition so that the attacker can break the properties. Our security properties notably imply forward secrecy, resistance to harvest now decrypt later attacks, resistance to key compromise impersonation, and session independence. 95 | 96 | The main limitation of our analysis are: 97 | 98 | * the assumption in CryptoVerif that the identity key and signature key are in fact two distinct keys; 99 | * we don't have all possible compromise we would want in the CryptoVerif model, so the computational analysis could be improved in this direction; 100 | * putting the public key in the AD does not exactly mimic the behaviour of what Kyber does by including the public key in the shared secret. 101 | 102 | ## Proposals for revision 3 103 | 104 | Exchanging with Signal's developers, we are currently investigating several changes to the specification that would imply changes to the implementation. 105 | 106 | We provide here a model where: 107 | - P1: the F prefix in the hash function is removed. It does not seem to be needed anymore, compared to X3DH, as the info avoid any confusion. 108 | - P2: we directly put more information inside the KDF, at least the KEM pubic key, and are considering putting even more elements of the transcript. 109 | - P3: we add public key type identifiers (last resort or one time) in the signatures of the PQPK. 110 | 111 | 112 | Based on the new models, the observations are that: 113 | - P1 does not indeed change the security results, so it is a good simplification 114 | - P2 is interesting, it in fact simplifies the proof, by making sure even without the AEAD so two parties compute the same key only when they use the same PQPK. 115 | - P3 does not enable us to verify any new security properties, as an untrusted server can always only send the last resort PQPK (but at least, now the initiator is aware of it). Improved monitoring of the server still requires some work. 116 | 117 | Adding even more elements to the transcript, such as the other DH public keys, while good practice, does not always bring any obvious new security property. This also needs further exploring. 118 | Some remarks on this: 119 | - adding the IK_A and IK_B in the transcript also enables us to remove them from the AEAD AD, and as for the PQPK, make sure that two parties derive the same key only when they agree on the IKs used. This one is a bit more important, as for the moment, we need the AEAD AD to ensure that no small sub group equivalent public key of IK_A or IK_B was used. 120 | - adding the SPK, OPK and EK_A seems more optional. Currently, the parties only agree on those values modulo the small sub group elements. Yet, it does not have any clear security impact. 121 | 122 | Overall, including information directly inside the KDF rather than in the AD makes the cryptographic proof simpler, and notably less dependent on the security of the AEAD. 123 | 124 | ## Revision 1 with Kyber 125 | 126 | To justify the security of the existing implementation, we pushed further the analysis regarding the fact that " Kyber in facts ties the shared secret to the KEM public key." 127 | 128 | We developped a dedicated collision resistance notion on KEMs that capture this intuition, proved it for Kyber, and proved that PQXDH under this additional assumption does ensure the authentication o the PQPSK, thus thwarting F4. 129 | 130 | 131 | ## Acknowledgements 132 | 133 | This analysis is joint work between INRIA and Cryspen, and was carried 134 | out by Karthikean Barghavan, Charlie Jacomme and Franziskus Kiefer. It 135 | was inspired by a previous CryptoVerif model for TextSecure (a variant 136 | of X3DH) made by Bruno Blanchet. Theophile Wallez gave precious 137 | insights w.r.t. to the encodings used in the specification. 138 | 139 | We thank Rolfe Schmidt and Ehren Kret for the fruitful interactions 140 | and many insights into the specification and Signal's implementation. 141 | 142 | 143 | # References 144 | 145 | 146 | [PQXDH]: https://signal.org/docs/specifications/pqxdh/, The PQXDH Key Agreement Protocol 147 | 148 | [MCR]: https://classic.mceliece.org/mceliece-rationale-20221023.pdf, Classic McEliece: conservative code-based cryptography: design rationale 149 | -------------------------------------------------------------------------------- /revision1-WithKyberCR/Kyber.ocv: -------------------------------------------------------------------------------- 1 | 2 | proof { 3 | simplify coll_elim(variables:z_1); 4 | insert before "if secb" "if pk_6 = pk' then"; 5 | insert before_nth 1 "if secb" "if ct_3 = c_1 then"; 6 | 7 | all_simplify; 8 | all_simplify; 9 | success 10 | } 11 | 12 | 13 | (* Types for abstract CPA KEM *) 14 | 15 | 16 | type cpa_pk [bounded]. 17 | type cpa_sk [bounded]. 18 | type cpa_ciphertext [bounded]. 19 | type cpa_key_seed [large,fixed]. 20 | type cpa_enc_seed [large,fixed]. 21 | 22 | 23 | 24 | 25 | (* Types for final CCA KEM *) 26 | 27 | type kemskey [bounded]. 28 | 29 | type ciphertext. 30 | type kem_seed [large,fixed]. 31 | type kem_enc_seed [large,fixed]. 32 | 33 | 34 | type kemsec [large,fixed]. 35 | 36 | type kem_keypair. 37 | fun KEM_KeyPair(cpa_pk, kemskey) : kem_keypair [data]. 38 | 39 | 40 | 41 | (* Hash functions *) 42 | 43 | type hashes [bounded]. (* H output *) 44 | 45 | type B [large,fixed]. 46 | type hashkey [large,fixed]. 47 | proba qH2. 48 | expand CollisionResistant_hash_2(hashkey, B,hashes, kemsec, KDF, hashoracleKDF, qH2). 49 | 50 | type hashkey2 [large,fixed]. 51 | 52 | proba qH3. 53 | expand CollisionResistant_hash_1(hashkey2, B, hashes, H, hashoracleH, qH3). 54 | 55 | fun cpa_pk_to_B(cpa_pk) :B [data]. 56 | 57 | fun cpa_ct_to_B(cpa_ciphertext) :B [data]. 58 | 59 | letfun H1(hk2:hashkey2,pk: cpa_pk) = 60 | H(hk2, cpa_pk_to_B(pk)). 61 | 62 | letfun H2(hk2:hashkey2,b: B) = 63 | H(hk2, b). 64 | 65 | letfun H3(hk2:hashkey2,ct:cpa_ciphertext) = 66 | H(hk2, cpa_ct_to_B(ct) ). 67 | 68 | 69 | proba qH4. 70 | type hashkey3 [large,fixed]. 71 | expand CollisionResistant_hash_2(hashkey3, hashes, hashes, B, G1, hashoracleG1, qH4). 72 | 73 | 74 | 75 | fun G2(hashes,hashes) : cpa_enc_seed. 76 | 77 | 78 | 79 | 80 | 81 | fun cpa_pkgen(cpa_key_seed):cpa_pk. 82 | fun cpa_skgen(cpa_key_seed):cpa_sk. 83 | 84 | fun cpa_enc(cpa_pk,hashes, cpa_enc_seed) : cpa_ciphertext. 85 | fun cpa_dec(cpa_sk,cpa_ciphertext) : hashes. 86 | 87 | 88 | 89 | 90 | equation forall m:hashes, s:cpa_key_seed, r:cpa_enc_seed; 91 | cpa_dec( cpa_skgen(s), cpa_enc( cpa_pkgen(s), m, r)) =m. 92 | 93 | 94 | (* type kempkey [bounded]. *) (* this is already cpa_pk *) 95 | 96 | 97 | fun kem_to_cpa_seed(kem_seed) : cpa_key_seed. 98 | 99 | 100 | 101 | 102 | 103 | fun concat4(cpa_sk,cpa_pk,hashes,B) : kemskey [data]. 104 | 105 | letfun cca_gen(hk2: hashkey2, k : kem_seed) = 106 | z <-R B; 107 | cpas <- kem_to_cpa_seed(k); 108 | pk <- cpa_pkgen(cpas); 109 | sk' <- cpa_skgen(cpas); 110 | sk <- concat4(sk',pk,H1(hk2,pk),z); 111 | KEM_KeyPair(pk, sk). 112 | 113 | 114 | 115 | 116 | 117 | type encapspair. 118 | fun KEMEncaps(cpa_ciphertext,kemsec) : encapspair [data]. 119 | 120 | 121 | fun kseedToB(kem_enc_seed) : B. 122 | letfun cca_encaps(hk:hashkey, hk2: hashkey2, hk3: hashkey3, pk : cpa_pk, k : kem_enc_seed) = 123 | m' <- kseedToB(k); 124 | m <- H2(hk2,m'); 125 | Kt <- G1(hk3,m,H1(hk2,pk)); 126 | r <- G2(m,H1(hk2,pk)); 127 | c <- cpa_enc(pk,m,r); 128 | KEMEncaps(c,KDF(hk,Kt, H3(hk2,c))). 129 | 130 | 131 | const nullsec : kemsec. 132 | 133 | letfun cca_decap(hk:hashkey, hk2: hashkey2, hk3: hashkey3, c : cpa_ciphertext, sk : kemskey) = 134 | let concat4(sk',pk,h,z) = sk in 135 | m' <- cpa_dec(sk',c); 136 | Kt' <- G1(hk3, m',H1(hk2,pk)); 137 | r' <- G2(m',H1(hk2, pk)); 138 | c' <- cpa_enc(pk,m',r'); 139 | (if c = c' then 140 | KDF(hk,Kt', H3(hk2,c')) 141 | else 142 | KDF(hk,z, H3(hk2,c))) 143 | else 144 | nullsec (*cannot occur *) 145 | . 146 | 147 | 148 | (* We prove the equivalent of 149 | 150 | fun decap(ciphertext, kemskey): kemsec. 151 | 152 | fun kem_secret(kempkey, kem_enc_seed) : kemsec. 153 | fun kem_encap(kempkey, kem_enc_seed): ciphertext. 154 | 155 | collision r <-R kem_seed; k <-R kem_enc_seed; forall ct: ciphertext, pk:kempkey; 156 | return(decap(ct,kemskgen(r))= kem_secret(pk,k)) 157 | <=(KEMcollNew)=> return(ct=kem_encap(pk,k) && pk = kempkgen(r)). 158 | *) 159 | 160 | 161 | query secret secb [cv_bit]. 162 | 163 | set autoMergeBranches = true. 164 | set autoSARename = true. 165 | 166 | process 167 | Start() := 168 | hk <-R hashkey; 169 | hk2 <-R hashkey2; 170 | hk3 <-R hashkey3; 171 | secb <-R bool; 172 | r <-R kem_seed; 173 | k <-R kem_enc_seed; 174 | let KEM_KeyPair(pk, sk) = cca_gen(hk2,r) in 175 | return(r,k); 176 | 177 | run hashoracleKDF(hk) | 178 | run hashoracleH(hk2) | 179 | run hashoracleG1(hk3) | 180 | OChall(ct: cpa_ciphertext, pk':cpa_pk) := 181 | let KEMEncaps(c,K) = cca_encaps(hk,hk2,hk3,pk',k) in 182 | ( 183 | if secb then 184 | return(cca_decap(hk,hk2,hk3,ct, sk) = K) 185 | else 186 | return( (ct = c) && (pk' = pk)) 187 | ) 188 | 189 | 190 | -------------------------------------------------------------------------------- /revision1-WithKyberCR/Makefile: -------------------------------------------------------------------------------- 1 | MAIN = PQXDH 2 | 3 | dh: 4 | m4 -D DH $(MAIN).m4.ocv > _models/$(MAIN).DH.ocv 5 | time cryptoverif _models/$(MAIN).DH.ocv 6 | 7 | kem: 8 | m4 -D KEM $(MAIN).m4.ocv > _models/$(MAIN).KEM.ocv 9 | time cryptoverif _models/$(MAIN).KEM.ocv 10 | -------------------------------------------------------------------------------- /revision1-WithKyberCR/PQXDH.m4.ocv: -------------------------------------------------------------------------------- 1 | (* 2 | 3 | This files models the PQXDH protocol, as specified in: 4 | 5 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 6 | The PQXDH Key Agreement Protocol 7 | Revision 2, 8 | 9 | See the README next to this file for details on the modeling and how 10 | to run the file. 11 | 12 | This is the CryptoVerif part of the analysis, see the README at the 13 | root directory for details on the joint analysis. 14 | 15 | Authors: ********************************************* 16 | 17 | Based on previous textsecure models by Bruno Blanchet. 18 | 19 | *) 20 | 21 | set useKnownEqualitiesWithFunctionsInMatching = true. 22 | 23 | 24 | 25 | 26 | 27 | ifdef(`KEM',` 28 | 29 | 30 | (* The proof instructions needed to guide CryptoVerif in the KEM case. *) 31 | proof { 32 | 33 | crypto uf_cma_corrupt(sign) signAseed; 34 | out_game "g1.cv" occ; 35 | 36 | insert before "EKSecA1 <-R Z" "find j <= Nrecv, k <= Nidentity suchthat defined (seed[j,k], signAseed[k], IKA[k], PQPKB[j,k]) && x_IKBsign = pkgen2(signAseed[k]) && PQPKPubB = PQPKB[j,k] then"; 37 | 38 | SArename CT_2; 39 | 40 | out_game "g11.cv" occ; 41 | 42 | insert after "RecvOPK(" "find u2 <= NsendOPK suchthat defined(CT_3[u2],PQPKPubB[u2]) && CT_3[u2] = CT_1 && PQPKB = PQPKPubB[u2] then"; 43 | insert after "RecvOPK(" "find u1 <= Nidentity suchthat defined(signAseed[u1], IKA[u1]) && 44 | x_IKAsign = pkgen2(signAseed[u1]) then if defined(corrupted_2[u1]) then"; 45 | 46 | insert after "kseed_4 <-R" "let fencap = kempair(kem_secret(PQPKPubB,kseed_4),kem_encap(PQPKPubB,kseed_4)) in"; 47 | out_game "g3.cv" occ; 48 | 49 | replace at_nth 1 1 "SS: kemsec <- {[0-9]+}" "get_secret(fencap_2)"; 50 | replace at_nth 1 1 "CT_3: ciphertext <- {[0-9]+}" "get_encap(fencap_2)"; 51 | 52 | crypto ind_cca(Encap) [variables: seed -> seed, kseed_4 -> kseed_1 .]; 53 | 54 | out_game "g31.cv" occ; 55 | 56 | insert before "EKSecA1p <-R Z" "find j2 <= Nrecv, k2 <= Nidentity suchthat defined (seed_1[j2,k2], signAseed[k2], IKA[k2], PQPKB[j2,k2]) && x_IKBsignp = pkgen2(signAseed[k2]) && PQPKPubBp = PQPKB[j2,k2] then"; 57 | 58 | SArename CTp_2; 59 | 60 | out_game "g32.cv" occ; 61 | 62 | insert after "RecvNoOPK(" "find u22 <= NsendOPK suchthat defined(CT_3[u22],PQPKPubB[u22]) && CT_3[u22] = CTp_1 && PQPKB = PQPKPubB[u22] then"; 63 | insert after "RecvNoOPK(" "find u12 <= Nidentity suchthat defined(signAseed[u12], IKA[u12]) && 64 | x_IKAsignp = pkgen2(signAseed[u12]) then if defined(corrupted_2[u12]) then"; 65 | 66 | 67 | insert after "kseedp_2 <-R" "let fencap = kempair(kem_secret(PQPKPubBp,kseedp_2),kem_encap(PQPKPubBp,kseedp_2)) in"; 68 | out_game "g33.cv"; 69 | replace at_nth 1 1 "SSp: kemsec <- {[0-9]+}" "get_secret(fencap_3)"; 70 | replace at_nth 1 1 "CTp_3: ciphertext <- {[0-9]+}" "get_encap(fencap_3)"; 71 | 72 | 73 | crypto ind_cca(Encap) [variables: seed_1 -> seed, kseedp_2 -> kseed_1.]; 74 | 75 | crypto prf(H) *; 76 | out_game "g4.cv" ; 77 | 78 | crypto int_ctxt(enc) *; 79 | crypto ind_cpa(enc) **; 80 | out_game "g5.cv"; 81 | success 82 | } 83 | 84 | ',`') 85 | 86 | 87 | ifdef(`DH',` 88 | 89 | 90 | (* The proof instructions needed to guide CryptoVerif in the DH case. *) 91 | proof { 92 | crypto uf_cma_corrupt(sign) signAseed; 93 | out_game "g1.cv" occ; 94 | 95 | insert before "EKSecA1 <-R Z" "find j <= Nrecv, k <= Nidentity suchthat defined (SPKPubB1[j,k], IKA[k]) && pow8(SPKPubB) = pow8(SPKPubB1[j,k]) && pow8(x_IKB) = pow8(IKA[k]) then"; 96 | insert after "RecvOPK(" "find u1 <= Nidentity suchthat defined(signAseed[u1], IKA[u1]) && pow8(x_IKA) = pow8(IKA[u1]) then if defined(corrupted_1[u1]) then"; 97 | 98 | out_game "g11.cv" occ; 99 | insert after "OH_1(" "let (subGtoG(x1p), subGtoG(x2p), subGtoG(x3p), subGtoG(x4p), x5p : kemsec) = (x1_1, x2_1, x3_1, x4_1, x5) in"; 100 | crypto rom(H2); 101 | 102 | out_game "g2.cv" occ; 103 | insert before "EKSecA1p <-R Z" "find j2 <= Nrecv, k2 <= Nidentity suchthat defined (SPKPubB1[j2,k2], IKA[k2]) && pow8(SPKPubBp) = pow8(SPKPubB1[j2,k2]) && pow8(x_IKBp) = pow8(IKA[k2]) then"; 104 | insert after "RecvNoOPK(" "find u2 <= Nidentity suchthat defined(signAseed[u2], IKA[u2]) && pow8(x_IKAp) = pow8(IKA[u2]) then if defined(corrupted_1[u2]) then"; 105 | 106 | 107 | out_game "g12.cv"occ; 108 | 109 | insert after "OH(" "let (subGtoG(x1_1p), subGtoG(x2_1p), subGtoG(x3_1p), x4_1p : kemsec) = (x1, x2, x3, x4) in"; 110 | crypto rom(H1); 111 | 112 | out_game "g3.cv"; 113 | 114 | 115 | crypto gdh(gexp_div_8) [variables: secIKA0 -> a, SPKSecB1 -> a, OPKSecB1 -> a, EKSecA1 -> a, EKSecA1p -> a .]; 116 | 117 | crypto int_ctxt(enc) *; 118 | crypto ind_cpa(enc) **; 119 | out_game "g4.cv"; 120 | crypto int_ctxt_corrupt(enc) r_23; 121 | crypto int_ctxt_corrupt(enc) r_50; 122 | crypto int_ctxt_corrupt(enc) r_44; 123 | success 124 | } 125 | 126 | 127 | ',`') 128 | 129 | 130 | 131 | 132 | (* KEM definitions *) 133 | type kempkey [bounded]. 134 | type kemskey [bounded]. 135 | type ciphertext. 136 | type kem_seed [large,fixed]. 137 | type kem_enc_seed [large,fixed]. 138 | 139 | type kemsec [fixed]. 140 | 141 | fun kempkgen(kem_seed):kempkey. 142 | fun kemskgen(kem_seed):kemskey. 143 | 144 | fun decap(ciphertext, kemskey): kemsec. 145 | 146 | fun kem_secret(kempkey, kem_enc_seed) : kemsec. 147 | fun kem_encap(kempkey, kem_enc_seed): ciphertext. 148 | 149 | type encaps_return. 150 | fun kempair(kemsec,ciphertext) : encaps_return [data]. 151 | 152 | letfun encaps(pk : kempkey, kseed : kem_enc_seed) = 153 | kempair(kem_secret(pk,kseed ), kem_encap(pk,kseed)). 154 | 155 | equation forall kseed: kem_seed, seed:kem_enc_seed; 156 | decap( kem_encap( kempkgen(kseed), seed), kemskgen(kseed)) = kem_secret( kempkgen(kseed),seed). 157 | 158 | fun get_encap(encaps_return) : ciphertext. 159 | fun get_secret(encaps_return) : kemsec. 160 | 161 | equation forall c:ciphertext, s:kemsec; 162 | get_encap( kempair(s,c)) = c. 163 | 164 | equation forall c:ciphertext, s:kemsec; 165 | get_secret(kempair( s,c))= s. 166 | 167 | ifdef(`KEM',` 168 | 169 | (* KEM security assumptions -> IND-CCA *) 170 | 171 | 172 | param Nc, Qeperuser, Qdperuser. 173 | 174 | proba CCA. 175 | 176 | table E(Nc, ciphertext, kemsec). 177 | 178 | equiv(ind_cca(Encap)) 179 | 180 | foreach i <= Nc do seed <-R kem_seed; ( 181 | Opk() := return(kempkgen(seed)) 182 | | 183 | foreach id <= Qdperuser do 184 | OADecap(enc: ciphertext) [useful_change] := 185 | return(decap(enc, kemskgen(seed))) 186 | ) 187 | | 188 | foreach ie <= Qeperuser do 189 | kseed <-R kem_enc_seed; ( 190 | 191 | OE(pk_R:kempkey) [useful_change] := return( encaps(pk_R, kseed) ) 192 | 193 | ) 194 | <=(CCA(time, Nc, #OE, #OADecap))=> 195 | foreach i <= Nc do seed <-R kem_seed; ( 196 | Opk() := return(kempkgen(seed)) | 197 | foreach id <= Qdperuser do ( 198 | OADecap(cd: ciphertext) := 199 | get E(=i, =cd, k2) in ( 200 | return(k2) 201 | ) else ( 202 | return(decap(cd, kemskgen(seed))) 203 | 204 | )) ) 205 | | 206 | foreach ie <= Qeperuser do 207 | kseed <-R kem_enc_seed; 208 | ( 209 | OE(pk_R: kempkey) := 210 | find i2 <= Nc suchthat defined(seed[i2]) && pk_R = kempkgen(seed[i2]) then ( 211 | k1 <-R kemsec; 212 | insert E(i2, kem_encap(pk_R, kseed) , k1); 213 | return( kempair(k1, kem_encap(pk_R, kseed))) 214 | ) else ( 215 | return(encaps(pk_R, kseed) ) 216 | ) 217 | 218 | ) 219 | 220 | 221 | . 222 | ',`') 223 | 224 | 225 | (* We always have some basic properties oj the KEM, e.g., public keys 226 | are not independent of their seed. *) 227 | 228 | proba KEMcoll1. 229 | proba KEMcoll2. 230 | 231 | 232 | 233 | collision r <-R kem_seed; forall Y: kempkey; 234 | return(kempkgen(r) = Y) <=(KEMcoll1)=> return(false) if Y independent-of r. 235 | 236 | 237 | collision r <-R kem_seed; k <-R kem_enc_seed; forall Y: ciphertext; 238 | return(kem_encap(kempkgen(r),k) = Y) <=(KEMcoll2)=> return(false) if Y independent-of k. 239 | 240 | 241 | proba KEMcollNew. 242 | 243 | collision r <-R kem_seed; k <-R kem_enc_seed; forall ct: ciphertext, pk:kempkey; 244 | return(decap(ct,kemskgen(r))= kem_secret(pk,k)) 245 | <=(KEMcollNew)=> return(ct=kem_encap(pk,k) && pk = kempkgen(r)). 246 | 247 | 248 | 249 | 250 | (* DH definitions *) 251 | type emkey [fixed,large]. 252 | 253 | type Z [bounded,large,nonuniform]. (* Exponents *) 254 | type G [bounded,large,nonuniform]. (* Diffie-Hellman group *) 255 | type subG [bounded,large,nonuniform]. (* Diffie-Hellman group *) 256 | 257 | (* Gap Diffie-Hellman *) 258 | (* In the PQ setting, we only assume the informatic theoritic collision properties *) 259 | 260 | (* Note: the secret keys in Signal are really normalized to be multiples of k, 261 | as specified in RFC 7748. The normalization is commented out in the exponentiation 262 | function: 263 | https://github.com/signalapp/libsignal-protocol-javascript/blob/f5a838f1ccc9bddb5e93b899a63de2dea9670e10/native/curve25519-donna.c/#L860 264 | but done when generating a key pair: 265 | https://github.com/signalapp/libsignal-protocol-javascript/blob/f5a838f1ccc9bddb5e93b899a63de2dea9670e10/src/curve25519_wrapper.js#L25 266 | *) 267 | 268 | expand DH_X25519(G, Z, g, gexp, mult, subG, g_8, gexp_div_8, gexp_div_8p, pow8, subGtoG, is_zero_G, is_zero_subG). 269 | 270 | 271 | ifdef(`DH',` 272 | 273 | (* We now make the gapDH assumption. *) 274 | proba psqGDH. 275 | proba pDistRerandom. 276 | expand square_GDH_RSR(subG, Z, g_8, gexp_div_8, gexp_div_8p, mult, psqGDH, pDistRerandom). 277 | 278 | ',`') 279 | 280 | 281 | (* Key derivation *) 282 | 283 | ifdef(`KEM',` 284 | 285 | 286 | (* We model the kdf as a prf function, which is keyed by the KEM shared secret. *) 287 | fun H(bitstring,kemsec): emkey. 288 | 289 | proba Pprf. 290 | equiv(prf(H)) special prf("key_last", H, Pprf, (k, r, x, y, z, u)). 291 | 292 | equiv(prf_partial(H)) special prf_partial("key_last", H, Pprf, (k, r, x, y, z, u)) [manual]. 293 | 294 | 295 | fun c3(G,G,G):bitstring. 296 | fun c4(G,G,G,G):bitstring. 297 | 298 | letfun H4(g1:G,g2:G,g3:G, k:kemsec) = H(c3(g1,g2,g3),k). 299 | letfun H5(g1:G,g2:G,g3:G,g4:G, k:kemsec) = H(c4(g1,g2,g3,g4),k). 300 | 301 | equation forall g1:G, g2:G, g3:G, g4:G; 302 | c3(g1,g2,g3) <> c4(g1,g2,g3,g4). 303 | 304 | 305 | ',`') 306 | 307 | 308 | ifdef(`DH',` 309 | 310 | 311 | (* we model the kdf a Random Oracles. *) 312 | 313 | type hashkey [large,fixed]. (* unused in PQ setting *) 314 | type hashkey2 [large,fixed]. (* unused in PQ setting *) 315 | 316 | expand ROM_hash_large_4(hashkey, G, G, G, kemsec, emkey, H1, hashoracle, qH2). 317 | expand ROM_hash_large_5(hashkey2, G, G ,G ,G, kemsec, emkey, H2, hashoracle2, qH3). 318 | 319 | letfun H4(g1:G,g2:G,g3:G, k:kemsec,hk:hashkey) = H1(hk,g1,g2,g3,k). 320 | 321 | letfun H5(g1:G,g2:G,g3:G,g4:G, k:kemsec, hk:hashkey2) = H2(hk,g1,g2,g3,g4,k). 322 | 323 | 324 | ',`') 325 | 326 | 327 | 328 | 329 | (* Signatures *) 330 | 331 | 332 | type keyseed [large, fixed]. 333 | type pkey [bounded]. 334 | type skey [bounded]. 335 | type t_sign. 336 | 337 | 338 | proba Psign. 339 | proba Psigncoll. 340 | expand UF_CMA_proba_signature(keyseed, pkey, skey, bitstring, t_sign, skgen, pkgen, sign, checksign, Psign, Psigncoll). 341 | 342 | (* Encoding of public keys for signatures *) 343 | 344 | fun encodeEC(G) : bitstring [data]. 345 | fun encodeKEM(kempkey) : bitstring [data]. 346 | 347 | 348 | letfun signKEM(pk:kempkey,sk:skey) = sign(encodeKEM(pk),sk). 349 | letfun checksignKEM(m:kempkey, p:pkey, s:t_sign) = checksign(encodeKEM(m),p,s). 350 | 351 | 352 | letfun signEC(el:G,sk:skey) = sign(encodeEC(el),sk). 353 | letfun checksignEC(m:G, p:pkey,s:t_sign) = checksign(encodeEC(m),p,s). 354 | 355 | 356 | (* We rely here on the assumption made in the spec that the encodings of public keys are disjoints. 357 | This is both stated explicitly in [PQXDH], and verified in the implementation, with the encodings having a unique one byte prefix: 358 | 359 | (curve25519 |-> 0x05, curve448 |-> 0x06, Kyber768 |-> 0x07, Kyber1024 |-> 0x08) 360 | 361 | This correspond to the KeyType field of libsignal, as defined here for KEMs 362 | https://github.com/signalapp/libsignal/blob/d1f9dff273e6da059af699c6afe860fb93406032/rust/protocol/src/kem.rs#L153 363 | and here for curve 25519: 364 | https://github.com/signalapp/libsignal/blob/d1f9dff273e6da059af699c6afe860fb93406032/rust/protocol/src/curve.rs#L33 365 | 366 | *) 367 | 368 | 369 | equation forall pkdh:G, pkkem:kempkey; 370 | encodeEC(pkdh) <> encodeKEM(pkkem). 371 | 372 | (* AEAD *) 373 | 374 | type t_data. 375 | proba Penc. 376 | proba Pencctxt. 377 | 378 | 379 | (* We assume IND-CPA + INT-CTXT for the AEAD, in both cases. *) 380 | expand AEAD(emkey, bitstring, bitstring, t_data, enc, dec, injbot, Zero, Penc, Pencctxt). 381 | 382 | const const1: bitstring. 383 | fun concatAD(G,pkey,G,pkey):t_data [data]. 384 | 385 | 386 | param Nidentity, Nrecv, NsendOPK, NsendNoOPK, Nsignedprekey, Nsignedprekey2. 387 | 388 | (* Table of keys *) 389 | table keys(Z, G, skey, pkey). 390 | (* Table of keys of corrupted participants *) 391 | table corrupted(G,pkey). 392 | 393 | 394 | (* Security properties *) 395 | 396 | event SendWithOPK(G, pkey, G, pkey, G, G, G, kempkey, bitstring). 397 | event RecvWithOPK(bool, G, pkey, G, pkey, G, G, G, kempkey, bitstring). 398 | event SendWithoutOPK(G, pkey, G, pkey, G, G, kempkey, bitstring). 399 | event RecvWithoutOPK(bool, G, pkey, G, pkey, G, G, kempkey, bitstring). 400 | (* Arguments of events 401 | - for RecvWithOPK/RecvWithoutOPK: a boolean true when Blake is corrupted 402 | - public keys of sender (DH and signature), IKA and IKAsign 403 | - public keys of receiver (DH and signature), IKB and IKBsign 404 | - signed ephemeral, SPKB. 405 | - one-time ephemeral [optional], OPK, 406 | - sender first ephemeral, EPK, 407 | - the signed kem public key, PQPK, 408 | - sent message 409 | *) 410 | 411 | ifdef(`DH',` 412 | 413 | query Bcorrupted:bool,a0:G,as:pkey,b0:G,bs:pkey,sb:G,sb2:G,ob:G,a1:G,ob2:G,a12:G,pk1:kempkey,m:bitstring; 414 | inj-event(RecvWithOPK(Bcorrupted,a0,as,b0,bs,sb,ob,a1,pk1,m)) ==> inj-event(SendWithOPK(a0,as,b0,bs,sb2,ob2,a12,pk1,m)) && pow8(ob) = pow8(ob2) && pow8(a1) = pow8(a12) && pow8(sb) = pow8(sb2) && (Bcorrupted || sb = sb2) 415 | public_vars secb. 416 | query Bcorrupted:bool,a0:G,as:pkey,b0:G,bs:pkey,sb:G,sb2:G,a1:G,a12:G,pk1:kempkey,m:bitstring; 417 | event(RecvWithoutOPK(Bcorrupted,a0,as,b0,bs,sb,a1,pk1,m)) ==> event(SendWithoutOPK(a0,as,b0,bs,sb2,a12,pk1,m)) && pow8(a1) = pow8(a12) && pow8(sb) = pow8(sb2) && (Bcorrupted || sb = sb2) 418 | public_vars secb. 419 | 420 | (* Blake receives => Alex sent is proved provided Alex is not corrupted 421 | (event Recv/RecvWithoutOPK is executed when Alex is not corrupted). 422 | That proves KCI resistance against the compromise of long-term keys. 423 | . 424 | We cannot prove that sb = sb2 when Blake signature key is 425 | compromised. The adversary can then forge a signature of the signed 426 | ephemeral sb. The Diffie-Hellman key exchange just guarantees that 427 | pow8(sb) = pow8(sb2). 428 | 429 | To note, we cannot prove that the two parties agree on the KEM public key used. 430 | 431 | *) 432 | 433 | 434 | ',`') 435 | 436 | 437 | (* Identifiers for public keys *) 438 | 439 | type ids. 440 | 441 | fun idPKDH(G):ids. 442 | fun idPKKEM(kempkey):ids. 443 | 444 | query secret secb [cv_bit]. 445 | 446 | (* The secrecy of secb shows the secrecy of the message sent by Alex to Blake, 447 | provided Blake is not corrupted yet when Alex send the message (secb is used 448 | to choose between 2 messages only when Blake is not corrupted). That 449 | shows in particular forward secrecy. *) 450 | 451 | 452 | (********************) 453 | (**** INITIATOR *****) 454 | (********************) 455 | 456 | (* Alex using prekeys and sending a message to a participant (Blake or other). 457 | The received x_IKB:G, x_IKBsign:pkey choose Alex's interlocutor. 458 | This sender uses an optional OPK. 459 | *) 460 | (* section 3.3 *) 461 | let SendInitialWithOPK(secb1:bool,secIKA:Z , IKA:G, secIKAsign:skey, IKAsign:pkey 462 | ifdef(`DH',`, hk2:hashkey2',`') 463 | ) 464 | = 465 | (* Key exchange + send message m1 or m2 *) 466 | SendFirstMessageOPK(x_IKB:G, x_IKBsign:pkey, SPKPubB:G,SPKsign:t_sign,OPKPubB:G,PQPKPubB:kempkey,PQPKsign:t_sign,m1: bitstring, m2:bitstring) := 467 | 468 | 469 | (* Classical DH part *) 470 | new EKSecA1: Z; 471 | let EKPubA = gexp(g, EKSecA1) in 472 | let dh1 = gexp(SPKPubB, secIKA) in 473 | let dh2 = gexp(x_IKB, EKSecA1) in 474 | let dh3 = gexp(SPKPubB, EKSecA1) in 475 | let dh4 = gexp(OPKPubB, EKSecA1) in 476 | 477 | (* Kem additionnal part *) 478 | new kseed: kem_enc_seed; 479 | 480 | let fencap = encaps(PQPKPubB,kseed) in 481 | let SS = get_secret(fencap) in 482 | let CT = get_encap(fencap) in 483 | 484 | let SK_opk : emkey = H5(dh1, dh2, dh3, dh4, SS 485 | ifdef(`DH',`, hk2',`') 486 | ) in 487 | 488 | 489 | ifdef(`KEM',`get keys(secIKB, x_IKB2, secIKBsign, =x_IKBsign) in',`') 490 | ifdef(`DH',`get keys(secIKB, =x_IKB, secIKBsign, =x_IKBsign) in',`') 491 | ( 492 | ifdef(`KEM',`get corrupted(dummy,=x_IKBsign) in',`') 493 | ifdef(`DH',`get corrupted(=x_IKB,dummy) in',`') 494 | ( 495 | (* Alex talks to a corrupted participant; the message cannot be secret *) 496 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 497 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 498 | if m1 = m2 then 499 | let msg = m1 in 500 | let cipher = enc(msg, concatAD(IKA, IKAsign, x_IKB, x_IKBsign), SK_opk) in 501 | event SendWithOPK(IKA,IKAsign,x_IKB,x_IKBsign,SPKPubB,OPKPubB,EKPubA,PQPKPubB,msg); 502 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 503 | ) 504 | else 505 | ( 506 | (* Alex talks to a honest participant Blake *) 507 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 508 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 509 | (* Check that m1 and m2 have the same length *) 510 | if Zero(m1) = Zero(m2) then 511 | (* Send either m1 or m2 depending on the value of the secret bit b *) 512 | let msg = if_fun(secb1, m1, m2) in 513 | let cipher = enc(msg, concatAD(IKA, IKAsign, x_IKB, x_IKBsign), SK_opk) in 514 | event SendWithOPK(IKA,IKAsign,x_IKB,x_IKBsign,SPKPubB,OPKPubB,EKPubA,PQPKPubB,msg); 515 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 516 | ) 517 | ) 518 | else 519 | ( 520 | (* Alex talks to a dishonest participant *) 521 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 522 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 523 | if m1 = m2 then 524 | let msg = m1 in 525 | let cipher = enc(msg, concatAD(IKA, IKAsign, x_IKB, x_IKBsign), SK_opk) in 526 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 527 | ). 528 | 529 | (* Same as before, but without the optional OPK. *) 530 | let SendInitialNoOPK(secb1:bool,secIKAp:Z , IKAp:G, secIKAsignp:skey, IKAsignp:pkey 531 | ifdef(`DH',`, hk:hashkey',`') 532 | ) = 533 | (* Key exchange + send message m1 or m2 *) 534 | SendFirstMessageNoOPK(x_IKBp:G, x_IKBsignp:pkey, SPKPubBp:G,SPKsignp:t_sign,PQPKPubBp:kempkey,PQPKsignp:t_sign,m1p: bitstring, m2p:bitstring) := 535 | 536 | (* Classical DH part *) 537 | new EKSecA1p: Z; 538 | let EKPubAp = gexp(g, EKSecA1p) in 539 | let dh1 = gexp(SPKPubBp, secIKAp) in 540 | let dh2 = gexp(x_IKBp, EKSecA1p) in 541 | let dh3 = gexp(SPKPubBp, EKSecA1p) in 542 | 543 | (* Kem additionnal part *) 544 | new kseedp: kem_enc_seed; 545 | 546 | let fencap = encaps(PQPKPubBp,kseedp) in 547 | let SSp = get_secret(fencap) in 548 | let CTp = get_encap(fencap) in 549 | 550 | let SK_nopk = H4(dh1, dh2, dh3, SSp 551 | ifdef(`DH',`, hk',`') 552 | ) in 553 | 554 | ifdef(`KEM',`get keys(secIKB, x_IKB, secIKBsign, =x_IKBsignp) in',`') 555 | ifdef(`DH',`get keys(secIKB, =x_IKBp, secIKBsign, =x_IKBsignp) in',`') 556 | ( 557 | ifdef(`KEM',`get corrupted(dummy,=x_IKBsignp) in',`') 558 | ifdef(`DH',`get corrupted(=x_IKBp,dummy) in',`') 559 | ( 560 | (* Alex talks to a corrupted participant; the message cannot be secret *) 561 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 562 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 563 | if m1p = m2p then 564 | let msg = m1p in 565 | let cipher = enc(msg, concatAD(IKAp, IKAsignp, x_IKBp, x_IKBsignp), SK_nopk) in 566 | event SendWithoutOPK(IKAp,IKAsignp,x_IKBp,x_IKBsignp,SPKPubBp,EKPubAp,PQPKPubBp,msg); 567 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 568 | ) 569 | else 570 | ( 571 | (* Alex talks to a honest participant Blake *) 572 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 573 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 574 | (* Check that m1 and m2 have the same length *) 575 | if Zero(m1p) = Zero(m2p) then 576 | (* Send either m1 or m2 depending on the value of b *) 577 | let msg = if_fun(secb1, m1p, m2p) in 578 | let cipher = enc(msg, concatAD(IKAp, IKAsignp, x_IKBp, x_IKBsignp), SK_nopk) in 579 | event SendWithoutOPK(IKAp,IKAsignp,x_IKBp,x_IKBsignp,SPKPubBp,EKPubAp,PQPKPubBp,msg); 580 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 581 | ) 582 | ) 583 | else 584 | ( 585 | (* Alex talks to a dishonest participant *) 586 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 587 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 588 | if m1p = m2p then 589 | let msg = m1p in 590 | let cipher = enc(msg, concatAD(IKAp, IKAsignp, x_IKBp, x_IKBsignp), SK_nopk) in 591 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 592 | ). 593 | 594 | 595 | (* Blake generating prekeys and running the protocol, with Alex 596 | or with any other participant *) 597 | 598 | (* Sec 3.4 of spec *) 599 | let GenOPKThenRecv(secIKB : Z, IKB : G, secIKBsign : skey, IKBsign : pkey 600 | ifdef(`DH',`, hk:hashkey, hk2:hashkey2',`') 601 | ) = 602 | GenSPK():= 603 | (* Signed PQPK, last resort that can be reused *) 604 | new seed:kem_seed; 605 | let PQPKB = kempkgen(seed) in 606 | let PQPKBsig = signKEM(PQPKB, secIKBsign) in 607 | 608 | 609 | (* new SPK DH based *) 610 | new SPKSecB1: Z; 611 | let SPKPubB1: G = gexp(g, SPKSecB1) in 612 | let SPKsignature = signEC(SPKPubB1, secIKBsign) in 613 | return(SPKPubB1,SPKsignature, PQPKB, PQPKBsig 614 | ifdef(`DH',`,seed',`') (* in the DH case, we in fact completely leak the seed *) 615 | 616 | ); 617 | (( 618 | ! Nsignedprekey 619 | (* One-time prekey DH based*) 620 | GenOPK():= 621 | new OPKSecB1: Z; 622 | let OPKPubB = gexp(g, OPKSecB1) in 623 | return(OPKPubB); 624 | (* 2nd part of key exchange, 625 | using prekey OPKPubB and signed prekey SPKPubB1 *) 626 | RecvOPK(x_IKA: G,x_IKAsign: pkey, EPKPubA: G, idSPK:ids, idOPK:ids, idPQPK:ids, CT : ciphertext, msgenc: bitstring) := 627 | 628 | (* Here, we check if the keys we already have in the current 629 | process state have the same id has the received ones. This simulate 630 | a general process fetching the public keys from the received id, by 631 | matching the id against the ids of the keys in the database. *) 632 | 633 | if idSPK = idPKDH(SPKPubB1) then 634 | if idOPK = idPKDH(OPKPubB) then 635 | if idPQPK = idPKKEM(PQPKB) then 636 | 637 | let dh1 = gexp(x_IKA,SPKSecB1) in 638 | let dh2 = gexp(EPKPubA, secIKB) in 639 | let dh3 = gexp(EPKPubA, SPKSecB1) in 640 | let dh4 = gexp(EPKPubA, OPKSecB1) in 641 | 642 | let ss = decap(CT,kemskgen(seed)) in 643 | 644 | let sk_opk = H5(dh1, dh2, dh3, dh4, ss 645 | ifdef(`DH',`, hk2',`') 646 | ) in 647 | 648 | let injbot(msg) = dec(msgenc, concatAD(x_IKA, x_IKAsign, IKB, IKBsign), sk_opk) in 649 | ifdef(`KEM',` 650 | get keys(secIKA, x_IKA2, secIKAsign, =x_IKAsign) in 651 | (* In the KEM case, we simply have no authentication property *) 652 | (* find peer_i1 <= NsendOPK, peer_i2 <= Nidentity suchthat 653 | defined(SK_opk[peer_i1,peer_i2]) && SK_opk[peer_i1,peer_i2] = sk_opk then 654 | yield 655 | else*) 656 | yield 657 | else 658 | return(msg) 659 | ',`') 660 | 661 | ifdef(`DH',` 662 | (* Execute event Recv only if the sender Alex is honest and not corrupted *) 663 | get keys(secIKA, =x_IKA, secIKAsign, x_IKAsign2) in 664 | ( 665 | get corrupted(=x_IKA,dummy) in 666 | yield 667 | else 668 | let Bcorrupted = get corrupted(=IKB,dummy2) in true else false in 669 | event RecvWithOPK(Bcorrupted,x_IKA,x_IKAsign,IKB,IKBsign,SPKPubB1,OPKPubB,EPKPubA,PQPKB,msg) 670 | ) 671 | else 672 | return(msg) 673 | ',`') 674 | 675 | ) 676 | 677 | | 678 | 679 | ( 680 | ! Nsignedprekey2 681 | 682 | (* Version without the optional one-time prekey *) 683 | RecvNoOPK(x_IKAp: G,x_IKAsignp: pkey, EPKPubAp: G, idSPK:ids, idPQPK:ids, CTp : ciphertext, msgencp: bitstring) := 684 | 685 | (* Here, we check if the keys we already have in the current 686 | process state have the same id has the received ones. This simulate 687 | a general process fetching the public keys from the received id, by 688 | matching the id against the ids of the keys in the database. *) 689 | 690 | if idSPK = idPKDH(SPKPubB1) then 691 | if idPQPK = idPKKEM(PQPKB) then 692 | 693 | 694 | 695 | let dh1 = gexp(x_IKAp,SPKSecB1) in 696 | let dh2 = gexp(EPKPubAp, secIKB) in 697 | let dh3 = gexp(EPKPubAp, SPKSecB1) in 698 | 699 | let ss = decap(CTp,kemskgen(seed)) in 700 | let sk_nopk = H4(dh1, dh2, dh3, ss 701 | ifdef(`DH',`, hk',`') 702 | ) in 703 | let injbot(msg) = dec(msgencp, concatAD(x_IKAp, x_IKAsignp, IKB, IKBsign), sk_nopk) in 704 | ifdef(`KEM',` 705 | get keys(secIKA, x_IKA2p, secIKAsign, =x_IKAsignp) in 706 | (* In the KEM case, we simply have no authentication property *) 707 | yield 708 | else 709 | return(msg) 710 | ',`') 711 | 712 | ifdef(`DH',` 713 | get keys(secIKA, =x_IKAp, secIKAsign, x_IKAsignp2) in 714 | ( 715 | get corrupted(=x_IKAp,dummy2) in 716 | yield 717 | else 718 | let Bcorrupted = get corrupted(=IKB,dummy) in true else false in 719 | event RecvWithoutOPK(Bcorrupted,x_IKAp,x_IKAsignp,IKB,IKBsign,SPKPubB1,EPKPubAp,PQPKB,msg) 720 | ) 721 | else 722 | return(msg) 723 | ',`') 724 | 725 | ) 726 | ) 727 | . 728 | 729 | 730 | 731 | process 732 | Start() := 733 | new secb: bool; 734 | ifdef(`DH',`new hk:hashkey;',`') 735 | ifdef(`DH',`new hk2:hashkey2;',`') 736 | return(); 737 | 738 | ! Nidentity 739 | ( 740 | InitPrin() := 741 | new secIKA0:Z; 742 | let IKA = gexp(g,secIKA0) in 743 | new signAseed: keyseed; 744 | let secIKAsign = skgen(signAseed) in 745 | let IKAsign = pkgen(signAseed) in 746 | insert keys(secIKA0, IKA, secIKAsign, IKAsign); 747 | return(IKA, IKAsign); 748 | (* Corruption, for forward secrecy and key compromise impersonation *) 749 | ( (Corrupt() := 750 | insert corrupted(IKA,IKAsign); 751 | 752 | return(secIKA0, secIKAsign)) 753 | | (!Nrecv run GenOPKThenRecv(secIKA0,IKA,secIKAsign,IKAsign 754 | ifdef(`DH',`, hk, hk2',`') 755 | )) 756 | | (!NsendOPK run SendInitialWithOPK(secb,secIKA0,IKA,secIKAsign,IKAsign 757 | ifdef(`DH',`, hk2',`') 758 | )) 759 | | (!NsendNoOPK run SendInitialNoOPK(secb,secIKA0,IKA,secIKAsign,IKAsign 760 | ifdef(`DH',`, hk',`') 761 | )) 762 | ) 763 | 764 | ) 765 | ifdef(`DH',`| run hashoracle(hk)',`') 766 | ifdef(`DH',`| run hashoracle2(hk2)',`') 767 | 768 | ifdef(`KEM',` 769 | (* 770 | All queries proved. 771 | 27.36user 0.07system 0:27.53elapsed 99%CPU (0avgtext+0avgdata 122088maxresident)k 772 | *) 773 | ') 774 | ifdef(`DH',` 775 | (* 776 | All queries proved. 777 | 217.64user 0.17system 3:38.21elapsed 99%CPU (0avgtext+0avgdata 307172maxresident)k 778 | 0inputs+808outputs (0major+84900minor)pagefaults 0swaps 779 | *) 780 | ',`') -------------------------------------------------------------------------------- /revision1-WithKyberCR/README.md: -------------------------------------------------------------------------------- 1 | In this folder, we define a novel security property for KEMs. 2 | 3 | 4 | # Novel assumption 5 | In CryptoVerif, it is expressed as 6 | ``` 7 | collision r <-R kem_seed; k <-R kem_enc_seed; forall ct: ciphertext, pk:kempkey; 8 | return(decap(ct,kemskgen(r))= kem_secret(pk,k)) 9 | <=(KEMcollNew)=> 10 | return(ct=kem_encap(pk,k) && pk = kempkgen(r)). 11 | ``` 12 | In a more classical game based notation, given KEM.Encaps, KEM.Keygen() an KEM.Decaps(), it corresponds to the attacker winning the game BoundPKExp being negligible. 13 | 14 | BoundPKExp: 15 | (pk, sk) <- KEM.Keygen(); 16 | seed ct || pk'<> pk) ) 20 | 21 | If the attacker wins the game, given a secret and public key pair (pk, sk), it can produce a malicious ciphertext ct' and public key pk' such that the decapsulation of ct' and sk is equal to the shared secret encapsulation against pk', and such that either ct'<> ct or pk' <> pk. 22 | 23 | 24 | We prove in the next PQXDH.m4.ocv file that this property is indeed enough to ensure the authentication of the KEM public key in PQXDH revision 1. (using `make dh` or `make kem`) 25 | 26 | We also show in a dedicated file Kyber, from https://pq-crystals.org/kyber/data/kyber-specification-round3-20210804.pdf, verifies this assumption. 27 | 28 | Insecure: 29 | * MCEliece is insecure, as "modifying one bit in a public key has a significant chance of not 30 | affecting any particular ciphertext" (section 3, https://classic.mceliece.org/mceliece-rationale-20221023.pdf) 31 | -------------------------------------------------------------------------------- /revision1/cryptoverif/.gitignore: -------------------------------------------------------------------------------- 1 | _models/* -------------------------------------------------------------------------------- /revision1/cryptoverif/Makefile: -------------------------------------------------------------------------------- 1 | MAIN = PQXDH 2 | 3 | dh: 4 | m4 -D DH $(MAIN).m4.ocv > _models/$(MAIN).DH.ocv 5 | time cryptoverif _models/$(MAIN).DH.ocv 6 | 7 | kem: 8 | m4 -D KEM $(MAIN).m4.ocv > _models/$(MAIN).KEM.ocv 9 | time cryptoverif _models/$(MAIN).KEM.ocv 10 | -------------------------------------------------------------------------------- /revision1/cryptoverif/PQXDH.m4.ocv: -------------------------------------------------------------------------------- 1 | (* 2 | 3 | This files models the PQXDH protocol, as specified in: 4 | 5 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 6 | The PQXDH Key Agreement Protocol 7 | Revision 1, 2023-05-24, Last Updated: 2023-09-20 8 | 9 | 10 | See the README next to this file for details on the modeling and how 11 | to run the file. 12 | 13 | This is the CryptoVerif part of the analysis, see the README at the 14 | root directory for details on the joint analysis. 15 | 16 | Authors: Charlie Jacomme 17 | 18 | Based on previous textsecure models by Bruno Blanchet. 19 | 20 | *) 21 | 22 | set useKnownEqualitiesWithFunctionsInMatching = true. 23 | 24 | 25 | 26 | 27 | 28 | ifdef(`KEM',` 29 | 30 | 31 | (* The proof instructions needed to guide CryptoVerif in the KEM case. *) 32 | proof { 33 | 34 | crypto uf_cma_corrupt(sign) signAseed; 35 | out_game "g1.cv" occ; 36 | 37 | insert before "EKSecA1 <-R Z" "find j <= Nrecv, k <= Nidentity suchthat defined (seed[j,k], signAseed[k], IKA[k], PQPKB[j,k]) && x_IKBsign = pkgen2(signAseed[k]) && PQPKPubB = PQPKB[j,k] then"; 38 | 39 | SArename CT_2; 40 | 41 | out_game "g11.cv" occ; 42 | 43 | insert after "RecvOPK(" "find u2 <= NsendOPK suchthat defined(CT_3[u2],PQPKPubB[u2]) && CT_3[u2] = CT_1 && PQPKB = PQPKPubB[u2] then"; 44 | insert after "RecvOPK(" "find u1 <= Nidentity suchthat defined(signAseed[u1], IKA[u1]) && 45 | x_IKAsign = pkgen2(signAseed[u1]) then if defined(corrupted_2[u1]) then"; 46 | 47 | insert after "kseed_4 <-R" "let fencap = kempair(kem_secret(PQPKPubB,kseed_4),kem_encap(PQPKPubB,kseed_4)) in"; 48 | out_game "g3.cv" occ; 49 | 50 | replace at_nth 1 1 "SS: kemsec <- {[0-9]+}" "get_secret(fencap_2)"; 51 | replace at_nth 1 1 "CT_3: ciphertext <- {[0-9]+}" "get_encap(fencap_2)"; 52 | 53 | crypto ind_cca(Encap) [variables: seed -> seed, kseed_4 -> kseed_1 .]; 54 | 55 | out_game "g31.cv" occ; 56 | 57 | insert before "EKSecA1p <-R Z" "find j2 <= Nrecv, k2 <= Nidentity suchthat defined (seed_1[j2,k2], signAseed[k2], IKA[k2], PQPKB[j2,k2]) && x_IKBsignp = pkgen2(signAseed[k2]) && PQPKPubBp = PQPKB[j2,k2] then"; 58 | 59 | SArename CTp_2; 60 | 61 | out_game "g32.cv" occ; 62 | 63 | insert after "RecvNoOPK(" "find u22 <= NsendOPK suchthat defined(CT_3[u22],PQPKPubB[u22]) && CT_3[u22] = CTp_1 && PQPKB = PQPKPubB[u22] then"; 64 | insert after "RecvNoOPK(" "find u12 <= Nidentity suchthat defined(signAseed[u12], IKA[u12]) && 65 | x_IKAsignp = pkgen2(signAseed[u12]) then if defined(corrupted_2[u12]) then"; 66 | 67 | 68 | insert after "kseedp_2 <-R" "let fencap = kempair(kem_secret(PQPKPubBp,kseedp_2),kem_encap(PQPKPubBp,kseedp_2)) in"; 69 | out_game "g33.cv"; 70 | replace at_nth 1 1 "SSp: kemsec <- {[0-9]+}" "get_secret(fencap_3)"; 71 | replace at_nth 1 1 "CTp_3: ciphertext <- {[0-9]+}" "get_encap(fencap_3)"; 72 | 73 | 74 | crypto ind_cca(Encap) [variables: seed_1 -> seed, kseedp_2 -> kseed_1.]; 75 | 76 | crypto prf(H) *; 77 | out_game "g4.cv" ; 78 | 79 | crypto int_ctxt(enc) *; 80 | crypto ind_cpa(enc) **; 81 | out_game "g5.cv"; 82 | success 83 | } 84 | 85 | ',`') 86 | 87 | 88 | ifdef(`DH',` 89 | 90 | 91 | (* The proof instructions needed to guide CryptoVerif in the DH case. *) 92 | proof { 93 | crypto uf_cma_corrupt(sign) signAseed; 94 | out_game "g1.cv" occ; 95 | 96 | insert before "EKSecA1 <-R Z" "find j <= Nrecv, k <= Nidentity suchthat defined (SPKPubB1[j,k], IKA[k]) && pow8(SPKPubB) = pow8(SPKPubB1[j,k]) && pow8(x_IKB) = pow8(IKA[k]) then"; 97 | insert after "RecvOPK(" "find u1 <= Nidentity suchthat defined(signAseed[u1], IKA[u1]) && pow8(x_IKA) = pow8(IKA[u1]) then if defined(corrupted_1[u1]) then"; 98 | 99 | out_game "g11.cv" occ; 100 | insert after "OH_1(" "let (subGtoG(x1p), subGtoG(x2p), subGtoG(x3p), subGtoG(x4p), x5p : kemsec) = (x1_1, x2_1, x3_1, x4_1, x5) in"; 101 | crypto rom(H2); 102 | 103 | out_game "g2.cv" occ; 104 | insert before "EKSecA1p <-R Z" "find j2 <= Nrecv, k2 <= Nidentity suchthat defined (SPKPubB1[j2,k2], IKA[k2]) && pow8(SPKPubBp) = pow8(SPKPubB1[j2,k2]) && pow8(x_IKBp) = pow8(IKA[k2]) then"; 105 | insert after "RecvNoOPK(" "find u2 <= Nidentity suchthat defined(signAseed[u2], IKA[u2]) && pow8(x_IKAp) = pow8(IKA[u2]) then if defined(corrupted_1[u2]) then"; 106 | 107 | 108 | out_game "g12.cv"occ; 109 | 110 | insert after "OH(" "let (subGtoG(x1_1p), subGtoG(x2_1p), subGtoG(x3_1p), x4_1p : kemsec) = (x1, x2, x3, x4) in"; 111 | crypto rom(H1); 112 | 113 | out_game "g3.cv"; 114 | 115 | 116 | crypto gdh(gexp_div_8) [variables: secIKA0 -> a, SPKSecB1 -> a, OPKSecB1 -> a, EKSecA1 -> a, EKSecA1p -> a .]; 117 | 118 | crypto int_ctxt(enc) *; 119 | crypto ind_cpa(enc) **; 120 | out_game "g4.cv"; 121 | crypto int_ctxt_corrupt(enc) r_23; 122 | crypto int_ctxt_corrupt(enc) r_50; 123 | success 124 | } 125 | 126 | 127 | ',`') 128 | 129 | 130 | 131 | 132 | (* KEM definitions *) 133 | type kempkey [bounded]. 134 | type kemskey [bounded]. 135 | type ciphertext. 136 | type kem_seed [large,fixed]. 137 | type kem_enc_seed [large,fixed]. 138 | 139 | type kemsec [fixed]. 140 | 141 | fun kempkgen(kem_seed):kempkey. 142 | fun kemskgen(kem_seed):kemskey. 143 | 144 | fun decap(ciphertext, kemskey): kemsec. 145 | 146 | fun kem_secret(kempkey, kem_enc_seed) : kemsec. 147 | fun kem_encap(kempkey, kem_enc_seed): ciphertext. 148 | 149 | type encaps_return. 150 | fun kempair(kemsec,ciphertext) : encaps_return [data]. 151 | 152 | letfun encaps(pk : kempkey, kseed : kem_enc_seed) = 153 | kempair(kem_secret(pk,kseed ), kem_encap(pk,kseed)). 154 | 155 | equation forall kseed: kem_seed, seed:kem_enc_seed; 156 | decap( kem_encap( kempkgen(kseed), seed), kemskgen(kseed)) = kem_secret( kempkgen(kseed),seed). 157 | 158 | fun get_encap(encaps_return) : ciphertext. 159 | fun get_secret(encaps_return) : kemsec. 160 | 161 | equation forall c:ciphertext, s:kemsec; 162 | get_encap( kempair(s,c)) = c. 163 | 164 | equation forall c:ciphertext, s:kemsec; 165 | get_secret(kempair( s,c))= s. 166 | 167 | ifdef(`KEM',` 168 | 169 | (* KEM security assumptions -> IND-CCA *) 170 | 171 | param Nc, Qeperuser, Qdperuser. 172 | 173 | proba CCA. 174 | 175 | table E(Nc, ciphertext, kemsec). 176 | 177 | equiv(ind_cca(Encap)) 178 | 179 | foreach i <= Nc do seed <-R kem_seed; ( 180 | Opk() := return(kempkgen(seed)) 181 | | 182 | foreach id <= Qdperuser do 183 | OADecap(enc: ciphertext) [useful_change] := 184 | return(decap(enc, kemskgen(seed))) 185 | ) 186 | | 187 | foreach ie <= Qeperuser do 188 | kseed <-R kem_enc_seed; ( 189 | 190 | OE(pk_R:kempkey) [useful_change] := return( encaps(pk_R, kseed) ) 191 | 192 | ) 193 | <=(CCA(time, Nc, #OE, #OADecap))=> 194 | foreach i <= Nc do seed <-R kem_seed; ( 195 | Opk() := return(kempkgen(seed)) | 196 | foreach id <= Qdperuser do ( 197 | OADecap(cd: ciphertext) := 198 | get E(=i, =cd, k2) in ( 199 | return(k2) 200 | ) else ( 201 | return(decap(cd, kemskgen(seed))) 202 | 203 | )) ) 204 | | 205 | foreach ie <= Qeperuser do 206 | kseed <-R kem_enc_seed; 207 | ( 208 | OE(pk_R: kempkey) := 209 | find i2 <= Nc suchthat defined(seed[i2]) && pk_R = kempkgen(seed[i2]) then ( 210 | k1 <-R kemsec; 211 | insert E(i2, kem_encap(pk_R, kseed) , k1); 212 | return( kempair(k1, kem_encap(pk_R, kseed))) 213 | ) else ( 214 | return(encaps(pk_R, kseed) ) 215 | ) 216 | 217 | ) 218 | 219 | 220 | . 221 | ',`') 222 | 223 | 224 | (* We always have some basic properties of the KEM, e.g., public keys 225 | are not independent of their seed. *) 226 | 227 | proba KEMcoll1. 228 | proba KEMcoll2. 229 | 230 | collision r <-R kem_seed; forall Y: kempkey; 231 | return(kempkgen(r) = Y) <=(KEMcoll1)=> return(false) if Y independent-of r. 232 | 233 | 234 | collision r <-R kem_seed; k <-R kem_enc_seed; forall Y: ciphertext; 235 | return(kem_encap(kempkgen(r),k) = Y) <=(KEMcoll2)=> return(false) if Y independent-of k. 236 | 237 | 238 | 239 | 240 | (* DH definitions *) 241 | type emkey [fixed,large]. 242 | 243 | type Z [bounded,large,nonuniform]. (* Exponents *) 244 | type G [bounded,large,nonuniform]. (* Diffie-Hellman group *) 245 | type subG [bounded,large,nonuniform]. (* Diffie-Hellman group *) 246 | 247 | (* Gap Diffie-Hellman *) 248 | (* In the PQ setting, we only assume the informatic theoritic collision properties *) 249 | 250 | (* Note: the secret keys in Signal are really normalized to be multiples of k, 251 | as specified in RFC 7748. The normalization is commented out in the exponentiation 252 | function: 253 | https://github.com/signalapp/libsignal-protocol-javascript/blob/f5a838f1ccc9bddb5e93b899a63de2dea9670e10/native/curve25519-donna.c/#L860 254 | but done when generating a key pair: 255 | https://github.com/signalapp/libsignal-protocol-javascript/blob/f5a838f1ccc9bddb5e93b899a63de2dea9670e10/src/curve25519_wrapper.js#L25 256 | *) 257 | 258 | expand DH_X25519(G, Z, g, gexp, mult, subG, g_8, gexp_div_8, gexp_div_8p, pow8, subGtoG, is_zero_G, is_zero_subG). 259 | 260 | 261 | ifdef(`DH',` 262 | 263 | (* We now make the gapDH assumption. *) 264 | proba psqGDH. 265 | proba pDistRerandom. 266 | expand square_GDH_RSR(subG, Z, g_8, gexp_div_8, gexp_div_8p, mult, psqGDH, pDistRerandom). 267 | 268 | ',`') 269 | 270 | 271 | (* Key derivation *) 272 | 273 | ifdef(`KEM',` 274 | 275 | 276 | (* We model the kdf as a prf function, which is keyed by the KEM shared secret. *) 277 | fun H(bitstring,kemsec): emkey. 278 | 279 | proba Pprf. 280 | equiv(prf(H)) special prf("key_last", H, Pprf, (k, r, x, y, z, u)). 281 | 282 | equiv(prf_partial(H)) special prf_partial("key_last", H, Pprf, (k, r, x, y, z, u)) [manual]. 283 | 284 | 285 | fun c3(G,G,G):bitstring. 286 | fun c4(G,G,G,G):bitstring. 287 | 288 | letfun H4(g1:G,g2:G,g3:G, k:kemsec) = H(c3(g1,g2,g3),k). 289 | letfun H5(g1:G,g2:G,g3:G,g4:G, k:kemsec) = H(c4(g1,g2,g3,g4),k). 290 | 291 | equation forall g1:G, g2:G, g3:G, g4:G; 292 | c3(g1,g2,g3) <> c4(g1,g2,g3,g4). 293 | 294 | 295 | ',`') 296 | 297 | 298 | ifdef(`DH',` 299 | 300 | 301 | (* we model the kdf a Random Oracles. *) 302 | 303 | type hashkey [large,fixed]. (* unused in PQ setting *) 304 | type hashkey2 [large,fixed]. (* unused in PQ setting *) 305 | 306 | expand ROM_hash_large_4(hashkey, G, G, G, kemsec, emkey, H1, hashoracle, qH2). 307 | expand ROM_hash_large_5(hashkey2, G, G ,G ,G, kemsec, emkey, H2, hashoracle2, qH3). 308 | 309 | letfun H4(g1:G,g2:G,g3:G, k:kemsec,hk:hashkey) = H1(hk,g1,g2,g3,k). 310 | 311 | letfun H5(g1:G,g2:G,g3:G,g4:G, k:kemsec, hk:hashkey2) = H2(hk,g1,g2,g3,g4,k). 312 | 313 | 314 | ',`') 315 | 316 | 317 | 318 | 319 | (* Signatures *) 320 | 321 | 322 | type keyseed [large, fixed]. 323 | type pkey [bounded]. 324 | type skey [bounded]. 325 | type t_sign. 326 | 327 | 328 | proba Psign. 329 | proba Psigncoll. 330 | expand UF_CMA_proba_signature(keyseed, pkey, skey, bitstring, t_sign, skgen, pkgen, sign, checksign, Psign, Psigncoll). 331 | 332 | (* Encoding of public keys for signatures *) 333 | 334 | fun encodeEC(G) : bitstring [data]. 335 | fun encodeKEM(kempkey) : bitstring [data]. 336 | 337 | 338 | letfun signKEM(pk:kempkey,sk:skey) = sign(encodeKEM(pk),sk). 339 | letfun checksignKEM(m:kempkey, p:pkey, s:t_sign) = checksign(encodeKEM(m),p,s). 340 | 341 | 342 | letfun signEC(el:G,sk:skey) = sign(encodeEC(el),sk). 343 | letfun checksignEC(m:G, p:pkey,s:t_sign) = checksign(encodeEC(m),p,s). 344 | 345 | 346 | (* We rely here on an assumption which is not in [PQXDH]. However, it 347 | is in fact verified by the signal implementation, as all encodings are 348 | prefixed with a single byte corresponding to the scheme: 349 | 350 | (curve25519 |-> 0x05, curve448 |-> 0x06, Kyber768 |-> 0x07, Kyber1024 |-> 0x08) 351 | 352 | This correspond to the KeyType field of libsignal, as defined here for KEMs 353 | https://github.com/signalapp/libsignal/blob/d1f9dff273e6da059af699c6afe860fb93406032/rust/protocol/src/kem.rs#L153 354 | and here for curve 25519: 355 | https://github.com/signalapp/libsignal/blob/d1f9dff273e6da059af699c6afe860fb93406032/rust/protocol/src/curve.rs#L33 356 | 357 | *) 358 | 359 | 360 | equation forall pkdh:G, pkkem:kempkey; 361 | encodeEC(pkdh) <> encodeKEM(pkkem). 362 | 363 | (* AEAD *) 364 | 365 | type t_data. 366 | proba Penc. 367 | proba Pencctxt. 368 | 369 | 370 | (* We assume IND-CPA + INT-CTXT for the AEAD, in both cases. *) 371 | expand AEAD(emkey, bitstring, bitstring, t_data, enc, dec, injbot, Zero, Penc, Pencctxt). 372 | 373 | const const1: bitstring. 374 | fun concat4(G,pkey,G,pkey):t_data [data]. 375 | 376 | 377 | param Nidentity, Nrecv, NsendOPK, NsendNoOPK, Nsignedprekey, Nsignedprekey2. 378 | 379 | (* Table of keys *) 380 | table keys(Z, G, skey, pkey). 381 | (* Table of keys of corrupted participants *) 382 | table corrupted(G,pkey). 383 | 384 | 385 | (* Security properties *) 386 | 387 | event SendWithOPK(G, pkey, G, pkey, G, G, G, kempkey, bitstring). 388 | event RecvWithOPK(bool, G, pkey, G, pkey, G, G, G, kempkey, bitstring). 389 | event SendWithoutOPK(G, pkey, G, pkey, G, G, kempkey, bitstring). 390 | event RecvWithoutOPK(bool, G, pkey, G, pkey, G, G, kempkey, bitstring). 391 | (* Arguments of events 392 | - for RecvWithOPK/RecvWithoutOPK: a boolean true when Blake is corrupted 393 | - public keys of sender (DH and signature), IKA and IKAsign 394 | - public keys of receiver (DH and signature), IKB and IKBsign 395 | - signed ephemeral, SPKB. 396 | - one-time ephemeral [optional], OPK, 397 | - sender first ephemeral, EPK, 398 | - the signed kem public key, PQPK, 399 | - sent message 400 | *) 401 | 402 | ifdef(`DH',` 403 | 404 | query Bcorrupted:bool,a0:G,as:pkey,b0:G,bs:pkey,sb:G,sb2:G,ob:G,a1:G,ob2:G,a12:G,pk1,pk2:kempkey,m:bitstring; 405 | inj-event(RecvWithOPK(Bcorrupted,a0,as,b0,bs,sb,ob,a1,pk1,m)) ==> inj-event(SendWithOPK(a0,as,b0,bs,sb2,ob2,a12,pk2,m)) && pow8(ob) = pow8(ob2) && pow8(a1) = pow8(a12) && pow8(sb) = pow8(sb2) && (Bcorrupted || sb = sb2) 406 | public_vars secb. 407 | query Bcorrupted:bool,a0:G,as:pkey,b0:G,bs:pkey,sb:G,sb2:G,a1:G,a12:G,pk1,pk2:kempkey,m:bitstring; 408 | event(RecvWithoutOPK(Bcorrupted,a0,as,b0,bs,sb,a1,pk1,m)) ==> event(SendWithoutOPK(a0,as,b0,bs,sb2,a12,pk2,m)) && pow8(a1) = pow8(a12) && pow8(sb) = pow8(sb2) && (Bcorrupted || sb = sb2) 409 | public_vars secb. 410 | 411 | (* Blake receives => Alex sent is proved provided Alex is not corrupted 412 | (event Recv/RecvWithoutOPK is executed when Alex is not corrupted). 413 | That proves KCI resistance against the compromise of long-term keys. 414 | . 415 | We cannot prove that sb = sb2 when Blake signature key is 416 | compromised. The adversary can then forge a signature of the signed 417 | ephemeral sb. The Diffie-Hellman key exchange just guarantees that 418 | pow8(sb) = pow8(sb2). 419 | 420 | To note, we cannot prove that the two parties agree on the KEM public key used. 421 | 422 | *) 423 | 424 | 425 | ',`') 426 | 427 | 428 | (* Identifiers for public keys *) 429 | 430 | type ids. 431 | 432 | fun idPKDH(G):ids. 433 | fun idPKKEM(kempkey):ids. 434 | 435 | query secret secb [cv_bit]. 436 | 437 | (* The secrecy of secb shows the secrecy of the message sent by Alex to Blake, 438 | provided Blake is not corrupted yet when Alex send the message (secb is used 439 | to choose between 2 messages only when Blake is not corrupted). That 440 | shows in particular forward secrecy. *) 441 | 442 | 443 | (********************) 444 | (**** INITIATOR *****) 445 | (********************) 446 | 447 | (* Alex using prekeys and sending a message to a participant (Blake or other). 448 | The received x_IKB:G, x_IKBsign:pkey choose Alex's interlocutor. 449 | This sender uses an optional OPK. 450 | *) 451 | (* section 3.3 *) 452 | let SendInitialWithOPK(secb1:bool,secIKA:Z , IKA:G, secIKAsign:skey, IKAsign:pkey 453 | ifdef(`DH',`, hk2:hashkey2',`') 454 | ) 455 | = 456 | (* Key exchange + send message m1 or m2 *) 457 | SendFirstMessageOPK(x_IKB:G, x_IKBsign:pkey, SPKPubB:G,SPKsign:t_sign,OPKPubB:G,PQPKPubB:kempkey,PQPKsign:t_sign,m1: bitstring, m2:bitstring) := 458 | 459 | 460 | (* Classical DH part *) 461 | new EKSecA1: Z; 462 | let EKPubA = gexp(g, EKSecA1) in 463 | let dh1 = gexp(SPKPubB, secIKA) in 464 | let dh2 = gexp(x_IKB, EKSecA1) in 465 | let dh3 = gexp(SPKPubB, EKSecA1) in 466 | let dh4 = gexp(OPKPubB, EKSecA1) in 467 | 468 | (* Kem additionnal part *) 469 | new kseed: kem_enc_seed; 470 | 471 | let fencap = encaps(PQPKPubB,kseed) in 472 | let SS = get_secret(fencap) in 473 | let CT = get_encap(fencap) in 474 | 475 | let SK_opk : emkey = H5(dh1, dh2, dh3, dh4, SS 476 | ifdef(`DH',`, hk2',`') 477 | ) in 478 | 479 | 480 | ifdef(`KEM',`get keys(secIKB, x_IKB2, secIKBsign, =x_IKBsign) in',`') 481 | ifdef(`DH',`get keys(secIKB, =x_IKB, secIKBsign, =x_IKBsign) in',`') 482 | ( 483 | ifdef(`KEM',`get corrupted(dummy,=x_IKBsign) in',`') 484 | ifdef(`DH',`get corrupted(=x_IKB,dummy) in',`') 485 | ( 486 | (* Alex talks to a corrupted participant; the message cannot be secret *) 487 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 488 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 489 | if m1 = m2 then 490 | let msg = m1 in 491 | let cipher = enc(msg, concat4(IKA, IKAsign, x_IKB, x_IKBsign), SK_opk) in 492 | event SendWithOPK(IKA,IKAsign,x_IKB,x_IKBsign,SPKPubB,OPKPubB,EKPubA,PQPKPubB,msg); 493 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 494 | ) 495 | else 496 | ( 497 | (* Alex talks to a honest participant Blake *) 498 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 499 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 500 | (* Check that m1 and m2 have the same length *) 501 | if Zero(m1) = Zero(m2) then 502 | (* Send either m1 or m2 depending on the value of the secret bit b *) 503 | let msg = if_fun(secb1, m1, m2) in 504 | let cipher = enc(msg, concat4(IKA, IKAsign, x_IKB, x_IKBsign), SK_opk) in 505 | event SendWithOPK(IKA,IKAsign,x_IKB,x_IKBsign,SPKPubB,OPKPubB,EKPubA,PQPKPubB,msg); 506 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 507 | ) 508 | ) 509 | else 510 | ( 511 | (* Alex talks to a dishonest participant *) 512 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 513 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 514 | if m1 = m2 then 515 | let msg = m1 in 516 | let cipher = enc(msg, concat4(IKA, IKAsign, x_IKB, x_IKBsign), SK_opk) in 517 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 518 | ). 519 | 520 | (* Same as before, but without the optional OPK. *) 521 | let SendInitialNoOPK(secb1:bool,secIKAp:Z , IKAp:G, secIKAsignp:skey, IKAsignp:pkey 522 | ifdef(`DH',`, hk:hashkey',`') 523 | ) = 524 | (* Key exchange + send message m1 or m2 *) 525 | SendFirstMessageNoOPK(x_IKBp:G, x_IKBsignp:pkey, SPKPubBp:G,SPKsignp:t_sign,PQPKPubBp:kempkey,PQPKsignp:t_sign,m1p: bitstring, m2p:bitstring) := 526 | 527 | (* Classical DH part *) 528 | new EKSecA1p: Z; 529 | let EKPubAp = gexp(g, EKSecA1p) in 530 | let dh1 = gexp(SPKPubBp, secIKAp) in 531 | let dh2 = gexp(x_IKBp, EKSecA1p) in 532 | let dh3 = gexp(SPKPubBp, EKSecA1p) in 533 | 534 | (* Kem additionnal part *) 535 | new kseedp: kem_enc_seed; 536 | 537 | let fencap = encaps(PQPKPubBp,kseedp) in 538 | let SSp = get_secret(fencap) in 539 | let CTp = get_encap(fencap) in 540 | 541 | let SK_nopk = H4(dh1, dh2, dh3, SSp 542 | ifdef(`DH',`, hk',`') 543 | ) in 544 | 545 | ifdef(`KEM',`get keys(secIKB, x_IKB, secIKBsign, =x_IKBsignp) in',`') 546 | ifdef(`DH',`get keys(secIKB, =x_IKBp, secIKBsign, =x_IKBsignp) in',`') 547 | ( 548 | ifdef(`KEM',`get corrupted(dummy,=x_IKBsignp) in',`') 549 | ifdef(`DH',`get corrupted(=x_IKBp,dummy) in',`') 550 | ( 551 | (* Alex talks to a corrupted participant; the message cannot be secret *) 552 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 553 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 554 | if m1p = m2p then 555 | let msg = m1p in 556 | let cipher = enc(msg, concat4(IKAp, IKAsignp, x_IKBp, x_IKBsignp), SK_nopk) in 557 | event SendWithoutOPK(IKAp,IKAsignp,x_IKBp,x_IKBsignp,SPKPubBp,EKPubAp,PQPKPubBp,msg); 558 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 559 | ) 560 | else 561 | ( 562 | (* Alex talks to a honest participant Blake *) 563 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 564 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 565 | (* Check that m1 and m2 have the same length *) 566 | if Zero(m1p) = Zero(m2p) then 567 | (* Send either m1 or m2 depending on the value of b *) 568 | let msg = if_fun(secb1, m1p, m2p) in 569 | let cipher = enc(msg, concat4(IKAp, IKAsignp, x_IKBp, x_IKBsignp), SK_nopk) in 570 | event SendWithoutOPK(IKAp,IKAsignp,x_IKBp,x_IKBsignp,SPKPubBp,EKPubAp,PQPKPubBp,msg); 571 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 572 | ) 573 | ) 574 | else 575 | ( 576 | (* Alex talks to a dishonest participant *) 577 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 578 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 579 | if m1p = m2p then 580 | let msg = m1p in 581 | let cipher = enc(msg, concat4(IKAp, IKAsignp, x_IKBp, x_IKBsignp), SK_nopk) in 582 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 583 | ). 584 | 585 | 586 | (* Blake generating prekeys and running the protocol, with Alex 587 | or with any other participant *) 588 | 589 | (* Sec 3.4 of spec *) 590 | let GenOPKThenRecv(secIKB : Z, IKB : G, secIKBsign : skey, IKBsign : pkey 591 | ifdef(`DH',`, hk:hashkey, hk2:hashkey2',`') 592 | ) = 593 | GenSPK():= 594 | (* Signed PQPK, last resort that can be reused *) 595 | new seed:kem_seed; 596 | let PQPKB = kempkgen(seed) in 597 | let PQPKBsig = signKEM(PQPKB, secIKBsign) in 598 | 599 | 600 | (* new SPK DH based *) 601 | new SPKSecB1: Z; 602 | let SPKPubB1: G = gexp(g, SPKSecB1) in 603 | let SPKsignature = signEC(SPKPubB1, secIKBsign) in 604 | return(SPKPubB1,SPKsignature, PQPKB, PQPKBsig); 605 | (( 606 | ! Nsignedprekey 607 | (* One-time prekey DH based*) 608 | GenOPK():= 609 | new OPKSecB1: Z; 610 | let OPKPubB = gexp(g, OPKSecB1) in 611 | return(OPKPubB); 612 | (* 2nd part of key exchange, 613 | using prekey OPKPubB and signed prekey SPKPubB1 *) 614 | RecvOPK(x_IKA: G,x_IKAsign: pkey, EPKPubA: G, idSPK:ids, idOPK:ids, idPQPK:ids, CT : ciphertext, msgenc: bitstring) := 615 | 616 | (* Here, we check if the keys we already have in the current 617 | process state have the same id has the received ones. This simulate 618 | a general process fetching the public keys from the received id, by 619 | matching the id against the ids of the keys in the database. *) 620 | 621 | if idSPK = idPKDH(SPKPubB1) then 622 | if idOPK = idPKDH(OPKPubB) then 623 | if idPQPK = idPKKEM(PQPKB) then 624 | 625 | let dh1 = gexp(x_IKA,SPKSecB1) in 626 | let dh2 = gexp(EPKPubA, secIKB) in 627 | let dh3 = gexp(EPKPubA, SPKSecB1) in 628 | let dh4 = gexp(EPKPubA, OPKSecB1) in 629 | 630 | let ss = decap(CT,kemskgen(seed)) in 631 | 632 | let sk_opk = H5(dh1, dh2, dh3, dh4, ss 633 | ifdef(`DH',`, hk2',`') 634 | ) in 635 | 636 | let injbot(msg) = dec(msgenc, concat4(x_IKA, x_IKAsign, IKB, IKBsign), sk_opk) in 637 | ifdef(`KEM',` 638 | get keys(secIKA, x_IKA2, secIKAsign, =x_IKAsign) in 639 | (* In the KEM case, we simply have no authentication property *) 640 | (* find peer_i1 <= NsendOPK, peer_i2 <= Nidentity suchthat 641 | defined(SK_opk[peer_i1,peer_i2]) && SK_opk[peer_i1,peer_i2] = sk_opk then 642 | yield 643 | else*) 644 | yield 645 | else 646 | return(msg) 647 | ',`') 648 | 649 | ifdef(`DH',` 650 | (* Execute event Recv only if the sender Alex is honest and not corrupted *) 651 | get keys(secIKA, =x_IKA, secIKAsign, x_IKAsign2) in 652 | ( 653 | get corrupted(=x_IKA,dummy) in 654 | yield 655 | else 656 | let Bcorrupted = get corrupted(=IKB,dummy2) in true else false in 657 | event RecvWithOPK(Bcorrupted,x_IKA,x_IKAsign,IKB,IKBsign,SPKPubB1,OPKPubB,EPKPubA,PQPKB,msg) 658 | ) 659 | else 660 | return(msg) 661 | ',`') 662 | 663 | ) 664 | 665 | | 666 | 667 | ( 668 | ! Nsignedprekey2 669 | 670 | (* Version without the optional one-time prekey *) 671 | RecvNoOPK(x_IKAp: G,x_IKAsignp: pkey, EPKPubAp: G, idSPK:ids, idPQPK:ids, CTp : ciphertext, msgencp: bitstring) := 672 | 673 | (* Here, we check if the keys we already have in the current 674 | process state have the same id has the received ones. This simulate 675 | a general process fetching the public keys from the received id, by 676 | matching the id against the ids of the keys in the database. *) 677 | 678 | if idSPK = idPKDH(SPKPubB1) then 679 | if idPQPK = idPKKEM(PQPKB) then 680 | 681 | 682 | 683 | let dh1 = gexp(x_IKAp,SPKSecB1) in 684 | let dh2 = gexp(EPKPubAp, secIKB) in 685 | let dh3 = gexp(EPKPubAp, SPKSecB1) in 686 | 687 | let ss = decap(CTp,kemskgen(seed)) in 688 | let sk_nopk = H4(dh1, dh2, dh3, ss 689 | ifdef(`DH',`, hk',`') 690 | ) in 691 | let injbot(msg) = dec(msgencp, concat4(x_IKAp, x_IKAsignp, IKB, IKBsign), sk_nopk) in 692 | ifdef(`KEM',` 693 | get keys(secIKA, x_IKA2p, secIKAsign, =x_IKAsignp) in 694 | (* In the KEM case, we simply have no authentication property *) 695 | yield 696 | else 697 | return(msg) 698 | ',`') 699 | 700 | ifdef(`DH',` 701 | get keys(secIKA, =x_IKAp, secIKAsign, x_IKAsignp2) in 702 | ( 703 | get corrupted(=x_IKAp,dummy2) in 704 | yield 705 | else 706 | let Bcorrupted = get corrupted(=IKB,dummy) in true else false in 707 | event RecvWithoutOPK(Bcorrupted,x_IKAp,x_IKAsignp,IKB,IKBsign,SPKPubB1,EPKPubAp,PQPKB,msg) 708 | ) 709 | else 710 | return(msg) 711 | ',`') 712 | 713 | ) 714 | ) 715 | . 716 | 717 | 718 | 719 | process 720 | Start() := 721 | new secb: bool; 722 | ifdef(`DH',`new hk:hashkey;',`') 723 | ifdef(`DH',`new hk2:hashkey2;',`') 724 | return(); 725 | 726 | ! Nidentity 727 | ( 728 | InitPrin() := 729 | new secIKA0:Z; 730 | let IKA = gexp(g,secIKA0) in 731 | new signAseed: keyseed; 732 | let secIKAsign = skgen(signAseed) in 733 | let IKAsign = pkgen(signAseed) in 734 | insert keys(secIKA0, IKA, secIKAsign, IKAsign); 735 | return(IKA, IKAsign); 736 | (* Corruption, for forward secrecy and key compromise impersonation *) 737 | ( (Corrupt() := 738 | insert corrupted(IKA,IKAsign); 739 | 740 | return(secIKA0, secIKAsign)) 741 | | (!Nrecv run GenOPKThenRecv(secIKA0,IKA,secIKAsign,IKAsign 742 | ifdef(`DH',`, hk, hk2',`') 743 | )) 744 | | (!NsendOPK run SendInitialWithOPK(secb,secIKA0,IKA,secIKAsign,IKAsign 745 | ifdef(`DH',`, hk2',`') 746 | )) 747 | | (!NsendNoOPK run SendInitialNoOPK(secb,secIKA0,IKA,secIKAsign,IKAsign 748 | ifdef(`DH',`, hk',`') 749 | )) 750 | ) 751 | 752 | ) 753 | ifdef(`DH',`| run hashoracle(hk)',`') 754 | ifdef(`DH',`| run hashoracle2(hk2)',`') 755 | 756 | ifdef(`KEM',` 757 | (* 758 | All queries proved. 759 | 35.12user 0.08system 0:35.38elapsed 99%CPU (0avgtext+0avgdata 111352maxresident) 760 | *) 761 | ') 762 | ifdef(`DH',` 763 | (* 764 | All queries proved. 765 | 293.70user 0.43system 4:55.03elapsed 99%CPU (0avgtext+0avgdata 284532maxresident) 766 | *) 767 | ',`') -------------------------------------------------------------------------------- /revision1/cryptoverif/README.md: -------------------------------------------------------------------------------- 1 | This folder contains a model of the PQXDH protocol, as specified in: 2 | 3 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 4 | The PQXDH Key Agreement Protocol 5 | Revision 1, 2023-05-24, Last Updated: 2023-09-20 6 | 7 | 8 | # Model description 9 | 10 | The file models: 11 | - an arbitrary number of clients (devices) communicating with each other 12 | - each device uploads Curve and KEM keys to a server which is fully untrusted (modelled as a public channel) 13 | - each connection consists of one message from the initiator to the responder (with no follow-up messages) 14 | - each connection can optionnaly use an OPK 15 | - PQPK can always be reused, this is a worst case scenario where they are all last resort 16 | - the identifiers of the prefkeys do not need to verify any particular assumption 17 | 18 | ## Limitations 19 | 20 | The main limitation of the model is that we split the identity key IK into two keys, one DH key and one signing key. In practice, a single DH IK is used both for X25519 operations and XEdDSA signatures. To be completely precise, one would thus need to analyze the protocol under the gapDH assumption while also assuming that the attacker can obtain DH computations through the oracle signature. Such a proof does not exist in the litterature (also, while Ed25519 is proved, XEdDSA is not, which is a small gap). For instance, [8] proves the security of X3DH assuming gapDH, but also assuming that in fact all signed prekeys are pre-authenticated, and simply drop the signature question. Our model is thus more fine-grained. 21 | 22 | *Feedback 1*: Remark that in this respect, the related work introduced in section 4 of the PQXDH spec is a bit imprecise, as it only says "was formally studied in [8] and proven secure under the Gap Diffie-Hellman assumption (GDH)[9].". The need for a full proof of PQXDH (and X3DH) under a joint gapDH and EUF-CMA security over X25519 and XEdDSA is still present. 23 | 24 | A second limitation here is that we cannot prove anything w.r.t. to whether an untrusted server only gives the last resort PQPK, or never gives any OPK. This echoes the security consideration in 4.9. 25 | 26 | # Threat models 27 | 28 | In term of key compromise, we allow compromise of long term identity keys IK. 29 | 30 | In addition, the file includes two distinct set of threat models, one assuming the security of DH, and the other one assuming the security of the KEM. This notably correspond to either considering that the attacker is classical or post-quantum. 31 | 32 | ## Secure DH threat model 33 | 34 | In the classical setting, this file assumes that 35 | * the KDF function is a ROM 36 | * the signature Sig is EUF-CMA 37 | * the X25519 curve is gapDH 38 | * the final AEAD is IND-CPA and IND-CTXT 39 | 40 | 41 | 42 | ## Secure KEM threat model 43 | 44 | In the PQ setting, this file assumes that: 45 | * the KDF function is a post-quantum PRF w.r.t. to the kem secret position 46 | * the EUF-CMA is a post-quantum EUF-CMA signature 47 | * the KEM is post-quantum IND-CCA 48 | * the final AEAD is post-quanum IND-CPA and IND-CTXT 49 | 50 | Remark here that it looks like we assume that the signature Sig is post-quantum secure. Yet, as we allow compromise of signing keys, we can also see it as, the scheme is secure as long as the attacker does not try to compromise it using its quantum power, the only assumption being that we are able to know when the attacker does so. 51 | 52 | 53 | # Security Results 54 | 55 | In both cases, we remarked that it is impossible to do the proof without making an additional assumption compared to the specification: 56 | ``` 57 | equation forall a:G, pk:kempkey; 58 | encodeEC(a) <> encodeKEM(pk). 59 | ``` 60 | Without this, we cannot apply the IND-CCA or the gapDH assumptions, as the attacker may confuse PQPK and SPK, and we can't apply IND-CCA when the KEM is used with a public key from an SPK. 61 | 62 | Here, using a companion ProVerif model demonstrates that without this assumption, there is indeed a practical attack. 63 | 64 | *Feedback 2*: We need to ensure that our extra assumption is in the spec to allow for provable security, and in fact to avoid potential future attacks if one day KEM and DH public keys can indeed be confused. 65 | 66 | ## Secure DH case 67 | 68 | For this setting, we prove authentication and the secrecy of the first sent message. Remark here that for authentication, as X25519 as a small subgroup, we do not in fact have authentication where we can say that both parties use precisely the same one time key OPK (or other DH keys), but only that they use the same modulo this small sub group. 69 | 70 | The results in this case are very similar to the previous CryptoVerif analysis of TextSecure, a X3DH like protocol [A]. 71 | 72 | *Feedback 3*: We cannot prove authentication of the KEM public key, as just under IND-CCA the public key is not tied to the shared secret. See the ProVerif analysis for more details, where the issue is more salient. 73 | 74 | ## Secure KEM case 75 | 76 | In the secure KEM case, we do not look at the authentication case. We prove that secrecy still holds, as long as the signature scheme was secure when the conversation took place. 77 | 78 | *Feedback 4:* Remark that we can only prove the security here assuming that the final AEAD is post-quantum IND-CPA. If the threat model is the PQ setting, one then need to take care of ensuring that the key size is ok for such attackers, but no mention of this is made in the specification. 79 | 80 | # File usage 81 | 82 | Both scenarios are generated from a single model. The makefile allows to run the proof for both scenarios, using either `make dh` or `make kem`. 83 | 84 | # References 85 | 86 | [A] N. Kobeissi, K. Bhargavan and B. Blanchet. Automated Verification for Secure Messaging Protocols and their Implementations: A Symbolic and Computational Approach. EuroS&P'17. 87 | 88 | [8] K. Cohn-Gordon, C. Cremers, B. Dowling, L. Garratt, and D. Stebila, “A formal security analysis of the signal messaging protocol,” J. Cryptol., vol. 33, no. 4, 2020. https://doi.org/10.1007/s00145-020-09360-1 89 | -------------------------------------------------------------------------------- /revision1/pqxdh-rev1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inria-Prosecco/pqxdh-analysis/a09439cc350629ec430fb486305baa4001abbee1/revision1/pqxdh-rev1.pdf -------------------------------------------------------------------------------- /revision1/proverif/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | ./run.sh pqxdh-model SecrecyInit SecrecyResp Authentication 3 | 4 | confuseKemEc: 5 | ./run.sh pqxdh-model ConfuseKemEc UnbreakableDH UnbreakableKEM DisableNoOPK 6 | 7 | reach: 8 | ./run.sh pqxdh-model Reach 9 | 10 | reEncaps: 11 | ./run.sh pqxdh-model ReEncaps UnbreakableDH UnbreakableKEM DisableNoOPK 12 | 13 | clean: 14 | rm *.gen.pv -f 15 | 16 | 17 | -------------------------------------------------------------------------------- /revision1/proverif/README.md: -------------------------------------------------------------------------------- 1 | This folder contains ProVerif model of the PQXDH protocol, as specified in: 2 | 3 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 4 | The PQXDH Key Agreement Protocol 5 | Revision 1, 2023-05-24, Last Updated: 2023-09-20 6 | 7 | # Model description 8 | 9 | The file models: 10 | - an arbitrary number of clients (devices) communicating with each other 11 | - each device uploads Curve and KEM keys to a server which is fully untrusted (modelled as a public channel) 12 | - each connection consists of one message from the initiator to the responder (with no follow-up messages) 13 | - each connection can optionnaly use an OPK 14 | - PQPK can always be reused, this is a worst case scenario where they are all last resort 15 | 16 | ## Threat Model 17 | 18 | Possible Key Compromise Scenario, with distinct cases: 19 | - IK secrets (this allow the adversary to compust maliciously signed SPK and PQPK) 20 | - OPK secrets 21 | - PQPK secrets 22 | - SPK secrets 23 | 24 | Additional Threat Model: 25 | - The attacker may suddenly be able to compute discrete logs. 26 | - The attacker may suddendly be able to extract a secret kem key from any public key. 27 | - Enable confusion between encodeKEM and encodeEC, where honestly generated KEM public keys are weak DH keys and honestly generated DH keys are weak KEM keys. (this scenario is compatible with the gapDH and KEM IND-CCA assumptions) 28 | - Consider an honest KEM encapsulation result (ss,ct) for some key pair (sk,pk), and an additional keypair (sk',pk'). If the attacker knows (ct,sk,pk'), we consider that the attacker can compute ct' such that decaps(ct',sk') = ss, that is produce a valid encapsulation for pk' of the same shared secret ss. This scenario is compatible with the IND-CCA2 assumption, but is e.g. impossible with Kyber. 29 | 30 | ## Properties 31 | 32 | We try to verify the secrecy of the key computed by the initiator, of the one computed by responder, and the authentication between a responder and an initiator. 33 | 34 | For each case, we try to come up with an optimal query, which precisely specifies which set of compromise falsify the query. We thus verify the strongest possible kind of property for each, and notably capture in single queries many classical properties such as KCI, FS, ... 35 | 36 | ## Attacks 37 | 38 | We report here in details on two attacks, that we believe should be fixed on future versions of the protocol. While given the current implementation of PQXDH they do not break the security, the can still happen under the classical gapDH and IND-CCA2 assumptions, and the specification is thus currently impossible to prove under classical assumptions, and may not be secure with later instantiations of primitives. 39 | 40 | ### KEM/DH confusion 41 | 42 | Here, we consider that for some honestly generated SPKB and PQPKB, we may in fact mix them up when sending them to the initiator. 43 | 44 | 1) Attacker gets from B, (IKB, SPKB, SPKB_sig, PQPKB, PQPKB_sig) 45 | 2) Attacker sends to A (IKB, SPKB, SPKB_sig, SPKB, SPKB_sig), replacing the KEM part with the DH part. 46 | 3) A verifies the signature over encodeEC(SPKB) and encodeKEM(SPKB), but as no domain separation is inforced in the spec, the encodeKEM(SPKB) may typically be equal to encodeEC(SPKB) and the check may go through. Then, after computing the DH normally, the attacker would compute encaps(SPKB), and once again, there is no reason for this to be secure, and ss may be predictable by the attacker. A then succeeds and output the messages. 47 | 4) Here, ss can be computed by the attacker. 48 | 49 | So, this attack would completely disable the post DH break down aim. 50 | 51 | In addition, it introduces other weird side behaviour, as it introduces further ways to compromise secrecy and authentication when assuming other compromises. The most complex case being where in fact in step 2, we have 52 | 2) Attacker sends to A (IKB, PQPKB, PQPKB_sig, SPKB, SPKB_sig), completely swapping the KEM and DH based parts 53 | 3) In addition to before, when A generates some EKA, and computes e.g. DH(EKA_s, PQPKB), There is no reason why an honestly generated KEM public key would be a strong valid DH public key, so this may be a weak DH value predicatable by the attacker. 54 | 4) In the end, DH1, DH3 and ss can be computed by the attacker. 55 | 56 | ### Fix 1 57 | 58 | It is is easy to fix this, by simply enforcing that we always have encodeEC(x) <> encodeKEM(y). 59 | Importantly, this attack would also allow to mix up KEM keys for different algorithm, so the KEM byte algo identifier should be a MUST. 60 | 61 | ### Re-encapsulation confusions 62 | 63 | Consider the following execution. 64 | 1) Attacker gets from B, (IKB, SPKB, SPKB\_sig, PQPKB, PQPKB\_sig). It also get an additional PQPKB2 and PQPKB2\_sig, which was compromised for some reason. 65 | 2) Attacker sends to A (IKB, SPKB, SPKB_sig, PQPKB2, PPKB2_sig). 66 | 3) The initiator A proceeds normally, and send back the values (EKA,IKB,SPKB,PQPKB,CT,msg). 67 | 4) Here, the attacker computes SS from CT. (we assumed that PQPKB2 was compromised at step 1). 68 | 5) Now, the attacker, not violating IND-CCA2, comes up with CT', valid for PQPKB and such that decap(CT', SPKB_s) = SS. 69 | 6) The attacker forwards (EKA,IKB,SPKB,PQPKB,CT',msg) to the responder. 70 | 7) The responder succeeds in computing the key. 71 | 72 | This makes it so that when a responder accepts a conversation believing to have used some PQPKB, the initiator may have used another one. 73 | 74 | Here, the compromise of a PQPK, one time or last resort, that a responder did not use for this session, still allow the attacker to obtain the ss of this responder session. This breaks an usual session independency feature (compromise of ephemeral material of other sessions should not impact the security of an uncompromised session). And it in fact implies that the compromise of a single responder's PQPK implies the compromise of all its other PQPKs. 75 | 76 | Importantly, Kyber does not allow such reencapsulation by tying the shared secret to the public key, but once again, this is not covered by the IND-CCA assumption. 77 | 78 | Defining the precise assumption needed over the KEM to get security is unclear. Notably, compared to other notions such as (w/s)CFR-CCA, called collision freeness, see e.g. [KX, Fig 2], in our case, the attacker does have access to the secret key of one KEM. This seems to indicate that the existing notions are not satisfactory for the current Signal use case. 79 | 80 | ### Fix 2 81 | 82 | Adding PQPKB on the initiator side in the AD of the AEAD would for instance fix this. 83 | 84 | ## Exhaustive Security results 85 | 86 | We now report on the multiple results, obtained when enabling all possible compromises, except the encodeEC/encodeKEM confusion. 87 | 88 | ### Secrecy of the initiator 89 | 90 | The key SK computed by the initiator A is secret, unless: 91 | 1) IKB was compromised before the completion of the key exchange 92 | (this is to be expected, this is essentially a malicious B case) 93 | 2) IKB was compromised after the key exchange, as well as some SPK, and either KEMs are broken or the corresponding PQPK has been compromised 94 | (the attacker can then trivially recompute DH1 DH2 and DH3 given EKA and IKA, and also ss, but then, the attacker must have sent to B a malicious OPK) 95 | 3) DH was broken before the key exchange 96 | (similar to case 1) 97 | 4) Or DH was broken after the key exchange, and either KEMs are broken or the PQPK compromised 98 | (similar to case 2) 99 | 100 | All those cases appear normal, we notably have KCI (compromised IKa does not affect security) and FS implied by our result. 101 | 102 | Remark that using an OPK does not change anything here, as they are not authenticated. But of course, compromising the OPK makes it so that the honest responder will never receive and answer the message. 103 | 104 | 105 | ### Secrecy of the responder 106 | 107 | The key SK computed by the responder B is secret, unless: 108 | 1) IKA was compromised before the communication. 109 | (this allows a full impersonation of A) 110 | 2) Some SPKB was compromised before the communication. 111 | (more surprisingly, but inevitably, this allows a full impersonation of A) 112 | 3) IKB was compromised before the communication, and some still honest SPK was compromised after and either no OPK was used or it was compromised. 113 | (see the re-encapsulation above. here, the attacker makes an honest initiator run with some honest SPK, but uses IKB to submit a malicious PQPK. The attacker can then complete the exchange by re-encapsulating the honest ct against an honest PQPK. And once SPK becomes compromise, the attacker can compute everything. 114 | 4) IKB, SPK and PQPK are compromised after the communication, and either no OPK was used or it was compromised. 115 | (all secret material of B is leaked here, natural case) 116 | 5) IKB, SPK are compromised after the communication, KEMs are broken, and either no OPK was used or it was compromised. 117 | (similar to 4) 118 | 6) IKB, SPK are compromised after the communication, a PQPK not used by the responder was compromised, and either no OPK was used or it was compromised. 119 | (similar to 3, initiator and responder don't agree on which PQPK was used due to reencapsulation) 120 | 7) DH was broken before the exchange 121 | (naturally breaks everything) 122 | 8) DH was broken after the exchange, and the PQPK used by the responder was compromised 123 | (similar to 4) 124 | 9) DH was broken after the exchange, and KEMs are broken 125 | (similar to 5) 126 | 10) DH was broken after the exchange, and a PQPK not used by the responder was compromised. 127 | (similar to 3) 128 | 11) IKb was compromised before the exchange and DH was broken after the exchange. 129 | (similar to 3) 130 | 131 | Here, we have multiple surprising cases, but which are due to the re-encapsulation confusion outlined above. When fixed, this should be simplified. 132 | 133 | ### Authentication 134 | 135 | Whenever a responder accepts (resp with or without an OPK), then, there exists an initiator that also accepted (resp with or without an OPK) with the same SPK and PQPK, unless: 136 | 1) IKA was compromised before the exchange 137 | ( allows impersonation of A) 138 | 2) A PQPK was compromised before the exchange 139 | (see re-encapsulation attack, this allows to reencapsulate and have distinct PQPK on both sides, ) 140 | 3) KEM have become broken before the exchange 141 | (similar to 2) 142 | 4) IKb has been compromised before the exchange 143 | (similuar to 2, but where IKB allows to sign a dishonnest PQPK, and then reencapsulate ct for a valide one) 144 | 5) Some SPK was compromised before the exchange 145 | (knowing SPK allows to impersonate A) 146 | 6) DH has been broken before the exchange. 147 | 148 | 149 | ### Conclusions 150 | 151 | Except for two side behaviours, we can see that secrecy does hold, even in the future where either one of DH or KEM would be broken. 152 | 153 | # Usage 154 | 155 | We use the cpp preprocessor to generate many possible scenarios from a single modeling file. 156 | 157 | One can call `./run.sh tag1 ... tagn` with the list of valid tags to verify the corresponding scenario. In the `Makefile`, we provide a few of the main interesting scenarios. 158 | 159 | The main possible tags are: 160 | - ConfuseKemEc - enables the confusion between encodeKEM and encodeEC, and activate a deticated simplified initiator query checking that the secrecy after DH breaks down here. 161 | - Reach - include the reachaility queries 162 | - SecrecyInit - Include the initiator secrecy query 163 | - SecrecyResp - Include the responder secrecy query 164 | - Authentication - Include the authentication query 165 | 166 | Some simplifying tags allow to verify simpler scenarios: 167 | - DisableNoOPK - Forces all communications to use an OPK 168 | - UnbreakableDH - Remove the potential arrival of the discrete log algo 169 | 170 | 171 | With the Makefile: 172 | - `make` sets SecrecyInit, SecrecyResp and Authentication, used to reproduce the main results. 173 | - `make reach` sets Reach, for sanity checks 174 | - `make confuseKemEc` sets ConfuseKemEc, and to simplify, also UnbreakableDH UnbreakableKEM and DisableNoOPK, enabling to get a trace for the confusion. 175 | - `make reEncaps` sets a troncated version of SecrecyResp, enabling to obtain an attack trace for the reencapsulation. (also enabling UnbreakableDH UnbreakableKEM DisableNoOPK to quicken up the search) 176 | 177 | 178 | For each scenario, timings and expected result can be found at the bottom of the file. 179 | 180 | 181 | # References 182 | 183 | [KX]Anonymity of NIST PQC Round 3 KEMs. Keita Xagawa. EuroCrypt 22. https://eprint.iacr.org/2021/1323.pdf 184 | -------------------------------------------------------------------------------- /revision1/proverif/pqxdh-model.cpp.pv: -------------------------------------------------------------------------------- 1 | (*************************************) 2 | (* 3 | 4 | See the README next to this file for details on the modeling 5 | and how to run the file. 6 | 7 | This is the ProVerif part of the analysis, see the README at the root 8 | directory for details on the join analysis. 9 | 10 | Authors: Karthikeyan Bhargavan 11 | Charlie Jacomme 12 | Franziskus Kiefer 13 | *) 14 | (*-----------------------------------*) 15 | (* A Symbolic Cryptographic Model *) 16 | (* for primitives in Sec 2.2 Cryptographic notation *) 17 | (*-----------------------------------*) 18 | 19 | (* Elliptic Curve Diffie-Hellman *) 20 | type scalar. 21 | type point. 22 | 23 | const G:point. 24 | const Gneutral:point. 25 | 26 | fun SMUL(scalar,point):point. 27 | equation forall y : scalar, z : scalar; 28 | SMUL(y, SMUL(z, G)) = SMUL(z, SMUL(y, G)). 29 | 30 | fun smul(scalar,point):point 31 | reduc forall x:scalar; 32 | smul(x,Gneutral) = Gneutral 33 | otherwise forall x:scalar, y:point; smul(x,y) = SMUL(x,y). 34 | 35 | 36 | letfun s2p(s:scalar) = SMUL(s,G). 37 | letfun dh(s:scalar,p:point) = smul(s,p). 38 | 39 | (* KEM Encapsulation *) 40 | type kempriv. 41 | type kempub. 42 | 43 | fun kempk(kempriv):kempub. 44 | fun penc(kempub,bitstring):bitstring. 45 | fun pdec(kempriv,bitstring):bitstring 46 | reduc forall sk:kempriv,m:bitstring; 47 | pdec(sk,penc(kempk(sk),m)) = m. 48 | 49 | letfun kempriv2pub(k:kempriv) = kempk(k). 50 | 51 | letfun pqkem_enc(pk:kempub) = 52 | new ss:bitstring; 53 | (penc(pk,ss),ss). 54 | 55 | letfun pqkem_dec(sk:kempriv,ct:bitstring) = 56 | pdec(sk,ct). 57 | 58 | 59 | (* Encodings for signing *) 60 | 61 | (* Here, we produce a variant where the signature of a an eliptic curve value can be confused with the signature of a KEM pkey. *) 62 | #ifdef ConfuseKemEc 63 | 64 | (* Encoding with key type confusions *) 65 | fun encodeEC(point):bitstring. 66 | fun encodeKEM(kempub):bitstring. 67 | 68 | 69 | 70 | fun ECasKEM(point):kempub [typeConverter]. 71 | 72 | (* equation for the confusion *) 73 | equation forall x:scalar; encodeKEM(ECasKEM(SMUL(x,G))) = encodeEC(SMUL(x,G)). 74 | 75 | (* equation modeling the fact that a kem based on an EC public key is insecure *) 76 | reduc forall x:scalar,k:bitstring; weakECasKEM(penc(ECasKEM(SMUL(x,G)),k)) = k. 77 | 78 | #else 79 | 80 | fun encodeEC(point):bitstring [data]. 81 | fun encodeKEM(kempub):bitstring [data]. 82 | 83 | 84 | #endif 85 | 86 | (* Bitstring manipulations *) 87 | 88 | (* Constants *) 89 | const zero: bitstring. 90 | const one: bitstring. 91 | 92 | (* A zero-filled byte sequence with length equal to the hash output length, in bytes. *) 93 | const zeroes_sha512:bitstring. 94 | 95 | (* A byte sequence containing 32 0xFF bytes if curve is curve25519 *) 96 | const ff_x25519:bitstring. 97 | 98 | (* A byte sequence containing 57 0xFF bytes if curve is curve448 *) 99 | const ff_x448:bitstring. 100 | 101 | (* The concatenation of string representations of the 4 PQXDH parameters info, curve, hash, and pqkem into a single string separated with ‘_’ such as “MyProtocol_CURVE25519_SHA-512_CRYSTALS-KYBER-1024”. *) 102 | const info_x25519_sha512_kyber1024:bitstring. 103 | 104 | (* Unambiguous concatenation, assumes that length of first element is known *) 105 | fun concatIK(point,point): bitstring [data]. 106 | 107 | fun concat(bitstring,bitstring): bitstring [data]. 108 | fun concat5(point,point,point,point,bitstring):bitstring [data]. 109 | fun concat4(point,point,point,bitstring):bitstring [data]. 110 | 111 | (* HKDF *) 112 | 113 | (* One-shot HKDF(input_key_material, salt, info) *) 114 | type symkey. 115 | fun hkdf(bitstring, bitstring, bitstring) : symkey. 116 | 117 | letfun kdf(km:bitstring) = 118 | hkdf(concat(ff_x25519,km), 119 | zeroes_sha512, 120 | info_x25519_sha512_kyber1024). 121 | 122 | 123 | (* AEAD Encryption *) 124 | type nonce. 125 | const empty_nonce:nonce. 126 | 127 | fun aead_enc(symkey,nonce,bitstring,bitstring):bitstring. 128 | fun aead_dec(symkey,nonce,bitstring,bitstring):bitstring 129 | reduc forall k:symkey,n:nonce,m:bitstring,ad:bitstring; 130 | aead_dec(k,n,aead_enc(k,n,m,ad),ad) = m. 131 | 132 | (* XEdDSA Signatures *) 133 | fun sign(scalar,bitstring,nonce):bitstring. 134 | fun verify(point,bitstring,bitstring):bool 135 | reduc forall sk:scalar,m:bitstring,n:nonce; 136 | verify(SMUL(sk,G),m,sign(sk,m,n)) = true. 137 | 138 | 139 | 140 | event isBool(bool). 141 | 142 | restriction b:bool; 143 | event(isBool(b)) ==> b=true || b=false. 144 | 145 | (*-----------------------------------*) 146 | (* PKI *) 147 | (*-----------------------------------*) 148 | 149 | (* Clients representing devices: Alice, Bob, etc. *) 150 | type client. 151 | 152 | (* Global PKI maintained by the Server, checked by Clients *) 153 | 154 | (* For each client Bob: 155 | - Bob’s curve identity key IKB *) 156 | 157 | table identity_pubkeys(client,point). 158 | 159 | 160 | (*-----------------------------------*) 161 | (* Security Model and Properties *) 162 | (*-----------------------------------*) 163 | 164 | (* A channel for the attacker *) 165 | free att:channel. 166 | 167 | (* A channel for the server, but in fact controlled by the attacker. *) 168 | 169 | free server:channel. 170 | 171 | 172 | (* An event triggered when the private keys of a client are compromised *) 173 | 174 | (* Handshake Events *) 175 | event InitDone(client,client,bool,point,point,kempub,symkey). 176 | event RespondDone(client,client,bool,point,point,kempub,symkey). 177 | 178 | (* Application Messages and Events *) 179 | fun app_message(client,client,bitstring):bitstring [private]. 180 | event AppSend(client,client,bitstring). 181 | event AppRecv(client,client,bitstring). 182 | 183 | 184 | (* Compromise Events *) 185 | event CompromiseIK(client). 186 | event CompromiseSPK(client,point). 187 | event CompromiseOPK(client,point). 188 | event CompromisePQPK(client,kempub). 189 | 190 | #ifdef Reach 191 | 192 | (* Reachability Queries *) 193 | query i:client, r:client, ts:symkey, m:bitstring, opk:point, spk:point, pqpk:kempub; 194 | event(InitDone(i,r,true,opk,spk,pqpk,ts)); 195 | event(InitDone(i,r,false,opk,spk,pqpk,ts)); 196 | event(RespondDone(r,i,true,opk,spk,pqpk,ts)); 197 | event(RespondDone(r,i,false,opk,spk,pqpk,ts)). 198 | 199 | #endif 200 | 201 | 202 | (*-----------------------------------*) 203 | (* Security Model and Properties *) 204 | (*-----------------------------------*) 205 | 206 | 207 | 208 | #ifdef SecrecyInit 209 | 210 | query a,b:client, useOPK:bool, opk,spk:point, pqpk:kempub, ts:symkey, i,j:time; 211 | event(InitDone(a,b,useOPK,opk,spk,pqpk,ts))@i && attacker(ts) ==> 212 | (* A compromise of IKB in the past is enough to break everything. *) 213 | (event(CompromiseIK(b))@j && (j < i 214 | || 215 | ( 216 | event(CompromiseSPK(b,spk)) 217 | && 218 | (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM)) 219 | )) 220 | 221 | ) 222 | || 223 | (event(BrokenDH())@j && (j < i 224 | || event(CompromisePQPK(b,pqpk)) 225 | || event(BrokenKEM) 226 | )) 227 | . 228 | 229 | 230 | #endif 231 | 232 | #ifdef SecrecyResp 233 | 234 | query a,b:client, useOPK:bool, opk,spk:point, pqpk,pqpk2:kempub, ts:symkey, i,j1,j2,j3:time; 235 | event(RespondDone(b,a, useOPK, opk,spk,pqpk,ts))@i && attacker(ts) ==> 236 | (* The compromise of IKA allows the attacker to play the role of A, and thus know the key *) 237 | (event(CompromiseIK(a))@j1 && j1 < i) 238 | || 239 | (* If IKA is not corrupted, we must corrupt some information on the side of B *) 240 | (event(CompromiseSPK(b,spk))@j1 && 241 | (* A compromise of some SPKb in the past allow an attacker to impersonate any A *) 242 | (j1 < i || 243 | ( 244 | event(CompromiseIK(b))@j2 && (j2 < i 245 | || 246 | event(CompromisePQPK(b,pqpk)) 247 | || 248 | event(BrokenKEM) 249 | || 250 | 251 | (event(CompromisePQPK(b,pqpk2))@j3 && j3 279 | (* authentication including over the opk *) 280 | (useOPK = true && event(InitDone(a,b,true, opk,spk,pqpk,ts))) 281 | || 282 | (* authentication not over the opk *) 283 | (useOPK = false && event(InitDone(a,b,false, opk2,spk,pqpk,ts))) 284 | (* Unless *) 285 | || 286 | (* The compromise of IKA allows the attacker to play the role of A, and thus know the key *) 287 | (event(CompromiseIK(a))@j && j < i) 288 | || 289 | (* this allow to decrypt ct and reencrypt it for other pq key *) 290 | (event(CompromisePQPK(b,pqpk2))@j && j 315 | (* A compromise of IKB in the past is enough to break everything. *) 316 | (event(CompromiseIK(b))@j && (j < i 317 | || 318 | ( 319 | event(CompromiseSPK(b,spk)) 320 | && 321 | (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM)) 322 | )) 323 | 324 | ) 325 | || 326 | (event(BrokenDH())@j && (j < i 327 | || event(CompromisePQPK(b,pqpk)) 328 | || event(BrokenKEM) 329 | )) 330 | . 331 | 332 | 333 | #endif 334 | 335 | 336 | 337 | 338 | 339 | #ifdef ReEncaps 340 | 341 | (* Here, we put the authentication security property that we in fact 342 | would like to be able to prove. However, due to the possible 343 | rencapsulation attacks, ProVerif falsifies this query This 344 | demonstrates that we do not have session independance, and that if 345 | the KEM is insecure, we in fact have lower guarantees with it than 346 | without it. We are thus forced here to prove in Authentication and 347 | RespSecrecy weaker properties. *) 348 | 349 | 350 | query a,b:client, useOPK:bool, opk,opk2,spk:point, pqpk:kempub, ts:symkey, i,j:time; 351 | event(RespondDone(b,a,useOPK,opk,spk,pqpk,ts))@i ==> 352 | (* authentication including over the opk *) 353 | (useOPK = true && event(InitDone(a,b,true, opk,spk,pqpk,ts))) 354 | || 355 | (* authentication not over the opk *) 356 | (useOPK = false && event(InitDone(a,b,false, opk2,spk,pqpk,ts))) 357 | (* Unless *) 358 | || 359 | (* The compromise of IKA allows the attacker to play the role of A, and thus know the key *) 360 | (event(CompromiseIK(a))@j && j < i) 361 | || 362 | (* Knowing a SPK completely allows to break auth *) 363 | (event(CompromiseSPK(b,spk))@j && j < i ) 364 | || 365 | (event(BrokenDH())@j && j < i) 366 | . 367 | 368 | 369 | 370 | #endif 371 | 372 | 373 | nounif z:scalar; attacker(SMUL(z,G)) / 30000. 374 | 375 | 376 | (* DH breaks down *) 377 | fun discreteLog(point): scalar 378 | reduc forall s:scalar, y:point; 379 | discreteLog(SMUL(s,G)) = s [private]. (* Remark, this dL does not work on a g^xy model *) 380 | 381 | event BrokenDH. 382 | 383 | let dh_attacks() = 384 | in (att,p:point); 385 | event BrokenDH; 386 | out (att,discreteLog(p)). 387 | 388 | 389 | 390 | (* KEM breaks down *) 391 | fun inversePK(kempub): kempriv 392 | reduc forall s:kempriv; 393 | inversePK(kempk(s)) = s [private]. 394 | 395 | event BrokenKEM. 396 | 397 | let kem_attacks() = 398 | in (att,p:kempub); 399 | event BrokenKEM; 400 | out (att,inversePK(p)). 401 | 402 | 403 | (*-----------------------------------*) 404 | (* Protocol Processes *) 405 | (*-----------------------------------*) 406 | 407 | (* PQXDH Protocol *) 408 | 409 | (* Alice then sends Bob an initial message containing: 410 | - Alice’s identity key IKA 411 | - Alice’s ephemeral key EKA 412 | - The pqkem ciphertext CT encapsulating SS for PQPKB 413 | - Identifiers stating which of Bob’s prekeys Alice used 414 | - An initial ciphertext encrypted with some AEAD encryption scheme [5] using AD as associated data and using an encryption key which is either SK or the output from some cryptographic PRF keyed by SK. 415 | *) 416 | 417 | const init:bitstring. 418 | const resp:bitstring. 419 | 420 | 421 | let Initiator(i:client, IKA_s:scalar) = 422 | (* we let the attacker choose the responder we will communicate with *) 423 | in(att,r:client); 424 | (* if not(r=i) then *) 425 | (* Initiator public key *) 426 | let IKA_p = s2p(IKA_s) in 427 | 428 | (* Retrieve the responder identity key *) 429 | get identity_pubkeys(=r,IKB_p) in 430 | 431 | (* receive from the server the pre key bundles *) 432 | in(server, (SPKB_p:point,SPKB_sig:bitstring,PQPKB_p:kempub,PQPKB_sig:bitstring)); 433 | (* Verify the signatures *) 434 | if verify(IKB_p,encodeEC(SPKB_p),SPKB_sig) then 435 | if verify(IKB_p,encodeKEM(PQPKB_p),PQPKB_sig) then (* Here, we don't know whether we have a last resort or one time key. *) 436 | 437 | ( 438 | (* Optionally receive a one time key *) 439 | in (server,(useOPK:bool,OPKB_p:point)); 440 | event isBool(useOPK); 441 | let (CT:bitstring,SS:bitstring) = pqkem_enc(PQPKB_p) in 442 | 443 | new EKA_s:scalar; 444 | 445 | let EKA_p = s2p(EKA_s) in 446 | let DH1 = dh(IKA_s,SPKB_p) in 447 | let DH2 = dh(EKA_s,IKB_p) in 448 | let DH3 = dh(EKA_s,SPKB_p) in 449 | 450 | let SK = 451 | (if useOPK then (let DH4 = dh(EKA_s,OPKB_p) in kdf(concat5(DH1,DH2,DH3,DH4,SS))) 452 | else kdf(concat4(DH1,DH2,DH3,SS))) 453 | in 454 | 455 | event InitDone(i,r,useOPK,OPKB_p,SPKB_p,PQPKB_p,SK); 456 | 457 | (* Removing from the ad the PQPKB_p reintroduces the re-encapsulation attack *) 458 | let ad = concatIK(IKA_p,IKB_p) in 459 | new msg_nonce: bitstring; 460 | let msg = app_message(i,r,msg_nonce) in 461 | let enc_msg = aead_enc(SK,empty_nonce,msg,ad) in 462 | (* Send Message *) 463 | out(server, (IKA_p,EKA_p,CT, OPKB_p, SPKB_p, PQPKB_p, enc_msg)) 464 | 465 | ). 466 | 467 | 468 | let Responder_mess(r:client, IKB_s:scalar, SPKB_s:scalar,PQSPKB_s:kempriv)= 469 | ( 470 | let IKB_p = s2p(IKB_s) in 471 | let SPKB_p = s2p(SPKB_s) in 472 | let PQSPKB_p = kempriv2pub(PQSPKB_s) in 473 | 474 | (* Generate a new one time OPKB *) 475 | new OPKB_s:scalar; 476 | let OPKB_p = s2p(OPKB_s) in 477 | 478 | (* send public keys to server *) 479 | out (server,OPKB_p ); 480 | 481 | (in(att, =r); event CompromiseOPK(r, OPKB_p); out(att,OPKB_s)) 482 | | 483 | 484 | 485 | (* Receive Message with the currently stored public keys *) 486 | ( 487 | in (server,(IKA_p:point,EKA_p:point,CT:bitstring,useOPK:bool,=OPKB_p,=SPKB_p,=PQSPKB_p, enc_msg:bitstring)); 488 | event isBool(useOPK); 489 | (* Verify remote identity key *) 490 | get identity_pubkeys(i,=IKA_p) in 491 | (* if not(r=i) then *) 492 | ( 493 | (* Retrieve one-time private keys *) 494 | 495 | let SS = pqkem_dec(PQSPKB_s,CT) in 496 | let DH1 = dh(SPKB_s,IKA_p) in 497 | let DH2 = dh(IKB_s, EKA_p) in 498 | let DH3 = dh(SPKB_s,EKA_p) in 499 | let SK = 500 | (if useOPK then (let DH4 = dh(OPKB_s,EKA_p) in kdf(concat5(DH1,DH2,DH3,DH4,SS))) 501 | else kdf(concat4(DH1,DH2,DH3,SS))) 502 | in 503 | 504 | (* Removing from the ad the PQPKB_p reintroduces the re-encapsulation attack *) 505 | let ad = concatIK(IKA_p,IKB_p) in 506 | let msg = aead_dec(SK,empty_nonce,enc_msg,ad) in 507 | event RespondDone(r,i,useOPK,OPKB_p,SPKB_p,PQSPKB_p,SK) 508 | 509 | ) 510 | ) 511 | ). 512 | 513 | 514 | let Responder(r:client, IKB_s:scalar) = 515 | let IKB_p = s2p(IKB_s) in 516 | 517 | (* Creates a new DH based Signed Pre-Key *) 518 | new SPKB_s:scalar; 519 | let SPKB_p = s2p(SPKB_s) in 520 | new zSPKB:nonce; 521 | let SPKB_sig = sign(IKB_s,encodeEC(SPKB_p),zSPKB) in 522 | 523 | (* Creates a new KEM based Signed Pre-Key *) 524 | new PQSPKB_s:kempriv; 525 | let PQSPKB_p = kempriv2pub(PQSPKB_s) in 526 | new zPQSPKB:nonce; 527 | let PQSPKB_sig = sign(IKB_s,encodeKEM(PQSPKB_p),zPQSPKB) in 528 | 529 | 530 | (* send public keys to server *) 531 | out (att,(SPKB_p,SPKB_sig,PQSPKB_p,PQSPKB_sig)); 532 | 533 | (in(att, =r); event CompromiseSPK(r, SPKB_p); out(att,SPKB_s)) 534 | | 535 | (in(att, =r); event CompromisePQPK(r, PQSPKB_p); out(att,PQSPKB_s)) 536 | | 537 | (! Responder_mess(r, IKB_s, SPKB_s,PQSPKB_s)) 538 | . 539 | 540 | 541 | (* A process to create a new client and publish its keys *) 542 | let Launch_pqxdh_client(p:client) = 543 | (* Create a new client *) 544 | new IK_s:scalar; 545 | let IK_p = s2p(IK_s) in 546 | insert identity_pubkeys(p,IK_p); 547 | (* Publish the public key identity pair. *) 548 | out(att,IK_p); 549 | ( 550 | (* Initiator role *) 551 | ! Initiator(p, IK_s) 552 | (* Responder role *) 553 | | ! Responder(p, IK_s) 554 | 555 | | 556 | (* Compromise of the identity key *) 557 | (in(att, =p); event CompromiseIK(p); out(att,IK_s)) 558 | ). 559 | 560 | 561 | 562 | 563 | (* Main Process: any number of clients and members *) 564 | 565 | process 566 | ! in(att,c:client); Launch_pqxdh_client(c) 567 | #if !defined(UnbreakableDH) 568 | | ! dh_attacks 569 | #endif 570 | #if !defined(UnbreakableKEM) 571 | | ! kem_attacks 572 | #endif 573 | 574 | 575 | 576 | (*************************************) 577 | (* EXPECTED RESULTS *) 578 | (*************************************) 579 | 580 | (* 581 | 582 | $ make reach 583 | 584 | -------------------------------------------------------------- 585 | Verification summary: 586 | 587 | Query not event(InitDone(i_2,r_1,true,opk,spk,pqpk,ts)) is false. 588 | 589 | Query not event(InitDone(i_2,r_1,false,opk,spk,pqpk,ts)) is false. 590 | 591 | Query not event(RespondDone(r_1,i_2,true,opk,spk,pqpk,ts)) is false. 592 | 593 | Query not event(RespondDone(r_1,i_2,false,opk,spk,pqpk,ts)) is false. 594 | 595 | -------------------------------------------------------------- 596 | 597 | 598 | real 0m0,9 599 | 600 | *) 601 | 602 | (* 603 | -------------------------------------------------------------- 604 | Verification summary: 605 | 606 | Query(ies): 607 | - Query event(InitDone(a,b,useOPK_2,opk,spk,pqpk,ts))@i_1 && attacker(ts) ==> (event(CompromiseIK(b))@j && (i_1 > j || (event(CompromiseSPK(b,spk)) && (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))))) || (event(BrokenDH)@j && (i_1 > j || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))) is true. 608 | - Query event(RespondDone(b,a,useOPK_2,opk,spk,pqpk,ts))@i_1 && attacker(ts) ==> (event(CompromiseIK(a))@j1 && i_1 > j1) || (event(CompromiseSPK(b,spk))@j1 && (i_1 > j1 || (event(CompromiseIK(b))@j2 && (i_1 > j2 || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM) || (event(CompromisePQPK(b,pqpk2))@j3 && i_1 > j3)) && (useOPK_2 = false || event(CompromiseOPK(b,opk)))))) || (event(BrokenDH)@j1 && (i_1 > j1 || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM) || (event(CompromisePQPK(b,pqpk2))@j2 && i_1 > j2) || (event(CompromiseIK(b))@j2 && i_1 > j2))) is true. 609 | - Query event(RespondDone(b,a,useOPK_2,opk,spk,pqpk,ts))@i_1 ==> (useOPK_2 = true && event(InitDone(a,b,true,opk,spk,pqpk,ts))) || (useOPK_2 = false && event(InitDone(a,b,false,opk2,spk,pqpk,ts))) || (event(CompromiseIK(a))@j && i_1 > j) || (event(CompromisePQPK(b,pqpk2))@j && i_1 > j) || (event(BrokenKEM)@j && i_1 > j) || (event(CompromiseIK(b))@j && i_1 > j) || (event(CompromiseSPK(b,spk))@j && i_1 > j) || (event(BrokenDH)@j && i_1 > j) is true. 610 | Associated restriction(s): 611 | - Restriction event(isBool(b)) ==> b = true || b = false encoded as event(isBool(b)) ==> b = true || b = false in process 1. 612 | 613 | -------------------------------------------------------------- 614 | 615 | 616 | real 3m25,305s 617 | *) 618 | 619 | (* 620 | -------------------------------------------------------------- 621 | Verification summary: 622 | 623 | Query(ies): 624 | - Query event(InitDone(a,b,useOPK_2,opk,spk,pqpk,ts))@i_1 && attacker(ts) ==> (event(CompromiseIK(b))@j && (i_1 > j || (event(CompromiseSPK(b,spk)) && (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))))) || (event(BrokenDH)@j && (i_1 > j || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))) cannot be proved. 625 | Associated restriction(s): 626 | - Restriction event(isBool(b)) ==> b = true || b = false encoded as event(isBool(b)) ==> b = true || b = false in process 1. 627 | 628 | -------------------------------------------------------------- 629 | 630 | 631 | real 0m11,823s 632 | 633 | 634 | Remark: cannot be proved, but only because the trace obtained by ProVerif does the CompromiseIK unecessarily early. 635 | *) 636 | 637 | (* 638 | -------------------------------------------------------------- 639 | Verification summary: 640 | 641 | Query(ies): 642 | - Query event(RespondDone(b,a,useOPK_2,opk,spk,pqpk,ts))@i_1 ==> (useOPK_2 = true && event(InitDone(a,b,true,opk,spk,pqpk,ts))) || (useOPK_2 = false && event(InitDone(a,b,false,opk2,spk,pqpk,ts))) || (event(CompromiseIK(a))@j && i_1 > j) || (event(CompromiseSPK(b,spk))@j && i_1 > j) || (event(BrokenDH)@j && i_1 > j) is false. 643 | Associated restriction(s): 644 | - Restriction event(isBool(b)) ==> b = true || b = false encoded as event(isBool(b)) ==> b = true || b = false in process 1. 645 | 646 | -------------------------------------------------------------- 647 | 648 | 649 | real 0m16,570s 650 | *) 651 | 652 | -------------------------------------------------------------------------------- /revision1/proverif/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run the C preprocessor on the input file, produces a .pv file and runs proverif on it 3 | 4 | res="" 5 | name="" 6 | for def in ${@:2} 7 | do 8 | res+=" --define-macro=$def=$def" 9 | name+="$def" 10 | done 11 | if [ "$name" = "" ]; then 12 | file="$1.gen.pv" 13 | else 14 | file="$1-$name.gen.pv" 15 | fi 16 | eval "( 17 | echo \"(* !!! WARNING !!! *)\"; 18 | echo \"(* File generated from with ./run.sh $@*)\"; 19 | echo \"(* Read the README for more informations *)\"; 20 | echo \"(* ------------------------------------- *)\"; 21 | cpp -P -E -w $res $1.cpp.pv; 22 | ) > $file" 23 | time proverif $file 24 | -------------------------------------------------------------------------------- /revision2/cryptoverif/.gitignore: -------------------------------------------------------------------------------- 1 | _models/* -------------------------------------------------------------------------------- /revision2/cryptoverif/Makefile: -------------------------------------------------------------------------------- 1 | MAIN = PQXDH 2 | 3 | dh: 4 | m4 -D DH $(MAIN).m4.ocv > _models/$(MAIN).DH.ocv 5 | time cryptoverif _models/$(MAIN).DH.ocv 6 | 7 | kem: 8 | m4 -D KEM $(MAIN).m4.ocv > _models/$(MAIN).KEM.ocv 9 | time cryptoverif _models/$(MAIN).KEM.ocv 10 | -------------------------------------------------------------------------------- /revision2/cryptoverif/PQXDH.m4.ocv: -------------------------------------------------------------------------------- 1 | (* 2 | 3 | This files models the PQXDH protocol, as specified in: 4 | 5 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 6 | The PQXDH Key Agreement Protocol 7 | Revision 2, 8 | 9 | See the README next to this file for details on the modeling and how 10 | to run the file. 11 | 12 | This is the CryptoVerif part of the analysis, see the README at the 13 | root directory for details on the joint analysis. 14 | 15 | Authors: Charlie Jacomme 16 | 17 | Based on previous textsecure models by Bruno Blanchet. 18 | 19 | *) 20 | 21 | set useKnownEqualitiesWithFunctionsInMatching = true. 22 | 23 | 24 | 25 | 26 | 27 | ifdef(`KEM',` 28 | 29 | 30 | (* The proof instructions needed to guide CryptoVerif in the KEM case. *) 31 | proof { 32 | 33 | crypto uf_cma_corrupt(sign) signAseed; 34 | out_game "g1.cv" occ; 35 | 36 | insert before "EKSecA1 <-R Z" "find j <= Nrecv, k <= Nidentity suchthat defined (seed[j,k], signAseed[k], IKA[k], PQPKB[j,k]) && x_IKBsign = pkgen2(signAseed[k]) && PQPKPubB = PQPKB[j,k] then"; 37 | 38 | SArename CT_2; 39 | 40 | out_game "g11.cv" occ; 41 | 42 | insert after "RecvOPK(" "find u2 <= NsendOPK suchthat defined(CT_3[u2],PQPKPubB[u2]) && CT_3[u2] = CT_1 && PQPKB = PQPKPubB[u2] then"; 43 | insert after "RecvOPK(" "find u1 <= Nidentity suchthat defined(signAseed[u1], IKA[u1]) && 44 | x_IKAsign = pkgen2(signAseed[u1]) then if defined(corrupted_2[u1]) then"; 45 | 46 | insert after "kseed_4 <-R" "let fencap = kempair(kem_secret(PQPKPubB,kseed_4),kem_encap(PQPKPubB,kseed_4)) in"; 47 | out_game "g3.cv" occ; 48 | 49 | replace at_nth 1 1 "SS: kemsec <- {[0-9]+}" "get_secret(fencap_2)"; 50 | replace at_nth 1 1 "CT_3: ciphertext <- {[0-9]+}" "get_encap(fencap_2)"; 51 | 52 | crypto ind_cca(Encap) [variables: seed -> seed, kseed_4 -> kseed_1 .]; 53 | 54 | out_game "g31.cv" occ; 55 | 56 | insert before "EKSecA1p <-R Z" "find j2 <= Nrecv, k2 <= Nidentity suchthat defined (seed_1[j2,k2], signAseed[k2], IKA[k2], PQPKB[j2,k2]) && x_IKBsignp = pkgen2(signAseed[k2]) && PQPKPubBp = PQPKB[j2,k2] then"; 57 | 58 | SArename CTp_2; 59 | 60 | out_game "g32.cv" occ; 61 | 62 | insert after "RecvNoOPK(" "find u22 <= NsendOPK suchthat defined(CT_3[u22],PQPKPubB[u22]) && CT_3[u22] = CTp_1 && PQPKB = PQPKPubB[u22] then"; 63 | insert after "RecvNoOPK(" "find u12 <= Nidentity suchthat defined(signAseed[u12], IKA[u12]) && 64 | x_IKAsignp = pkgen2(signAseed[u12]) then if defined(corrupted_2[u12]) then"; 65 | 66 | 67 | insert after "kseedp_2 <-R" "let fencap = kempair(kem_secret(PQPKPubBp,kseedp_2),kem_encap(PQPKPubBp,kseedp_2)) in"; 68 | out_game "g33.cv"; 69 | replace at_nth 1 1 "SSp: kemsec <- {[0-9]+}" "get_secret(fencap_3)"; 70 | replace at_nth 1 1 "CTp_3: ciphertext <- {[0-9]+}" "get_encap(fencap_3)"; 71 | 72 | 73 | crypto ind_cca(Encap) [variables: seed_1 -> seed, kseedp_2 -> kseed_1.]; 74 | 75 | crypto prf(H) *; 76 | out_game "g4.cv" ; 77 | 78 | crypto int_ctxt(enc) *; 79 | crypto ind_cpa(enc) **; 80 | out_game "g5.cv"; 81 | success 82 | } 83 | 84 | ',`') 85 | 86 | 87 | ifdef(`DH',` 88 | 89 | 90 | (* The proof instructions needed to guide CryptoVerif in the DH case. *) 91 | proof { 92 | crypto uf_cma_corrupt(sign) signAseed; 93 | out_game "g1.cv" occ; 94 | 95 | insert before "EKSecA1 <-R Z" "find j <= Nrecv, k <= Nidentity suchthat defined (SPKPubB1[j,k], IKA[k]) && pow8(SPKPubB) = pow8(SPKPubB1[j,k]) && pow8(x_IKB) = pow8(IKA[k]) then"; 96 | insert after "RecvOPK(" "find u1 <= Nidentity suchthat defined(signAseed[u1], IKA[u1]) && pow8(x_IKA) = pow8(IKA[u1]) then if defined(corrupted_1[u1]) then"; 97 | 98 | out_game "g11.cv" occ; 99 | insert after "OH_1(" "let (subGtoG(x1p), subGtoG(x2p), subGtoG(x3p), subGtoG(x4p), x5p : kemsec) = (x1_1, x2_1, x3_1, x4_1, x5) in"; 100 | crypto rom(H2); 101 | 102 | out_game "g2.cv" occ; 103 | insert before "EKSecA1p <-R Z" "find j2 <= Nrecv, k2 <= Nidentity suchthat defined (SPKPubB1[j2,k2], IKA[k2]) && pow8(SPKPubBp) = pow8(SPKPubB1[j2,k2]) && pow8(x_IKBp) = pow8(IKA[k2]) then"; 104 | insert after "RecvNoOPK(" "find u2 <= Nidentity suchthat defined(signAseed[u2], IKA[u2]) && pow8(x_IKAp) = pow8(IKA[u2]) then if defined(corrupted_1[u2]) then"; 105 | 106 | 107 | out_game "g12.cv"occ; 108 | 109 | insert after "OH(" "let (subGtoG(x1_1p), subGtoG(x2_1p), subGtoG(x3_1p), x4_1p : kemsec) = (x1, x2, x3, x4) in"; 110 | crypto rom(H1); 111 | 112 | out_game "g3.cv"; 113 | 114 | 115 | crypto gdh(gexp_div_8) [variables: secIKA0 -> a, SPKSecB1 -> a, OPKSecB1 -> a, EKSecA1 -> a, EKSecA1p -> a .]; 116 | 117 | crypto int_ctxt(enc) *; 118 | crypto ind_cpa(enc) **; 119 | out_game "g4.cv"; 120 | crypto int_ctxt_corrupt(enc) r_23; 121 | crypto int_ctxt_corrupt(enc) r_50; 122 | success 123 | } 124 | 125 | 126 | ',`') 127 | 128 | 129 | 130 | 131 | (* KEM definitions *) 132 | type kempkey [bounded]. 133 | type kemskey [bounded]. 134 | type ciphertext. 135 | type kem_seed [large,fixed]. 136 | type kem_enc_seed [large,fixed]. 137 | 138 | type kemsec [fixed]. 139 | 140 | fun kempkgen(kem_seed):kempkey. 141 | fun kemskgen(kem_seed):kemskey. 142 | 143 | fun decap(ciphertext, kemskey): kemsec. 144 | 145 | fun kem_secret(kempkey, kem_enc_seed) : kemsec. 146 | fun kem_encap(kempkey, kem_enc_seed): ciphertext. 147 | 148 | type encaps_return. 149 | fun kempair(kemsec,ciphertext) : encaps_return [data]. 150 | 151 | letfun encaps(pk : kempkey, kseed : kem_enc_seed) = 152 | kempair(kem_secret(pk,kseed ), kem_encap(pk,kseed)). 153 | 154 | equation forall kseed: kem_seed, seed:kem_enc_seed; 155 | decap( kem_encap( kempkgen(kseed), seed), kemskgen(kseed)) = kem_secret( kempkgen(kseed),seed). 156 | 157 | fun get_encap(encaps_return) : ciphertext. 158 | fun get_secret(encaps_return) : kemsec. 159 | 160 | equation forall c:ciphertext, s:kemsec; 161 | get_encap( kempair(s,c)) = c. 162 | 163 | equation forall c:ciphertext, s:kemsec; 164 | get_secret(kempair( s,c))= s. 165 | 166 | ifdef(`KEM',` 167 | 168 | (* KEM security assumptions -> IND-CCA *) 169 | 170 | 171 | param Nc, Qeperuser, Qdperuser. 172 | 173 | proba CCA. 174 | 175 | table E(Nc, ciphertext, kemsec). 176 | 177 | equiv(ind_cca(Encap)) 178 | 179 | foreach i <= Nc do seed <-R kem_seed; ( 180 | Opk() := return(kempkgen(seed)) 181 | | 182 | foreach id <= Qdperuser do 183 | OADecap(enc: ciphertext) [useful_change] := 184 | return(decap(enc, kemskgen(seed))) 185 | ) 186 | | 187 | foreach ie <= Qeperuser do 188 | kseed <-R kem_enc_seed; ( 189 | 190 | OE(pk_R:kempkey) [useful_change] := return( encaps(pk_R, kseed) ) 191 | 192 | ) 193 | <=(CCA(time, Nc, #OE, #OADecap))=> 194 | foreach i <= Nc do seed <-R kem_seed; ( 195 | Opk() := return(kempkgen(seed)) | 196 | foreach id <= Qdperuser do ( 197 | OADecap(cd: ciphertext) := 198 | get E(=i, =cd, k2) in ( 199 | return(k2) 200 | ) else ( 201 | return(decap(cd, kemskgen(seed))) 202 | 203 | )) ) 204 | | 205 | foreach ie <= Qeperuser do 206 | kseed <-R kem_enc_seed; 207 | ( 208 | OE(pk_R: kempkey) := 209 | find i2 <= Nc suchthat defined(seed[i2]) && pk_R = kempkgen(seed[i2]) then ( 210 | k1 <-R kemsec; 211 | insert E(i2, kem_encap(pk_R, kseed) , k1); 212 | return( kempair(k1, kem_encap(pk_R, kseed))) 213 | ) else ( 214 | return(encaps(pk_R, kseed) ) 215 | ) 216 | 217 | ) 218 | 219 | 220 | . 221 | ',`') 222 | 223 | 224 | (* We always have some basic properties oj the KEM, e.g., public keys 225 | are not independent of their seed. *) 226 | 227 | proba KEMcoll1. 228 | proba KEMcoll2. 229 | 230 | collision r <-R kem_seed; forall Y: kempkey; 231 | return(kempkgen(r) = Y) <=(KEMcoll1)=> return(false) if Y independent-of r. 232 | 233 | 234 | collision r <-R kem_seed; k <-R kem_enc_seed; forall Y: ciphertext; 235 | return(kem_encap(kempkgen(r),k) = Y) <=(KEMcoll2)=> return(false) if Y independent-of k. 236 | 237 | 238 | 239 | 240 | (* DH definitions *) 241 | type emkey [fixed,large]. 242 | 243 | type Z [bounded,large,nonuniform]. (* Exponents *) 244 | type G [bounded,large,nonuniform]. (* Diffie-Hellman group *) 245 | type subG [bounded,large,nonuniform]. (* Diffie-Hellman group *) 246 | 247 | (* Gap Diffie-Hellman *) 248 | (* In the PQ setting, we only assume the informatic theoritic collision properties *) 249 | 250 | (* Note: the secret keys in Signal are really normalized to be multiples of k, 251 | as specified in RFC 7748. The normalization is commented out in the exponentiation 252 | function: 253 | https://github.com/signalapp/libsignal-protocol-javascript/blob/f5a838f1ccc9bddb5e93b899a63de2dea9670e10/native/curve25519-donna.c/#L860 254 | but done when generating a key pair: 255 | https://github.com/signalapp/libsignal-protocol-javascript/blob/f5a838f1ccc9bddb5e93b899a63de2dea9670e10/src/curve25519_wrapper.js#L25 256 | *) 257 | 258 | expand DH_X25519(G, Z, g, gexp, mult, subG, g_8, gexp_div_8, gexp_div_8p, pow8, subGtoG, is_zero_G, is_zero_subG). 259 | 260 | 261 | ifdef(`DH',` 262 | 263 | (* We now make the gapDH assumption. *) 264 | proba psqGDH. 265 | proba pDistRerandom. 266 | expand square_GDH_RSR(subG, Z, g_8, gexp_div_8, gexp_div_8p, mult, psqGDH, pDistRerandom). 267 | 268 | ',`') 269 | 270 | 271 | (* Key derivation *) 272 | 273 | ifdef(`KEM',` 274 | 275 | 276 | (* We model the kdf as a prf function, which is keyed by the KEM shared secret. *) 277 | fun H(bitstring,kemsec): emkey. 278 | 279 | proba Pprf. 280 | equiv(prf(H)) special prf("key_last", H, Pprf, (k, r, x, y, z, u)). 281 | 282 | equiv(prf_partial(H)) special prf_partial("key_last", H, Pprf, (k, r, x, y, z, u)) [manual]. 283 | 284 | 285 | fun c3(G,G,G):bitstring. 286 | fun c4(G,G,G,G):bitstring. 287 | 288 | letfun H4(g1:G,g2:G,g3:G, k:kemsec) = H(c3(g1,g2,g3),k). 289 | letfun H5(g1:G,g2:G,g3:G,g4:G, k:kemsec) = H(c4(g1,g2,g3,g4),k). 290 | 291 | equation forall g1:G, g2:G, g3:G, g4:G; 292 | c3(g1,g2,g3) <> c4(g1,g2,g3,g4). 293 | 294 | 295 | ',`') 296 | 297 | 298 | ifdef(`DH',` 299 | 300 | 301 | (* we model the kdf a Random Oracles. *) 302 | 303 | type hashkey [large,fixed]. (* unused in PQ setting *) 304 | type hashkey2 [large,fixed]. (* unused in PQ setting *) 305 | 306 | expand ROM_hash_large_4(hashkey, G, G, G, kemsec, emkey, H1, hashoracle, qH2). 307 | expand ROM_hash_large_5(hashkey2, G, G ,G ,G, kemsec, emkey, H2, hashoracle2, qH3). 308 | 309 | letfun H4(g1:G,g2:G,g3:G, k:kemsec,hk:hashkey) = H1(hk,g1,g2,g3,k). 310 | 311 | letfun H5(g1:G,g2:G,g3:G,g4:G, k:kemsec, hk:hashkey2) = H2(hk,g1,g2,g3,g4,k). 312 | 313 | 314 | ',`') 315 | 316 | 317 | 318 | 319 | (* Signatures *) 320 | 321 | 322 | type keyseed [large, fixed]. 323 | type pkey [bounded]. 324 | type skey [bounded]. 325 | type t_sign. 326 | 327 | 328 | proba Psign. 329 | proba Psigncoll. 330 | expand UF_CMA_proba_signature(keyseed, pkey, skey, bitstring, t_sign, skgen, pkgen, sign, checksign, Psign, Psigncoll). 331 | 332 | (* Encoding of public keys for signatures *) 333 | 334 | fun encodeEC(G) : bitstring [data]. 335 | fun encodeKEM(kempkey) : bitstring [data]. 336 | 337 | 338 | letfun signKEM(pk:kempkey,sk:skey) = sign(encodeKEM(pk),sk). 339 | letfun checksignKEM(m:kempkey, p:pkey, s:t_sign) = checksign(encodeKEM(m),p,s). 340 | 341 | 342 | letfun signEC(el:G,sk:skey) = sign(encodeEC(el),sk). 343 | letfun checksignEC(m:G, p:pkey,s:t_sign) = checksign(encodeEC(m),p,s). 344 | 345 | 346 | (* We rely here on the assumption made in the spec that the encodings of public keys are disjoints. 347 | This is both stated explicitly in [PQXDH], and verified in the implementation, with the encodings having a unique one byte prefix: 348 | 349 | (curve25519 |-> 0x05, curve448 |-> 0x06, Kyber768 |-> 0x07, Kyber1024 |-> 0x08) 350 | 351 | This correspond to the KeyType field of libsignal, as defined here for KEMs 352 | https://github.com/signalapp/libsignal/blob/d1f9dff273e6da059af699c6afe860fb93406032/rust/protocol/src/kem.rs#L153 353 | and here for curve 25519: 354 | https://github.com/signalapp/libsignal/blob/d1f9dff273e6da059af699c6afe860fb93406032/rust/protocol/src/curve.rs#L33 355 | 356 | *) 357 | 358 | 359 | equation forall pkdh:G, pkkem:kempkey; 360 | encodeEC(pkdh) <> encodeKEM(pkkem). 361 | 362 | (* AEAD *) 363 | 364 | type t_data. 365 | proba Penc. 366 | proba Pencctxt. 367 | 368 | 369 | (* We assume IND-CPA + INT-CTXT for the AEAD, in both cases. *) 370 | expand AEAD(emkey, bitstring, bitstring, t_data, enc, dec, injbot, Zero, Penc, Pencctxt). 371 | 372 | const const1: bitstring. 373 | fun concatAD(G,pkey,G,pkey,kempkey):t_data [data]. 374 | 375 | 376 | param Nidentity, Nrecv, NsendOPK, NsendNoOPK, Nsignedprekey, Nsignedprekey2. 377 | 378 | (* Table of keys *) 379 | table keys(Z, G, skey, pkey). 380 | (* Table of keys of corrupted participants *) 381 | table corrupted(G,pkey). 382 | 383 | 384 | (* Security properties *) 385 | 386 | event SendWithOPK(G, pkey, G, pkey, G, G, G, kempkey, bitstring). 387 | event RecvWithOPK(bool, G, pkey, G, pkey, G, G, G, kempkey, bitstring). 388 | event SendWithoutOPK(G, pkey, G, pkey, G, G, kempkey, bitstring). 389 | event RecvWithoutOPK(bool, G, pkey, G, pkey, G, G, kempkey, bitstring). 390 | (* Arguments of events 391 | - for RecvWithOPK/RecvWithoutOPK: a boolean true when Blake is corrupted 392 | - public keys of sender (DH and signature), IKA and IKAsign 393 | - public keys of receiver (DH and signature), IKB and IKBsign 394 | - signed ephemeral, SPKB. 395 | - one-time ephemeral [optional], OPK, 396 | - sender first ephemeral, EPK, 397 | - the signed kem public key, PQPK, 398 | - sent message 399 | *) 400 | 401 | ifdef(`DH',` 402 | 403 | query Bcorrupted:bool,a0:G,as:pkey,b0:G,bs:pkey,sb:G,sb2:G,ob:G,a1:G,ob2:G,a12:G,pk1:kempkey,m:bitstring; 404 | inj-event(RecvWithOPK(Bcorrupted,a0,as,b0,bs,sb,ob,a1,pk1,m)) ==> inj-event(SendWithOPK(a0,as,b0,bs,sb2,ob2,a12,pk1,m)) && pow8(ob) = pow8(ob2) && pow8(a1) = pow8(a12) && pow8(sb) = pow8(sb2) && (Bcorrupted || sb = sb2) 405 | public_vars secb. 406 | query Bcorrupted:bool,a0:G,as:pkey,b0:G,bs:pkey,sb:G,sb2:G,a1:G,a12:G,pk1:kempkey,m:bitstring; 407 | event(RecvWithoutOPK(Bcorrupted,a0,as,b0,bs,sb,a1,pk1,m)) ==> event(SendWithoutOPK(a0,as,b0,bs,sb2,a12,pk1,m)) && pow8(a1) = pow8(a12) && pow8(sb) = pow8(sb2) && (Bcorrupted || sb = sb2) 408 | public_vars secb. 409 | 410 | (* Blake receives => Alex sent is proved provided Alex is not corrupted 411 | (event Recv/RecvWithoutOPK is executed when Alex is not corrupted). 412 | That proves KCI resistance against the compromise of long-term keys. 413 | . 414 | We cannot prove that sb = sb2 when Blake signature key is 415 | compromised. The adversary can then forge a signature of the signed 416 | ephemeral sb. The Diffie-Hellman key exchange just guarantees that 417 | pow8(sb) = pow8(sb2). 418 | 419 | To note, we cannot prove that the two parties agree on the KEM public key used. 420 | 421 | *) 422 | 423 | 424 | ',`') 425 | 426 | 427 | (* Identifiers for public keys *) 428 | 429 | type ids. 430 | 431 | fun idPKDH(G):ids. 432 | fun idPKKEM(kempkey):ids. 433 | 434 | query secret secb [cv_bit]. 435 | 436 | (* The secrecy of secb shows the secrecy of the message sent by Alex to Blake, 437 | provided Blake is not corrupted yet when Alex send the message (secb is used 438 | to choose between 2 messages only when Blake is not corrupted). That 439 | shows in particular forward secrecy. *) 440 | 441 | 442 | (********************) 443 | (**** INITIATOR *****) 444 | (********************) 445 | 446 | (* Alex using prekeys and sending a message to a participant (Blake or other). 447 | The received x_IKB:G, x_IKBsign:pkey choose Alex's interlocutor. 448 | This sender uses an optional OPK. 449 | *) 450 | (* section 3.3 *) 451 | let SendInitialWithOPK(secb1:bool,secIKA:Z , IKA:G, secIKAsign:skey, IKAsign:pkey 452 | ifdef(`DH',`, hk2:hashkey2',`') 453 | ) 454 | = 455 | (* Key exchange + send message m1 or m2 *) 456 | SendFirstMessageOPK(x_IKB:G, x_IKBsign:pkey, SPKPubB:G,SPKsign:t_sign,OPKPubB:G,PQPKPubB:kempkey,PQPKsign:t_sign,m1: bitstring, m2:bitstring) := 457 | 458 | 459 | (* Classical DH part *) 460 | new EKSecA1: Z; 461 | let EKPubA = gexp(g, EKSecA1) in 462 | let dh1 = gexp(SPKPubB, secIKA) in 463 | let dh2 = gexp(x_IKB, EKSecA1) in 464 | let dh3 = gexp(SPKPubB, EKSecA1) in 465 | let dh4 = gexp(OPKPubB, EKSecA1) in 466 | 467 | (* Kem additionnal part *) 468 | new kseed: kem_enc_seed; 469 | 470 | let fencap = encaps(PQPKPubB,kseed) in 471 | let SS = get_secret(fencap) in 472 | let CT = get_encap(fencap) in 473 | 474 | let SK_opk : emkey = H5(dh1, dh2, dh3, dh4, SS 475 | ifdef(`DH',`, hk2',`') 476 | ) in 477 | 478 | 479 | ifdef(`KEM',`get keys(secIKB, x_IKB2, secIKBsign, =x_IKBsign) in',`') 480 | ifdef(`DH',`get keys(secIKB, =x_IKB, secIKBsign, =x_IKBsign) in',`') 481 | ( 482 | ifdef(`KEM',`get corrupted(dummy,=x_IKBsign) in',`') 483 | ifdef(`DH',`get corrupted(=x_IKB,dummy) in',`') 484 | ( 485 | (* Alex talks to a corrupted participant; the message cannot be secret *) 486 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 487 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 488 | if m1 = m2 then 489 | let msg = m1 in 490 | let cipher = enc(msg, concatAD(IKA, IKAsign, x_IKB, x_IKBsign,PQPKPubB), SK_opk) in 491 | event SendWithOPK(IKA,IKAsign,x_IKB,x_IKBsign,SPKPubB,OPKPubB,EKPubA,PQPKPubB,msg); 492 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 493 | ) 494 | else 495 | ( 496 | (* Alex talks to a honest participant Blake *) 497 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 498 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 499 | (* Check that m1 and m2 have the same length *) 500 | if Zero(m1) = Zero(m2) then 501 | (* Send either m1 or m2 depending on the value of the secret bit b *) 502 | let msg = if_fun(secb1, m1, m2) in 503 | let cipher = enc(msg, concatAD(IKA, IKAsign, x_IKB, x_IKBsign,PQPKPubB), SK_opk) in 504 | event SendWithOPK(IKA,IKAsign,x_IKB,x_IKBsign,SPKPubB,OPKPubB,EKPubA,PQPKPubB,msg); 505 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 506 | ) 507 | ) 508 | else 509 | ( 510 | (* Alex talks to a dishonest participant *) 511 | if checksignEC(SPKPubB, x_IKBsign, SPKsign) then 512 | if checksignKEM(PQPKPubB, x_IKBsign, PQPKsign) then 513 | if m1 = m2 then 514 | let msg = m1 in 515 | let cipher = enc(msg, concatAD(IKA, IKAsign, x_IKB, x_IKBsign,PQPKPubB), SK_opk) in 516 | return((IKA, IKAsign), EKPubA, idPKDH(SPKPubB), idPKDH(OPKPubB), idPKKEM(PQPKPubB), CT, cipher) 517 | ). 518 | 519 | (* Same as before, but without the optional OPK. *) 520 | let SendInitialNoOPK(secb1:bool,secIKAp:Z , IKAp:G, secIKAsignp:skey, IKAsignp:pkey 521 | ifdef(`DH',`, hk:hashkey',`') 522 | ) = 523 | (* Key exchange + send message m1 or m2 *) 524 | SendFirstMessageNoOPK(x_IKBp:G, x_IKBsignp:pkey, SPKPubBp:G,SPKsignp:t_sign,PQPKPubBp:kempkey,PQPKsignp:t_sign,m1p: bitstring, m2p:bitstring) := 525 | 526 | (* Classical DH part *) 527 | new EKSecA1p: Z; 528 | let EKPubAp = gexp(g, EKSecA1p) in 529 | let dh1 = gexp(SPKPubBp, secIKAp) in 530 | let dh2 = gexp(x_IKBp, EKSecA1p) in 531 | let dh3 = gexp(SPKPubBp, EKSecA1p) in 532 | 533 | (* Kem additionnal part *) 534 | new kseedp: kem_enc_seed; 535 | 536 | let fencap = encaps(PQPKPubBp,kseedp) in 537 | let SSp = get_secret(fencap) in 538 | let CTp = get_encap(fencap) in 539 | 540 | let SK_nopk = H4(dh1, dh2, dh3, SSp 541 | ifdef(`DH',`, hk',`') 542 | ) in 543 | 544 | ifdef(`KEM',`get keys(secIKB, x_IKB, secIKBsign, =x_IKBsignp) in',`') 545 | ifdef(`DH',`get keys(secIKB, =x_IKBp, secIKBsign, =x_IKBsignp) in',`') 546 | ( 547 | ifdef(`KEM',`get corrupted(dummy,=x_IKBsignp) in',`') 548 | ifdef(`DH',`get corrupted(=x_IKBp,dummy) in',`') 549 | ( 550 | (* Alex talks to a corrupted participant; the message cannot be secret *) 551 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 552 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 553 | if m1p = m2p then 554 | let msg = m1p in 555 | let cipher = enc(msg, concatAD(IKAp, IKAsignp, x_IKBp, x_IKBsignp,PQPKPubBp), SK_nopk) in 556 | event SendWithoutOPK(IKAp,IKAsignp,x_IKBp,x_IKBsignp,SPKPubBp,EKPubAp,PQPKPubBp,msg); 557 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 558 | ) 559 | else 560 | ( 561 | (* Alex talks to a honest participant Blake *) 562 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 563 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 564 | (* Check that m1 and m2 have the same length *) 565 | if Zero(m1p) = Zero(m2p) then 566 | (* Send either m1 or m2 depending on the value of b *) 567 | let msg = if_fun(secb1, m1p, m2p) in 568 | let cipher = enc(msg, concatAD(IKAp, IKAsignp, x_IKBp, x_IKBsignp,PQPKPubBp), SK_nopk) in 569 | event SendWithoutOPK(IKAp,IKAsignp,x_IKBp,x_IKBsignp,SPKPubBp,EKPubAp,PQPKPubBp,msg); 570 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 571 | ) 572 | ) 573 | else 574 | ( 575 | (* Alex talks to a dishonest participant *) 576 | if checksignEC(SPKPubBp, x_IKBsignp, SPKsignp) then 577 | if checksignKEM(PQPKPubBp, x_IKBsignp, PQPKsignp) then 578 | if m1p = m2p then 579 | let msg = m1p in 580 | let cipher = enc(msg, concatAD(IKAp, IKAsignp, x_IKBp, x_IKBsignp,PQPKPubBp), SK_nopk) in 581 | return((IKAp, IKAsignp), EKPubAp, idPKDH(SPKPubBp), idPKKEM(PQPKPubBp), CTp, cipher) 582 | ). 583 | 584 | 585 | (* Blake generating prekeys and running the protocol, with Alex 586 | or with any other participant *) 587 | 588 | (* Sec 3.4 of spec *) 589 | let GenOPKThenRecv(secIKB : Z, IKB : G, secIKBsign : skey, IKBsign : pkey 590 | ifdef(`DH',`, hk:hashkey, hk2:hashkey2',`') 591 | ) = 592 | GenSPK():= 593 | (* Signed PQPK, last resort that can be reused *) 594 | new seed:kem_seed; 595 | let PQPKB = kempkgen(seed) in 596 | let PQPKBsig = signKEM(PQPKB, secIKBsign) in 597 | 598 | 599 | (* new SPK DH based *) 600 | new SPKSecB1: Z; 601 | let SPKPubB1: G = gexp(g, SPKSecB1) in 602 | let SPKsignature = signEC(SPKPubB1, secIKBsign) in 603 | return(SPKPubB1,SPKsignature, PQPKB, PQPKBsig); 604 | (( 605 | ! Nsignedprekey 606 | (* One-time prekey DH based*) 607 | GenOPK():= 608 | new OPKSecB1: Z; 609 | let OPKPubB = gexp(g, OPKSecB1) in 610 | return(OPKPubB); 611 | (* 2nd part of key exchange, 612 | using prekey OPKPubB and signed prekey SPKPubB1 *) 613 | RecvOPK(x_IKA: G,x_IKAsign: pkey, EPKPubA: G, idSPK:ids, idOPK:ids, idPQPK:ids, CT : ciphertext, msgenc: bitstring) := 614 | 615 | (* Here, we check if the keys we already have in the current 616 | process state have the same id has the received ones. This simulate 617 | a general process fetching the public keys from the received id, by 618 | matching the id against the ids of the keys in the database. *) 619 | 620 | if idSPK = idPKDH(SPKPubB1) then 621 | if idOPK = idPKDH(OPKPubB) then 622 | if idPQPK = idPKKEM(PQPKB) then 623 | 624 | let dh1 = gexp(x_IKA,SPKSecB1) in 625 | let dh2 = gexp(EPKPubA, secIKB) in 626 | let dh3 = gexp(EPKPubA, SPKSecB1) in 627 | let dh4 = gexp(EPKPubA, OPKSecB1) in 628 | 629 | let ss = decap(CT,kemskgen(seed)) in 630 | 631 | let sk_opk = H5(dh1, dh2, dh3, dh4, ss 632 | ifdef(`DH',`, hk2',`') 633 | ) in 634 | 635 | let injbot(msg) = dec(msgenc, concatAD(x_IKA, x_IKAsign, IKB, IKBsign,PQPKB), sk_opk) in 636 | ifdef(`KEM',` 637 | get keys(secIKA, x_IKA2, secIKAsign, =x_IKAsign) in 638 | (* In the KEM case, we simply have no authentication property *) 639 | (* find peer_i1 <= NsendOPK, peer_i2 <= Nidentity suchthat 640 | defined(SK_opk[peer_i1,peer_i2]) && SK_opk[peer_i1,peer_i2] = sk_opk then 641 | yield 642 | else*) 643 | yield 644 | else 645 | return(msg) 646 | ',`') 647 | 648 | ifdef(`DH',` 649 | (* Execute event Recv only if the sender Alex is honest and not corrupted *) 650 | get keys(secIKA, =x_IKA, secIKAsign, x_IKAsign2) in 651 | ( 652 | get corrupted(=x_IKA,dummy) in 653 | yield 654 | else 655 | let Bcorrupted = get corrupted(=IKB,dummy2) in true else false in 656 | event RecvWithOPK(Bcorrupted,x_IKA,x_IKAsign,IKB,IKBsign,SPKPubB1,OPKPubB,EPKPubA,PQPKB,msg) 657 | ) 658 | else 659 | return(msg) 660 | ',`') 661 | 662 | ) 663 | 664 | | 665 | 666 | ( 667 | ! Nsignedprekey2 668 | 669 | (* Version without the optional one-time prekey *) 670 | RecvNoOPK(x_IKAp: G,x_IKAsignp: pkey, EPKPubAp: G, idSPK:ids, idPQPK:ids, CTp : ciphertext, msgencp: bitstring) := 671 | 672 | (* Here, we check if the keys we already have in the current 673 | process state have the same id has the received ones. This simulate 674 | a general process fetching the public keys from the received id, by 675 | matching the id against the ids of the keys in the database. *) 676 | 677 | if idSPK = idPKDH(SPKPubB1) then 678 | if idPQPK = idPKKEM(PQPKB) then 679 | 680 | 681 | 682 | let dh1 = gexp(x_IKAp,SPKSecB1) in 683 | let dh2 = gexp(EPKPubAp, secIKB) in 684 | let dh3 = gexp(EPKPubAp, SPKSecB1) in 685 | 686 | let ss = decap(CTp,kemskgen(seed)) in 687 | let sk_nopk = H4(dh1, dh2, dh3, ss 688 | ifdef(`DH',`, hk',`') 689 | ) in 690 | let injbot(msg) = dec(msgencp, concatAD(x_IKAp, x_IKAsignp, IKB, IKBsign,PQPKB), sk_nopk) in 691 | ifdef(`KEM',` 692 | get keys(secIKA, x_IKA2p, secIKAsign, =x_IKAsignp) in 693 | (* In the KEM case, we simply have no authentication property *) 694 | yield 695 | else 696 | return(msg) 697 | ',`') 698 | 699 | ifdef(`DH',` 700 | get keys(secIKA, =x_IKAp, secIKAsign, x_IKAsignp2) in 701 | ( 702 | get corrupted(=x_IKAp,dummy2) in 703 | yield 704 | else 705 | let Bcorrupted = get corrupted(=IKB,dummy) in true else false in 706 | event RecvWithoutOPK(Bcorrupted,x_IKAp,x_IKAsignp,IKB,IKBsign,SPKPubB1,EPKPubAp,PQPKB,msg) 707 | ) 708 | else 709 | return(msg) 710 | ',`') 711 | 712 | ) 713 | ) 714 | . 715 | 716 | 717 | 718 | process 719 | Start() := 720 | new secb: bool; 721 | ifdef(`DH',`new hk:hashkey;',`') 722 | ifdef(`DH',`new hk2:hashkey2;',`') 723 | return(); 724 | 725 | ! Nidentity 726 | ( 727 | InitPrin() := 728 | new secIKA0:Z; 729 | let IKA = gexp(g,secIKA0) in 730 | new signAseed: keyseed; 731 | let secIKAsign = skgen(signAseed) in 732 | let IKAsign = pkgen(signAseed) in 733 | insert keys(secIKA0, IKA, secIKAsign, IKAsign); 734 | return(IKA, IKAsign); 735 | (* Corruption, for forward secrecy and key compromise impersonation *) 736 | ( (Corrupt() := 737 | insert corrupted(IKA,IKAsign); 738 | 739 | return(secIKA0, secIKAsign)) 740 | | (!Nrecv run GenOPKThenRecv(secIKA0,IKA,secIKAsign,IKAsign 741 | ifdef(`DH',`, hk, hk2',`') 742 | )) 743 | | (!NsendOPK run SendInitialWithOPK(secb,secIKA0,IKA,secIKAsign,IKAsign 744 | ifdef(`DH',`, hk2',`') 745 | )) 746 | | (!NsendNoOPK run SendInitialNoOPK(secb,secIKA0,IKA,secIKAsign,IKAsign 747 | ifdef(`DH',`, hk',`') 748 | )) 749 | ) 750 | 751 | ) 752 | ifdef(`DH',`| run hashoracle(hk)',`') 753 | ifdef(`DH',`| run hashoracle2(hk2)',`') 754 | 755 | ifdef(`KEM',` 756 | (* 757 | All queries proved. 758 | 27.36user 0.07system 0:27.53elapsed 99%CPU (0avgtext+0avgdata 122088maxresident)k 759 | *) 760 | ') 761 | ifdef(`DH',` 762 | (* 763 | All queries proved. 764 | 217.64user 0.17system 3:38.21elapsed 99%CPU (0avgtext+0avgdata 307172maxresident)k 765 | 0inputs+808outputs (0major+84900minor)pagefaults 0swaps 766 | *) 767 | ',`') -------------------------------------------------------------------------------- /revision2/cryptoverif/README.md: -------------------------------------------------------------------------------- 1 | This folder contains a model of the PQXDH protocol, as specified in: 2 | 3 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 4 | The PQXDH Key Agreement Protocol 5 | Revision 2 6 | 7 | This is analysis of revision 2, see the `revision1` folder for the 8 | analysis. 9 | 10 | 11 | # Model description 12 | 13 | The file models: 14 | - an arbitrary number of clients (devices) communicating with each other 15 | - each device uploads Curve and KEM keys to a server which is fully untrusted (modelled as a public channel) 16 | - each connection consists of one message from the initiator to the responder (with no follow-up messages) 17 | - each connection can optionnaly use an OPK 18 | - PQPK can always be reused, this is a worst case scenario where they are all last resort 19 | - the identifiers of the prefkeys do not need to verify any particular assumption 20 | 21 | ## Limitations 22 | 23 | The main limitation of the model is that we split the identity key IK into two keys, one DH key and one signing key. In practice, a single DH IK is used both for X25519 operations and XEdDSA signatures. To be completely precise, one would thus need to analyze the protocol under the gapDH assumption while also assuming that the attacker can obtain DH computations through the oracle signature. Such a proof does not exist in the litterature (also, while Ed25519 is proved, XEdDSA is not, which is a small gap). For instance, [8] proves the security of X3DH assuming gapDH, but also assuming that in fact all signed prekeys are pre-authenticated, and simply drop the signature question. Our model is thus more fine-grained. 24 | 25 | This limitation is mentioned in [PQXDH, sec 4] 26 | 27 | A second limitation here is that we cannot prove anything w.r.t. to whether an untrusted server only gives the last resort PQPK, or never gives any OPK. This echoes the security consideration in 4.9. 28 | 29 | ## Changelog from revision 1 30 | 31 | * the assumption `equation forall pkdh:G, pkkem:kempkey; encodeEC(pkdh) <> encodeKEM(pkkem).` is now explicitly part of the spec; 32 | * the IND-CPA + INT-CTXT assumption is now part of the spec; 33 | * the PQPK public key is included within the AD of the first encrypted message (and we now prove the authentication of the corresponding public keys). 34 | 35 | # Threat models 36 | 37 | In term of key compromise, we allow compromise of long term identity keys IK. 38 | 39 | In addition, the file includes two distinct set of threat models, one assuming the security of DH, and the other one assuming the security of the KEM. This notably correspond to either considering that the attacker is classical or post-quantum. 40 | 41 | ## Secure DH threat model 42 | 43 | In the classical setting, this file assumes that 44 | * the KDF function is a ROM 45 | * the signature Sig is EUF-CMA 46 | * the X25519 curve is gapDH 47 | * the final AEAD is IND-CPA and IND-CTXT 48 | 49 | 50 | ## Secure KEM threat model 51 | 52 | In the PQ setting, this file assumes that: 53 | * the KDF function is a post-quantum PRF w.r.t. to the kem secret position 54 | * the EUF-CMA is a post-quantum EUF-CMA signature 55 | * the KEM is post-quantum IND-CCA 56 | * the final AEAD is post-quanum IND-CPA and IND-CTXT 57 | 58 | Remark here that it looks like we assume that the signature Sig is post-quantum secure. Yet, as we allow compromise of signing keys, we can also see it as, the scheme is secure as long as the attacker does not try to compromise it using its quantum power, the only assumption being that we are able to know when the attacker does so. 59 | 60 | 61 | # Security Results 62 | 63 | ## Secure DH case 64 | 65 | For this setting, we prove authentication and the secrecy of the first sent message. Remark here that for authentication, as X25519 as a small subgroup, we do not in fact have authentication where we can say that both parties use precisely the same one time key OPK (or other DH keys), but only that they use the same modulo this small sub group. 66 | 67 | Importantly, compared to revision 1, we do have authentication of the KEM public key used, as it is now in the AD. 68 | 69 | The results in this case are very similar to the previous CryptoVerif analysis of TextSecure, a X3DH like protocol [A]. 70 | 71 | ## Secure KEM case 72 | 73 | In the secure KEM case, we do not look at the authentication case. We prove that secrecy still holds, as long as the signature scheme was secure when the conversation took place. 74 | 75 | # File usage 76 | 77 | Both scenarios are generated from a single model. The makefile allows to run the proof for both scenarios, using either `make dh` or `make kem`. 78 | 79 | # References 80 | 81 | [A] N. Kobeissi, K. Bhargavan and B. Blanchet. Automated Verification for Secure Messaging Protocols and their Implementations: A Symbolic and Computational Approach. EuroS&P'17. 82 | 83 | [8] K. Cohn-Gordon, C. Cremers, B. Dowling, L. Garratt, and D. Stebila, “A formal security analysis of the signal messaging protocol,” J. Cryptol., vol. 33, no. 4, 2020. https://doi.org/10.1007/s00145-020-09360-1 84 | -------------------------------------------------------------------------------- /revision2/pqxdh-diff-rev-1-to-2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inria-Prosecco/pqxdh-analysis/a09439cc350629ec430fb486305baa4001abbee1/revision2/pqxdh-diff-rev-1-to-2.pdf -------------------------------------------------------------------------------- /revision2/pqxdh-rev2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inria-Prosecco/pqxdh-analysis/a09439cc350629ec430fb486305baa4001abbee1/revision2/pqxdh-rev2.pdf -------------------------------------------------------------------------------- /revision2/proverif/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | ./run.sh pqxdh-model SecrecyInit SecrecyResp Authentication 3 | 4 | test: 5 | ./run.sh pqxdh-model SecrecyInit 6 | 7 | reach: 8 | ./run.sh pqxdh-model Reach 9 | 10 | clean: 11 | rm *.gen.pv -f 12 | 13 | 14 | -------------------------------------------------------------------------------- /revision2/proverif/README.md: -------------------------------------------------------------------------------- 1 | This folder contains ProVerif model of the PQXDH protocol, as specified in: 2 | 3 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 4 | The PQXDH Key Agreement Protocol 5 | Revision 2 6 | 7 | # Model description 8 | 9 | The file models: 10 | - an arbitrary number of clients (devices) communicating with each other 11 | - each device uploads Curve and KEM keys to a server which is fully untrusted (modelled as a public channel) 12 | - each connection consists of one message from the initiator to the responder (with no follow-up messages) 13 | - each connection can optionnaly use an OPK 14 | - PQPK can always be reused, this is a worst case scenario where they are all last resort 15 | 16 | ## Changelog from revision 1 17 | 18 | * The `ConfuseKemEc` capability does not exist anymore, as the assumption that forbides this is explicit in [PQXDH]; 19 | * The PQPK public key is included in the AD. 20 | * The authentication properties are stronger, compromising PQPK public keys of other sessions is now useless. 21 | * Consequently, the `ReEncaps` flag is disabled, as it was used to provide an attack which does not exist anymore. 22 | 23 | ## Threat Model 24 | 25 | Possible Key Compromise Scenario, with distinct cases: 26 | - IK secrets (this allow the adversary to compust maliciously signed SPK and PQPK) 27 | - OPK secrets 28 | - PQPK secrets 29 | - SPK secrets 30 | 31 | Additional Threat Model: 32 | - The attacker may suddenly be able to compute discrete logs. 33 | - The attacker may suddendly be able to extract a secret kem key from any public key. 34 | 35 | ## Properties 36 | 37 | We try to verify the secrecy of the key computed by the initiator, of the one computed by responder, and the authentication between a responder and an initiator. 38 | 39 | For each case, we try to come up with an optimal query, which precisely specifies which set of compromise falsify the query. We thus verify the strongest possible kind of property for each, and notably capture in single queries many classical properties such as KCI, FS, ... 40 | 41 | 42 | ## Exhaustive Security results 43 | 44 | We now report on the multiple results, obtained when enabling all possible compromises. 45 | 46 | ### Secrecy of the initiator 47 | 48 | The key SK computed by the initiator A is secret, unless: 49 | 1) IKB was compromised before the completion of the key exchange 50 | (this is to be expected, this is essentially a malicious B case) 51 | 2) IKB was compromised after the key exchange, as well as some SPK, and either KEMs are broken or the corresponding PQPK has been compromised 52 | (the attacker can then trivially recompute DH1 DH2 and DH3 given EKA and IKA, and also ss, but then, the attacker must have sent to B a malicious OPK) 53 | 3) DH was broken before the key exchange 54 | (similar to case 1) 55 | 4) Or DH was broken after the key exchange, and either KEMs are broken or the PQPK compromised 56 | (similar to case 2) 57 | 58 | All those cases appear normal, we notably have KCI (compromised IKa does not affect security) and FS implied by our result. 59 | 60 | Remark that using an OPK does not change anything here, as they are not authenticated. But of course, compromising the OPK makes it so that the honest responder will never receive and answer the message. 61 | 62 | 63 | ### Secrecy of the responder 64 | 65 | The key SK computed by the responder B is secret, unless: 66 | 1) IKA was compromised before the communication. 67 | (this allows a full impersonation of A) 68 | 2) Some SPKB was compromised before the communication. 69 | (more surprisingly, but inevitably, this allows a full impersonation of A) 70 | 3) IKB, SPK and PQPK are compromised after the communication, and either no OPK was used or it was compromised. 71 | (all secret material of B is leaked here, natural case) 72 | 4) IKB, SPK are compromised after the communication, KEMs are broken, and either no OPK was used or it was compromised. 73 | (similar to 3) 74 | 75 | 5) DH was broken before the exchange 76 | (naturally breaks everything) 77 | 6) DH was broken after the exchange, and the PQPK used by the responder was compromised 78 | (similar to 3) 79 | 7) DH was broken after the exchange, and KEMs are broken 80 | (similar to 5) 81 | 82 | Here, all cases 83 | ### Authentication 84 | 85 | Whenever a responder accepts (resp with or without an OPK), then, there exists an initiator that also accepted (resp with or without an OPK) with the same SPK and PQPK, unless: 86 | 1) IKA was compromised before the exchange 87 | ( allows impersonation of A) 88 | 2) IKb has been compromised before the exchange 89 | (similuar to 2, but where IKB allows to sign a dishonnest PQPK, and then reencapsulate ct for a valide one) 90 | 3) Some SPK was compromised before the exchange 91 | (knowing SPK allows to impersonate A) 92 | 4) DH has been broken before the exchange. 93 | 94 | 95 | ### Conclusions 96 | 97 | We prove in the symbolic model both authentication and secrecy, enumerating precisely the necessary condition so that the attacker can break the properties. Our security properties notably imply forward secrecy, resistance to harvest now decrypt later attacks, resistance to key compromise impersonation, and session independence. 98 | 99 | # Usage 100 | 101 | We use the cpp preprocessor to generate many possible scenarios from a single modeling file. 102 | 103 | One can call `./run.sh tag1 ... tagn` with the list of valid tags to verify the corresponding scenario. In the `Makefile`, we provide a few of the main interesting scenarios. 104 | 105 | The main possible tags are: 106 | - Reach - include the reachaility queries 107 | - SecrecyInit - Include the initiator secrecy query 108 | - SecrecyResp - Include the responder secrecy query 109 | - Authentication - Include the authentication query 110 | 111 | Some simplifying tags allow to verify simpler scenarios: 112 | - DisableNoOPK - Forces all communications to use an OPK 113 | - UnbreakableDH - Remove the potential arrival of the discrete log algo 114 | 115 | 116 | With the Makefile: 117 | - `make` sets SecrecyInit, SecrecyResp and Authentication, used to reproduce the main results. 118 | - `make reach` sets Reach, for sanity checks 119 | 120 | For each scenario, timings and expected result can be found at the bottom of the file. 121 | -------------------------------------------------------------------------------- /revision2/proverif/pqxdh-model.cpp.pv: -------------------------------------------------------------------------------- 1 | (*************************************) 2 | (* 3 | 4 | See the README next to this file for details on the modeling 5 | and how to run the file. 6 | 7 | This is the ProVerif part of the analysis, see the README at the root 8 | directory for details on the join analysis. 9 | 10 | Authors: Karthikeyan Bhargavan 11 | Charlie Jacomme 12 | Franziskus Kiefer 13 | *) 14 | (*-----------------------------------*) 15 | (* A Symbolic Cryptographic Model *) 16 | (* for primitives in Sec 2.2 Cryptographic notation *) 17 | (*-----------------------------------*) 18 | 19 | (* Elliptic Curve Diffie-Hellman *) 20 | type scalar. 21 | type point. 22 | 23 | const G:point. 24 | const Gneutral:point. 25 | 26 | fun SMUL(scalar,point):point. 27 | equation forall y : scalar, z : scalar; 28 | SMUL(y, SMUL(z, G)) = SMUL(z, SMUL(y, G)). 29 | 30 | fun smul(scalar,point):point 31 | reduc forall x:scalar; 32 | smul(x,Gneutral) = Gneutral 33 | otherwise forall x:scalar, y:point; smul(x,y) = SMUL(x,y). 34 | 35 | 36 | letfun s2p(s:scalar) = SMUL(s,G). 37 | letfun dh(s:scalar,p:point) = smul(s,p). 38 | 39 | (* KEM Encapsulation *) 40 | type kempriv. 41 | type kempub. 42 | 43 | fun kempk(kempriv):kempub. 44 | fun penc(kempub,bitstring):bitstring. 45 | fun pdec(kempriv,bitstring):bitstring 46 | reduc forall sk:kempriv,m:bitstring; 47 | pdec(sk,penc(kempk(sk),m)) = m. 48 | 49 | letfun kempriv2pub(k:kempriv) = kempk(k). 50 | 51 | letfun pqkem_enc(pk:kempub) = 52 | new ss:bitstring; 53 | (penc(pk,ss),ss). 54 | 55 | letfun pqkem_dec(sk:kempriv,ct:bitstring) = 56 | pdec(sk,ct). 57 | 58 | 59 | (* the domains are disjoints, see [PQXDH] *) 60 | fun encodeEC(point):bitstring [data]. 61 | fun encodeKEM(kempub):bitstring [data]. 62 | 63 | 64 | (* Bitstring manipulations *) 65 | 66 | (* Constants *) 67 | const zero: bitstring. 68 | const one: bitstring. 69 | 70 | (* A zero-filled byte sequence with length equal to the hash output length, in bytes. *) 71 | const zeroes_sha512:bitstring. 72 | 73 | (* A byte sequence containing 32 0xFF bytes if curve is curve25519 *) 74 | const ff_x25519:bitstring. 75 | 76 | (* A byte sequence containing 57 0xFF bytes if curve is curve448 *) 77 | const ff_x448:bitstring. 78 | 79 | (* The concatenation of string representations of the 4 PQXDH parameters info, curve, hash, and pqkem into a single string separated with ‘_’ such as “MyProtocol_CURVE25519_SHA-512_CRYSTALS-KYBER-1024”. *) 80 | const info_x25519_sha512_kyber1024:bitstring. 81 | 82 | (* Unambiguous concatenation, assumes that length of first element is known *) 83 | fun concatIKKEM(point,point,kempub): bitstring [data]. 84 | 85 | fun concat(bitstring,bitstring): bitstring [data]. 86 | fun concat5(point,point,point,point,bitstring):bitstring [data]. 87 | fun concat4(point,point,point,bitstring):bitstring [data]. 88 | 89 | (* HKDF *) 90 | 91 | (* One-shot HKDF(input_key_material, salt, info) *) 92 | type symkey. 93 | fun hkdf(bitstring, bitstring, bitstring) : symkey. 94 | 95 | letfun kdf(km:bitstring) = 96 | hkdf(concat(ff_x25519,km), 97 | zeroes_sha512, 98 | info_x25519_sha512_kyber1024). 99 | 100 | 101 | (* AEAD Encryption *) 102 | type nonce. 103 | const empty_nonce:nonce. 104 | 105 | fun aead_enc(symkey,nonce,bitstring,bitstring):bitstring. 106 | fun aead_dec(symkey,nonce,bitstring,bitstring):bitstring 107 | reduc forall k:symkey,n:nonce,m:bitstring,ad:bitstring; 108 | aead_dec(k,n,aead_enc(k,n,m,ad),ad) = m. 109 | 110 | (* XEdDSA Signatures *) 111 | fun sign(scalar,bitstring,nonce):bitstring. 112 | fun verify(point,bitstring,bitstring):bool 113 | reduc forall sk:scalar,m:bitstring,n:nonce; 114 | verify(SMUL(sk,G),m,sign(sk,m,n)) = true. 115 | 116 | 117 | 118 | event isBool(bool). 119 | 120 | restriction b:bool; 121 | event(isBool(b)) ==> b=true || b=false. 122 | 123 | (*-----------------------------------*) 124 | (* PKI *) 125 | (*-----------------------------------*) 126 | 127 | (* Clients representing devices: Alice, Bob, etc. *) 128 | type client. 129 | 130 | (* Global PKI maintained by the Server, checked by Clients *) 131 | 132 | (* For each client Bob: 133 | - Bob’s curve identity key IKB *) 134 | 135 | table identity_pubkeys(client,point). 136 | 137 | 138 | (*-----------------------------------*) 139 | (* Security Model and Properties *) 140 | (*-----------------------------------*) 141 | 142 | (* A channel for the attacker *) 143 | free att:channel. 144 | 145 | (* A channel for the server, but in fact controlled by the attacker. *) 146 | 147 | free server:channel. 148 | 149 | 150 | (* An event triggered when the private keys of a client are compromised *) 151 | 152 | (* Handshake Events *) 153 | event InitDone(client,client,bool,point,point,kempub,symkey). 154 | event RespondDone(client,client,bool,point,point,kempub,symkey). 155 | 156 | (* Application Messages and Events *) 157 | fun app_message(client,client,bitstring):bitstring [private]. 158 | event AppSend(client,client,bitstring). 159 | event AppRecv(client,client,bitstring). 160 | 161 | 162 | (* Compromise Events *) 163 | event CompromiseIK(client). 164 | event CompromiseSPK(client,point). 165 | event CompromiseOPK(client,point). 166 | event CompromisePQPK(client,kempub). 167 | 168 | #ifdef Reach 169 | 170 | (* Reachability Queries *) 171 | query i:client, r:client, ts:symkey, m:bitstring, opk:point, spk:point, pqpk:kempub; 172 | event(InitDone(i,r,true,opk,spk,pqpk,ts)); 173 | event(InitDone(i,r,false,opk,spk,pqpk,ts)); 174 | event(RespondDone(r,i,true,opk,spk,pqpk,ts)); 175 | event(RespondDone(r,i,false,opk,spk,pqpk,ts)). 176 | 177 | #endif 178 | 179 | 180 | (*-----------------------------------*) 181 | (* Security Model and Properties *) 182 | (*-----------------------------------*) 183 | 184 | #ifdef SecrecyInit 185 | 186 | query a,b:client, useOPK:bool, opk,spk:point, pqpk:kempub, ts:symkey, i,j:time; 187 | event(InitDone(a,b,useOPK,opk,spk,pqpk,ts))@i && attacker(ts) ==> 188 | (* A compromise of IKB in the past is enough to break everything. *) 189 | (event(CompromiseIK(b))@j && (j < i 190 | || 191 | ( 192 | event(CompromiseSPK(b,spk)) 193 | && 194 | (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM)) 195 | )) 196 | 197 | ) 198 | || 199 | (event(BrokenDH())@j && (j < i 200 | || event(CompromisePQPK(b,pqpk)) 201 | || event(BrokenKEM) 202 | )) 203 | . 204 | 205 | 206 | 207 | #endif 208 | 209 | #ifdef SecrecyResp 210 | 211 | query a,b:client, useOPK:bool, opk,spk:point, pqpk:kempub, ts:symkey, i,j1,j2:time; 212 | event(RespondDone(b,a, useOPK, opk,spk,pqpk,ts))@i && attacker(ts) ==> 213 | (* The compromise of IKA allows the attacker to play the role of A, and thus know the key *) 214 | (event(CompromiseIK(a))@j1 && j1 < i) 215 | || 216 | (* If IKA is not corrupted, we must corrupt some information on the side of B *) 217 | (event(CompromiseSPK(b,spk))@j1 && 218 | (* A compromise of some SPKb in the past allow an attacker to impersonate any A *) 219 | (j1 < i || 220 | ( 221 | event(CompromiseIK(b))@j2 && (j2 < i 222 | || 223 | event(CompromisePQPK(b,pqpk)) 224 | || 225 | event(BrokenKEM) 226 | 227 | ) 228 | && (useOPK=false || event(CompromiseOPK(b,opk))) 229 | 230 | ) 231 | ) 232 | ) 233 | || 234 | (event(BrokenDH())@j1 && (j1 < i 235 | || 236 | event(CompromisePQPK(b,pqpk)) 237 | || 238 | event(BrokenKEM) 239 | ) 240 | ) 241 | . 242 | 243 | #endif 244 | 245 | #ifdef Authentication 246 | 247 | query a,b:client, useOPK:bool, opk,opk2,spk:point, pqpk:kempub, ts:symkey, i,j:time; 248 | event(RespondDone(b,a,useOPK,opk,spk,pqpk,ts))@i ==> 249 | (* authentication including over the opk *) 250 | (useOPK = true && event(InitDone(a,b,true, opk,spk,pqpk,ts))) 251 | || 252 | (* authentication not over the opk *) 253 | (useOPK = false && event(InitDone(a,b,false, opk2,spk,pqpk,ts))) 254 | (* Unless *) 255 | || 256 | (* The compromise of IKA allows the attacker to play the role of A, and thus know the key *) 257 | (event(CompromiseIK(a))@j && j < i) 258 | || 259 | (* Knowing a SPK completely allows to break auth *) 260 | (event(CompromiseSPK(b,spk))@j && j < i ) 261 | || 262 | (event(BrokenDH())@j && j < i) 263 | . 264 | 265 | #endif 266 | 267 | 268 | 269 | nounif z:scalar; attacker(SMUL(z,G)) / 30000. 270 | 271 | 272 | (* DH breaks down *) 273 | fun discreteLog(point): scalar 274 | reduc forall s:scalar, y:point; 275 | discreteLog(SMUL(s,G)) = s [private]. (* Remark, this dL does not work on a g^xy model *) 276 | 277 | event BrokenDH. 278 | 279 | let dh_attacks() = 280 | in (att,p:point); 281 | event BrokenDH; 282 | out (att,discreteLog(p)). 283 | 284 | 285 | 286 | (* KEM breaks down *) 287 | fun inversePK(kempub): kempriv 288 | reduc forall s:kempriv; 289 | inversePK(kempk(s)) = s [private]. 290 | 291 | event BrokenKEM. 292 | 293 | let kem_attacks() = 294 | in (att,p:kempub); 295 | event BrokenKEM; 296 | out (att,inversePK(p)). 297 | 298 | 299 | (*-----------------------------------*) 300 | (* Protocol Processes *) 301 | (*-----------------------------------*) 302 | 303 | (* PQXDH Protocol *) 304 | 305 | (* Alice then sends Bob an initial message containing: 306 | - Alice’s identity key IKA 307 | - Alice’s ephemeral key EKA 308 | - The pqkem ciphertext CT encapsulating SS for PQPKB 309 | - Identifiers stating which of Bob’s prekeys Alice used 310 | - An initial ciphertext encrypted with some AEAD encryption scheme [5] using AD as associated data and using an encryption key which is either SK or the output from some cryptographic PRF keyed by SK. 311 | *) 312 | 313 | const init:bitstring. 314 | const resp:bitstring. 315 | 316 | 317 | let Initiator(i:client, IKA_s:scalar) = 318 | (* we let the attacker choose the responder we will communicate with *) 319 | in(att,r:client); 320 | (* if not(r=i) then *) 321 | (* Initiator public key *) 322 | let IKA_p = s2p(IKA_s) in 323 | 324 | (* Retrieve the responder identity key *) 325 | get identity_pubkeys(=r,IKB_p) in 326 | 327 | (* receive from the server the pre key bundles *) 328 | in(server, (SPKB_p:point,SPKB_sig:bitstring,PQPKB_p:kempub,PQPKB_sig:bitstring)); 329 | (* Verify the signatures *) 330 | if verify(IKB_p,encodeEC(SPKB_p),SPKB_sig) then 331 | if verify(IKB_p,encodeKEM(PQPKB_p),PQPKB_sig) then (* Here, we don't know whether we have a last resort or one time key. *) 332 | 333 | ( 334 | (* Optionally receive a one time key *) 335 | in (server,(useOPK:bool,OPKB_p:point)); 336 | event isBool(useOPK); 337 | let (CT:bitstring,SS:bitstring) = pqkem_enc(PQPKB_p) in 338 | 339 | new EKA_s:scalar; 340 | 341 | let EKA_p = s2p(EKA_s) in 342 | let DH1 = dh(IKA_s,SPKB_p) in 343 | let DH2 = dh(EKA_s,IKB_p) in 344 | let DH3 = dh(EKA_s,SPKB_p) in 345 | 346 | let SK = 347 | (if useOPK then (let DH4 = dh(EKA_s,OPKB_p) in kdf(concat5(DH1,DH2,DH3,DH4,SS))) 348 | else kdf(concat4(DH1,DH2,DH3,SS))) 349 | in 350 | 351 | event InitDone(i,r,useOPK,OPKB_p,SPKB_p,PQPKB_p,SK); 352 | 353 | (* Removing from the ad the PQPKB_p reintroduces the re-encapsulation attack *) 354 | let ad = concatIKKEM(IKA_p,IKB_p,PQPKB_p) in 355 | new msg_nonce: bitstring; 356 | let msg = app_message(i,r,msg_nonce) in 357 | let enc_msg = aead_enc(SK,empty_nonce,msg,ad) in 358 | (* Send Message *) 359 | out(server, (IKA_p,EKA_p,CT, OPKB_p, SPKB_p, PQPKB_p, enc_msg)) 360 | 361 | ). 362 | 363 | 364 | let Responder_mess(r:client, IKB_s:scalar, SPKB_s:scalar,PQSPKB_s:kempriv)= 365 | ( 366 | let IKB_p = s2p(IKB_s) in 367 | let SPKB_p = s2p(SPKB_s) in 368 | let PQSPKB_p = kempriv2pub(PQSPKB_s) in 369 | 370 | (* Generate a new one time OPKB *) 371 | new OPKB_s:scalar; 372 | let OPKB_p = s2p(OPKB_s) in 373 | 374 | (* send public keys to server *) 375 | out (server,OPKB_p ); 376 | 377 | (in(att, =r); event CompromiseOPK(r, OPKB_p); out(att,OPKB_s)) 378 | | 379 | 380 | 381 | (* Receive Message with the currently stored public keys *) 382 | ( 383 | in (server,(IKA_p:point,EKA_p:point,CT:bitstring,useOPK:bool,=OPKB_p,=SPKB_p,=PQSPKB_p, enc_msg:bitstring)); 384 | event isBool(useOPK); 385 | (* Verify remote identity key *) 386 | get identity_pubkeys(i,=IKA_p) in 387 | (* if not(r=i) then *) 388 | ( 389 | (* Retrieve one-time private keys *) 390 | 391 | let SS = pqkem_dec(PQSPKB_s,CT) in 392 | let DH1 = dh(SPKB_s,IKA_p) in 393 | let DH2 = dh(IKB_s, EKA_p) in 394 | let DH3 = dh(SPKB_s,EKA_p) in 395 | let SK = 396 | (if useOPK then (let DH4 = dh(OPKB_s,EKA_p) in kdf(concat5(DH1,DH2,DH3,DH4,SS))) 397 | else kdf(concat4(DH1,DH2,DH3,SS))) 398 | in 399 | 400 | (* Removing from the ad the PQPKB_p reintroduces the re-encapsulation attack *) 401 | let ad = concatIKKEM(IKA_p,IKB_p,PQSPKB_p) in 402 | let msg = aead_dec(SK,empty_nonce,enc_msg,ad) in 403 | event RespondDone(r,i,useOPK,OPKB_p,SPKB_p,PQSPKB_p,SK) 404 | 405 | ) 406 | ) 407 | ). 408 | 409 | 410 | let Responder(r:client, IKB_s:scalar) = 411 | let IKB_p = s2p(IKB_s) in 412 | 413 | (* Creates a new DH based Signed Pre-Key *) 414 | new SPKB_s:scalar; 415 | let SPKB_p = s2p(SPKB_s) in 416 | new zSPKB:nonce; 417 | let SPKB_sig = sign(IKB_s,encodeEC(SPKB_p),zSPKB) in 418 | 419 | (* Creates a new KEM based Signed Pre-Key *) 420 | new PQSPKB_s:kempriv; 421 | let PQSPKB_p = kempriv2pub(PQSPKB_s) in 422 | new zPQSPKB:nonce; 423 | let PQSPKB_sig = sign(IKB_s,encodeKEM(PQSPKB_p),zPQSPKB) in 424 | 425 | 426 | (* send public keys to server *) 427 | out (att,(SPKB_p,SPKB_sig,PQSPKB_p,PQSPKB_sig)); 428 | 429 | (in(att, =r); event CompromiseSPK(r, SPKB_p); out(att,SPKB_s)) 430 | | 431 | (in(att, =r); event CompromisePQPK(r, PQSPKB_p); out(att,PQSPKB_s)) 432 | | 433 | (! Responder_mess(r, IKB_s, SPKB_s,PQSPKB_s)) 434 | . 435 | 436 | 437 | (* A process to create a new client and publish its keys *) 438 | let Launch_pqxdh_client(p:client) = 439 | (* Create a new client *) 440 | new IK_s:scalar; 441 | let IK_p = s2p(IK_s) in 442 | insert identity_pubkeys(p,IK_p); 443 | (* Publish the public key identity pair. *) 444 | out(att,IK_p); 445 | ( 446 | (* Initiator role *) 447 | ! Initiator(p, IK_s) 448 | (* Responder role *) 449 | | ! Responder(p, IK_s) 450 | 451 | | 452 | (* Compromise of the identity key *) 453 | (in(att, =p); event CompromiseIK(p); out(att,IK_s)) 454 | ). 455 | 456 | 457 | 458 | 459 | (* Main Process: any number of clients and members *) 460 | 461 | process 462 | ! in(att,c:client); Launch_pqxdh_client(c) 463 | #if !defined(UnbreakableDH) 464 | | ! dh_attacks 465 | #endif 466 | #if !defined(UnbreakableKEM) 467 | | ! kem_attacks 468 | #endif 469 | 470 | 471 | 472 | (*************************************) 473 | (* EXPECTED RESULTS *) 474 | (*************************************) 475 | 476 | (* 477 | 478 | $ make 479 | 480 | 481 | -------------------------------------------------------------- 482 | Verification summary: 483 | 484 | Query event(InitDone(a,b,useOPK_1,opk,spk,pqpk,ts))@i_2 && attacker(ts) ==> (event(CompromiseIK(b))@j && (i_2 > j || (event(CompromiseSPK(b,spk)) && (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))))) || (event(BrokenDH)@j && (i_2 > j || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))) is true. 485 | 486 | Query event(RespondDone(b,a,useOPK_1,opk,spk,pqpk,ts))@i_2 && attacker(ts) ==> (event(CompromiseIK(a))@j1 && i_2 > j1) || (event(CompromiseSPK(b,spk))@j1 && (i_2 > j1 || (event(CompromiseIK(b))@j2 && (i_2 > j2 || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM)) && (useOPK_1 = false || event(CompromiseOPK(b,opk)))))) || (event(BrokenDH)@j1 && (i_2 > j1 || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))) is true. 487 | 488 | Query event(RespondDone(b,a,useOPK_1,opk,spk,pqpk,ts))@i_2 ==> (useOPK_1 = true && event(InitDone(a,b,true,opk,spk,pqpk,ts))) || (useOPK_1 = false && event(InitDone(a,b,false,opk2,spk,pqpk,ts))) || (event(CompromiseIK(a))@j && i_2 > j) || (event(CompromiseSPK(b,spk))@j && i_2 > j) || (event(BrokenDH)@j && i_2 > j) is true. 489 | 490 | -------------------------------------------------------------- 491 | 492 | real 1m43,094s 493 | 494 | 495 | *) 496 | 497 | (* 498 | 499 | $ make reach 500 | 501 | -------------------------------------------------------------- 502 | Verification summary: 503 | 504 | Query not event(InitDone(i_2,r_1,true,opk,spk,pqpk,ts)) is false. 505 | 506 | Query not event(InitDone(i_2,r_1,false,opk,spk,pqpk,ts)) is false. 507 | 508 | Query not event(RespondDone(r_1,i_2,true,opk,spk,pqpk,ts)) is false. 509 | 510 | Query not event(RespondDone(r_1,i_2,false,opk,spk,pqpk,ts)) is false. 511 | 512 | -------------------------------------------------------------- 513 | 514 | 515 | real 0m0,9 516 | 517 | *) -------------------------------------------------------------------------------- /revision2/proverif/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run the C preprocessor on the input file, produces a .pv file and runs proverif on it 3 | 4 | res="" 5 | name="" 6 | for def in ${@:2} 7 | do 8 | res+=" --define-macro=$def=$def" 9 | name+="$def" 10 | done 11 | if [ "$name" = "" ]; then 12 | file="$1.gen.pv" 13 | else 14 | file="$1-$name.gen.pv" 15 | fi 16 | eval "( 17 | echo \"(* !!! WARNING !!! *)\"; 18 | echo \"(* File generated from with ./run.sh $@*)\"; 19 | echo \"(* Read the README for more informations *)\"; 20 | echo \"(* ------------------------------------- *)\"; 21 | cpp -P -E -w $res $1.cpp.pv; 22 | ) > $file" 23 | time proverif $file 24 | -------------------------------------------------------------------------------- /revision3/cryptoverif/.gitignore: -------------------------------------------------------------------------------- 1 | _models/* -------------------------------------------------------------------------------- /revision3/cryptoverif/Makefile: -------------------------------------------------------------------------------- 1 | MAIN = PQXDH 2 | 3 | dh: 4 | m4 -D DH $(MAIN).m4.ocv > _models/$(MAIN).DH.ocv 5 | time cryptoverif _models/$(MAIN).DH.ocv 6 | 7 | kem: 8 | m4 -D KEM $(MAIN).m4.ocv > _models/$(MAIN).KEM.ocv 9 | time cryptoverif _models/$(MAIN).KEM.ocv 10 | -------------------------------------------------------------------------------- /revision3/cryptoverif/README.md: -------------------------------------------------------------------------------- 1 | This folder contains a model of the PQXDH protocol, as specified in: 2 | 3 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 4 | The PQXDH Key Agreement Protocol 5 | Revision 2 6 | 7 | this is in fact a proposal with exploration of changes for a revision 3. 8 | 9 | # Model description 10 | 11 | The file models: 12 | - an arbitrary number of clients (devices) communicating with each other 13 | - each device uploads Curve and KEM keys to a server which is fully untrusted (modelled as a public channel) 14 | - each connection consists of one message from the initiator to the responder (with no follow-up messages) 15 | - each connection can optionnaly use an OPK 16 | - PQPK can always be reused, this is a worst case scenario where they are all last resort 17 | - the identifiers of the prefkeys do not need to verify any particular assumption 18 | 19 | ## Limitations 20 | 21 | The main limitation of the model is that we split the identity key IK into two keys, one DH key and one signing key. In practice, a single DH IK is used both for X25519 operations and XEdDSA signatures. To be completely precise, one would thus need to analyze the protocol under the gapDH assumption while also assuming that the attacker can obtain DH computations through the oracle signature. Such a proof does not exist in the litterature (also, while Ed25519 is proved, XEdDSA is not, which is a small gap). For instance, [8] proves the security of X3DH assuming gapDH, but also assuming that in fact all signed prekeys are pre-authenticated, and simply drop the signature question. Our model is thus more fine-grained. 22 | 23 | This limitation is mentioned in [PQXDH, sec X] 24 | 25 | A second limitation here is that we cannot prove anything w.r.t. to whether an untrusted server only gives the last resort PQPK, or never gives any OPK. This echoes the security consideration in 4.9. 26 | 27 | ## Changelog from revision 2 28 | 29 | * the KDF now contains the IK_A, IK_B, and PQPK. 30 | * the AD is then set of 0. 31 | * added a boolean flag in the KEMEncode to identify last resort keys 32 | 33 | # Threat models 34 | 35 | In term of key compromise, we allow compromise of long term identity keys IK. 36 | 37 | In addition, the file includes two distinct set of threat models, one assuming the security of DH, and the other one assuming the security of the KEM. This notably correspond to either considering that the attacker is classical or post-quantum. 38 | 39 | ## Secure DH threat model 40 | 41 | In the classical setting, this file assumes that 42 | * the KDF function is a ROM 43 | * the signature Sig is EUF-CMA 44 | * the X25519 curve is gapDH 45 | * the final AEAD is IND-CPA and IND-CTXT 46 | 47 | 48 | ## Secure KEM threat model 49 | 50 | In the PQ setting, this file assumes that: 51 | * the KDF function is a post-quantum PRF w.r.t. to the kem secret position 52 | * the EUF-CMA is a post-quantum EUF-CMA signature 53 | * the KEM is post-quantum IND-CCA 54 | * the final AEAD is post-quanum IND-CPA and IND-CTXT 55 | 56 | Remark here that it looks like we assume that the signature Sig is post-quantum secure. Yet, as we allow compromise of signing keys, we can also see it as, the scheme is secure as long as the attacker does not try to compromise it using its quantum power, the only assumption being that we are able to know when the attacker does so. 57 | 58 | 59 | # Security Results 60 | 61 | ## Secure DH case 62 | 63 | For this setting, we prove authentication and the secrecy of the first sent message. Remark here that for authentication, as X25519 as a small subgroup, we do not in fact have authentication where we can say that both parties use precisely the same one time key OPK (or other DH keys), but only that they use the same modulo this small sub group. 64 | 65 | Importantly, compared to revision 1, we do have authentication of the KEM public key used, as it is now in the AD. 66 | 67 | The results in this case are very similar to the previous CryptoVerif analysis of TextSecure, a X3DH like protocol [A]. 68 | 69 | ## Secure KEM case 70 | 71 | In the secure KEM case, we do not look at the authentication case. We prove that secrecy still holds, as long as the signature scheme was secure when the conversation took place. 72 | 73 | # File usage 74 | 75 | Both scenarios are generated from a single model. The makefile allows to run the proof for both scenarios, using either `make dh` or `make kem`. 76 | 77 | # References 78 | 79 | [A] N. Kobeissi, K. Bhargavan and B. Blanchet. Automated Verification for Secure Messaging Protocols and their Implementations: A Symbolic and Computational Approach. EuroS&P'17. 80 | 81 | [8] K. Cohn-Gordon, C. Cremers, B. Dowling, L. Garratt, and D. Stebila, “A formal security analysis of the signal messaging protocol,” J. Cryptol., vol. 33, no. 4, 2020. https://doi.org/10.1007/s00145-020-09360-1 82 | -------------------------------------------------------------------------------- /revision3/proverif/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | ./run.sh pqxdh-model SecrecyInit SecrecyResp Authentication 3 | 4 | test: 5 | ./run.sh pqxdh-model SecrecyInit 6 | 7 | reach: 8 | ./run.sh pqxdh-model Reach 9 | 10 | clean: 11 | rm *.gen.pv -f 12 | 13 | 14 | -------------------------------------------------------------------------------- /revision3/proverif/README.md: -------------------------------------------------------------------------------- 1 | This folder contains ProVerif model of the PQXDH protocol, as specified in: 2 | 3 | [PQXDH] : https://signal.org/docs/specifications/pqxdh/ 4 | The PQXDH Key Agreement Protocol 5 | Revision 3 6 | 7 | 8 | this is in fact a proposal with exploration of changes for a revision 3. 9 | 10 | # Model description 11 | 12 | The file models: 13 | - an arbitrary number of clients (devices) communicating with each other 14 | - each device uploads Curve and KEM keys to a server which is fully untrusted (modelled as a public channel) 15 | - each connection consists of one message from the initiator to the responder (with no follow-up messages) 16 | - each connection can optionnaly use an OPK 17 | - PQPK can always be reused, this is a worst case scenario where they are all last resort 18 | 19 | ## Changelog from revision 2 20 | 21 | * PQPK, IKA and IKB included in KDF. 22 | * AD goes to zero 23 | * F flag is dropped 24 | * added a boolean flag in the KEMEncode to identify last resort keys 25 | 26 | 27 | ## Threat Model 28 | 29 | Possible Key Compromise Scenario, with distinct cases: 30 | - IK secrets (this allow the adversary to compust maliciously signed SPK and PQPK) 31 | - OPK secrets 32 | - PQPK secrets 33 | - SPK secrets 34 | 35 | Additional Threat Model: 36 | - The attacker may suddenly be able to compute discrete logs. 37 | - The attacker may suddendly be able to extract a secret kem key from any public key. 38 | 39 | ## Properties 40 | 41 | We try to verify the secrecy of the key computed by the initiator, of the one computed by responder, and the authentication between a responder and an initiator. 42 | 43 | For each case, we try to come up with an optimal query, which precisely specifies which set of compromise falsify the query. We thus verify the strongest possible kind of property for each, and notably capture in single queries many classical properties such as KCI, FS, ... 44 | 45 | 46 | ## Exhaustive Security results 47 | 48 | We now report on the multiple results, obtained when enabling all possible compromises. 49 | 50 | ### Secrecy of the initiator 51 | 52 | The key SK computed by the initiator A is secret, unless: 53 | 1) IKB was compromised before the completion of the key exchange 54 | (this is to be expected, this is essentially a malicious B case) 55 | 2) IKB was compromised after the key exchange, as well as some SPK, and either KEMs are broken or the corresponding PQPK has been compromised 56 | (the attacker can then trivially recompute DH1 DH2 and DH3 given EKA and IKA, and also ss, but then, the attacker must have sent to B a malicious OPK) 57 | 3) DH was broken before the key exchange 58 | (similar to case 1) 59 | 4) Or DH was broken after the key exchange, and either KEMs are broken or the PQPK compromised 60 | (similar to case 2) 61 | 62 | All those cases appear normal, we notably have KCI (compromised IKa does not affect security) and FS implied by our result. 63 | 64 | Remark that using an OPK does not change anything here, as they are not authenticated. But of course, compromising the OPK makes it so that the honest responder will never receive and answer the message. 65 | 66 | 67 | ### Secrecy of the responder 68 | 69 | The key SK computed by the responder B is secret, unless: 70 | 1) IKA was compromised before the communication. 71 | (this allows a full impersonation of A) 72 | 2) Some SPKB was compromised before the communication. 73 | (more surprisingly, but inevitably, this allows a full impersonation of A) 74 | 3) IKB, SPK and PQPK are compromised after the communication, and either no OPK was used or it was compromised. 75 | (all secret material of B is leaked here, natural case) 76 | 4) IKB, SPK are compromised after the communication, KEMs are broken, and either no OPK was used or it was compromised. 77 | (similar to 3) 78 | 79 | 5) DH was broken before the exchange 80 | (naturally breaks everything) 81 | 6) DH was broken after the exchange, and the PQPK used by the responder was compromised 82 | (similar to 3) 83 | 7) DH was broken after the exchange, and KEMs are broken 84 | (similar to 5) 85 | 86 | Here, all cases 87 | ### Authentication 88 | 89 | Whenever a responder accepts (resp with or without an OPK), then, there exists an initiator that also accepted (resp with or without an OPK) with the same SPK and PQPK, unless: 90 | 1) IKA was compromised before the exchange 91 | ( allows impersonation of A) 92 | 2) IKb has been compromised before the exchange 93 | (similuar to 2, but where IKB allows to sign a dishonnest PQPK, and then reencapsulate ct for a valide one) 94 | 3) Some SPK was compromised before the exchange 95 | (knowing SPK allows to impersonate A) 96 | 4) DH has been broken before the exchange. 97 | 98 | 99 | ### Conclusions 100 | 101 | We prove in the symbolic model both authentication and secrecy, enumerating precisely the necessary condition so that the attacker can break the properties. Our security properties notably imply forward secrecy, resistance to harvest now decrypt later attacks, resistance to key compromise impersonation, and session independence. 102 | 103 | # Usage 104 | 105 | We use the cpp preprocessor to generate many possible scenarios from a single modeling file. 106 | 107 | One can call `./run.sh tag1 ... tagn` with the list of valid tags to verify the corresponding scenario. In the `Makefile`, we provide a few of the main interesting scenarios. 108 | 109 | The main possible tags are: 110 | - Reach - include the reachaility queries 111 | - SecrecyInit - Include the initiator secrecy query 112 | - SecrecyResp - Include the responder secrecy query 113 | - Authentication - Include the authentication query 114 | 115 | Some simplifying tags allow to verify simpler scenarios: 116 | - DisableNoOPK - Forces all communications to use an OPK 117 | - UnbreakableDH - Remove the potential arrival of the discrete log algo 118 | 119 | 120 | With the Makefile: 121 | - `make` sets SecrecyInit, SecrecyResp and Authentication, used to reproduce the main results. 122 | - `make reach` sets Reach, for sanity checks 123 | 124 | For each scenario, timings and expected result can be found at the bottom of the file. 125 | -------------------------------------------------------------------------------- /revision3/proverif/pqxdh-model.cpp.pv: -------------------------------------------------------------------------------- 1 | (*************************************) 2 | (* 3 | 4 | See the README next to this file for details on the modeling 5 | and how to run the file. 6 | 7 | This is the ProVerif part of the analysis, see the README at the root 8 | directory for details on the joint analysis. 9 | 10 | Authors: Karthikeyan Bhargavan 11 | Charlie Jacomme 12 | Franziskus Kiefer 13 | *) 14 | (*-----------------------------------*) 15 | (* A Symbolic Cryptographic Model *) 16 | (* for primitives in Sec 2.2 Cryptographic notation *) 17 | (*-----------------------------------*) 18 | 19 | (* Elliptic Curve Diffie-Hellman *) 20 | type scalar. 21 | type point. 22 | 23 | const G:point. 24 | const Gneutral:point. 25 | 26 | fun SMUL(scalar,point):point. 27 | equation forall y : scalar, z : scalar; 28 | SMUL(y, SMUL(z, G)) = SMUL(z, SMUL(y, G)). 29 | 30 | fun smul(scalar,point):point 31 | reduc forall x:scalar; 32 | smul(x,Gneutral) = Gneutral 33 | otherwise forall x:scalar, y:point; smul(x,y) = SMUL(x,y). 34 | 35 | 36 | letfun s2p(s:scalar) = SMUL(s,G). 37 | letfun dh(s:scalar,p:point) = smul(s,p). 38 | 39 | (* KEM Encapsulation *) 40 | type kempriv. 41 | type kempub. 42 | 43 | fun kempk(kempriv):kempub. 44 | fun penc(kempub,bitstring):bitstring. 45 | fun pdec(kempriv,bitstring):bitstring 46 | reduc forall sk:kempriv,m:bitstring; 47 | pdec(sk,penc(kempk(sk),m)) = m. 48 | 49 | letfun kempriv2pub(k:kempriv) = kempk(k). 50 | 51 | letfun pqkem_enc(pk:kempub) = 52 | new ss:bitstring; 53 | (penc(pk,ss),ss). 54 | 55 | letfun pqkem_dec(sk:kempriv,ct:bitstring) = 56 | pdec(sk,ct). 57 | 58 | 59 | (* the domains are disjoints, see [PQXDH] *) 60 | fun encodeEC(point):bitstring [data]. 61 | 62 | (* a boolean is set to true when the PQSPKB is a last resort kempub and can be reused multiple times. *) 63 | fun encodeKEM(bool,kempub):bitstring [data]. 64 | 65 | 66 | (* Bitstring manipulations *) 67 | 68 | (* Constants *) 69 | const zero: bitstring. 70 | const one: bitstring. 71 | 72 | (* A zero-filled byte sequence with length equal to the hash output length, in bytes. *) 73 | const zeroes_sha512:bitstring. 74 | 75 | (* A byte sequence containing 32 0xFF bytes if curve is curve25519 *) 76 | const ff_x25519:bitstring. 77 | 78 | (* A byte sequence containing 57 0xFF bytes if curve is curve448 *) 79 | const ff_x448:bitstring. 80 | 81 | (* The concatenation of string representations of the 4 PQXDH parameters info, curve, hash, and pqkem into a single string separated with ‘_’ such as “MyProtocol_CURVE25519_SHA-512_CRYSTALS-KYBER-1024”. *) 82 | const info_x25519_sha512_kyber1024:bitstring. 83 | 84 | 85 | fun concat(bitstring,bitstring): bitstring [data]. 86 | fun concat9(point,point,point,point,bitstring,point,point,bool,kempub):bitstring [data]. 87 | fun concat8(point,point,point,bitstring,point,point,bool,kempub):bitstring [data]. 88 | 89 | (* HKDF *) 90 | 91 | (* One-shot HKDF(input_key_material, salt, info) *) 92 | type symkey. 93 | fun hkdf(bitstring, bitstring, bitstring) : symkey. 94 | 95 | letfun kdf(km:bitstring) = 96 | hkdf(concat(ff_x25519,km), 97 | zeroes_sha512, 98 | info_x25519_sha512_kyber1024). 99 | 100 | 101 | (* AEAD Encryption *) 102 | type nonce. 103 | const empty_nonce:nonce. 104 | 105 | fun aead_enc(symkey,nonce,bitstring,bitstring):bitstring. 106 | fun aead_dec(symkey,nonce,bitstring,bitstring):bitstring 107 | reduc forall k:symkey,n:nonce,m:bitstring,ad:bitstring; 108 | aead_dec(k,n,aead_enc(k,n,m,ad),ad) = m. 109 | 110 | (* XEdDSA Signatures *) 111 | fun sign(scalar,bitstring,nonce):bitstring. 112 | fun verify(point,bitstring,bitstring):bool 113 | reduc forall sk:scalar,m:bitstring,n:nonce; 114 | verify(SMUL(sk,G),m,sign(sk,m,n)) = true. 115 | 116 | 117 | 118 | event isBool(bool). 119 | 120 | restriction b:bool; 121 | event(isBool(b)) ==> b=true || b=false. 122 | 123 | (*-----------------------------------*) 124 | (* PKI *) 125 | (*-----------------------------------*) 126 | 127 | (* Clients representing devices: Alice, Bob, etc. *) 128 | type client. 129 | 130 | (* Global PKI maintained by the Server, checked by Clients *) 131 | 132 | (* For each client Bob: 133 | - Bob’s curve identity key IKB *) 134 | 135 | table identity_pubkeys(client,point). 136 | 137 | 138 | (*-----------------------------------*) 139 | (* Security Model and Properties *) 140 | (*-----------------------------------*) 141 | 142 | (* A channel for the attacker *) 143 | free att:channel. 144 | 145 | (* A channel for the server, but in fact controlled by the attacker. *) 146 | 147 | free server:channel. 148 | 149 | 150 | (* An event triggered when the private keys of a client are compromised *) 151 | 152 | (* Handshake Events *) 153 | event InitDone(client,client,bool,point,point,bool,kempub,symkey). 154 | event RespondDone(client,client,bool,point,point,bool,kempub,symkey). 155 | 156 | (* Application Messages and Events *) 157 | fun app_message(client,client,bitstring):bitstring [private]. 158 | event AppSend(client,client,bitstring). 159 | event AppRecv(client,client,bitstring). 160 | 161 | 162 | (* Compromise Events *) 163 | event CompromiseIK(client). 164 | event CompromiseSPK(client,point). 165 | event CompromiseOPK(client,point). 166 | event CompromisePQPK(client,kempub). 167 | 168 | #ifdef Reach 169 | 170 | (* Reachability Queries *) 171 | query i:client, r:client, ts:symkey, m:bitstring, opk:point, spk:point, pqpk:kempub; 172 | event(InitDone(i,r,true,opk,spk,true,pqpk,ts)); 173 | event(InitDone(i,r,false,opk,spk,true,pqpk,ts)); 174 | event(RespondDone(r,i,true,opk,spk,true,pqpk,ts)); 175 | event(RespondDone(r,i,false,opk,spk,true,pqpk,ts)). 176 | 177 | #endif 178 | 179 | 180 | (*-----------------------------------*) 181 | (* Security Model and Properties *) 182 | (*-----------------------------------*) 183 | 184 | #ifdef SecrecyInit 185 | 186 | query a,b:client, useOPK,last_resort_PQPK:bool, opk,spk:point, pqpk:kempub, ts:symkey, i,j:time; 187 | event(InitDone(a,b,useOPK,opk,spk,last_resort_PQPK,pqpk,ts))@i && attacker(ts) ==> 188 | (* A compromise of IKB in the past is enough to break everything. *) 189 | (event(CompromiseIK(b))@j && (j < i 190 | || 191 | ( 192 | event(CompromiseSPK(b,spk)) 193 | && 194 | (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM)) 195 | )) 196 | 197 | ) 198 | || 199 | (event(BrokenDH())@j && (j < i 200 | || event(CompromisePQPK(b,pqpk)) 201 | || event(BrokenKEM) 202 | )) 203 | . 204 | 205 | 206 | 207 | #endif 208 | 209 | #ifdef SecrecyResp 210 | 211 | query a,b:client, useOPK,last_resort_PQPK:bool, opk,spk:point, pqpk:kempub, ts:symkey, i,j1,j2:time; 212 | event(RespondDone(b,a, useOPK, opk,spk,last_resort_PQPK,pqpk,ts))@i && attacker(ts) ==> 213 | (* The compromise of IKA allows the attacker to play the role of A, and thus know the key *) 214 | (event(CompromiseIK(a))@j1 && j1 < i) 215 | || 216 | (* If IKA is not corrupted, we must corrupt some information on the side of B *) 217 | (event(CompromiseSPK(b,spk))@j1 && 218 | (* A compromise of some SPKb in the past allow an attacker to impersonate any A *) 219 | (j1 < i || 220 | ( 221 | event(CompromiseIK(b))@j2 && (j2 < i 222 | || 223 | event(CompromisePQPK(b,pqpk)) 224 | || 225 | event(BrokenKEM) 226 | 227 | ) 228 | && (useOPK=false || event(CompromiseOPK(b,opk))) 229 | 230 | ) 231 | ) 232 | ) 233 | || 234 | (event(BrokenDH())@j1 && (j1 < i 235 | || 236 | event(CompromisePQPK(b,pqpk)) 237 | || 238 | event(BrokenKEM) 239 | ) 240 | ) 241 | . 242 | 243 | #endif 244 | 245 | #ifdef Authentication 246 | 247 | query a,b:client, useOPK,last_resort_PQPK:bool, opk,opk2,spk:point, pqpk:kempub, ts:symkey, i,j:time; 248 | event(RespondDone(b,a,useOPK,opk,spk,last_resort_PQPK,pqpk,ts))@i ==> 249 | (* authentication including over the opk *) 250 | (useOPK = true && event(InitDone(a,b,true, opk,spk,last_resort_PQPK,pqpk,ts))) 251 | || 252 | (* authentication not over the opk *) 253 | (useOPK = false && event(InitDone(a,b,false, opk2,spk,last_resort_PQPK,pqpk,ts))) 254 | (* Unless *) 255 | || 256 | (* The compromise of IKA allows the attacker to play the role of A, and thus know the key *) 257 | (event(CompromiseIK(a))@j && j < i) 258 | || 259 | (* Knowing a SPK completely allows to break auth *) 260 | (event(CompromiseSPK(b,spk))@j && j < i ) 261 | || 262 | (event(BrokenDH())@j && j < i) 263 | . 264 | 265 | #endif 266 | 267 | 268 | 269 | nounif z:scalar; attacker(SMUL(z,G)) / 30000. 270 | 271 | 272 | (* DH breaks down *) 273 | fun discreteLog(point): scalar 274 | reduc forall s:scalar, y:point; 275 | discreteLog(SMUL(s,G)) = s [private]. (* Remark, this dL does not work on a g^xy model *) 276 | 277 | event BrokenDH. 278 | 279 | let dh_attacks() = 280 | in (att,p:point); 281 | event BrokenDH; 282 | out (att,discreteLog(p)). 283 | 284 | 285 | 286 | (* KEM breaks down *) 287 | fun inversePK(kempub): kempriv 288 | reduc forall s:kempriv; 289 | inversePK(kempk(s)) = s [private]. 290 | 291 | event BrokenKEM. 292 | 293 | let kem_attacks() = 294 | in (att,p:kempub); 295 | event BrokenKEM; 296 | out (att,inversePK(p)). 297 | 298 | 299 | (*-----------------------------------*) 300 | (* Protocol Processes *) 301 | (*-----------------------------------*) 302 | 303 | (* PQXDH Protocol *) 304 | 305 | (* Alice then sends Bob an initial message containing: 306 | - Alice’s identity key IKA 307 | - Alice’s ephemeral key EKA 308 | - The pqkem ciphertext CT encapsulating SS for PQPKB 309 | - Identifiers stating which of Bob’s prekeys Alice used 310 | - An initial ciphertext encrypted with some AEAD encryption scheme [5] using AD as associated data and using an encryption key which is either SK or the output from some cryptographic PRF keyed by SK. 311 | *) 312 | 313 | const init:bitstring. 314 | const resp:bitstring. 315 | 316 | 317 | let Initiator(i:client, IKA_s:scalar) = 318 | (* we let the attacker choose the responder we will communicate with *) 319 | in(att,r:client); 320 | (* if not(r=i) then *) 321 | (* Initiator public key *) 322 | let IKA_p = s2p(IKA_s) in 323 | 324 | (* Retrieve the responder identity key *) 325 | get identity_pubkeys(=r,IKB_p) in 326 | 327 | (* receive from the server the pre key bundles *) 328 | in(server, (SPKB_p:point,SPKB_sig:bitstring,is_last_restorPQPK:bool, PQPKB_p:kempub,PQPKB_sig:bitstring)); 329 | (* Verify the signatures *) 330 | if verify(IKB_p,encodeEC(SPKB_p),SPKB_sig) then 331 | if verify(IKB_p,encodeKEM(is_last_restorPQPK,PQPKB_p),PQPKB_sig) then (* Here, we don't know whether we have a last resort or one time key. *) 332 | 333 | ( 334 | (* Optionally receive a one time key *) 335 | in (server,(useOPK:bool,OPKB_p:point)); 336 | event isBool(useOPK); 337 | let (CT:bitstring,SS:bitstring) = pqkem_enc(PQPKB_p) in 338 | 339 | new EKA_s:scalar; 340 | 341 | let EKA_p = s2p(EKA_s) in 342 | let DH1 = dh(IKA_s,SPKB_p) in 343 | let DH2 = dh(EKA_s,IKB_p) in 344 | let DH3 = dh(EKA_s,SPKB_p) in 345 | 346 | let SK = 347 | (if useOPK then (let DH4 = dh(EKA_s,OPKB_p) in 348 | kdf(concat9(DH1,DH2,DH3,DH4,SS,IKA_p,IKB_p,is_last_restorPQPK,PQPKB_p))) 349 | else kdf(concat8(DH1,DH2,DH3,SS,IKA_p,IKB_p,is_last_restorPQPK,PQPKB_p))) 350 | in 351 | 352 | event InitDone(i,r,useOPK,OPKB_p,SPKB_p,is_last_restorPQPK,PQPKB_p,SK); 353 | 354 | let ad = zero in 355 | new msg_nonce: bitstring; 356 | let msg = app_message(i,r,msg_nonce) in 357 | let enc_msg = aead_enc(SK,empty_nonce,msg,ad) in 358 | (* Send Message *) 359 | out(server, (IKA_p,EKA_p,CT, OPKB_p, SPKB_p, PQPKB_p, enc_msg)) 360 | 361 | ). 362 | 363 | 364 | let Responder_mess(r:client, IKB_s:scalar, SPKB_s:scalar,PQSPKB_s:kempriv)= 365 | ( 366 | let IKB_p = s2p(IKB_s) in 367 | let SPKB_p = s2p(SPKB_s) in 368 | let PQSPKB_p = kempriv2pub(PQSPKB_s) in 369 | 370 | (* Generate a new one time OPKB *) 371 | new OPKB_s:scalar; 372 | let OPKB_p = s2p(OPKB_s) in 373 | 374 | (* send public keys to server *) 375 | out (server,OPKB_p ); 376 | 377 | (in(att, =r); event CompromiseOPK(r, OPKB_p); out(att,OPKB_s)) 378 | | 379 | 380 | 381 | (* Receive Message with the currently stored public keys *) 382 | ( 383 | in (server,(IKA_p:point,EKA_p:point,CT:bitstring,useOPK:bool,=OPKB_p,=SPKB_p,=PQSPKB_p, enc_msg:bitstring)); 384 | event isBool(useOPK); 385 | (* Verify remote identity key *) 386 | get identity_pubkeys(i,=IKA_p) in 387 | (* if not(r=i) then *) 388 | ( 389 | (* Retrieve one-time private keys *) 390 | 391 | let SS = pqkem_dec(PQSPKB_s,CT) in 392 | let DH1 = dh(SPKB_s,IKA_p) in 393 | let DH2 = dh(IKB_s, EKA_p) in 394 | let DH3 = dh(SPKB_s,EKA_p) in 395 | let SK = 396 | (if useOPK then (let DH4 = dh(OPKB_s,EKA_p) in 397 | kdf(concat9(DH1,DH2,DH3,DH4,SS,IKA_p,IKB_p,true,PQSPKB_p))) 398 | else kdf(concat8(DH1,DH2,DH3,SS,IKA_p,IKB_p,true,PQSPKB_p))) 399 | in 400 | 401 | let ad = zero in 402 | let msg = aead_dec(SK,empty_nonce,enc_msg,ad) in 403 | event RespondDone(r,i,useOPK,OPKB_p,SPKB_p,true,PQSPKB_p,SK) 404 | 405 | ) 406 | ) 407 | ). 408 | 409 | 410 | let Responder(r:client, IKB_s:scalar) = 411 | let IKB_p = s2p(IKB_s) in 412 | 413 | (* Creates a new DH based Signed Pre-Key *) 414 | new SPKB_s:scalar; 415 | let SPKB_p = s2p(SPKB_s) in 416 | new zSPKB:nonce; 417 | let SPKB_sig = sign(IKB_s,encodeEC(SPKB_p),zSPKB) in 418 | 419 | (* Creates a new KEM based Signed Pre-Key *) 420 | new PQSPKB_s:kempriv; 421 | let PQSPKB_p = kempriv2pub(PQSPKB_s) in 422 | new zPQSPKB:nonce; 423 | (* worst case setting, we assume all KEM PQSPKB are last resorts and can be reused. *) 424 | let PQSPKB_sig = sign(IKB_s,encodeKEM(true,PQSPKB_p),zPQSPKB) in 425 | 426 | 427 | (* send public keys to server *) 428 | out (att,(SPKB_p,SPKB_sig,PQSPKB_p,PQSPKB_sig)); 429 | 430 | (in(att, =r); event CompromiseSPK(r, SPKB_p); out(att,SPKB_s)) 431 | | 432 | (in(att, =r); event CompromisePQPK(r, PQSPKB_p); out(att,PQSPKB_s)) 433 | | 434 | (! Responder_mess(r, IKB_s, SPKB_s,PQSPKB_s)) 435 | . 436 | 437 | 438 | (* A process to create a new client and publish its keys *) 439 | let Launch_pqxdh_client(p:client) = 440 | (* Create a new client *) 441 | new IK_s:scalar; 442 | let IK_p = s2p(IK_s) in 443 | insert identity_pubkeys(p,IK_p); 444 | (* Publish the public key identity pair. *) 445 | out(att,IK_p); 446 | ( 447 | (* Initiator role *) 448 | ! Initiator(p, IK_s) 449 | (* Responder role *) 450 | | ! Responder(p, IK_s) 451 | 452 | | 453 | (* Compromise of the identity key *) 454 | (in(att, =p); event CompromiseIK(p); out(att,IK_s)) 455 | ). 456 | 457 | 458 | 459 | 460 | (* Main Process: any number of clients and members *) 461 | 462 | process 463 | ! in(att,c:client); Launch_pqxdh_client(c) 464 | #if !defined(UnbreakableDH) 465 | | ! dh_attacks 466 | #endif 467 | #if !defined(UnbreakableKEM) 468 | | ! kem_attacks 469 | #endif 470 | 471 | 472 | 473 | (*************************************) 474 | (* EXPECTED RESULTS *) 475 | (*************************************) 476 | 477 | (* 478 | 479 | $ make 480 | 481 | 482 | Verification summary: 483 | 484 | Query(ies): 485 | - Query event(InitDone(a,b,useOPK_2,opk,spk,last_resort_PQPK,pqpk,ts))@i_1 && attacker(ts) ==> (event(CompromiseIK(b))@j && (i_1 > j || (event(CompromiseSPK(b,spk)) && (event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))))) || (event(BrokenDH)@j && (i_1 > j || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))) is true. 486 | - Query event(RespondDone(b,a,useOPK_2,opk,spk,last_resort_PQPK,pqpk,ts))@i_1 && attacker(ts) ==> (event(CompromiseIK(a))@j1 && i_1 > j1) || (event(CompromiseSPK(b,spk))@j1 && (i_1 > j1 || (event(CompromiseIK(b))@j2 && (i_1 > j2 || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM)) && (useOPK_2 = false || event(CompromiseOPK(b,opk)))))) || (event(BrokenDH)@j1 && (i_1 > j1 || event(CompromisePQPK(b,pqpk)) || event(BrokenKEM))) is true. 487 | - Query event(RespondDone(b,a,useOPK_2,opk,spk,last_resort_PQPK,pqpk,ts))@i_1 ==> (useOPK_2 = true && event(InitDone(a,b,true,opk,spk,last_resort_PQPK,pqpk,ts))) || (useOPK_2 = false && event(InitDone(a,b,false,opk2,spk,last_resort_PQPK,pqpk,ts))) || (event(CompromiseIK(a))@j && i_1 > j) || (event(CompromiseSPK(b,spk))@j && i_1 > j) || (event(BrokenDH)@j && i_1 > j) is true. 488 | Associated restriction(s): 489 | - Restriction event(isBool(b)) ==> b = true || b = false encoded as event(isBool(b)) ==> b = true || b = false in process 1. 490 | 491 | -------------------------------------------------------------- 492 | 493 | 494 | real 1m45,219s 495 | 496 | 497 | 498 | *) 499 | 500 | (* 501 | 502 | $ make reach 503 | 504 | -------------------------------------------------------------- 505 | Verification summary: 506 | 507 | Query not event(InitDone(i_2,r_1,true,opk,spk,pqpk,ts)) is false. 508 | 509 | Query not event(InitDone(i_2,r_1,false,opk,spk,pqpk,ts)) is false. 510 | 511 | Query not event(RespondDone(r_1,i_2,true,opk,spk,pqpk,ts)) is false. 512 | 513 | Query not event(RespondDone(r_1,i_2,false,opk,spk,pqpk,ts)) is false. 514 | 515 | -------------------------------------------------------------- 516 | 517 | 518 | real 0m0,9 519 | 520 | *) -------------------------------------------------------------------------------- /revision3/proverif/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run the C preprocessor on the input file, produces a .pv file and runs proverif on it 3 | 4 | res="" 5 | name="" 6 | for def in ${@:2} 7 | do 8 | res+=" --define-macro=$def=$def" 9 | name+="$def" 10 | done 11 | if [ "$name" = "" ]; then 12 | file="$1.gen.pv" 13 | else 14 | file="$1-$name.gen.pv" 15 | fi 16 | eval "( 17 | echo \"(* !!! WARNING !!! *)\"; 18 | echo \"(* File generated from with ./run.sh $@*)\"; 19 | echo \"(* Read the README for more informations *)\"; 20 | echo \"(* ------------------------------------- *)\"; 21 | cpp -P -E -w $res $1.cpp.pv; 22 | ) > $file" 23 | time proverif $file 24 | --------------------------------------------------------------------------------