From 7b9f94aca5edec9eaba65318926be754668f3ecf Mon Sep 17 00:00:00 2001 From: Sander Dijkhuis <44112+sander@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:43:56 +0100 Subject: [PATCH] feat: enroll, authenticate, verify with SCAL3 --- .gitignore | 2 + Cargo.toml | 29 ++ src/README.md | 139 ++++++++ src/api.rs | 157 ++++++++++ src/api/provider.rs | 292 +++++++++++++++++ src/api/subscriber.rs | 251 +++++++++++++++ src/domain.rs | 644 ++++++++++++++++++++++++++++++++++++++ src/group.rs | 168 ++++++++++ src/lib.rs | 39 +++ src/program.rs | 77 +++++ src/program/provider.rs | 93 ++++++ src/program/subscriber.rs | 58 ++++ 12 files changed, 1949 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/README.md create mode 100644 src/api.rs create mode 100644 src/api/provider.rs create mode 100644 src/api/subscriber.rs create mode 100644 src/domain.rs create mode 100644 src/group.rs create mode 100644 src/lib.rs create mode 100644 src/program.rs create mode 100644 src/program/provider.rs create mode 100644 src/program/subscriber.rs diff --git a/.gitignore b/.gitignore index e43b0f9..d6b0901 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c038e9c --- /dev/null +++ b/Cargo.toml @@ -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 "] +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" diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..dca425f --- /dev/null +++ b/src/README.md @@ -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. + +
+Do not use this code for production. +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. +
+ +## 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, 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 p and common base +point G for all keys. + +To the provider and subscriber, signing shares are assigned of the form +si = + a10 + + a11i + + a20 + + a21i + (mod p) +where the provider has participant identifier i = 1 +and the subscriber has i = 2. +During enrollment, the provider has randomly generated a10 +and a11 and the subscriber has randomly generated +a20 and a21. +The other information is shared using the FROST distributed key generation +protocol. +The resulting joint verifying key equals +Vk = [a10 + a20]G. + +The SCAL3 user identifier consists of Vk and: + +- s1 + m1 (mod p) + where m1 is a key securely derived by the provider from + Vk using the HSM, for example using + HKDF-Expand(Vk) 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); +- s2 + m2 (mod p) + where m2 is a key securely derived by the subscriber from + the PIN, for example using HKDF-Expand(PIN) followed by + `hash_to_field`. + +During authentication, the subscriber generates an ephemeral ECDSA binding key +pair +(sb, Vb) +and forms a message M that includes Vb, +the instruction to authorize, and log metadata. +Applying FROST threshold signing, both parties generate secret nonces +(di, ei) +and together they form a joint signature +(c, z) over M. To do so, they compute with domain-separated +hash functions #1 and #2: + +- commitment shares + (Di, Ei) = + ([di]G, [ei]G); +- binding factors + ρi = #1(i, M, B) + where B represents a list of all commitment shares; +- commitment + R = + D1 + + [ρ1]E1 + + D2 + + [ρ2]E2; +- challenge c = #2(R, Vk, M); +- signature share + zi = + di + + eiρi + + cλisi + (mod p) + with λ1 = 2 and λ2 = −1; +- proof + z = z1 + z2. + +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 c; +- a binding signature created using sb over the device + signature. + +This construction makes sure that without simultaneous control over both +authentication factors, evidence cannot be forged. diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..b0a1530 --- /dev/null +++ b/src/api.rs @@ -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 ([d1]G, [d2]G). +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); + +impl TryFrom> for Payload { + type Error = (); + + fn try_from(value: Vec) -> Result { + if value.len() < MAXIMUM_PAYLOAD_SIZE { Ok(Payload(value)) } else { Err(()) } + } +} + +impl Serialize for Payload { + fn serialize(&self, serializer: S) -> Result 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); +} diff --git a/src/api/provider.rs b/src/api/provider.rs new file mode 100644 index 0000000..b6a07e1 --- /dev/null +++ b/src/api/provider.rs @@ -0,0 +1,292 @@ +//! Central system provider operating under sole control of subscribers. + +use crate::{api, Challenge, Device, Evidence, Mask, Pass, Payload, Randomness, Redemption, Validation, Voucher}; +use crate::domain::{KEY_DERIVATION_DOMAIN, ProofError, Validating}; +use crate::program::provider; + +/// Key derivation information to obtain the provider [Mask]. +/// +/// Contains a domain separation tag and the joint verifying key. +pub type Info = [u8; KEY_DERIVATION_DOMAIN.len() + 33]; + +/// Process handle for validation. +pub struct Process(Validating); + +/// Result of authenticating a [Pass]. +#[repr(C)] +#[derive(Debug, PartialEq)] +pub enum Authentication { + Ok, + BadRequest, + BadBinding, + BadDeviceSignature, + BadJointSignature, +} + +/// Creates a [Voucher] based on [Randomness] derived from voucher metadata. +/// +/// The provider shares this [Voucher] along with the metadata in an +/// integrity-protected message to the subscriber. For example, the message +/// may contain an HMAC-SHA256 authentication tag created using a +/// provider-secret key over the voucher metadata. +/// +/// # Examples +/// +/// Use a nonce and a timestamp to derive [Randomness], using some function +/// `hmac` that returns 32 bytes: +/// +/// ``` +/// # use std::time::{SystemTime, UNIX_EPOCH}; +/// # use hex_literal::hex; +/// # use hmac::digest::Output; +/// # use hmac::{Hmac, Mac}; +/// # use sha2::{Sha256, Sha256VarCore}; +/// use scal3::provider; +/// +/// # let key = hex!("90037986f5d6abbe2126814aa6b9b14b22111ff960172aa7967d6de90edeb38c"); +/// # type HmacSha256 = Hmac; +/// # fn hmac(key: &[u8; 32], info: &[u8]) -> Output { +/// # HmacSha256::new_from_slice(key).expect("known size") +/// # .chain_update(info) +/// # .finalize() +/// # .into_bytes() +/// # } +/// # fn time_millis() -> u64 { +/// # SystemTime::now() +/// # .duration_since(UNIX_EPOCH).expect("long ago") +/// # .as_millis() as u64 +/// # } +/// let mut metadata = [0; 16]; +/// metadata[..8].fill_with(rand::random); +/// metadata[8..].copy_from_slice(&time_millis().to_be_bytes()); +/// +/// let randomness = hmac(&key, &metadata).into(); +/// +/// let mut voucher = [0; 131]; +/// provider::vouch(&randomness, &mut voucher); +/// ``` +#[no_mangle] +#[export_name = "scal3_provider_vouch"] +pub extern "C" fn vouch( + randomness: &Randomness, + voucher: &mut Voucher, +) { + provider::vouch(randomness).clone_into(voucher); +} + +/// Starts validation of a [Voucher] in [Redemption]. +/// +/// Returns a null pointer if the [Redemption] is invalid. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// use scal3::provider; +/// +/// let randomness = hex!("80265e3f0039ef23727989d30a1f4fb27047adcb0cdc1c8b0e46\ +/// cc224b32af1b"); +/// let voucher = hex!("02eb2d0419022ab697478a79a0df68822442f4dc3b212e2e0fcc46b\ +/// 2e9abd515a0038f99465b183a4616b4881240ebc566ae42026ccbb3b22aa4b55113ecd6421e\ +/// 9e03df49380b26c57e8e148fbd90529cb774e1420b911ccd915a9c856aa503af04bc466fa02\ +/// cb6d19823770489f0816857e233b11547097eedfd5cdac23e5cfcce05"); +/// let redemption = hex!("025b8310f21847408edd1faa2f60f10fd431b65ee3e828e9e21f\ +/// 75f63f66386e9f02a23c999605480bfc2a0423824b9bf9e8b7b52c0ee09912556f048166997\ +/// 4b1e5031d9be24c2a9099d7bd64ddf07a31c22395e988ebfbf06fe196add200b2de142ba9fa\ +/// ce11998701d8ff2b67632c4aef325d1e9ca637c9798bd91122f9b9aeb77ce99e2004b8a3ffa\ +/// ffeb6d1179d28beffc2559acca702de0ec8198c160b7fc1c2"); +/// +/// let mut info = [0; 53]; +/// let process = provider::process(&randomness, &redemption, &mut info); +/// assert!(!process.is_null()) +/// ``` +#[no_mangle] +#[export_name = "scal3_provider_process"] +pub extern "C" fn process( + randomness: &Randomness, + redemption: &Redemption, + info: &mut Info, +) -> *mut Process { + provider::process(randomness, redemption).map_or(std::ptr::null_mut(), |draft| { + draft.key_derivation_info().clone_into(&mut info.into()); + Box::into_raw(Box::new(Process(draft))) + }) +} + +/// Completes a [Process] using a [Mask] derived from [Info]. +/// +/// # Examples +/// +/// ``` +/// # use hex_literal::hex; +/// # use hmac::digest::Output; +/// # use hmac::{Hmac, Mac}; +/// # use sha2::{Sha256, Sha256VarCore}; +/// use scal3::provider; +/// # type HmacSha256 = Hmac; +/// # fn hmac(key: &[u8; 32], info: &[u8]) -> Output { +/// # HmacSha256::new_from_slice(key).expect("known size") +/// # .chain_update(info) +/// # .finalize() +/// # .into_bytes() +/// # } +/// # let key = hex!("90037986f5d6abbe2126814aa6b9b14b22111ff960172aa7967d6de90edeb38c"); +/// +/// let randomness = hex!("80265e3f0039ef23727989d30a1f4fb27047adcb0cdc1c8b0e46\ +/// cc224b32af1b"); +/// let voucher = hex!("02eb2d0419022ab697478a79a0df68822442f4dc3b212e2e0fcc46b\ +/// 2e9abd515a0038f99465b183a4616b4881240ebc566ae42026ccbb3b22aa4b55113ecd6421e\ +/// 9e03df49380b26c57e8e148fbd90529cb774e1420b911ccd915a9c856aa503af04bc466fa02\ +/// cb6d19823770489f0816857e233b11547097eedfd5cdac23e5cfcce05"); +/// let redemption = hex!("025b8310f21847408edd1faa2f60f10fd431b65ee3e828e9e21f\ +/// 75f63f66386e9f02a23c999605480bfc2a0423824b9bf9e8b7b52c0ee09912556f048166997\ +/// 4b1e5031d9be24c2a9099d7bd64ddf07a31c22395e988ebfbf06fe196add200b2de142ba9fa\ +/// ce11998701d8ff2b67632c4aef325d1e9ca637c9798bd91122f9b9aeb77ce99e2004b8a3ffa\ +/// ffeb6d1179d28beffc2559acca702de0ec8198c160b7fc1c2"); +/// +/// let mut info = [0; 53]; +/// let process = provider::process(&randomness, &redemption, &mut info); +/// +/// let mask = hmac(&key, &info).into(); +/// +/// let mut validation = [0; 97]; +/// assert!(provider::validate(process, &mask, &mut validation)); +/// ``` +#[no_mangle] +#[export_name = "scal3_provider_validate"] +pub extern "C" fn validate( + draft: *mut Process, + mask: &Mask, + validation: &mut api::Validation, +) -> bool { + if draft.is_null() { return false } + let draft = unsafe { Box::from_raw(draft) }; + provider::validate(draft.0, mask).clone_into(validation); + true +} + +/// After [Validation], checks authorization for an [api::Identifier]. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// use scal3::provider; +/// +/// let validation = hex!("74f6d3ee0c981361ddb81d4db39a2946d9515ef3ca9cf3be64dd\ +/// e18b4417c6ec02bb2890eea9a8d1123c07d67ae358bd59659710d750565b9131f32e6af6517\ +/// 7166219b62bee024abdd74c6f94a7bb909fccef89863c8f6d2c239a0151493f064f"); +/// let identifier = hex!("02bb2890eea9a8d1123c07d67ae358bd59659710d750565b9131\ +/// f32e6af65177166219b62bee024abdd74c6f94a7bb909fccef89863c8f6d2c239a0151493f0\ +/// 64fd949263127ed3828c9457e5c3118b2131da53ec1ec2c8222a0399e096b316763"); +/// +/// assert!(provider::authorize(&validation, &identifier)); +/// ``` +#[no_mangle] +#[export_name = "scal3_provider_authorize"] +pub extern "C" fn authorize( + validation: &Validation, + identifier: &api::Identifier, +) -> bool { + provider::authorize(validation, identifier).is_ok() +} + +/// Creates a [Challenge] based on [Randomness] derived from challenge metadata. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// use scal3::provider; +/// +/// let randomness = hex!("80265e3f0039ef23727989d30a1f4fb27047adcb0cdc1c8b0e46\ +/// cc224b32af1b"); +/// +/// let mut challenge = [0; 66]; +/// provider::challenge(&randomness, &mut challenge); +/// ``` +#[no_mangle] +#[export_name = "scal3_provider_challenge"] +pub extern "C" fn challenge( + randomness: &Randomness, + challenge: &mut Challenge, +) { + provider::challenge(randomness).clone_into(challenge) +} + +/// Finishes [Authentication] by creating [Evidence] that [Pass] is correct. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// use scal3::{Payload, provider}; +/// +/// # fn main() -> Result<(), ()> { +/// let randomness = hex!("d277220c22a71ab93cc413370ce4fe7d37d7f472f0ad8b065beb\ +/// aa713c1df012"); +/// let identifier = hex!("03e745506f157c1b2613545c82e77852602a11ba3552feceaabf\ +/// 1a4a2a88e7dd0e2ffcb3690c8200f598fa0173e56fb63bf8cfac4723d5dc6cf17c340260d39\ +/// f4f6587d0237573e7d3b289265a416c5402abad39d857f0dbf18f2ba75f85a51c9d"); +/// let device = hex!("021749278d707befe54ffa476ad42d6a53ad5bec62b80e665fe52835\ +/// f6be33ef49"); +/// let mask = hex!("b6a4459e97c77a487ed8b4267be060f71d3b2a18632ac3cfe384e7b495\ +/// 48d632"); +/// let pass = hex!("031de5d7c377beb388d059555b875c7b2d16480db0e8495817f2b88a65\ +/// 823369a303cbf09a3b62c356cbc32679b3cdc32ca7a513e3534e6c0fdfcb47a5a8ba17b1310\ +/// 3ec4db8f9c8204291c3cfb577449f031cc5bbcc64c4426274edddfb69383631ca955b0bd755\ +/// 99f9d8c390633b02a22b7bf9ac13f1f69404b55752a67d6442f7c3f1ad4bf355fdc42ab6fb0\ +/// e90c59a76b76de2376b7508bff4c1395694d5e8364a2606023b20b91d66f6d9540d7d38f42d\ +/// 9b61ee743d05e9a49ee3a236bac47b770a302265cb93441d0a14a6825cf1500718dc7e74f93\ +/// 02bf6a6b1e89d928ad64d329a5060854a51b06d0a0ccaa66006cae146366afe779aba3a5f48\ +/// 4c858f84f5"); +/// +/// let payload = hex!("7b226f7065726174696f6e223a226c6f672d696e222c22736573736\ +/// 96f6e223a2236386339656565646466613566623530227d"); +/// let payload = Payload::try_from(payload.to_vec())?; +/// +/// let mut evidence = [0; 225]; +/// let authentication = provider::prove( +/// &randomness, +/// &identifier, +/// &device, +/// &mask, +/// &payload, +/// &pass, +/// &mut evidence, +/// ); +/// assert_eq!(authentication, provider::Authentication::Ok); +/// # Ok(()) +/// # } +/// ``` +#[export_name = "scal3_provider_prove"] +pub extern "C" fn prove( + randomness: &Randomness, + identifier: &api::Identifier, + device: &Device, + mask: &Mask, + payload: &Payload, + pass: &Pass, + evidence: &mut Evidence, +) -> Authentication { + match provider::prove( + randomness, + identifier, + device, + mask, + payload, + pass + ) { + Ok(e) => { + evidence.copy_from_slice(&e); + Authentication::Ok + } + Err(provider::ProofError::SyntaxError) => Authentication::BadRequest, + Err(provider::ProofError::ContentError(ProofError::BadBinding)) => + Authentication::BadBinding, + Err(provider::ProofError::ContentError(ProofError::BadDeviceSignature)) => + Authentication::BadDeviceSignature, + Err(provider::ProofError::ContentError(ProofError::BadJointSignature)) => + Authentication::BadJointSignature, + } +} diff --git a/src/api/subscriber.rs b/src/api/subscriber.rs new file mode 100644 index 0000000..d5ae285 --- /dev/null +++ b/src/api/subscriber.rs @@ -0,0 +1,251 @@ +//! User with a device under full control, subscribing to provider services. + +use std::ptr::null_mut; +use crate::domain::{self, DATA_TO_SIGN_DOMAIN, PassDraft}; +use crate::{Challenge, Device, Identifier, Mask, Pass, Payload, Redemption, Validation, Voucher}; +use crate::program::subscriber; + +/// Process handle for enrollment using a [Voucher]. +pub struct Enrollment(pub(crate) domain::Enrollment); + +/// Process handle for passing a [Challenge]. +pub struct Passing(PassDraft); + +/// Creates [Redemption] data for a [Voucher], starting an [Enrollment] process. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// use scal3::subscriber; +/// +/// let voucher = hex!("03ecc135f1028bb5ecf3dbfc221ef18eb019e351ea5d86a3ee01891\ +/// 55292a20f1502ce64b35cbdaca0ffa56808aa27eb715c776e7094b9b64847ef2a13cf5f3c91\ +/// cc035e2c9dc9fa8d7ffa758d3ac1930d3bed34c4c1bc137262e3b65329064a88902bd28f670\ +/// be048b3a9ad98973f551ba199371c8fbfade34943093766c26151254a"); +/// +/// let mut redemption = [0; 163]; +/// let enrollment = subscriber::redeem(&voucher, &mut redemption); +/// ``` +#[no_mangle] +#[export_name = "scal3_subscriber_redeem"] +pub extern "C" fn redeem( + voucher: &Voucher, + redemption: &mut Redemption, +) -> *mut Enrollment { + subscriber::redeem(voucher).map_or(std::ptr::null_mut(), |(r, e)| { + r.clone_into(redemption); + Box::into_raw(Box::new(Enrollment(e))) + }) +} + +/// Completes [Enrollment] by providing [Validation] and a [Mask], creating an +/// [Identifier]. +/// +/// Returns false if `enrollment` is null or `validation` is invalid. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// # use hmac::digest::Output; +/// # use hmac::{Hmac, Mac}; +/// # use sha2::{Sha256, Sha256VarCore}; +/// use scal3::{subscriber}; +/// # type HmacSha256 = Hmac; +/// # fn hmac(key: &[u8; 32], info: &[u8]) -> Output { +/// # HmacSha256::new_from_slice(key).expect("known size") +/// # .chain_update(info) +/// # .finalize() +/// # .into_bytes() +/// # } +/// # let key = hex!("997f5ef18a998189ced2abebcc74da9a534e4b4705eeef907722cb9ce\ +/// # 13ef9cb"); +/// +/// let randomness = hex!("80265e3f0039ef23727989d30a1f4fb27047adcb0cdc1c8b0e46\ +/// cc224b32af1b"); +/// let voucher = hex!("02eb2d0419022ab697478a79a0df68822442f4dc3b212e2e0fcc46b\ +/// 2e9abd515a0038f99465b183a4616b4881240ebc566ae42026ccbb3b22aa4b55113ecd6421e\ +/// 9e03df49380b26c57e8e148fbd90529cb774e1420b911ccd915a9c856aa503af04bc466fa02\ +/// cb6d19823770489f0816857e233b11547097eedfd5cdac23e5cfcce05"); +/// +/// let mut redemption = [0; 163]; +/// let enrollment = subscriber::redeem(&voucher, &mut redemption); +/// +/// let validation = hex!("74f6d3ee0c981361ddb81d4db39a2946d9515ef3ca9cf3be64dd\ +/// e18b4417c6ec02ddfc55f76cc2aee73b8bfb95fc3485e4640df462467e2df74706158d45c61\ +/// eb51fd97575a2e8dc484130044606ace983306eae6b0a0cdde13886d6f116bc1abf"); +/// +/// let pin = b"12345"; +/// let mask = hmac(&key, pin).into(); +/// +/// let mut identifier = [0; 97]; +/// assert!(subscriber::enroll(enrollment, &validation, &mask, &mut identifier)); +/// ``` +#[no_mangle] +#[export_name = "scal3_subscriber_enroll"] +pub extern "C" fn enroll( + enrollment: *mut Enrollment, + validation: &Validation, + mask: &Mask, + identifier: &mut Identifier, +) -> bool { + if enrollment.is_null() { return false; } + let enrollment = unsafe { Box::from_raw(enrollment) }.0; + subscriber::enroll(enrollment, validation, mask).map_or(false, |i| { + i.clone_into(identifier); + true + }) +} + +/// Data to be signed using the [Device] signing key. +/// +/// Contains a domain separation tag and the first part of the joint signature. +pub type Data = [u8; DATA_TO_SIGN_DOMAIN.len() + 32]; + +/// Starts passing a [Challenge]. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// # use hmac::digest::Output; +/// # use hmac::{Hmac, Mac}; +/// # use sha2::{Sha256, Sha256VarCore}; +/// use scal3::{Payload, subscriber}; +/// # type HmacSha256 = Hmac; +/// # fn hmac(key: &[u8; 32], info: &[u8]) -> Output { +/// # HmacSha256::new_from_slice(key).expect("known size") +/// # .chain_update(info) +/// # .finalize() +/// # .into_bytes() +/// # } +/// # fn main() -> Result<(), ()> { +/// # let key = hex!("997f5ef18a998189ced2abebcc74da9a534e4b4705eeef907722cb9ce\ +/// # 13ef9cb"); +/// +/// let identifier = hex!("03226bc3f0babd46deb93945df27e82ab317136803154954a921\ +/// 8c662c5761186aaf0bd07f3c00934341e836815bf12d0378ca36717648009064bb515d08d7b\ +/// fe88fc1a30d2a6353ab02caa79520a4c9786f1c81f9eec7ec6e5f59b3c109b8544e"); +/// let device = hex!("02c1fbeda869351e40e1d0c7b9cea64a015e288e407073d831286fc2\ +/// ab0fcf3ef0"); +/// let challenge = hex!("02ff11d31a1f0f874706113be3caf80ef4fb1a2eb5ba79015b44d\ +/// 946e959116cbe0307289ec04e8e4d169ab19ec1fc2de384f55dde9fb306fc07219ed4e0f341\ +/// fdaf"); +/// +/// let payload = b"{\"operation\":\"log-in\",\"session\":\"68c9eeeddfa5fb50\"}" +/// .to_vec() +/// .try_into()?; +/// +/// let pin = b"12345"; +/// let mask = hmac(&key, pin).into(); +/// +/// let mut data = [0; 52]; +/// let pass = subscriber::authenticate( +/// &identifier, +/// &device, +/// &challenge, +/// &mask, +/// &payload, +/// &mut data +/// ); +/// assert!(!pass.is_null()); +/// # Ok(()) +/// # } +/// ``` +#[no_mangle] +#[export_name = "scal3_subscriber_authenticate"] +pub extern "C" fn authenticate( + identifier: &Identifier, + device: &Device, + challenge: &Challenge, + mask: &Mask, + payload: &Payload, + data: &mut Data, +) -> *mut Passing { + subscriber::authenticate(identifier, device, challenge, mask, payload).map_or(null_mut(), |p| { + let data_under_device_sig: Data = p.data_under_device_sig + .clone() + .try_into() + .expect("known serialization"); + data_under_device_sig.clone_into(data); + Box::into_raw(Box::new(Passing(p))) + }) +} + +/// Signature over [Data] with [Device]. +pub type Signature = [u8; 64]; + +/// Finishes [Passing] using a [Signature]. +/// +/// # Examples +/// +/// ``` +/// use hex_literal::hex; +/// # use hmac::digest::Output; +/// # use hmac::{Hmac, Mac}; +/// # use sha2::{Sha256, Sha256VarCore}; +/// use scal3::{Payload, subscriber}; +/// # type HmacSha256 = Hmac; +/// # fn hmac(key: &[u8; 32], info: &[u8]) -> Output { +/// # HmacSha256::new_from_slice(key).expect("known size") +/// # .chain_update(info) +/// # .finalize() +/// # .into_bytes() +/// # } +/// # fn main() -> Result<(), ()> { +/// # use p256::ecdsa; +/// # use rand::thread_rng; +/// use signature::Signer; +/// let key = hex!("997f5ef18a998189ced2abebcc74da9a534e4b4705eeef907722cb9ce\ +/// # 13ef9cb"); +/// # let device_sk = ecdsa::SigningKey::random(&mut thread_rng()); +/// +/// let identifier = hex!("03226bc3f0babd46deb93945df27e82ab317136803154954a921\ +/// 8c662c5761186aaf0bd07f3c00934341e836815bf12d0378ca36717648009064bb515d08d7b\ +/// fe88fc1a30d2a6353ab02caa79520a4c9786f1c81f9eec7ec6e5f59b3c109b8544e"); +/// let challenge = hex!("02ff11d31a1f0f874706113be3caf80ef4fb1a2eb5ba79015b44d\ +/// 946e959116cbe0307289ec04e8e4d169ab19ec1fc2de384f55dde9fb306fc07219ed4e0f341\ +/// fdaf"); +/// # let device = device_sk.verifying_key().to_encoded_point(true) +/// # .to_bytes().to_vec().try_into().expect("known serialization"); +/// +/// let payload = b"{\"operation\":\"log-in\",\"session\":\"68c9eeeddfa5fb50\"}" +/// .to_vec() +/// .try_into()?; +/// +/// let pin = b"12345"; +/// let mask = hmac(&key, pin).into(); +/// +/// let mut data = [0; 52]; +/// let passing = subscriber::authenticate( +/// &identifier, +/// &device, +/// &challenge, +/// &mask, +/// &payload, +/// &mut data +/// ); +/// +/// let (device_sig, _) = device_sk.sign(&data); +/// let device_sig: [u8; 64] = device_sig.to_bytes().into(); +/// +/// let mut pass = [0; 259]; +/// assert!(subscriber::pass(passing, &device_sig, &mut pass)); +/// # Ok(()) +/// # } +/// ``` +#[no_mangle] +#[export_name = "scal3_subscriber_pass"] +pub extern "C" fn pass( + passing: *mut Passing, + device_sig: &Signature, + pass: &mut Pass, +) -> bool { + if passing.is_null() { return false } + let draft = unsafe { Box::from_raw(passing) }.0; + subscriber::pass(draft, device_sig).map_or(false, |p| { + p.clone_into(pass); + true + }) +} diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..efc39ab --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,644 @@ +use std::borrow::ToOwned; +use serde::{Deserialize, Serialize}; +use frost_p256 as frost; +use frost_p256::keys::{dkg, KeyPackage, SecretShare, VerifyingShare}; +use p256::{ecdsa, FieldBytes, ProjectivePoint}; +use zeroize::ZeroizeOnDrop; +use frost_p256::{P256Group, P256ScalarField, P256Sha256}; +use p256::elliptic_curve::hash2curve::{ExpandMsgXmd, hash_to_field}; +use sha2::Sha256; +use std::collections::{BTreeMap, HashMap}; +use signature::rand_core::CryptoRngCore; +use frost_p256::keys::dkg::round2::Package; +use frost_core::frost::keys::SigningShare; +use p256::ecdsa::VerifyingKey; +use frost_core::frost::round1::NonceCommitment; +use frost_p256::round1::SigningCommitments; +use frost_core::{Field, Group}; +use std::ops::Mul; +use p256::elliptic_curve::group::GroupEncoding; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use p256::elliptic_curve::ops::MulByGenerator; +use signature::{Signer, Verifier}; +use crate::group::{Element, Proof, Scalar, Signature}; +use crate::Payload; + +#[derive(Clone, ZeroizeOnDrop)] +pub(crate) struct Provider { + pub(crate) randomness: [u8; 32], +} + +impl Provider { + pub(crate) fn vouch(self) -> (VoucherSecret, Voucher) { + let mut scalars = [P256ScalarField::zero(), P256ScalarField::zero(), P256ScalarField::zero()]; + let domain = "SCAL3-FROST-v1share".as_bytes(); + hash_to_field::, p256::Scalar>(&[&self.randomness], &[domain], &mut scalars) + .expect("message expansion should never fail"); + let coefficient_commitment: Vec<_> = scalars + .iter() + .map(|c| P256Group::generator() * *c) + .collect(); + let proof_randomizer = scalars[2]; + let proof_commitment = P256Group::generator() * proof_randomizer; + let mut proof_challenge = [P256ScalarField::zero()]; + let domain = "FROST-P256-SHA256-v1dkg".as_bytes(); + let mut preimage = vec![]; + preimage.extend_from_slice(Role::Provider.identifier().serialize().as_ref()); + preimage.extend_from_slice(P256Group::serialize(&coefficient_commitment[0]).as_ref()); + preimage.extend_from_slice(P256Group::serialize(&proof_commitment).as_ref()); + hash_to_field::, p256::Scalar>(&[&preimage], &[domain], &mut proof_challenge) + .expect("message expansion should never fail"); + let proof_challenge = proof_challenge[0]; + let proof_of_knowledge = frost::Signature::new(proof_commitment, proof_randomizer + scalars[0] * proof_challenge); + let payload = Voucher { + commitment_share: (Element(coefficient_commitment[0]), Element(coefficient_commitment[1])), + proof_of_knowledge: proof_of_knowledge.into(), + }; + let secret = VoucherSecret { coefficients: (scalars[0], scalars[1]) }; + (secret, payload) + } + + pub(crate) fn validate(self, redemption: Redemption) -> Option { + let (secret, payload) = self.vouch(); + let a10 = secret.coefficients.0; + let a11 = secret.coefficients.1; + let proof_commitment = redemption.proof.0.0; + let mu2 = &redemption.proof.1.0; + let phi20 = redemption.commitments.0.0; + let mut c2 = [P256ScalarField::zero()]; + let domain = "FROST-P256-SHA256-v1dkg".as_bytes(); + let mut preimage = vec![]; + preimage.extend_from_slice(Role::Subscriber.identifier().serialize().as_ref()); + preimage.extend_from_slice(P256Group::serialize(&phi20).as_ref()); + preimage.extend_from_slice(P256Group::serialize(&proof_commitment).as_ref()); + hash_to_field::, p256::Scalar>(&[&preimage], &[domain], &mut c2) + .expect("message expansion should never fail"); + let c2 = c2[0]; + if proof_commitment != P256Group::generator() * mu2 - phi20 * c2 { return None; } + let f12 = a10 + Role::Subscriber.identifier().mul(a11); + let f11 = a10 + Role::Provider.identifier().mul(a11); + let f21 = &redemption.share; + let f21 = frost::keys::SigningShare::deserialize(<[u8; 32]>::from(f21.0.to_bytes())).unwrap(); + let commitment0 = redemption.commitments.0.0.to_bytes().try_into().expect("known serialization"); + let commitment1 = redemption.commitments.1.0.to_bytes().try_into().expect("known serialization"); + let commitments = frost::keys::VerifiableSecretSharingCommitment::deserialize(vec!( + commitment0, commitment1 + )).expect("known deserialization"); + + let secret_share = SecretShare::new(Role::Provider.identifier(), f21, commitments.clone()); + let _ = secret_share.verify().expect("valid secret share"); + let own_signing_share = frost::keys::SigningShare::new(f21.to_scalar() + f11); + let group_public = commitments.serialize(); + let group_public = P256Group::deserialize(group_public.first().unwrap()).unwrap() + payload.commitment_share.0.0; + let group_public = frost::VerifyingKey::new(group_public); + let package = dkg::round2::Package::new(frost::keys::SigningShare::new(f12)); + Some(Validating { + round2_package: package, + joint_vk: group_public, + signing_share: own_signing_share, + }) + } + + /// This implementation expects high quality randomness, so does not hedge against weak random + /// number generators by mixing in the signing share value. + pub(crate) fn challenge(self) -> (Randomizer, ChallengePayload) { + let mut scalars = [P256ScalarField::zero(), P256ScalarField::zero()]; + let domain = "FROST-P256-SHA256-v1nonce".as_bytes(); + hash_to_field::, p256::Scalar>(&[&self.randomness], &[domain], &mut scalars) + .expect("message expansion should never fail"); + let commitments = frost::round1::SigningCommitments::new( + frost::round1::NonceCommitment::from(&nonce(scalars[0])), + frost::round1::NonceCommitment::from(&nonce(scalars[1])), + ); + let randomizer = Randomizer { hiding_nonce: scalars[0], binding_nonce: scalars[1] }; + let share = commitments.into(); + (randomizer, share) + } + + pub(crate) fn prove( + self, registration: &Subscriber, mask: Mask, payload: &Payload, pass: Pass, + ) -> Result { + if !pass.is_well_bound() { return Err(ProofError::BadBinding); } + let signing_share = frost::keys::SigningShare::new(registration.identifier.prov_sks_masked.0 - p256::Scalar::from(mask)); + let message = pass.message(&payload); + let provider = Role::Provider.identifier(); + let (randomizer, share) = self.challenge(); + let package = frost::SigningPackage::new(BTreeMap::from([ + (Role::Subscriber.identifier(), pass.clone().into()), + (provider, share.into()), + ]), &message); + let binding_factors = frost_core::frost::compute_binding_factor_list( + &package, &frost::VerifyingKey::new(registration.identifier.joint_vk.0), &[]); + let binding_factor = binding_factors.get(&provider) + .expect("getting the provider binding factor should never fail"); + let commitment = frost_core::frost::compute_group_commitment(&package, &binding_factors) + .expect("only possible error is identity commitment; too unlikely to catch"); + let lambda_i = frost_core::frost::derive_interpolating_value(&provider, &package) + .expect("cannot fail"); + let challenge = frost_core::challenge::( + &commitment.clone().to_element(), ®istration.identifier.joint_vk.0, &message); + let device_sig: ecdsa::Signature = pass.device_sig.clone().into(); + if !registration.device_vk.verify( + &[ + DATA_TO_SIGN_DOMAIN, + challenge.clone().to_scalar().to_bytes().as_slice() + ].concat(), + &device_sig, + ).is_ok() { return Err(ProofError::BadDeviceSignature); } + let z = pass.signature_share.0 + &randomizer.hiding_nonce + + (randomizer.binding_nonce * Scalar::from(binding_factor).0) + + (lambda_i * signing_share.to_scalar() * &challenge.clone().to_scalar()); + if !frost::VerifyingKey::new(registration.identifier.joint_vk.0).verify( + &message, &frost::Signature::new(commitment.to_element(), z), + ).is_ok() { return Err(ProofError::BadJointSignature); } + let joint_sig = JointSignature { c: challenge.to_scalar().into(), z: z.into() }; + let evidence = Evidence { + binding_vk: pass.binding_vk.into(), + joint_sig, + device_sig: pass.device_sig.into(), + binding_sig: pass.binding_sig.into(), + }; + Ok(evidence) + } +} + +impl From<[u8; 32]> for Provider { + fn from(randomness: [u8; 32]) -> Self { + Provider { randomness } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Voucher { + commitment_share: (Element, Element), + proof_of_knowledge: Proof, +} + +impl Voucher { + pub(crate) fn redeem(self, rng: &mut impl CryptoRngCore) -> Option<(Enrollment, Redemption)> { + let provider = Role::Provider.identifier(); + let subs = Role::Subscriber.identifier(); + let (subs_dkg1_secret, subs_dkg1_share) = dkg::part1(subs, 2, 2, rng) + .expect("the dkg first part should never fail"); + let subs_dkg1_in = HashMap::from([(provider, self.clone().into())]); + let (subs_dkg2_secret, subs_dkg2_share) = dkg::part2(subs_dkg1_secret, &subs_dkg1_in).ok()?; + let commitments = subs_dkg1_share.commitment().serialize(); + let commitments = ( + Element::deserialize(&commitments[0]).expect("known serialization"), + Element::deserialize(&commitments[1]).expect("known serialization"), + ); + let proof = (*subs_dkg1_share.proof_of_knowledge()).into(); + let share = Scalar(subs_dkg2_share[&provider].secret_share().to_scalar()); + Some((Enrollment { + payload: self, + package: subs_dkg2_secret, + }, Redemption { commitments, proof, share })) + } +} + +impl From for dkg::round1::Package { + fn from(value: Voucher) -> Self { + let commitment_share: Vec<[u8; 33]> = [value.commitment_share.0, value.commitment_share.1] + .iter() + .map(|c| c.0.to_bytes()) + .map(|c| <[u8; 33]>::from(c)) + .collect(); + let commitment_share = frost_core::frost::keys::VerifiableSecretSharingCommitment::::deserialize(commitment_share) + .expect("commitment share deserialization should never fail"); + dkg::round1::Package::new(commitment_share, value.proof_of_knowledge.into()) + } +} + +#[derive(Debug)] +pub struct Enrollment { + payload: Voucher, + package: dkg::round2::SecretPackage, +} + +impl Enrollment { + pub(crate) fn complete(self, validation: Validation, subs_mask: Mask) -> Option { + let prov_sks_masked = validation.prov_sks_masked.clone(); + let prov_dkg1_share = self.payload.into(); + let prov_dkg2_share = validation.into(); + let subs_dkg2_secret = self.package; + let prov = Role::Provider.identifier(); + let subs_dkg1_in = HashMap::from([(prov, prov_dkg1_share)]); + let subs_dkg2_in = HashMap::from([(prov, prov_dkg2_share)]); + let (subs_sks, subs_pks) = dkg::part3(&subs_dkg2_secret, &subs_dkg1_in, &subs_dkg2_in).ok()?; + let subs_sks_masked = subs_sks.secret_share().to_scalar() + p256::Scalar::from(subs_mask); + Some(Identifier { + joint_vk: subs_pks.group_public().to_element().into(), + prov_sks_masked: prov_sks_masked.0.into(), + subs_sks_masked: subs_sks_masked.into(), + }) + } +} + +pub(crate) struct VoucherSecret { + coefficients: (p256::Scalar, p256::Scalar), +} + +pub struct Validating { + round2_package: dkg::round2::Package, + joint_vk: frost::VerifyingKey, + signing_share: frost::keys::SigningShare, +} + +pub(crate) const KEY_DERIVATION_DOMAIN: &[u8; 20] = b"SCAL3-FROST-v1derive"; + +fn key_derivation_info( + joint_vk: &frost::VerifyingKey, +) -> Vec { + [ + &KEY_DERIVATION_DOMAIN[..], + &joint_vk.serialize(), + ].concat() +} + +impl Validating { + pub(crate) fn key_derivation_info(&self) -> Vec { + key_derivation_info(&self.joint_vk) + } + + pub(crate) fn finalize_with_mask(self, mask: Mask) -> Validation { + let prov_sks_masked = self.signing_share.to_scalar() + p256::Scalar::from(mask); + Validation { + share: self.round2_package.secret_share().to_scalar().into(), + joint_vk: self.joint_vk.to_element().into(), + prov_sks_masked: prov_sks_masked.into(), + } + } + + fn finalize(self, f: F) -> Validation where F: Fn(&[u8]) -> Mask { + let info = self.key_derivation_info(); + self.finalize_with_mask(f(&info)) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Validation { + share: Scalar, + joint_vk: Element, + pub prov_sks_masked: Scalar, +} + +impl Validation { + pub(crate) fn authorize(self, identifier: &Identifier) -> bool { + identifier.joint_vk.0 == self.joint_vk.0 && identifier.prov_sks_masked.0 == self.prov_sks_masked.0 + } +} + +impl Into for Validation { + fn into(self) -> Package { + dkg::round2::Package::new(SigningShare::new(self.share.0)) + } +} + +#[derive(Clone, ZeroizeOnDrop)] +pub struct Mask(pub(crate) [u8; 32]); + +impl From for p256::Scalar { + fn from(value: Mask) -> Self { + let mut scalars = [P256ScalarField::zero()]; + let domain = "SCAL3-FROST-v1mask".as_bytes(); + hash_to_field::, p256::Scalar>(&[&value.0], &[domain], &mut scalars) + .expect("message expansion should never fail"); + scalars[0] + } +} + +#[derive(ZeroizeOnDrop)] +pub(crate) struct Randomizer { + hiding_nonce: p256::Scalar, + binding_nonce: p256::Scalar, +} + +#[derive(Serialize, Deserialize)] +pub struct ChallengePayload { + hiding_commitment: Element, + binding_commitment: Element, +} + +impl Into for ChallengePayload { + fn into(self) -> frost::round1::SigningCommitments { + let hiding_bytes: [u8; 33] = self.hiding_commitment.0.to_bytes().as_slice().try_into() + .expect("known serialization"); + let binding_bytes: [u8; 33] = self.binding_commitment.0.to_bytes().as_slice().try_into() + .expect("known serialization"); + frost::round1::SigningCommitments::new( + NonceCommitment::deserialize(hiding_bytes.into()).expect("known deserialization"), + NonceCommitment::deserialize(binding_bytes.into()).expect("known deserialization"), + ) + } +} + +impl From for ChallengePayload { + fn from(value: SigningCommitments) -> Self { + ChallengePayload { + hiding_commitment: Element::deserialize(&value.hiding().serialize()) + .expect("known serialization"), + binding_commitment: Element::deserialize(&value.binding().serialize()) + .expect("known serialization"), + } + } +} + +fn nonce(scalar: p256::Scalar) -> frost_core::frost::round1::Nonce { + let mut data = [0u8; 32]; + scalar.to_bytes().clone_into(<&mut FieldBytes>::from(&mut data)); + frost_core::frost::round1::Nonce::deserialize(data) + .expect("scalar deserialization should never fail") +} + +impl From<&Randomizer> for ChallengePayload { + fn from(value: &Randomizer) -> Self { + let mut nonce1data = [0u8; 32]; + let mut nonce2data = [0u8; 32]; + value.hiding_nonce.to_bytes().clone_into(<&mut FieldBytes>::from(&mut nonce1data)); + value.binding_nonce.to_bytes().clone_into(<&mut FieldBytes>::from(&mut nonce2data)); + let nonce1 = frost_core::frost::round1::Nonce::deserialize(nonce1data) + .expect("known serialization"); + let nonce2 = frost_core::frost::round1::Nonce::deserialize(nonce2data) + .expect("known serialization"); + let comm1 = frost::round1::NonceCommitment::from(&nonce1); + let comm2 = frost::round1::NonceCommitment::from(&nonce2); + let commitments = frost::round1::SigningCommitments::new(comm1, comm2); + commitments.into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Identifier { + pub joint_vk: Element, + pub prov_sks_masked: Scalar, + pub subs_sks_masked: Scalar, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JointSignature { + pub c: Scalar, + pub z: Scalar, +} + +impl JointSignature { + fn message(session_pk: &Element, payload: &[u8]) -> Vec { + [ + &session_pk.0.to_encoded_point(true).as_bytes(), + payload, + ].concat() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Evidence { + pub binding_vk: Element, + pub joint_sig: JointSignature, + pub device_sig: Signature, + pub binding_sig: Signature, +} + +#[derive(Debug)] +pub struct Subscriber { + pub device_vk: ecdsa::VerifyingKey, + pub identifier: Identifier, +} + +pub(crate) const DATA_TO_SIGN_DOMAIN: &[u8; 20] = b"SCAL3-FROST-v1device"; + +impl Subscriber { + pub(crate) fn pass( + &self, mask: Mask, share: ChallengePayload, payload: &Payload, rng: &mut impl CryptoRngCore, + ) -> PassDraft { + let subscriber = Role::Subscriber.identifier(); + let signing_share = frost::keys::SigningShare::new(self.identifier.subs_sks_masked.0 - p256::Scalar::from(mask)); + let verifying_share = VerifyingShare::from(signing_share); + let key_package = KeyPackage::new( + subscriber, signing_share, verifying_share, frost::VerifyingKey::new(self.identifier.joint_vk.0), 2u16); + let binding_sk = ecdsa::SigningKey::random(rng); + let binding_vk = *binding_sk.verifying_key(); + let message = JointSignature::message(&binding_vk.into(), &payload.0); + let (nonce, commitment_share) = frost_p256::round1::commit(&signing_share, rng); + let signing = frost::SigningPackage::new(BTreeMap::from([ + (subscriber, commitment_share), + (Role::Provider.identifier(), share.into()), + ]), &message); + let signature_share = frost::round2::sign(&signing, &nonce, &key_package) + .expect("only possible error is identity commitment; too unlikely to catch"); + let binding_factors = frost_core::frost::compute_binding_factor_list( + &signing, &key_package.group_public(), &[]); + let joint_commitment = frost_core::frost::compute_group_commitment( + &signing, &binding_factors) + .expect("signing succeeded so recalculating the joint commitment should not fail"); + let challenge = frost_core::challenge::( + &joint_commitment.clone().to_element(), &key_package.group_public().to_element(), + &message); + let data_under_device_proof = [ + DATA_TO_SIGN_DOMAIN, + challenge.to_scalar().to_bytes().as_slice() + ].concat(); + PassDraft { + data_under_device_sig: data_under_device_proof, + binding_vk, + binding_sk, + payload: payload.0.clone(), + commitment_share, + signature_share, + } + } + + pub(crate) fn verify(&self, payload: &Payload, evidence: &Evidence) -> bool { + let message = [ + &evidence.binding_vk.0.to_encoded_point(true).as_bytes(), + payload.0.as_slice(), + ].concat(); + let binding_vk: &VerifyingKey = &evidence.binding_vk.clone().into(); + let binding_sig: &ecdsa::Signature = &evidence.binding_sig.clone().into(); + let device_sig: &ecdsa::Signature = &evidence.device_sig.clone().into(); + binding_vk.verify(&device_sig.to_bytes(), binding_sig).is_ok() + && self.device_vk.verify( + &[ + DATA_TO_SIGN_DOMAIN, + evidence.joint_sig.c.clone().0.to_bytes().as_slice() + ].concat(), device_sig).is_ok() + && frost::VerifyingKey::new(self.identifier.joint_vk.0).verify(&message, &self.frost_signature(evidence)).is_ok() + } + + fn joint_commitment(&self, evidence: &Evidence) -> ProjectivePoint { + ProjectivePoint::mul_by_generator(&evidence.joint_sig.z.0) - self.identifier.joint_vk.0.mul(evidence.joint_sig.c.clone().0) + } + + fn frost_signature(&self, evidence: &Evidence) -> frost::Signature { + frost::Signature::new(self.joint_commitment(evidence), evidence.joint_sig.z.0) + } + + pub fn key_derivation_info(&self) -> Vec { + key_derivation_info(&frost::VerifyingKey::new(self.identifier.joint_vk.0)) + } +} + +#[derive(Clone, Debug)] +pub struct PassDraft { + pub data_under_device_sig: Vec, + pub binding_sk: ecdsa::SigningKey, + pub binding_vk: ecdsa::VerifyingKey, + pub payload: Vec, + pub commitment_share: frost::round1::SigningCommitments, + pub signature_share: frost::round2::SignatureShare, +} + +impl PassDraft { + pub(crate) fn finalize_with_signature(self, device_sig: ecdsa::Signature) -> Pass { + let (session_sig, _) = self.binding_sk.sign(&device_sig.to_bytes()); + Pass { + binding_vk: self.binding_vk.into(), + hiding_commitment: (*self.commitment_share.hiding()).into(), + binding_commitment: (*self.commitment_share.binding()).into(), + signature_share: (*self.signature_share.share()).into(), + device_sig: device_sig.into(), + binding_sig: session_sig.into(), + } + } + + pub fn finalize(self, f: F) -> Pass where F: Fn(&[u8]) -> ecdsa::Signature { + let signature = f(&self.data_under_device_sig); + self.finalize_with_signature(signature) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Pass { + pub binding_vk: Element, + pub hiding_commitment: Element, + pub binding_commitment: Element, + pub signature_share: Scalar, + pub device_sig: Signature, + pub binding_sig: Signature, +} + +impl Pass { + fn message(&self, payload: &Payload) -> Vec { + JointSignature::message(&self.binding_vk, &payload.0) + } + + fn is_well_bound(&self) -> bool { + let binding_vk: &VerifyingKey = &self.binding_vk.clone().into(); + let binding_sig: &ecdsa::Signature = &self.binding_sig.clone().into(); + binding_vk.verify(&self.device_sig.clone().to_bytes(), binding_sig).is_ok() + } +} + +impl Into for Pass { + fn into(self) -> frost::round1::SigningCommitments { + let hiding_bytes: [u8; 33] = self.hiding_commitment.0.to_bytes().as_slice().try_into() + .expect("known serialization"); + let binding_bytes: [u8; 33] = self.binding_commitment.0.to_bytes().as_slice().try_into() + .expect("known serialization"); + frost::round1::SigningCommitments::new( + NonceCommitment::deserialize(hiding_bytes.into()).expect("known deserialization"), + NonceCommitment::deserialize(binding_bytes.into()).expect("known deserialization"), + ) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub(crate) struct Redemption { + commitments: (Element, Element), + proof: Proof, + share: Scalar, +} + +impl Into for Redemption { + fn into(self) -> dkg::round1::Package { + let commitment0 = self.commitments.0.0.to_bytes().try_into().expect("known serialization"); + let commitment1 = self.commitments.1.0.to_bytes().try_into().expect("known serialization"); + let commitments = frost::keys::VerifiableSecretSharingCommitment::deserialize(vec!( + commitment0, commitment1 + )).expect("known deserialization"); + dkg::round1::Package::new(commitments, self.proof.into()) + } +} + +enum Role { + Provider, + Subscriber, +} + +impl Role { + fn identifier(&self) -> frost::Identifier { + frost::Identifier::try_from(match self { + Role::Provider => 1u16, + Role::Subscriber => 2u16, + }).expect("casting from these integers should never fail") + } +} + +#[derive(Debug)] +pub enum ProofError { + BadBinding, + BadDeviceSignature, + BadJointSignature, +} + +#[cfg(test)] +mod tests { + use hmac::{Hmac, Mac}; + use hmac::digest::Output; + use p256::ecdsa; + use rand::thread_rng; + use sha2::Sha256; + use signature::Signer; + use crate::domain::{Mask, Provider, Subscriber}; + use crate::Payload; + + const PROV_KEY: &[u8] = b"provider secret key"; + const SUBS_KEY: &[u8] = b"provider secret key"; + + type HmacSha256 = Hmac; + + fn derive(key: &[u8], info: &[u8]) -> [u8; 32] { + let mut output = [0; 32]; + let mut mac = HmacSha256::new_from_slice(key).expect("should not fail"); + + mac.update(info); + mac.finalize().into_bytes().clone_into(<&mut Output>>::from(&mut output)); + + output + } + + fn provider(info: &[u8]) -> Provider { derive(PROV_KEY, info).into() } + + fn enter(pin: &[u8]) -> Mask { Mask(derive(SUBS_KEY, pin)) } + + fn context(subscriber: &Subscriber) -> Mask { Mask(derive(PROV_KEY, &subscriber.key_derivation_info())) } + + #[test] + fn test_enrollment_and_authentication() { + let metadata = b"some unique voucher metadata, such as a nonce and timestamp"; + let pin = b"12345"; + + let (_, payload) = provider(metadata).vouch(); + let (enrollment, redemption) = payload.redeem(&mut thread_rng()).unwrap(); + let validation = provider(metadata).validate(redemption.clone()).unwrap() + .finalize(|info| Mask(derive(PROV_KEY, info))); + let device = ecdsa::SigningKey::random(&mut thread_rng()); + let identifier = enrollment.complete(validation.clone(), enter(pin)).unwrap(); + if !validation.authorize(&identifier) { panic!() } + let subscriber = Subscriber { device_vk: *device.verifying_key(), identifier }; + + let metadata = b"some unique challenge metadata, such as a nonce and a timestamp"; + + let (_, share) = provider(metadata).challenge(); + let payload = Payload("message to sign".as_bytes().to_vec()); + let pass = subscriber.pass( + enter(pin), + share, + &payload, + &mut thread_rng(), + ).finalize(|data| { + let (signature, _) = device.sign(data); + signature + }); + let evidence = provider(metadata).prove(&subscriber, context(&subscriber), &payload, pass).unwrap(); + if !subscriber.verify(&payload, &evidence) { panic!() } + } +} diff --git a/src/group.rs b/src/group.rs new file mode 100644 index 0000000..cf19a3a --- /dev/null +++ b/src/group.rs @@ -0,0 +1,168 @@ +use frost_core::{Field, Group}; +use frost_core::frost::BindingFactor; +use frost_p256 as frost; +use frost_p256::{P256Group, P256ScalarField, P256Sha256}; +use frost_p256::round1::NonceCommitment; +use p256::elliptic_curve::group::GroupEncoding; +use p256::elliptic_curve::PrimeField; +use p256::{ecdsa, FieldBytes}; +use p256::ecdsa::VerifyingKey; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use zeroize::ZeroizeOnDrop; + +#[derive(Clone, Debug)] +pub(crate) struct Element(pub(crate) frost_core::Element); + +impl Element { + pub(crate) fn deserialize(bytes: &[u8; 33]) -> Option { + P256Group::deserialize(bytes).ok().map(|result| Element(result)) + } +} + +impl From for Element { + fn from(value: p256::ProjectivePoint) -> Self { + Element(value) + } +} + +impl Into for Element { + fn into(self) -> VerifyingKey { + VerifyingKey::from_affine(self.0.to_affine()).expect("known serialization") + } +} + +impl From for Element { + fn from(value: VerifyingKey) -> Self { + Element(value.as_affine().into()) + } +} + +impl From for Element { + fn from(value: NonceCommitment) -> Self { + Self::deserialize(&value.serialize()).expect("known serialization") + } +} + +impl Serialize for Element { + fn serialize(&self, serializer: S) -> Result where S: Serializer { + serdect::array::serialize_hex_lower_or_bin(&self.0.to_bytes(), serializer) + .map_err(|_| serde::ser::Error::custom("could not serialize element")) + } +} + +impl<'de> Deserialize<'de> for Element { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + let mut bytes = [0; 33]; + serdect::array::deserialize_hex_or_bin(&mut bytes, deserializer)?; + let result = P256Group::deserialize(&bytes) + .map_err(|_| serde::de::Error::custom("invalid element"))?; + Ok(Element(result)) + } +} + +#[derive(Clone, Debug, ZeroizeOnDrop)] +pub(crate) struct Scalar(pub(crate) p256::Scalar); + +impl Serialize for Scalar { + fn serialize(&self, serializer: S) -> Result where S: Serializer { + serdect::array::serialize_hex_lower_or_bin(&self.0.to_bytes(), serializer) + .map_err(|_| serde::ser::Error::custom("could not serialize scalar")) + } +} + +impl<'de> Deserialize<'de> for Scalar { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + let mut bytes = [0; 32]; + serdect::array::deserialize_hex_or_bin(&mut bytes, deserializer)?; + let result = P256ScalarField::deserialize(&bytes) + .map_err(|_| serde::de::Error::custom("invalid scalar"))?; + Ok(Scalar(result)) + } +} + +impl From<&BindingFactor> for Scalar { + fn from(value: &BindingFactor) -> Self { + let mut factor_data = [0u8; 32]; + value.serialize().clone_into(&mut factor_data); + let value = p256::Scalar::from_repr(FieldBytes::from(factor_data)) + .expect("cannot fail given ciphersuite"); + Self(value) + } +} + +impl From for Scalar { + fn from(value: p256::Scalar) -> Self { + Scalar(value) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Proof(pub(crate) Element, pub(crate) Scalar); + +impl From for Proof { + fn from(value: frost::Signature) -> Self { + let serialization: [u8; 65] = value.serialize().try_into() + .expect("known signature size serialization"); + let commitment_bytes = serialization[0..33].try_into() + .expect("known element size serialization"); + let proof_bytes = serialization[33..65].try_into() + .expect("known scalar size serialization"); + let commitment = Element::deserialize(commitment_bytes) + .expect("known element format"); + let proof = P256ScalarField::deserialize(proof_bytes) + .expect("known scalar format"); + Proof(commitment, Scalar(proof)) + } +} + +impl From for frost::Signature { + fn from(value: Proof) -> Self { + frost::Signature::new(value.0.0, value.1.0) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct Signature { + r: Scalar, + s: Scalar, +} + +impl Signature { + pub(crate) fn to_bytes(self) -> [u8; 64] { + let signature: ecdsa::Signature = self.into(); + signature.to_bytes().try_into().expect("known size") + } +} + +impl Into for Signature { + fn into(self) -> ecdsa::Signature { + let binding = postcard::to_allocvec(&self) + .expect("serialization should not fail"); + let bytes = binding.as_slice(); + ecdsa::Signature::from_bytes(bytes.into()).expect("deserialization should not fail") + } +} + +impl From for Signature { + fn from(value: ecdsa::Signature) -> Self { + let binding = value.to_bytes(); + let (r, s) = binding.as_slice().split_at(32); + let r: [u8; 32] = r.try_into().expect("known size"); + let s: [u8; 32] = s.try_into().expect("known size"); + let r = Scalar(P256ScalarField::deserialize(&r).expect("known serialization")); + let s = Scalar(P256ScalarField::deserialize(&s).expect("known serialization")); + Self { r, s } + } +} + +impl TryFrom<&[u8; 64]> for Signature { + type Error = (); + + fn try_from(value: &[u8; 64]) -> Result { + let r: [u8; 32] = value[0..32].try_into().expect("fixed size"); + let s: [u8; 32] = value[32..64].try_into().expect("fixed size"); + let r = P256ScalarField::deserialize(&r).or(Err(()))?; + let s = P256ScalarField::deserialize(&s).or(Err(()))?; + Ok(Self { r: r.into(), s: s.into() }) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3d32aa0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,39 @@ +#![doc = include_str!("README.md")] +//! # Examples +//! +//! All [provider] functions are pure, enabling a mostly stateless server +//! implementation. +//! +//! ## Enrollment +//! +//! Aborting upon failure, the [provider] and [subscriber] execute their +//! assigned functions in this order: +//! +//! 1. [provider]: derive [Randomness], [provider::vouch], and send a [Voucher]; +//! 2. [subscriber::redeem] and send a [Redemption]; +//! 3. [provider::process], derive a [Mask], [provider::validate], and send +//! [Validation]; +//! 4. [subscriber]: derive a [Mask], [subscriber::enroll], and send an +//! [Identifier]; +//! 5. [provider::authorize]. +//! +//! ## Authentication +//! +//! Aborting upon failure: +//! +//! 1. [provider]: derive [Randomness], [provider::challenge], and send a +//! [Challenge]; +//! 2. [subscriber]: derive a [Mask], [subscriber::authenticate], create a +//! device signature, [subscriber::pass], and send a [Pass]; +//! 3. [provider::prove], and log [Evidence]. +//! +//! ## Auditing +//! +//! The [subscriber] or any other party with access can [verify] the [Evidence]. + +mod api; +mod group; +mod domain; +mod program; + +pub use api::*; diff --git a/src/program.rs b/src/program.rs new file mode 100644 index 0000000..8e546ad --- /dev/null +++ b/src/program.rs @@ -0,0 +1,77 @@ +use crate::{api, domain, group}; + +pub(crate) mod provider; +pub(crate) mod subscriber; + +pub fn verify( + identifier: &api::Identifier, + device: &api::Device, + payload: &api::Payload, + evidence: &api::Evidence, +) -> Option { + let identifier: domain::Identifier = postcard::from_bytes(identifier).ok()?; + let device: group::Element = postcard::from_bytes(device).ok()?; + let evidence: domain::Evidence = postcard::from_bytes(evidence).ok()?; + let subscriber = domain::Subscriber { device_vk: device.into(), identifier }; + subscriber.verify(payload, &evidence).then(|| evidence) +} + +#[cfg(test)] +mod tests { + use std::mem::size_of; + use p256::ecdsa; + use rand::{random, thread_rng}; + use signature::Signer; + use crate::program::{provider, subscriber}; + use crate::{Mask, Randomness}; + + #[test] + fn test_all() { + let mut randomness = [0; size_of::()]; + randomness.fill_with(random); + let voucher = provider::vouch(&randomness); + let (redemption, enrollment) = subscriber::redeem(&voucher).unwrap(); + let process = provider::process(&randomness, &redemption).unwrap(); + let mut provider_mask = [0; size_of::()]; + provider_mask.fill_with(random); + let validation = provider::validate(process, &provider_mask); + let mut subscriber_mask = [0; size_of::()]; + subscriber_mask.fill_with(random); + let identifier = subscriber::enroll(enrollment, &validation, &subscriber_mask).unwrap(); + provider::authorize(&validation, &identifier).unwrap(); + let device = ecdsa::SigningKey::random(&mut thread_rng()); + let device_vk = device.verifying_key().clone().to_encoded_point(true) + .to_bytes().to_vec().try_into().unwrap(); + randomness.fill_with(random); + let challenge = provider::challenge(&randomness); + let payload = b"{\"operation\":\"log-in\",\"session\":\"68c9eeeddfa5fb50\"}" + .to_vec() + .try_into() + .unwrap(); + let passing = subscriber::authenticate( + &identifier, + &device_vk, + &challenge, + &subscriber_mask, + &payload + ).unwrap(); + let (device_sig, _) = device.sign(&passing.data_under_device_sig); + let device_sig = device_sig.to_bytes().try_into().unwrap(); + let pass = subscriber::pass(passing, &device_sig).unwrap(); + let evidence = provider::prove( + &randomness, + &identifier, + &device_vk, + &provider_mask, + &payload, + &pass, + ).unwrap(); + println!("randomness {:?}", hex::encode(randomness)); + println!("identifier {:?}", hex::encode(identifier)); + println!("device {:?}", hex::encode(device_vk)); + println!("mask {:?}", hex::encode(provider_mask)); + println!("payload {:?}", hex::encode(payload.0)); + println!("pass {:?}", hex::encode(pass)); + println!("evidence {:?}", hex::encode(evidence)); + } +} diff --git a/src/program/provider.rs b/src/program/provider.rs new file mode 100644 index 0000000..588a37c --- /dev/null +++ b/src/program/provider.rs @@ -0,0 +1,93 @@ +use crate::{api, Challenge, domain, Mask, Randomness, Redemption, Validation, Voucher}; +use crate::domain::{Provider, Subscriber, Validating}; +use crate::group::Element; + +pub(crate) fn vouch( + randomness: &Randomness, +) -> Voucher { + let (_, payload) = Provider { randomness: *randomness }.vouch(); + postcard::to_allocvec(&payload) + .expect("serialization should not fail") + .try_into() + .expect("known size") +} + +pub(crate) fn process( + randomness: &Randomness, + redemption: &Redemption, +) -> Option { + let provider = Provider::from(*randomness); + let redemption = postcard::from_bytes(redemption).ok()?; + provider.validate(redemption) +} + +pub(crate) fn validate( + draft: Validating, + mask: &Mask, +) -> Validation { + let mask = domain::Mask(*mask); + let validation = draft.finalize_with_mask(mask); + postcard::to_allocvec(&validation) + .expect("serialization should not fail") + .try_into() + .expect("known size") +} + +#[derive(Debug)] +pub(crate) enum AuthorizationError { + InvalidInput, + Unauthorized, +} + +pub(crate) fn authorize( + validation: &api::Validation, + identifier: &api::Identifier, +) -> Result<(), AuthorizationError> { + let validation: domain::Validation = postcard::from_bytes(validation) + .map_err(|_| AuthorizationError::InvalidInput)?; + let identifier = postcard::from_bytes(identifier) + .map_err(|_| AuthorizationError::InvalidInput)?; + if validation.authorize(&identifier) { Ok(()) } else { + Err(AuthorizationError::Unauthorized) + } +} + +pub(crate) fn challenge( + randomness: &Randomness, +) -> Challenge { + let (_, challenge) = Provider { randomness: *randomness }.challenge(); + postcard::to_allocvec(&challenge) + .expect("serialization should not fail") + .try_into() + .expect("known size") +} + +#[derive(Debug)] +pub enum ProofError { + SyntaxError, + ContentError(domain::ProofError), +} + +pub(crate) fn prove( + randomness: &api::Randomness, + identifier: &api::Identifier, + device: &api::Device, + mask: &api::Mask, + payload: &api::Payload, + pass: &api::Pass, +) -> Result { + let mask = domain::Mask(*mask); + let pass = postcard::from_bytes(pass).map_err(|_| ProofError::SyntaxError)?; + let identifier = postcard::from_bytes(identifier) + .map_err(|_| ProofError::SyntaxError)?; + let device: Element = postcard::from_bytes(device) + .map_err(|_| ProofError::SyntaxError)?; + let provider = Provider::from(*randomness); + let subscriber = Subscriber { device_vk: device.into(), identifier }; + let evidence = provider.prove(&subscriber, mask, payload, pass) + .map_err(|e| ProofError::ContentError(e))?; + Ok(postcard::to_allocvec(&evidence) + .expect("serialization should not fail") + .try_into() + .expect("known size")) +} diff --git a/src/program/subscriber.rs b/src/program/subscriber.rs new file mode 100644 index 0000000..7cd5bdb --- /dev/null +++ b/src/program/subscriber.rs @@ -0,0 +1,58 @@ +use std::mem::size_of; +use p256::ecdsa; +use rand::thread_rng; +use crate::{api, Challenge, Device, domain, Identifier, Mask, Pass, Payload, Redemption, Voucher}; +use crate::domain::PassDraft; +use crate::subscriber::Signature; + +pub(crate) fn redeem(voucher: &Voucher) -> Option<(Redemption, domain::Enrollment)> { + let voucher = postcard::from_bytes::(voucher).ok()?; + let (enrollment, redemption) = voucher.redeem(&mut thread_rng())?; + let redemption: [u8; size_of::()] = postcard::to_allocvec(&redemption) + .expect("serialization should not fail") + .try_into() + .expect("known size"); + Some((redemption, enrollment)) +} + +pub(crate) fn enroll( + enrollment: domain::Enrollment, + validation: &api::Validation, + mask: &api::Mask, +) -> Option { + let mask = domain::Mask(*mask); + let validation = postcard::from_bytes(validation).ok()?; + let identifier = enrollment.complete(validation, mask)?; + Some(postcard::to_allocvec(&identifier) + .expect("serialization should not fail") + .try_into() + .expect("known size")) +} + +pub(crate) fn authenticate( + identifier: &Identifier, + device: &Device, + challenge: &Challenge, + mask: &Mask, + payload: &Payload, +) -> Option { + let identifier = postcard::from_bytes(identifier).ok()?; + let challenge = postcard::from_bytes(challenge).ok()?; + let mask = domain::Mask(*mask); + let device_vk = ecdsa::VerifyingKey::from_sec1_bytes(device).ok()?; + let subscriber = domain::Subscriber { device_vk, identifier }; + Some(subscriber.pass(mask, challenge, payload, &mut thread_rng())) +} + +pub(crate) fn pass( + draft: PassDraft, + device_sig: &Signature, +) -> Option { + let device_sig = ecdsa::Signature::from_bytes(device_sig.into()).ok()?; + let pass = draft.finalize_with_signature(device_sig); + let pass: Pass = postcard::to_allocvec(&pass) + .expect("serialization should not fail") + .try_into() + .expect("known size"); + Some(pass) +}