Skip to content

Commit

Permalink
feat: enroll, authenticate, verify with SCAL3
Browse files Browse the repository at this point in the history
  • Loading branch information
sander committed Feb 27, 2024
1 parent 8b4a26a commit 7b9f94a
Show file tree
Hide file tree
Showing 12 changed files with 1,949 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.DS_Store
target
Cargo.lock
29 changes: 29 additions & 0 deletions Cargo.toml
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"
139 changes: 139 additions & 0 deletions src/README.md
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.
157 changes: 157 additions & 0 deletions src/api.rs
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);
}
Loading

0 comments on commit 7b9f94a

Please sign in to comment.