scal3/
lib.rs

1//! # Sole Control Assurance Level 3
2//!
3//! [Verify that systems operate under your sole control](https://github.com/cleverbase/scal3).
4//! SCAL3 provides verifiable sole control assurance levels with tamper-evident
5//! logs for multi-factor authentication transparency. This prototype contains
6//! example functions and data. It implements the protocol from the technical
7//! report “Authentication and sole control at a high level of assurance on
8//! widespread smartphones with threshold signatures” in [Cryptology ePrint
9//! Archive, Paper 2025/267](https://eprint.iacr.org/2025/267).
10//!
11//! <div class="warning">
12//! <strong>Do not use this code for production.</strong>
13//! The specification has not been finalized and the security of this prototype
14//! code has not been evaluated.
15//! The code is available for transparency and to enable public review.
16//! </div>
17//!
18//! ## Legal
19//!
20//! Patent NL2037022 pending.
21//!
22//! Copyright Cleverbase ID B.V. 2024. The code and documentation are licensed under
23//! [Creative Commons Attribution-NonCommercial 4.0 International](https://creativecommons.org/licenses/by-nc/4.0/).
24//!
25//! To discuss other licensing options,
26//! [contact Cleverbase](mailto:sander.dijkhuis@cleverbase.com).
27//!
28//! ## Example application context
29//!
30//! A provider manages a central hardware security module (HSM) that performs
31//! instructions under sole control of its subscribers. Subscribers use a mobile
32//! wallet app to authorize operations using a PIN code.
33//!
34//! To achieve SCAL3, the provider manages three assets:
35//!
36//! - a public key certificate to link the subscriber to enrolled keys, e.g.
37//!   applying X.509 ([RFC 5280](https://www.rfc-editor.org/rfc/rfc5280));
38//! - a tamper-evident log to record evidence of authentic instructions, e.g.
39//!   applying [Trillian](https://transparency.dev/);
40//! - a PIN attempt counter, e.g. using HSM-synchronized state.
41//!
42//! To enroll for a certificate, the subscriber typically uses a protocol such as
43//! ACME ([RFC 8555](https://www.rfc-editor.org/rfc/rfc8555)). The
44//! certificate binds to the subscriber’s subject identifier an (attested) P-256
45//! ECDSA signing key from Secure Enclave, StrongBox Keymaster, or Android’s
46//! hardware-backed Keystore. This is the possession factor for authentication.
47//!
48//! During enrollment, the provider also performs generation of a SCAL3 user
49//! identifier and pre-authorization of this identifier for certificate issuance.
50//! This part of enrollment applies [FROST](https://eprint.iacr.org/2020/852)
51//! distributed key generation and requires the subscriber to set their PIN.
52//!
53//! During authentication, the certified identifier contains all information needed
54//! for the original provider and subscriber to determine their secret signing
55//! shares. The process applies FROST two-round threshold signing, combined with
56//! ECDSA to prove possession of the enrolled device. Successful authentication
57//! leads to recorded evidence that can be publicly verified.
58//!
59//! By design, the certificate and the evidence provide no information about the
60//! PIN.  This means that even attackers with access to the device, the certificate
61//! and  the log cannot bruteforce the PIN, since they would need to verify each
62//! attempt using the rate-limited provider service.
63//!
64//! ## Cryptography overview
65//!
66//! This prototype uses the P-256 elliptic curve with order <i>p</i> and common base
67//! point <i>G</i> for all keys.
68//!
69//! To the provider and subscriber, signing shares are assigned of the form
70//! <i>s</i><sub><i>i</i></sub> =
71//!   <i>a</i><sub>10</sub> +
72//!   <i>a</i><sub>11</sub><i>i</i> +
73//!   <i>a</i><sub>20</sub> +
74//!   <i>a</i><sub>21</sub><i>i</i>
75//!   (mod <i>p</i>)
76//! where the provider has participant identifier <i>i</i> = 1
77//! and the subscriber has <i>i</i> = 2.
78//! During enrollment, the subscriber has randomly generated joint secret key
79//! <i>s</i> = <i>s</i><sub>1</sub><i>s</i><sub>2</sub> and computed
80//! <i>a</i><sub><i>ij</i></sub> as a trusted dealer.
81//! The resulting joint verifying key equals
82//! <i>V</i><sub>k</sub> = [<i>a</i><sub>10</sub> + <i>a</i><sub>20</sub>]<i>G</i>.
83//!
84//! The SCAL3 user identifier consists of <i>V</i><sub>k</sub> and:
85//!
86//! - <i>s</i><sub>1</sub> encrypted for the provider;
87//! - <i>s</i><sub>2</sub> + <i>m</i><sub>2</sub> (mod <i>p</i>)
88//!   where <i>m</i><sub>2</sub> is a key securely derived by the subscriber from
89//!   the PIN, for example using PRF(<i>k</i>, <i>PIN</i>) with a local
90//!   hardware-backed key <i>k</i>, followed by `hash_to_field` from
91//!   [RFC 9380](https://www.rfc-editor.org/rfc/rfc9380).
92//!
93//! During authentication, the subscriber generates an ephemeral ECDSA binding key
94//! pair
95//! (<i>s</i><sub>b</sub>, <i>V</i><sub>b</sub>)
96//! and forms a message <i>M</i> that includes <i>V</i><sub>b</sub>,
97//! the instruction to authorize, and log metadata.
98//! Applying FROST threshold signing, both parties generate secret nonces
99//! (<i>d</i><sub><i>i</i></sub>, <i>e</i><sub><i>i</i></sub>)
100//! and together they form a joint signature
101//! (<i>c</i>, <i>z</i>) over <i>M</i>. To do so, they compute with domain-separated
102//! hash functions #<sub>1</sub> and #<sub>2</sub>:
103//!
104//! - commitment shares
105//!   (<i>D</i><sub><i>i</i></sub>, <i>E</i><sub><i>i</i></sub>) =
106//!   ([<i>d</i><sub><i>i</i></sub>]<i>G</i>, [<i>e</i><sub><i>i</i></sub>]<i>G</i>);
107//! - binding factors
108//!   <i>ρ</i><sub><i>i</i></sub> = #<sub>1</sub>(<i>i</i>, <i>M</i>, <i>B</i>)
109//!   where <i>B</i> represents a list of all commitment shares;
110//! - commitment
111//!   <i>R</i> =
112//!     <i>D</i><sub>1</sub> +
113//!     [<i>ρ</i><sub><i>1</i></sub>]<i>E</i><sub><i>1</i></sub> +
114//!     <i>D</i><sub>2</sub> +
115//!     [<i>ρ</i><sub><i>2</i></sub>]<i>E</i><sub><i>2</i></sub>;
116//! - challenge <i>c</i> = #<sub>2</sub>(<i>R</i>, <i>V</i><sub>k</sub>, <i>M</i>);
117//! - signature share
118//!   <i>z</i><sub><i>i</i></sub> =
119//!     <i>d</i><sub><i>i</i></sub> +
120//!     <i>e</i><sub><i>i</i></sub><i>ρ</i><sub><i>i</i></sub> +
121//!     <i>c</i><i>λ</i><sub><i>i</i></sub><i>s</i><sub><i>i</i></sub>
122//!     (mod <i>p</i>)
123//!   with <i>λ</i><sub>1</sub> = 2 and <i>λ</i><sub>2</sub> = −1;
124//! - proof
125//!   <i>z</i> = <i>z</i><sub>1</sub> + <i>z</i><sub>2</sub>.
126//!
127//! All subscriber’s contributions are part of a single “pass the authentication
128//! challenge” message that includes:
129//!
130//! - a device signature created using the possession factor over <i>c</i>;
131//! - a binding signature created using <i>s</i><sub>b</sub> over the device
132//!   signature.
133//!
134//! This construction makes sure that without simultaneous control over both
135//! authentication factors, evidence cannot be forged.
136//!
137//! # Examples
138//!
139//! All functions are pure, enabling a mostly stateless server
140//! implementation and easy integration on mobile client platforms.
141//!
142//! ## Setup
143//!
144//! Generate a P-256 ECDH key pair and a PRF secret key for the provider.
145//! In production, protect these with a hardware security module.
146//!
147//! ```
148//! # use hmac::digest::KeyInit;
149//! # use hmac::Hmac;
150//! # use p256::elliptic_curve::sec1::ToEncodedPoint;
151//! # use sha2::Sha256;
152//! # use signature::rand_core::OsRng;
153//! # use scal3::*;
154//! #
155//! # fn sec1_compressed(pk: p256::PublicKey) -> Key {
156//! #     pk.to_encoded_point(true).as_ref().try_into().unwrap()
157//! # }
158//! #
159//! let sk_provider = p256::SecretKey::random(&mut OsRng);
160//! let pk_provider = sec1_compressed(sk_provider.public_key());
161//! let k_provider = Hmac::<Sha256>::generate_key(&mut OsRng);
162//! ```
163//!
164//! ## Enrolment
165//!
166//! Generate a P-256 ECDSA key pair and a PRF secret key for the subscriber.
167//! In production, protect these with a local secure area.
168//!
169//! Aborting upon failure, the [provider] and [subscriber] execute their
170//! assigned functions in this order:
171//!
172//! 1. [subscriber]: derive a [Mask], obtain [Randomness],
173//!    [subscriber::register] and send a [Key] with [Verifier].
174//! 2. [provider]: derive the [Secret] and [provider::accept].
175//!
176//! In production, the [provider] would need to furthermore verify
177//! possession of the device [Key] and bind these, for example in
178//! a public key certificate.
179//!
180//! ```
181//! # use hmac::digest::{crypto_common, KeyInit};
182//! # use hmac::{Hmac, Mac};
183//! # use p256::elliptic_curve::sec1::ToEncodedPoint;
184//! # use signature::rand_core::{OsRng, RngCore};
185//! # use sha2::Sha256;
186//! # use scal3::*;
187//! # fn sec1_compressed(pk: p256::PublicKey) -> Key {
188//! #     pk.to_encoded_point(true).as_ref().try_into().unwrap()
189//! # }
190//! # fn prf(k: &crypto_common::Key<Hmac<Sha256>>, msg: &[u8]) -> [u8; 32] {
191//! #     <Hmac<Sha256> as Mac>::new(k)
192//! #         .chain_update(msg)
193//! #         .finalize()
194//! #         .into_bytes()
195//! #         .try_into()
196//! #         .unwrap()
197//! # }
198//! # fn ecdh(sk: &p256::SecretKey, pk: &[u8; 33]) -> Secret {
199//! #     let sk = p256::NonZeroScalar::from_repr(sk.to_bytes()).unwrap();
200//! #     let pk = p256::PublicKey::from_sec1_bytes(pk).unwrap();
201//! #     let secret = p256::ecdh::diffie_hellman(sk, pk.as_affine());
202//! #     let bytes = secret.raw_secret_bytes().clone();
203//! #     bytes.into()
204//! # }
205//! # let sk_provider = p256::SecretKey::random(&mut OsRng);
206//! # let pk_provider = sec1_compressed(sk_provider.public_key());
207//! # let k_provider = Hmac::<Sha256>::generate_key(&mut OsRng);
208//! # let mut randomness = [0u8; size_of::<Randomness>()];
209//! # let mut subscriber = [0u8; size_of::<Key>()];
210//! # let mut verifier = [0u8; size_of::<Verifier>()];
211//! # let mut challenge = [0u8; size_of::<Challenge>()];
212//! # let mut mask = [0u8; size_of::<Mask>()];
213//! let sk_subscriber = p256::ecdsa::SigningKey::random(&mut OsRng);
214//! let pk_subscriber = sec1_compressed(sk_subscriber.verifying_key().into());
215//! let k_subscriber = Hmac::<Sha256>::generate_key(&mut OsRng);
216//!
217//! mask.copy_from_slice(&prf(&k_subscriber, b"123456"));
218//! OsRng.fill_bytes(&mut randomness);
219//! subscriber::register(
220//!     &mask,
221//!     &randomness,
222//!     &pk_provider,
223//!     &mut subscriber,
224//!     &mut verifier,
225//! );
226//!
227//! assert!(provider::accept(
228//!     &pk_provider,
229//!     &ecdh(&sk_provider, &subscriber),
230//!     &verifier
231//! ));
232//! ```
233//!
234//! ## Authentication
235//!
236//! Aborting upon failure:
237//!
238//! 1. [provider]: derive [Randomness], [provider::challenge] and send
239//!    a [Challenge].
240//! 2. [subscriber]: derive a [Mask], obtain [Randomness],
241//!    [subscriber::authenticate], create [Proof] of possession,
242//!    [subscriber::pass] and send a [Pass].
243//! 3. [provider]: [provider::prove] authentication and log
244//!    [Authenticator], [Proof] and [Client] verification data.
245//!
246//! ```
247//! # use std::ptr::null_mut;
248//! # use hmac::digest::{crypto_common, KeyInit};
249//! # use hmac::{Hmac, Mac};
250//! # use p256::elliptic_curve::sec1::ToEncodedPoint;
251//! # use signature::rand_core::{OsRng, RngCore};
252//! # use sha2::{Digest as Sha2Digest, Sha256};
253//! # use signature::hazmat::PrehashSigner;
254//! # use scal3::*;
255//! # fn sec1_compressed(pk: p256::PublicKey) -> Key {
256//! #     pk.to_encoded_point(true).as_ref().try_into().unwrap()
257//! # }
258//! # fn prf(k: &crypto_common::Key<Hmac<Sha256>>, msg: &[u8]) -> [u8; 32] {
259//! #     <Hmac<Sha256> as Mac>::new(k)
260//! #         .chain_update(msg)
261//! #         .finalize()
262//! #         .into_bytes()
263//! #         .try_into()
264//! #         .unwrap()
265//! # }
266//! # fn ecdh(sk: &p256::SecretKey, pk: &[u8; 33]) -> Secret {
267//! #     let sk = p256::NonZeroScalar::from_repr(sk.to_bytes()).unwrap();
268//! #     let pk = p256::PublicKey::from_sec1_bytes(pk).unwrap();
269//! #     let secret = p256::ecdh::diffie_hellman(sk, pk.as_affine());
270//! #     let bytes = secret.raw_secret_bytes().clone();
271//! #     bytes.into()
272//! # }
273//! # fn sign_prehash(sk: &p256::ecdsa::SigningKey, hash: &Digest, proof: &mut Proof) {
274//! #     let (signature, _) = sk.sign_prehash(hash).unwrap();
275//! #     proof.copy_from_slice(&signature.to_bytes());
276//! # }
277//! # let sk_provider = p256::SecretKey::random(&mut OsRng);
278//! # let pk_provider = sec1_compressed(sk_provider.public_key());
279//! # let k_provider = Hmac::<Sha256>::generate_key(&mut OsRng);
280//! # let mut randomness = [0u8; size_of::<Randomness>()];
281//! # let mut subscriber = [0u8; size_of::<Key>()];
282//! # let mut verifier = [0u8; size_of::<Verifier>()];
283//! # let mut challenge = [0u8; size_of::<Challenge>()];
284//! # let mut mask = [0u8; size_of::<Mask>()];
285//! # let mut client_data_hash = [0u8; size_of::<Digest>()];
286//! # let mut to_sign = [0u8; size_of::<Digest>()];
287//! # let mut proof = [0u8; size_of::<Proof>()];
288//! # let mut sender = [0u8; size_of::<Key>()];
289//! # let mut pass = [0u8; size_of::<Pass>()];
290//! # let mut authenticator = [0u8; size_of::<Authenticator>()];
291//! # let mut client = [0u8; size_of::<Client>()];
292//! # let sk_subscriber = p256::ecdsa::SigningKey::random(&mut OsRng);
293//! # let pk_subscriber = sec1_compressed(sk_subscriber.verifying_key().into());
294//! # let k_subscriber = Hmac::<Sha256>::generate_key(&mut OsRng);
295//! # mask.copy_from_slice(&prf(&k_subscriber, b"123456"));
296//! # OsRng.fill_bytes(&mut randomness);
297//! # subscriber::register(
298//! #     &mask,
299//! #     &randomness,
300//! #     &pk_provider,
301//! #     &mut subscriber,
302//! #     &mut verifier,
303//! # );
304//! let challenge_data = b"ts=1743930934&nonce=000001";
305//! randomness.copy_from_slice(&prf(&k_provider, challenge_data));
306//! provider::challenge(&randomness, &mut challenge);
307//!
308//! mask.copy_from_slice(&prf(&k_subscriber, b"123456"));
309//! OsRng.fill_bytes(&mut randomness);
310//! let client_data = b"{\"operation\":\"log-in\",\"session\":\"68c9eeeddfa5fb50\"}";
311//! client_data_hash.copy_from_slice(Sha256::digest(client_data).as_slice());
312//! let authentication = subscriber::authenticate(
313//!     &mask,
314//!     &randomness,
315//!     &pk_provider,
316//!     &subscriber,
317//!     &verifier,
318//!     &challenge,
319//!     &client_data_hash,
320//!     &mut to_sign,
321//! );
322//! assert_ne!(null_mut(), authentication);
323//! sign_prehash(&sk_subscriber, &to_sign, &mut proof);
324//! assert!(subscriber::pass(
325//!     authentication,
326//!     &proof,
327//!     &mut sender,
328//!     &mut pass
329//! ));
330//! randomness.copy_from_slice(&prf(&k_provider, challenge_data));
331//!
332//! assert!(provider::prove(
333//!     &randomness,
334//!     &pk_provider,
335//!     &ecdh(&sk_provider, &subscriber),
336//!     &verifier,
337//!     &pk_subscriber,
338//!     &client_data_hash,
339//!     &ecdh(&sk_provider, &sender),
340//!     &pass,
341//!     &mut authenticator,
342//!     &mut proof,
343//!     &mut client
344//! ));
345//! # assert!(verify(
346//! #     &verifier,
347//! #     &pk_subscriber,
348//! #     &client_data_hash,
349//! #     &authenticator,
350//! #     &proof,
351//! #     &client
352//! # ));
353//! ```
354//!
355//! ## Auditing
356//!
357//! The [subscriber] or any other party with access can [verify] the evidence
358//! consisting of [Authenticator], [Proof] and [Client] data.
359//!
360//! ```ignore
361//! assert!(verify(
362//!    &verifier,
363//!    &pk_subscriber,
364//!    &client_data_hash,
365//!    &authenticator,
366//!    &proof,
367//!    &client
368//! ))
369//! ```
370//!
371//! # Risks
372//!
373//! - The implementation may still be vulnerability to side channel attacks,
374//!   such as timing attacks and reading memory that was not zeroized in time.
375//!   The security dependencies offer functions to implement this properly.
376//! - Not all pass details are protected using the device signature, enabling
377//!   a denial-of-service attack by changing details.
378
379mod program;
380mod domain;
381mod kem;
382mod rng;
383pub(crate) mod api;
384
385pub use api::*;