forked from cleverbase/scal3
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: enroll, authenticate, verify with SCAL3
- Loading branch information
Showing
12 changed files
with
1,949 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
.DS_Store | ||
target | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
[package] | ||
name = "scal3" | ||
description = "Verify that systems operate under your sole control (prototype, patent pending)" | ||
license = "CC-BY-NC-4.0" | ||
version = "0.1.0" | ||
edition = "2021" | ||
repository = "https://github.com/cleverbase/scal3" | ||
authors = ["Sander Dijkhuis <[email protected]>"] | ||
readme = "src/README.md" | ||
categories = ["authentication", "cryptography"] | ||
exclude = ["/README.md", "/docs/media/scal3.png"] | ||
|
||
[dependencies] | ||
frost-core = { version = "0.7.0", features = ["internals"] } | ||
frost-p256 = { version = "0.7.0" } | ||
libc = "0.2.153" | ||
p256 = "0.13.2" | ||
postcard = "1.0.8" | ||
rand = "0.8.5" | ||
serde = { version = "1.0.197", features = ["derive"] } | ||
serdect = "0.2.0" | ||
sha2 = "0.10.8" | ||
signature = { version = "2.2.0", features = ["digest"] } | ||
zeroize = "1.7.0" | ||
|
||
[dev-dependencies] | ||
hex = "0.4.3" | ||
hex-literal = "0.4.1" | ||
hmac = "0.12.1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
# 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. | ||
|
||
<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:[email protected]). | ||
|
||
## 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, 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 provider has randomly generated <i>a</i><sub>10</sub> | ||
and <i>a</i><sub>11</sub> and the subscriber has randomly generated | ||
<i>a</i><sub>20</sub> and <i>a</i><sub>21</sub>. | ||
The other information is shared using the FROST distributed key generation | ||
protocol. | ||
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> + <i>m</i><sub>1</sub> (mod <i>p</i>) | ||
where <i>m</i><sub>1</sub> is a key securely derived by the provider from | ||
<i>V</i><sub>k</sub> using the HSM, for example using | ||
HKDF-Expand(<i>V</i><sub>k</sub>) from | ||
[RFC 5869](https://www.rfc-editor.org/rfc/rfc5869) with an HSM key, | ||
followed by `hash_to_field` from | ||
[RFC 9380](https://www.rfc-editor.org/rfc/rfc9380); | ||
- <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 HKDF-Expand(<i>PIN</i>) followed by | ||
`hash_to_field`. | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
pub mod provider; | ||
pub mod subscriber; | ||
|
||
use std::ptr::null_mut; | ||
use std::slice; | ||
use libc::size_t; | ||
use serde::{Serialize, Serializer}; | ||
use crate::program; | ||
|
||
/// Enrolled authenticator data for the subscriber. | ||
/// | ||
/// Contains a joint verifying key, and two signing shares. Each signing share | ||
/// is masked by adding a scalar derived from the corresponding mask. | ||
pub type Identifier = [u8; 97]; | ||
|
||
/// Authenticator device verifying key. | ||
/// | ||
/// Contains a compressed P-256 point. | ||
pub type Device = [u8; 33]; | ||
|
||
/// Evidence that some subscriber passed some payload. | ||
/// | ||
/// Contains a P-256 binding verifying key, a joint signature, an ECDSA P-256 | ||
/// device signature, and an ECDSA P-256 binding signature. | ||
pub type Evidence = [u8; 225]; | ||
|
||
/// Secret entropy derived from execution context data using a hardware-backed key. | ||
pub type Randomness = [u8; 32]; | ||
|
||
/// Secret entropy derived from application-provided data using a hardware-backed key. | ||
/// | ||
/// Gets added to the corresponding signing share in the [Identifier]. | ||
pub type Mask = [u8; 32]; | ||
|
||
/// Entitlement to enroll. | ||
/// | ||
/// Contains provider commitments `([a10]G, [a11]G)` and a proof of knowledge | ||
/// of `a10`. | ||
pub type Voucher = [u8; 131]; | ||
|
||
/// Attempt to redeem a [Voucher] to enroll. | ||
/// | ||
/// Contains subscriber commitments `([a20]G, [a21]G)`, a proof of knowledge | ||
/// of `a20`, and a secret share `a20 + a21 * 1` for the provider. | ||
pub type Redemption = [u8; 163]; | ||
|
||
/// Validation of a [Voucher] with [Redemption]. | ||
/// | ||
/// Contains a secret share `a10 + a11 * 2` for the [subscriber], a joint | ||
/// verifying key, and a masked provider signing share. | ||
pub type Validation = [u8; 97]; | ||
|
||
/// Authentication challenge data. | ||
/// | ||
/// Contains [provider] commitments ([<i>d</i><sub>1</sub>]<i>G</i>, [<i>d</i><sub>2</sub>]<i>G</i>). | ||
pub type Challenge = [u8; 66]; | ||
|
||
/// Response to a [Challenge]. | ||
/// | ||
/// Contains a binding verifying key, [subscriber] commitments `([d2]G, [e2]G)`, | ||
/// [subscriber] signature share `z2`, a device signature, and a binding | ||
/// signature. | ||
pub type Pass = [u8; 259]; | ||
|
||
/// Log metadata and instructions. | ||
#[derive(Clone, Debug)] | ||
pub struct Payload(pub(crate) Vec<u8>); | ||
|
||
impl TryFrom<Vec<u8>> for Payload { | ||
type Error = (); | ||
|
||
fn try_from(value: Vec<u8>) -> Result<Self, ()> { | ||
if value.len() < MAXIMUM_PAYLOAD_SIZE { Ok(Payload(value)) } else { Err(()) } | ||
} | ||
} | ||
|
||
impl Serialize for Payload { | ||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer { | ||
serdect::array::serialize_hex_lower_or_bin(&self.0, serializer) | ||
.map_err(|_| serde::ser::Error::custom("could not serialize element")) | ||
} | ||
} | ||
|
||
/// The maximum size of a payload. | ||
pub const MAXIMUM_PAYLOAD_SIZE: usize = 1024; | ||
|
||
/// Verifies [Evidence] that the identified [subscriber] passed the [Payload]. | ||
/// | ||
/// # Examples | ||
/// | ||
/// ``` | ||
/// use hex_literal::hex; | ||
/// use scal3::{pay, release, verify}; | ||
/// | ||
/// let identifier = hex!("03790293a35b3e293619f84628dd2d1b9d508a57a28f3aaea7fb\ | ||
/// 39ede6dc45e61dd329cc44569bc3b7cbe05947d79e92c1e14f2d61391994db6cc895d255ce4\ | ||
/// 4470881ce68a102d13905e67e086a2c88b7d20da3fe595211e4800d19af5e5e5802"); | ||
/// let device = hex!("02650501ae4d0eb95c8128b47f6348fb7a071e42cc4f8d5afdcda01f\ | ||
/// 83149f29ba"); | ||
/// let payload = hex!("7b226f7065726174696f6e223a226c6f672d696e222c22736573736\ | ||
/// 96f6e223a2236386339656565646466613566623530227d"); | ||
/// let evidence = hex!("03fe24affb03808ece87bf8b3d59f30a55b56c616c11546243b4c6\ | ||
/// 37e9a0b4e5f725c52ca44f91eabee9d42a2679e456bcf9203e651ca120ccdce692d03d742ed\ | ||
/// bf5e7d24cf6074f08c6466196fe39752068d8021b6e5f617cbb070eaaefe93f269c9c4d335e\ | ||
/// b1fd9026cf9ac937803a32d0062bf5499ca81e70450498fd2c8171cd9cf28714590093d6178\ | ||
/// e4d1762b83173605f27dd54c9002f9d8e880ace3f63e4867a639f8aa7f8cdf4b7ff20ad092b\ | ||
/// f01d01e3a53866e1913cd5ae8f12c1f112b21aebd97761f6548d6f9c9d48408fe27e1a931b3\ | ||
/// d97e902b6320b9c6dcb59"); | ||
/// | ||
/// let payload_handle = unsafe { pay(payload.as_ptr(), payload.len()) }; | ||
/// assert!(verify( | ||
/// &identifier, | ||
/// &device, | ||
/// payload_handle, | ||
/// &evidence, | ||
/// )); | ||
/// unsafe { release(payload_handle) }; | ||
/// ``` | ||
#[no_mangle] | ||
#[export_name = "scal3_verify"] | ||
pub extern "C" fn verify( | ||
identifier: &Identifier, | ||
device: &Device, | ||
payload: *mut Payload, | ||
evidence: &Evidence, | ||
) -> bool { | ||
if payload.is_null() { return false } | ||
let payload_box = unsafe { Box::from_raw(payload) }; | ||
let result = program::verify(identifier, device, &payload_box, evidence); | ||
unsafe { Box::into_raw(payload_box) }; | ||
result.is_some() | ||
} | ||
|
||
/// Constructs a [Payload] from a pointer and a size in bytes. | ||
#[no_mangle] | ||
#[export_name = "scal3_pay"] | ||
pub unsafe extern "C" fn pay( | ||
value: *const u8, | ||
size: size_t | ||
) -> *mut Payload { | ||
if value.is_null() { panic!("null input") } | ||
let value = slice::from_raw_parts(value, size); | ||
match Payload::try_from(value.to_vec()) { | ||
Ok(payload) => Box::into_raw(Box::new(payload)), | ||
Err(_) => null_mut(), | ||
} | ||
} | ||
|
||
/// Releases a [Payload] from memory. | ||
#[no_mangle] | ||
#[export_name = "scal3_release"] | ||
pub unsafe extern "C" fn release( | ||
payload: *mut Payload, | ||
) { | ||
if payload.is_null() { return } | ||
let _ = Box::from_raw(payload); | ||
} |
Oops, something went wrong.