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