diff --git a/.gitignore b/.gitignore index 4fffb2f..c9ae557 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +drand-client-rs.iml diff --git a/Cargo.toml b/Cargo.toml index e97206c..9e63c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,9 @@ edition = "2021" [dependencies] hex = { version = "0.4.3", features = ["serde"] } -url = "2.3.1" -reqwest = { version = "0.11", features = ["blocking"] } -json = "0.12.4" -bls-signatures = "0.13.0" +sha2 = "0.9" +bls12_381 = { version = "0.8.0", features = ["experimental"] } thiserror = "1.0.38" -sha256 = "1.1.1" -serde = { version = "1.0.151", features = ["derive"] } -serde_json = "1.0.91" \ No newline at end of file +serde = { version = "1.0.187", features = ["derive"] } +serde_json = "1.0.105" +reqwest = { version = "0.11.20", features = ["blocking", "json"] } \ No newline at end of file diff --git a/src/bls.rs b/src/bls.rs deleted file mode 100644 index 49ef78f..0000000 --- a/src/bls.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::chain_info::ChainInfo; -use crate::SchemeError; -use bls_signatures::{hash, verify, PublicKey, Serialize, Signature}; - -pub trait BlsVerifiable { - fn signature(&self) -> &Vec; - fn to_message(&self) -> Result, SchemeError>; -} - -pub(crate) fn bls_verify(info: &ChainInfo, beacon: B) -> Result { - let public_key = PublicKey::from_bytes(info.public_key.as_slice()) - .map_err(|_| SchemeError::InvalidChainInfo)?; - - let signature = Signature::from_bytes(beacon.signature().as_slice()) - .map_err(|_| SchemeError::InvalidBeacon)?; - - let bls_message_bytes = beacon - .to_message() - .map(|bytes| sha256::digest(bytes.as_slice())) - .and_then(|hex_str| hex::decode(hex_str).map_err(|_| SchemeError::InvalidBeacon))?; - - let point_on_curve = hash(bls_message_bytes.as_slice()); - if !verify(&signature, &[point_on_curve], &[public_key]) { - Err(SchemeError::InvalidBeacon) - } else { - Ok(beacon) - } -} diff --git a/src/chain_info.rs b/src/chain_info.rs index 4b4e572..cde7c11 100644 --- a/src/chain_info.rs +++ b/src/chain_info.rs @@ -1,9 +1,10 @@ use serde::Deserialize; +use crate::scheme::SchemeID; #[derive(Deserialize, Debug, PartialEq, Clone)] pub struct ChainInfo { #[serde(alias = "schemeID")] - pub scheme_id: String, + pub scheme_id: SchemeID, #[serde(with = "hex")] pub public_key: Vec, #[serde(with = "hex", alias = "hash")] diff --git a/src/chained.rs b/src/chained.rs deleted file mode 100644 index 30266b5..0000000 --- a/src/chained.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::bls::BlsVerifiable; -use crate::chain_info::ChainInfo; -use crate::{bls, Scheme, SchemeError}; -use serde::Deserialize; -use std::io::Write; - -#[derive(Deserialize, Debug, PartialEq, Clone)] -pub struct ChainedBeacon { - #[serde(alias = "round")] - pub round_number: u64, - #[serde(with = "hex")] - pub randomness: Vec, - #[serde(with = "hex")] - pub signature: Vec, - #[serde(with = "hex")] - pub previous_signature: Vec, -} - -pub struct ChainedScheme {} - -impl Scheme for ChainedScheme { - fn supports(&self, scheme_id: &str) -> bool { - scheme_id.eq_ignore_ascii_case("pedersen-bls-chained") - } - - fn verify( - &self, - info: &ChainInfo, - beacon: ChainedBeacon, - ) -> Result { - if !self.supports(&info.scheme_id) { - Err(SchemeError::InvalidScheme) - } else { - bls::bls_verify(info, beacon) - } - } -} - -impl BlsVerifiable for ChainedBeacon { - fn signature(&self) -> &Vec { - &self.signature - } - - fn to_message(&self) -> Result, SchemeError> { - let mut bytes: Vec = vec![]; - - if bytes.write_all(self.previous_signature.as_slice()).is_err() { - return Err(SchemeError::InvalidBeacon); - } - if bytes.write_all(&self.round_number.to_be_bytes()).is_err() { - Err(SchemeError::InvalidBeacon) - } else { - Ok(bytes) - } - } -} diff --git a/src/http.rs b/src/http.rs index f58e57e..64f4f73 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,33 +1,31 @@ use reqwest::blocking::Client; use reqwest::StatusCode; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum HttpError { - #[error("not found")] - NotFound, - #[error("unexpected")] - Unexpected, -} +use crate::{Transport, TransportError}; pub struct HttpTransport { pub client: Client, } -impl HttpTransport { - pub fn fetch(&self, url: &str) -> Result { +impl Transport for HttpTransport { + fn fetch(&self, url: &str) -> Result { let res = self .client .get(url) .send() - .map_err(|_| HttpError::Unexpected)?; + .map_err(|_| TransportError::Unexpected)?; match res.status() { - StatusCode::OK => res.text().map_err(|_| HttpError::Unexpected), + StatusCode::OK => res.text().map_err(|_| TransportError::Unexpected), - StatusCode::NOT_FOUND => Err(HttpError::NotFound), + StatusCode::NOT_FOUND => Err(TransportError::NotFound), - _ => Err(HttpError::Unexpected), + _ => Err(TransportError::Unexpected), } } } + +pub fn new_http_transport() -> HttpTransport { + HttpTransport { + client: Client::new() + } +} diff --git a/src/lib.rs b/src/lib.rs index 4dcdbf6..d63fd4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,65 +1,34 @@ extern crate core; -mod bls; mod chain_info; -mod chained; mod http; -mod unchained; +mod verify; +mod scheme; use crate::chain_info::ChainInfo; -use crate::chained::{ChainedBeacon, ChainedScheme}; -use crate::http::HttpTransport; -use crate::unchained::{UnchainedBeacon, UnchainedScheme}; use crate::DrandClientError::{InvalidChainInfo, InvalidRound}; -use reqwest::blocking::Client; -use serde::de::DeserializeOwned; use thiserror::Error; +use crate::http::{HttpTransport, new_http_transport}; +use crate::scheme::{Beacon, verify_beacon}; -pub struct DrandClient<'a, B> { - scheme: &'a dyn Scheme, - transport: HttpTransport, +pub struct DrandClient<'a, T: Transport> { + transport: T, base_url: &'a str, chain_info: ChainInfo, } -pub fn new_chained_client(base_url: &str) -> Result, DrandClientError> { - return new_client(&ChainedScheme {}, base_url); -} - -pub fn new_unchained_client( - base_url: &str, -) -> Result, DrandClientError> { - return new_client(&UnchainedScheme {}, base_url); -} - -pub fn new_client<'a, S: Scheme, B>( - scheme: &'a S, - base_url: &'a str, -) -> Result, DrandClientError> { - let http_transport = HttpTransport { - client: Client::new(), - }; +fn new_http_client(base_url: &str) -> Result, DrandClientError> { + let http_transport = new_http_transport(); let chain_info = fetch_chain_info(&http_transport, base_url)?; - let client = DrandClient { + return Ok(DrandClient { + base_url, transport: http_transport, chain_info, - scheme, - base_url, - }; - - Ok(client) + }); } -#[derive(Error, Debug, PartialEq)] -pub enum DrandClientError { - #[error("invalid round")] - InvalidRound, - #[error("invalid beacon")] - InvalidBeacon, - #[error("invalid chain info")] - InvalidChainInfo, - #[error("not responding")] - NotResponding, +pub trait Transport { + fn fetch(&self, url: &str) -> Result; } pub fn fetch_chain_info( @@ -68,66 +37,79 @@ pub fn fetch_chain_info( ) -> Result { let url = format!("{}/info", base_url); match transport.fetch(&url) { - Err(_) => Err(DrandClientError::NotResponding), - Ok(body) => serde_json::from_str(&body).map_err(|_| InvalidChainInfo), + Err(_) => + Err(DrandClientError::NotResponding), + Ok(body) => + serde_json::from_str(&body) + .map_err(|e| { + println!("{}", e); + InvalidChainInfo + }) } } -impl<'a, B> DrandClient<'a, B> -where - B: DeserializeOwned, -{ - pub fn latest_randomness(&self) -> Result { +impl<'a, T: Transport> DrandClient<'a, T> { + pub fn latest_randomness(&self) -> Result { self.fetch_beacon_tag("latest") } - pub fn randomness(&self, round_number: u64) -> Result { + pub fn randomness(&self, round_number: u64) -> Result { if round_number == 0 { return Err(InvalidRound); } self.fetch_beacon_tag(&format!("{}", round_number)) } - fn fetch_beacon_tag(&self, tag: &str) -> Result { + fn fetch_beacon_tag(&self, tag: &str) -> Result { let url = format!("{}/public/{}", self.base_url, tag); + match self.transport.fetch(&url) { Err(_) => Err(DrandClientError::NotResponding), - Ok(body) => match serde_json::from_str(&body) { - Ok(json) => self - .scheme - .verify(&self.chain_info, json) - .map_err(|_| DrandClientError::InvalidBeacon), + Ok(body) => match serde_json::from_str::(&body) { + Ok(beacon) => { + verify_beacon(&self.chain_info.scheme_id, &self.chain_info.public_key, &beacon) + .map_err(|_| DrandClientError::FailedVerification)?; + Ok(beacon) + } Err(_) => Err(DrandClientError::InvalidBeacon), }, } } } -#[derive(Error, Debug)] -pub enum SchemeError { +#[derive(Error, Debug, PartialEq)] +pub enum DrandClientError { + #[error("invalid round")] + InvalidRound, #[error("invalid beacon")] InvalidBeacon, - #[error("invalid scheme")] - InvalidScheme, + #[error("beacon failed verification")] + FailedVerification, #[error("invalid chain info")] InvalidChainInfo, + #[error("not responding")] + NotResponding, } -pub trait Scheme { - fn supports(&self, scheme_id: &str) -> bool; - fn verify(&self, info: &ChainInfo, beacon: B) -> Result; +#[derive(Error, Debug)] +pub enum TransportError { + #[error("not found")] + NotFound, + #[error("unexpected")] + Unexpected, } + #[cfg(test)] mod test { use crate::DrandClientError::InvalidRound; - use crate::{new_chained_client, new_unchained_client, DrandClientError}; + use crate::{DrandClientError, new_http_client}; #[test] fn request_chained_randomness_success() -> Result<(), DrandClientError> { let chained_url = "https://api.drand.sh"; - let client = new_chained_client(chained_url)?; + let client = new_http_client(chained_url)?; let randomness = client.latest_randomness()?; assert!(randomness.round_number > 0); return Ok(()); @@ -136,37 +118,35 @@ mod test { #[test] fn request_unchained_randomness_success() -> Result<(), DrandClientError> { let unchained_url = "https://pl-eu.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf"; - let client = new_unchained_client(unchained_url)?; + let client = new_http_client(unchained_url)?; let randomness = client.latest_randomness()?; assert!(randomness.round_number > 0); return Ok(()); } #[test] - fn request_unchained_randomness_wrong_client_error() -> Result<(), DrandClientError> { - let unchained_url = "https://pl-eu.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf"; - let client = new_chained_client(unchained_url)?; - let result = client.latest_randomness(); + fn request_genesis_returns_error() -> Result<(), DrandClientError> { + let chained_url = "https://api.drand.sh"; + let client = new_http_client(chained_url)?; + let result = client.randomness(0); assert!(result.is_err()); + assert_eq!(result.unwrap_err(), InvalidRound); return Ok(()); } #[test] - fn request_chained_randomness_wrong_client_error() -> Result<(), DrandClientError> { - let chained_url = "https://api.drand.sh"; - let client = new_unchained_client(chained_url)?; - let result = client.latest_randomness(); - assert!(result.is_err()); - return Ok(()); + fn request_g1g2swapped_beacon_succeeds() -> Result<(), DrandClientError> { + let unchained_url = "https://api.drand.sh/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493"; + let client = new_http_client(unchained_url)?; + client.randomness(1)?; + Ok(()) } #[test] - fn request_genesis_returns_error() -> Result<(), DrandClientError> { - let chained_url = "https://api.drand.sh"; - let client = new_chained_client(chained_url); - let result = client?.randomness(0); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), InvalidRound); - return Ok(()); + fn request_g1g2swapped_rfc_beacon_succeeds() -> Result<(), DrandClientError> { + let unchained_url = "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"; + let client = new_http_client(unchained_url)?; + client.randomness(1)?; + Ok(()) } } diff --git a/src/scheme.rs b/src/scheme.rs new file mode 100644 index 0000000..65e2528 --- /dev/null +++ b/src/scheme.rs @@ -0,0 +1,141 @@ +use sha2::{Digest, Sha256}; +use serde::{Deserialize, Deserializer}; +use crate::verify::{verify_on_g1, verify_on_g2}; + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub struct Beacon { + #[serde(alias = "round")] + pub round_number: u64, + #[serde(with = "hex")] + pub randomness: Vec, + #[serde(with = "hex")] + pub signature: Vec, + #[serde(default, with = "hex")] + pub previous_signature: Vec, +} + +const DST_G1: &str = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; +const DST_G2: &str = "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; + +#[derive(Debug, PartialEq, Clone)] +pub enum SchemeID { + PedersenBlsChained, + PedersenBlsUnchained, + UnchainedOnG1, + UnchainedOnG1RFC9380, +} + +impl<'de> Deserialize<'de> for SchemeID { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = Deserialize::deserialize(deserializer)?; + match s { + "pedersen-bls-chained" => Ok(SchemeID::PedersenBlsChained), + "pedersen-bls-unchained" => Ok(SchemeID::PedersenBlsUnchained), + "bls-unchained-on-g1" => Ok(SchemeID::UnchainedOnG1), + "bls-unchained-g1-rfc9380" => Ok(SchemeID::UnchainedOnG1RFC9380), + _ => Err(serde::de::Error::unknown_variant(&s, &["pedersen-bls-chained", "pedersen-bls-unchained", "bls-unchained-on-g1", "bls-unchained-g1-rfc9380"])) + } + } +} + +pub fn verify_beacon<'a>(scheme_id: &SchemeID, public_key: &[u8], beacon: &'a Beacon) -> Result<(), &'a str> { + match scheme_id { + SchemeID::PedersenBlsChained => + verify_on_g2(public_key, &chained_beacon_message(beacon)?, &beacon.signature, DST_G2), + SchemeID::PedersenBlsUnchained => + verify_on_g2(public_key, &unchained_beacon_message(beacon)?, &beacon.signature, DST_G2), + SchemeID::UnchainedOnG1 => + verify_on_g1(public_key, &unchained_beacon_message(beacon)?, &beacon.signature, DST_G2), + SchemeID::UnchainedOnG1RFC9380 => + verify_on_g1(public_key, &unchained_beacon_message(beacon)?, &beacon.signature, DST_G1), + } +} + +fn unchained_beacon_message<'a>(beacon: &Beacon) -> Result, &'a str> { + if beacon.previous_signature.len() > 0 { + return Err("unchained schemes cannot contain a `previous_signature`"); + } + let round_bytes = beacon.round_number.to_be_bytes(); + + Ok(Sha256::digest(&round_bytes).to_vec()) +} + +fn chained_beacon_message<'a>(beacon: &Beacon) -> Result, &'a str> { + if beacon.previous_signature.len() == 0 { + Err("chained beacons must have a `previous_signature`") + } else { + // surely there's a better way to concat two slices + let mut message = Vec::new(); + message.extend_from_slice(&beacon.previous_signature.as_slice()); + message.extend_from_slice(&beacon.round_number.to_be_bytes()); + Ok(Sha256::digest(message.as_slice()).to_vec()) + } +} + +#[cfg(test)] +mod test { + use crate::scheme::{Beacon, SchemeID, verify_beacon}; + + #[test] + fn default_beacon_verifies() { + let public_key = hexify("88a8227b75dba145599d894d33eebde3b36fef900d456ae2cc4388867adb4769c40359f783750a41b4d17e40f578bfdb"); + let prev_sig = hexify("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce"); + + let beacon = Beacon { + round_number: 397089, + randomness: hexify("cd435675735e459fb4d9c68a9d9f7b719e59e0a9f5f86fe6bd86b730d01fba42"), + signature: hexify("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a"), + previous_signature: prev_sig, + }; + + assert!(matches!(verify_beacon(&SchemeID::PedersenBlsChained, &public_key, &beacon), Ok(()))); + } + + #[test] + fn testnet_unchained_beacon_verifies() { + let public_key = hexify("8d91ae0f4e3cd277cfc46aba26680232b0d5bb4444602cdb23442d62e17f43cdffb1104909e535430c10a6a1ce680a65"); + let beacon = Beacon { + round_number: 397092, + randomness: hexify("7731783ab8118d7484d0e8e237f3023a4c7ef4532f35016f2e56e89a7570c796"), + signature: hexify("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539"), + previous_signature: Vec::new(), + }; + + assert!(matches!(verify_beacon(&SchemeID::PedersenBlsUnchained, &public_key, &beacon), Ok(_))); + } + + #[test] + fn g1g2_swap_non_rfc_beacon_verifies() { + let public_key = hexify("a0b862a7527fee3a731bcb59280ab6abd62d5c0b6ea03dc4ddf6612fdfc9d01f01c31542541771903475eb1ec6615f8d0df0b8b6dce385811d6dcf8cbefb8759e5e616a3dfd054c928940766d9a5b9db91e3b697e5d70a975181e007f87fca5e"); + let beacon = Beacon { + round_number: 3, + randomness: hexify("a4eb0ed6c4132da066843c3bfdce732ce5013eda86e74c136ab8ccc387b798dd"), + signature: hexify("8176555f90d71aa49ceb37739683749491c2bab15a46094b255289ed25cf8f01cdfb1fe8bd9cd5a19eb09448a3e53186"), + previous_signature: Vec::new(), + }; + + assert!(matches!(verify_beacon(&SchemeID::UnchainedOnG1, &public_key, &beacon), Ok(_))); + } + + #[test] + fn g1g2_swap_rfc_beacon_verifies() { + let public_key = hexify("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"); + let beacon = Beacon { + round_number: 1000, + randomness: hexify("fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd"), + signature: hexify("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"), + previous_signature: Vec::new(), + }; + + assert!(matches!(verify_beacon(&SchemeID::UnchainedOnG1RFC9380, &public_key, &beacon), Ok(_))); + } + + fn hexify(s: &str) -> Vec { + return hex::decode(s) + .unwrap() + .to_vec(); + } +} diff --git a/src/unchained.rs b/src/unchained.rs deleted file mode 100644 index 3fd5207..0000000 --- a/src/unchained.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::bls::BlsVerifiable; -use crate::chain_info::ChainInfo; -use crate::{bls, Scheme, SchemeError}; -use serde::Deserialize; -use std::io::Write; - -#[derive(Deserialize, Debug, PartialEq, Clone)] -pub struct UnchainedBeacon { - #[serde(alias = "round")] - pub round_number: u64, - #[serde(with = "hex")] - pub randomness: Vec, - #[serde(with = "hex")] - pub signature: Vec, -} - -pub struct UnchainedScheme {} - -impl Scheme for UnchainedScheme { - fn supports(&self, scheme_id: &str) -> bool { - scheme_id.eq_ignore_ascii_case("pedersen-bls-unchained") - } - - fn verify( - &self, - info: &ChainInfo, - beacon: UnchainedBeacon, - ) -> Result { - if !self.supports(&info.scheme_id) { - Err(SchemeError::InvalidScheme) - } else { - bls::bls_verify(info, beacon) - } - } -} - -impl BlsVerifiable for UnchainedBeacon { - fn signature(&self) -> &Vec { - &self.signature - } - - fn to_message(&self) -> Result, SchemeError> { - let mut bytes: Vec = vec![]; - - if bytes.write_all(&self.round_number.to_be_bytes()).is_err() { - Err(SchemeError::InvalidBeacon) - } else { - Ok(bytes) - } - } -} diff --git a/src/verify.rs b/src/verify.rs new file mode 100644 index 0000000..41e8b58 --- /dev/null +++ b/src/verify.rs @@ -0,0 +1,161 @@ +use std::ops::{Neg}; +use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt, multi_miller_loop}; +use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve}; + +pub fn verify_on_g2<'a>(public_key: &[u8], message: &[u8], signature: &[u8], domain_separation_tag: &str) -> Result<(), &'a str> { + let pub_key_bytes: &[u8; 48] = public_key + .try_into() + .map_err(|_| "public key wrong length")?; + + let sig_bytes: &[u8; 96] = signature + .try_into() + .map_err(|_| "signature wrong length")?; + + let p = G1Affine::from_compressed(pub_key_bytes).unwrap(); + let q = G2Affine::from_compressed(sig_bytes).unwrap(); + + if p.is_on_curve().unwrap_u8() != 1 { + return Err("not on curve"); + } + + if p.is_identity().unwrap_u8() == 1 { + return Err("cannot use point at infinity"); + } + + if message.len() == 0 { + return Err("message can't be empty"); + } + + if signature.len() == 0 { + return Err("signature can't be empty"); + } + + let m = >>::hash_to_curve( + &message, domain_separation_tag.as_bytes(), + ); + + let m_prepared = G2Prepared::from(G2Affine::from(m)); + let q_prepared = G2Prepared::from(q); + let exp = multi_miller_loop( + &[ + (&p.neg(), &m_prepared), + (&G1Affine::generator(), &q_prepared) + ] + ); + + if exp.final_exponentiation() != Gt::identity() { + Err("verification failed") + } else { + Ok(()) + } +} + +pub fn verify_on_g1<'a>(public_key: &[u8], message: &[u8], signature: &[u8], domain_separation_tag: &str) -> Result<(), &'a str> { + let pub_key_bytes: &[u8; 96] = public_key + .try_into() + .map_err(|_| "public key wrong length")?; + + let sig_bytes: &[u8; 48] = signature + .try_into() + .map_err(|_| "signature wrong length")?; + + let signature_point = G1Affine::from_compressed(sig_bytes).unwrap(); + let pubkey_point = G2Affine::from_compressed(pub_key_bytes).unwrap(); + + if pubkey_point.is_on_curve().unwrap_u8() != 1 { + return Err("not on curve"); + } + + if pubkey_point.is_identity().unwrap_u8() == 1 { + return Err("cannot use point at infinity"); + } + + if message.len() == 0 { + return Err("message can't be empty"); + } + + if signature.len() == 0 { + return Err("signature can't be empty"); + } + + let m = >>::hash_to_curve( + &message, domain_separation_tag.as_bytes(), + ); + + let pubkey_prepared = G2Prepared::from(pubkey_point.neg()); + let g2_base = G2Prepared::from(G2Affine::generator()); + let exp = multi_miller_loop( + &[ + (&G1Affine::from(m), &pubkey_prepared), + (&signature_point, &g2_base) + ] + ); + + if exp.final_exponentiation() != Gt::identity() { + Err("verification failed") + } else { + Ok(()) + } +} + +#[cfg(test)] +mod test { + use sha2::Digest; + use crate::verify::{verify_on_g2, verify_on_g1}; + + #[test] + fn g1_verifies_a_beacon() -> Result<(), ()> { + let public_key = hex::decode("88a8227b75dba145599d894d33eebde3b36fef900d456ae2cc4388867adb4769c40359f783750a41b4d17e40f578bfdb").unwrap(); + let round: u64 = 397089; + let sig = hex::decode("88ccd9a91946bc0bbef2c6c60a09bbf4a247b1d2059522449aa1a35758feddfad85efe818bbde3e1e4ab0c852d96e65f0b1f97f239bf3fc918860ea846cbb500fcf7c9d0dd3d851320374460b5fc596b8cfd629f4c07c7507c259bf9beca850a").unwrap(); + let prev_sig = hex::decode("a2237ee39a1a6569cb8e02c6e979c07efe1f30be0ac501436bd325015f1cd6129dc56fd60efcdf9158d74ebfa34bfcbd17803dbca6d2ae8bc3a968e4dc582f8710c69de80b2e649663fef5742d22fff7d1619b75d5f222e8c9b8840bc2044bce").unwrap(); + + let mut message = Vec::new(); + message.extend(prev_sig); + message.extend(round.to_be_bytes()); + + let m = sha2::Sha256::digest(message.as_slice()); + + let _ = verify_on_g2(&public_key, &m.to_vec(), &sig, "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_").unwrap(); + Ok(()) + } + + #[test] + fn g1_unchained_beacon_verifies() -> Result<(), ()> { + let public_key = hex::decode("8d91ae0f4e3cd277cfc46aba26680232b0d5bb4444602cdb23442d62e17f43cdffb1104909e535430c10a6a1ce680a65").unwrap(); + let round: u64 = 397092; + let sig = hex::decode("94da96b5b985a22a3d99fa3051a42feb4da9218763f6c836fca3770292dbf4b01f5d378859a113960548d167eaa144250a2c8e34c51c5270152ac2bc7a52632236f746545e0fae52f69068c017745204240d19dae2b4d038cef3c6047fcd6539").unwrap(); + + let mut message = Vec::new(); + message.extend(round.to_be_bytes()); + + let m = sha2::Sha256::digest(message.as_slice()); + + let _ = verify_on_g2(&public_key, &m.to_vec(), &sig, "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_").unwrap(); + Ok(()) + } + + #[test] + fn g2_non_rfc_verifies_a_beacon() -> Result<(), ()> { + let public_key = hex::decode("a0b862a7527fee3a731bcb59280ab6abd62d5c0b6ea03dc4ddf6612fdfc9d01f01c31542541771903475eb1ec6615f8d0df0b8b6dce385811d6dcf8cbefb8759e5e616a3dfd054c928940766d9a5b9db91e3b697e5d70a975181e007f87fca5e").unwrap(); + let round: u64 = 3; + let sig = hex::decode("8176555f90d71aa49ceb37739683749491c2bab15a46094b255289ed25cf8f01cdfb1fe8bd9cd5a19eb09448a3e53186").unwrap(); + + let m = sha2::Sha256::digest(round.to_be_bytes().as_slice()); + + let _ = verify_on_g1(&public_key, &m.to_vec(), &sig, "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_").unwrap(); + Ok(()) + } + + #[test] + fn g2_rfc_verifies_a_beacon() -> Result<(), ()> { + let public_key = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a").unwrap(); + let round: u64 = 1000; + let sig = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39").unwrap(); + + let m = sha2::Sha256::digest(round.to_be_bytes().as_slice()); + + let _ = verify_on_g1(&public_key, &m.to_vec(), &sig, "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_").unwrap(); + Ok(()) + } +} \ No newline at end of file