use std::collections::BTreeMap;
use ed25519_dalek::{Signature, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
use crate::circuit::{GateIndex, Slice};
pub const MAX_EXPR_DEPTH: usize = 8;
pub const MAX_EXPR_NODES: usize = 64;
#[cfg_attr(test, derive(strum::AsRefStr, strum::VariantNames))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[repr(C)]
pub enum DigestAlgorithm {
Sha256,
}
impl DigestAlgorithm {
pub const fn output_bits(&self) -> u32 {
match self {
DigestAlgorithm::Sha256 => 256,
}
}
fn hash(&self, bytes: &[u8]) -> Vec<u8> {
match self {
DigestAlgorithm::Sha256 => Sha256::digest(bytes).to_vec(),
}
}
}
#[cfg_attr(test, derive(strum::AsRefStr, strum::VariantNames))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[repr(C)]
pub enum Encoding {
Base64UrlNoPad,
}
impl Encoding {
pub const fn decoded_bits(&self, encoded_bits: u32) -> Option<u32> {
match self {
Encoding::Base64UrlNoPad => {
if !encoded_bits.is_multiple_of(8) {
return None;
}
let chars = encoded_bits / 8;
if chars % 4 == 1 {
return None;
}
Some(8 * (3 * chars / 4))
}
}
}
fn decode(&self, bytes: &[u8]) -> Option<Vec<u8>> {
match self {
Encoding::Base64UrlNoPad => {
let mut out = Vec::with_capacity(3 * bytes.len() / 4);
for group in bytes.chunks(4) {
if group.len() == 1 {
return None;
}
let mut acc = 0u32;
for byte in group {
acc = (acc << 6) | u32::from(base64url_digit(*byte)?);
}
let whole_bytes = group.len() - 1;
let spare = 6 * group.len() - 8 * whole_bytes;
if acc & ((1 << spare) - 1) != 0 {
return None;
}
acc >>= spare;
for i in (0..whole_bytes).rev() {
out.push((acc >> (8 * i)) as u8);
}
}
Some(out)
}
}
}
}
const fn base64url_digit(byte: u8) -> Option<u8> {
match byte {
b'A'..=b'Z' => Some(byte - b'A'),
b'a'..=b'z' => Some(byte - b'a' + 26),
b'0'..=b'9' => Some(byte - b'0' + 52),
b'-' => Some(62),
b'_' => Some(63),
_ => None,
}
}
#[cfg_attr(test, derive(strum::AsRefStr, strum::VariantNames))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub enum OnAmbiguity {
Fail,
TakeSmallestBits,
}
#[cfg_attr(test, derive(strum::AsRefStr, strum::VariantNames))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub enum ConstraintExpr {
Slice(Slice),
Constant(Vec<u8>),
Concat(Vec<ConstraintExpr>),
Digest {
algorithm: DigestAlgorithm,
of: Box<ConstraintExpr>,
},
Wire(GateIndex),
Decode {
encoding: Encoding,
of: Box<ConstraintExpr>,
},
}
impl ConstraintExpr {
pub fn slices(&self) -> Vec<&Slice> {
match self {
ConstraintExpr::Slice(slice) => vec![slice],
ConstraintExpr::Constant(_) | ConstraintExpr::Wire(_) => Vec::new(),
ConstraintExpr::Concat(parts) => {
parts.iter().flat_map(ConstraintExpr::slices).collect()
}
ConstraintExpr::Digest { of, .. } | ConstraintExpr::Decode { of, .. } => of.slices(),
}
}
pub fn wires(&self) -> Vec<GateIndex> {
match self {
ConstraintExpr::Wire(wire) => vec![*wire],
ConstraintExpr::Slice(_) | ConstraintExpr::Constant(_) => Vec::new(),
ConstraintExpr::Concat(parts) => parts.iter().flat_map(ConstraintExpr::wires).collect(),
ConstraintExpr::Digest { of, .. } | ConstraintExpr::Decode { of, .. } => of.wires(),
}
}
pub fn wires_mut(&mut self) -> Vec<&mut GateIndex> {
match self {
ConstraintExpr::Wire(wire) => vec![wire],
ConstraintExpr::Slice(_) | ConstraintExpr::Constant(_) => Vec::new(),
ConstraintExpr::Concat(parts) => parts
.iter_mut()
.flat_map(ConstraintExpr::wires_mut)
.collect(),
ConstraintExpr::Digest { of, .. } | ConstraintExpr::Decode { of, .. } => of.wires_mut(),
}
}
pub fn depth(&self) -> usize {
match self {
ConstraintExpr::Slice(_) | ConstraintExpr::Constant(_) | ConstraintExpr::Wire(_) => 1,
ConstraintExpr::Concat(parts) => {
1 + parts.iter().map(ConstraintExpr::depth).max().unwrap_or(0)
}
ConstraintExpr::Digest { of, .. } | ConstraintExpr::Decode { of, .. } => 1 + of.depth(),
}
}
pub fn node_count(&self) -> usize {
match self {
ConstraintExpr::Slice(_) | ConstraintExpr::Constant(_) | ConstraintExpr::Wire(_) => 1,
ConstraintExpr::Concat(parts) => {
1 + parts.iter().map(ConstraintExpr::node_count).sum::<usize>()
}
ConstraintExpr::Digest { of, .. } | ConstraintExpr::Decode { of, .. } => {
1 + of.node_count()
}
}
}
pub fn static_len<F>(&self, wire_bits: &F) -> Option<u32>
where
F: Fn(GateIndex) -> Option<u32>,
{
match self {
ConstraintExpr::Slice(slice) => Some(slice.len()),
ConstraintExpr::Constant(bytes) => u32::try_from(8 * bytes.len()).ok(),
ConstraintExpr::Wire(wire) => wire_bits(*wire),
ConstraintExpr::Concat(parts) => parts.iter().try_fold(0u32, |acc, part| {
part.static_len(wire_bits)
.and_then(|len| acc.checked_add(len))
}),
ConstraintExpr::Digest { algorithm, .. } => Some(algorithm.output_bits()),
ConstraintExpr::Decode { encoding, of } => of
.static_len(wire_bits)
.and_then(|bits| encoding.decoded_bits(bits)),
}
}
pub fn eval(&self, bits: &[bool], wires: &BTreeMap<GateIndex, Vec<bool>>) -> Option<Vec<bool>> {
match self {
ConstraintExpr::Slice(slice) => slice
.get_indices()
.into_iter()
.map(|i| bits.get(i as usize).copied())
.collect(),
ConstraintExpr::Constant(bytes) => Some(bytes_to_bits(bytes)),
ConstraintExpr::Wire(wire) => wires.get(wire).cloned(),
ConstraintExpr::Concat(parts) => {
let mut out = Vec::new();
for part in parts {
out.extend(part.eval(bits, wires)?);
}
Some(out)
}
ConstraintExpr::Digest { algorithm, of } => {
let inner = of.eval(bits, wires)?;
Some(bytes_to_bits(&algorithm.hash(&bits_to_bytes(&inner)?)))
}
ConstraintExpr::Decode { encoding, of } => {
let inner = of.eval(bits, wires)?;
Some(bytes_to_bits(&encoding.decode(&bits_to_bytes(&inner)?)?))
}
}
}
}
#[cfg_attr(test, derive(strum::AsRefStr, strum::VariantNames))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[repr(C)]
pub enum Relation {
AtMost,
AtLeast,
}
#[cfg_attr(test, derive(strum::AsRefStr, strum::VariantNames))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[repr(C)]
pub enum SignatureScheme {
Ed25519,
}
impl SignatureScheme {
pub const fn signature_bits(&self) -> u32 {
match self {
SignatureScheme::Ed25519 => 512,
}
}
pub const fn public_key_bits(&self) -> u32 {
match self {
SignatureScheme::Ed25519 => 256,
}
}
}
#[cfg_attr(test, derive(strum::AsRefStr, strum::VariantNames))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub enum PlaintextBitConstraint {
Signature {
scheme: SignatureScheme,
signature: ConstraintExpr,
message: ConstraintExpr,
public_key: ConstraintExpr,
},
Equality {
bits: ConstraintExpr,
expected: ConstraintExpr,
},
Comparison {
relation: Relation,
lhs: ConstraintExpr,
rhs: ConstraintExpr,
},
}
impl PlaintextBitConstraint {
pub fn operands(&self) -> Vec<&ConstraintExpr> {
match self {
PlaintextBitConstraint::Signature {
signature,
message,
public_key,
..
} => vec![signature, message, public_key],
PlaintextBitConstraint::Equality { bits, expected } => vec![bits, expected],
PlaintextBitConstraint::Comparison { lhs, rhs, .. } => vec![lhs, rhs],
}
}
fn operands_mut(&mut self) -> Vec<&mut ConstraintExpr> {
match self {
PlaintextBitConstraint::Signature {
signature,
message,
public_key,
..
} => vec![signature, message, public_key],
PlaintextBitConstraint::Equality { bits, expected } => vec![bits, expected],
PlaintextBitConstraint::Comparison { lhs, rhs, .. } => vec![lhs, rhs],
}
}
pub fn covering_operands(&self) -> Vec<&ConstraintExpr> {
match self {
PlaintextBitConstraint::Signature {
signature, message, ..
} => vec![signature, message],
PlaintextBitConstraint::Equality { bits, expected } => {
match (bits.slices().is_empty(), expected.slices().is_empty()) {
(true, _) => vec![expected],
(_, true) => vec![bits],
_ => Vec::new(),
}
}
PlaintextBitConstraint::Comparison { .. } => Vec::new(),
}
}
pub fn slices(&self) -> Vec<&Slice> {
self.operands()
.into_iter()
.flat_map(ConstraintExpr::slices)
.collect()
}
pub fn wires(&self) -> Vec<GateIndex> {
self.operands()
.into_iter()
.flat_map(ConstraintExpr::wires)
.collect()
}
pub fn wires_mut(&mut self) -> Vec<&mut GateIndex> {
self.operands_mut()
.into_iter()
.flat_map(ConstraintExpr::wires_mut)
.collect()
}
pub fn is_satisfied(&self, bits: &[bool], wires: &BTreeMap<GateIndex, Vec<bool>>) -> bool {
match self {
PlaintextBitConstraint::Signature {
scheme,
signature,
message,
public_key,
} => {
let (Some(signature), Some(message), Some(public_key)) = (
signature.eval(bits, wires),
message.eval(bits, wires),
public_key.eval(bits, wires),
) else {
return false;
};
let (Some(signature), Some(message), Some(public_key)) = (
bits_to_bytes(&signature),
bits_to_bytes(&message),
bits_to_bytes(&public_key),
) else {
return false;
};
match scheme {
SignatureScheme::Ed25519 => {
let (Ok(public_key), Ok(signature)) = (
<[u8; 32]>::try_from(public_key),
<[u8; 64]>::try_from(signature),
) else {
return false;
};
match VerifyingKey::from_bytes(&public_key) {
Ok(key) => key
.verify_strict(&message, &Signature::from_bytes(&signature))
.is_ok(),
Err(_) => false,
}
}
}
}
PlaintextBitConstraint::Equality {
bits: lhs,
expected,
} => match (lhs.eval(bits, wires), expected.eval(bits, wires)) {
(Some(lhs), Some(rhs)) => lhs == rhs,
_ => false,
},
PlaintextBitConstraint::Comparison { relation, lhs, rhs } => {
let (Some(lhs), Some(rhs)) = (lhs.eval(bits, wires), rhs.eval(bits, wires)) else {
return false;
};
let (Some(lhs), Some(rhs)) = (bits_to_bytes(&lhs), bits_to_bytes(&rhs)) else {
return false;
};
if lhs.len() != rhs.len() {
return false;
}
match relation {
Relation::AtMost => lhs <= rhs,
Relation::AtLeast => lhs >= rhs,
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ConstraintClause(Vec<PlaintextBitConstraint>);
impl ConstraintClause {
pub fn new(constraints: Vec<PlaintextBitConstraint>) -> Self {
Self(constraints)
}
pub fn constraints(&self) -> &[PlaintextBitConstraint] {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn wires(&self) -> Vec<GateIndex> {
self.0
.iter()
.flat_map(PlaintextBitConstraint::wires)
.collect()
}
pub fn wires_mut(&mut self) -> Vec<&mut GateIndex> {
self.0
.iter_mut()
.flat_map(PlaintextBitConstraint::wires_mut)
.collect()
}
pub fn is_satisfied(&self, bits: &[bool], wires: &BTreeMap<GateIndex, Vec<bool>>) -> bool {
self.0
.iter()
.all(|constraint| constraint.is_satisfied(bits, wires))
}
}
fn bytes_to_bits(bytes: &[u8]) -> Vec<bool> {
bytes
.iter()
.flat_map(|byte| (0..8).map(move |i| (byte >> i) & 1u8 == 1u8))
.collect()
}
fn bits_to_bytes(bits: &[bool]) -> Option<Vec<u8>> {
if !bits.len().is_multiple_of(8) {
return None;
}
Some(
bits.chunks(8)
.map(|chunk| {
chunk
.iter()
.enumerate()
.fold(0u8, |acc, (i, bit)| acc | (u8::from(*bit) << i))
})
.collect(),
)
}
#[cfg(test)]
mod tests {
use ed25519_dalek::{Signer, SigningKey};
use super::*;
fn bytes_to_bits(bytes: &[u8]) -> Vec<bool> {
bytes
.iter()
.flat_map(|byte| (0..8).map(move |i| (byte >> i) & 1u8 == 1u8))
.collect()
}
fn signed_batch(key: &SigningKey, message: &[u8]) -> (Vec<bool>, Vec<bool>) {
let signature = key.sign(message);
let mut bits = bytes_to_bits(message);
bits.extend(bytes_to_bits(&signature.to_bytes()));
(bits, bytes_to_bits(key.verifying_key().as_bytes()))
}
fn slice(start: u32, size: u32) -> ConstraintExpr {
ConstraintExpr::Slice(Slice::range(start, size, 1).unwrap())
}
fn wires(public_key: &[bool]) -> BTreeMap<GateIndex, Vec<bool>> {
BTreeMap::from([(0, public_key.to_vec())])
}
fn signature_constraint(message_bytes: u32) -> PlaintextBitConstraint {
PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: slice(8 * message_bytes, 512),
message: slice(0, 8 * message_bytes),
public_key: ConstraintExpr::Wire(0),
}
}
#[test]
fn test_signature_constraint() {
let key = SigningKey::from_bytes(&[7u8; 32]);
let message = b"{\"price\":42}";
let (bits, public_key) = signed_batch(&key, message);
let constraint = signature_constraint(message.len() as u32);
assert!(constraint.is_satisfied(&bits, &wires(&public_key)));
let mut tampered = bits.clone();
tampered[3] = !tampered[3];
assert!(!constraint.is_satisfied(&tampered, &wires(&public_key)));
let other = SigningKey::from_bytes(&[9u8; 32]);
let other_key = bytes_to_bits(other.verifying_key().as_bytes());
assert!(!constraint.is_satisfied(&bits, &wires(&other_key)));
}
#[test]
fn test_signature_constraint_rejects_malformed_key() {
let key = SigningKey::from_bytes(&[7u8; 32]);
let message = b"{\"price\":42}";
let (bits, _) = signed_batch(&key, message);
let public_key = vec![true; 256];
assert!(
!signature_constraint(message.len() as u32).is_satisfied(&bits, &wires(&public_key))
);
}
#[test]
fn test_equality_constraint() {
let bits = bytes_to_bits(b"header:body");
let constraint = PlaintextBitConstraint::Equality {
bits: slice(0, 48),
expected: ConstraintExpr::Wire(0),
};
assert!(constraint.is_satisfied(&bits, &wires(&bytes_to_bits(b"header"))));
assert!(!constraint.is_satisfied(&bits, &wires(&bytes_to_bits(b"HEADER"))));
}
#[test]
fn test_bits_to_bytes_is_lsb_first() {
assert_eq!(
bits_to_bytes(&bytes_to_bits(&[0x01, 0x80, 0xa5])).unwrap(),
[0x01, 0x80, 0xa5]
);
assert_eq!(bits_to_bytes(&[true; 4]), None);
}
#[test]
fn test_signature_over_a_composed_message() {
let key = SigningKey::from_bytes(&[7u8; 32]);
let payload = b"{\"price\":42}";
let signed = {
let mut signed = vec![0x31u8];
signed.extend(Sha256::digest(payload));
signed
};
let mut bits = bytes_to_bits(payload);
bits.extend(bytes_to_bits(&key.sign(&signed).to_bytes()));
let constraint = PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: slice(8 * payload.len() as u32, 512),
message: ConstraintExpr::Concat(vec![
ConstraintExpr::Constant(vec![0x31]),
ConstraintExpr::Digest {
algorithm: DigestAlgorithm::Sha256,
of: Box::new(slice(0, 8 * payload.len() as u32)),
},
]),
public_key: ConstraintExpr::Wire(0),
};
let public_key = bytes_to_bits(key.verifying_key().as_bytes());
assert!(constraint.is_satisfied(&bits, &wires(&public_key)));
let mut tampered = bits.clone();
tampered[3] = !tampered[3];
assert!(!constraint.is_satisfied(&tampered, &wires(&public_key)));
}
#[test]
fn test_equality_against_a_digest() {
let content = b"the content";
let mut bits = bytes_to_bits(content);
bits.extend(bytes_to_bits(&Sha256::digest(content)));
let constraint = PlaintextBitConstraint::Equality {
bits: slice(8 * content.len() as u32, 256),
expected: ConstraintExpr::Digest {
algorithm: DigestAlgorithm::Sha256,
of: Box::new(slice(0, 8 * content.len() as u32)),
},
};
assert!(constraint.is_satisfied(&bits, &BTreeMap::new()));
let mut tampered = bits.clone();
tampered[0] = !tampered[0];
assert!(!constraint.is_satisfied(&tampered, &BTreeMap::new()));
}
#[test]
fn test_static_len_adds_up() {
let expr = ConstraintExpr::Concat(vec![
ConstraintExpr::Constant(vec![0u8; 3]),
slice(0, 5),
ConstraintExpr::Digest {
algorithm: DigestAlgorithm::Sha256,
of: Box::new(ConstraintExpr::Wire(0)),
},
ConstraintExpr::Wire(1),
]);
assert_eq!(
expr.static_len(&|wire| Some(wire + 7)),
Some(24 + 5 + 256 + 8)
);
assert_eq!(expr.static_len(&|_| None), None);
}
#[test]
fn test_eval_refuses_a_slice_past_the_batch() {
assert_eq!(slice(0, 16).eval(&[true; 8], &BTreeMap::new()), None);
}
#[test]
fn test_eval_refuses_a_digest_over_a_partial_byte() {
let expr = ConstraintExpr::Digest {
algorithm: DigestAlgorithm::Sha256,
of: Box::new(slice(0, 4)),
};
assert_eq!(expr.eval(&[true; 8], &BTreeMap::new()), None);
}
#[test]
fn test_depth_and_node_count() {
let expr = ConstraintExpr::Concat(vec![
slice(0, 1),
ConstraintExpr::Digest {
algorithm: DigestAlgorithm::Sha256,
of: Box::new(ConstraintExpr::Concat(vec![slice(1, 1), slice(2, 1)])),
},
]);
assert_eq!(expr.depth(), 4);
assert_eq!(expr.node_count(), 6);
}
const JWS: &[u8] = b"eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE3NTYxMDAwMDAsInB4IjoiMDAwMDAwNDI0MiJ9.\
ZdO1q9RcSfUrdq8UhqZYHVNBHp1OsDLgKG16bQDd-txuigbHkeuG-Bqbu335MrjoPL5Ssq6e\
3mJiJpXOTW6nCw";
const JWS_SIGNING_INPUT_BYTES: u32 = 20 + 1 + 48;
const JWS_SIGNATURE_BYTES: u32 = 86;
fn jws_constraint() -> PlaintextBitConstraint {
PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: ConstraintExpr::Decode {
encoding: Encoding::Base64UrlNoPad,
of: Box::new(slice(
8 * (JWS_SIGNING_INPUT_BYTES + 1),
8 * JWS_SIGNATURE_BYTES,
)),
},
message: slice(0, 8 * JWS_SIGNING_INPUT_BYTES),
public_key: ConstraintExpr::Wire(0),
}
}
#[test]
fn test_jws_verifies_through_a_decoded_signature() {
let key = SigningKey::from_bytes(&[7u8; 32]);
let public_key = bytes_to_bits(key.verifying_key().as_bytes());
assert_eq!(JWS.len(), 156);
let bits = bytes_to_bits(JWS);
assert!(jws_constraint().is_satisfied(&bits, &wires(&public_key)));
let mut tampered = JWS.to_vec();
tampered[60] ^= 0x01;
assert!(!jws_constraint().is_satisfied(&bytes_to_bits(&tampered), &wires(&public_key)));
}
#[test]
fn test_a_signature_that_does_not_decode_fails_the_constraint() {
let key = SigningKey::from_bytes(&[7u8; 32]);
let public_key = bytes_to_bits(key.verifying_key().as_bytes());
for (what, byte) in [
("padding", b'='),
("standard alphabet", b'+'),
("junk", b'!'),
] {
let mut body = JWS.to_vec();
body[80] = byte;
assert!(
!jws_constraint().is_satisfied(&bytes_to_bits(&body), &wires(&public_key)),
"a {what} character should not decode"
);
}
}
const JWS_STALE: &[u8] = b"eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE3MDAwMDAwMDAsInB4IjoiMDAwMDAwNDI0\
MiJ9.Sp-OEJIDpCKuVEuTyxKkMZyNP-2pI86wCfWxN59KPONfJBNC4ILVEMSOdLhl\
kjPEu4XYEgzIyHNoHFbyHQ8cCg";
fn iat_window() -> ConstraintExpr {
ConstraintExpr::Decode {
encoding: Encoding::Base64UrlNoPad,
of: Box::new(slice(8 * 29, 8 * 16)),
}
}
fn iat_bound(relation: Relation, bound: &[u8]) -> PlaintextBitConstraint {
PlaintextBitConstraint::Comparison {
relation,
lhs: iat_window(),
rhs: ConstraintExpr::Constant(bound.to_vec()),
}
}
#[test]
fn test_a_comparison_bounds_a_text_timestamp() {
let not_before = iat_bound(Relation::AtLeast, b":1756000000,");
let not_after = iat_bound(Relation::AtMost, b":1757000000,");
let no_wires = BTreeMap::new();
let fresh = bytes_to_bits(JWS);
assert!(not_before.is_satisfied(&fresh, &no_wires));
assert!(not_after.is_satisfied(&fresh, &no_wires));
let key = SigningKey::from_bytes(&[7u8; 32]);
let stale = bytes_to_bits(JWS_STALE);
assert!(jws_constraint().is_satisfied(
&stale,
&wires(&bytes_to_bits(key.verifying_key().as_bytes()))
));
assert!(!not_before.is_satisfied(&stale, &no_wires));
assert!(not_after.is_satisfied(&stale, &no_wires));
}
#[test]
fn test_a_comparison_is_over_bytes_not_the_bit_vector() {
let lower = b"1756100001";
let higher = b"1756100002";
assert!(lower < higher, "as byte strings");
assert!(
bytes_to_bits(lower) > bytes_to_bits(higher),
"and the other way as LSB-first bit vectors, which is the trap"
);
let no_wires = BTreeMap::new();
let bits = bytes_to_bits(lower);
let at_most = PlaintextBitConstraint::Comparison {
relation: Relation::AtMost,
lhs: slice(0, 8 * 10),
rhs: ConstraintExpr::Constant(higher.to_vec()),
};
let at_least = PlaintextBitConstraint::Comparison {
relation: Relation::AtLeast,
lhs: slice(0, 8 * 10),
rhs: ConstraintExpr::Constant(higher.to_vec()),
};
assert!(
at_most.is_satisfied(&bits, &no_wires),
"1756100001 <= 1756100002"
);
assert!(!at_least.is_satisfied(&bits, &no_wires));
}
#[test]
fn test_a_comparison_refuses_unequal_widths() {
let constraint = PlaintextBitConstraint::Comparison {
relation: Relation::AtMost,
lhs: slice(0, 8 * 4),
rhs: ConstraintExpr::Constant(b"12345".to_vec()),
};
assert!(!constraint.is_satisfied(&bytes_to_bits(b"1234"), &BTreeMap::new()));
}
#[test]
fn test_only_pinned_operands_cover() {
let n = |c: PlaintextBitConstraint| c.covering_operands().len();
assert_eq!(n(iat_bound(Relation::AtLeast, b":1756000000,")), 0);
assert_eq!(n(jws_constraint()), 2);
assert!(jws_constraint()
.covering_operands()
.iter()
.all(|operand| !matches!(operand, ConstraintExpr::Wire(_))));
assert_eq!(n(self_signed_constraint()), 2);
let key_bits = 512 + 8 * 12;
assert!(
self_signed_constraint()
.covering_operands()
.iter()
.flat_map(|operand| operand.slices())
.flat_map(|slice| slice.get_indices())
.all(|index| index < key_bits),
"the key's own bytes must not be covered by its signature"
);
assert_eq!(
n(PlaintextBitConstraint::Equality {
bits: slice(0, 8),
expected: ConstraintExpr::Constant(vec![b'.']),
}),
1
);
assert_eq!(
n(PlaintextBitConstraint::Equality {
bits: ConstraintExpr::Constant(vec![b'.']),
expected: slice(0, 8),
}),
1
);
assert_eq!(
n(PlaintextBitConstraint::Equality {
bits: slice(0, 8),
expected: slice(8, 8),
}),
0
);
}
fn self_signed_constraint() -> PlaintextBitConstraint {
PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: slice(0, 512),
message: slice(512, 8 * 12),
public_key: slice(512 + 8 * 12, 256),
}
}
#[test]
fn test_a_self_signed_batch_satisfies_its_own_constraint() {
let attacker = SigningKey::from_bytes(&[42u8; 32]);
let message = b"whatever it li";
let message = &message[..12];
let signature = attacker.sign(message);
let mut bits = bytes_to_bits(&signature.to_bytes());
bits.extend(bytes_to_bits(message));
bits.extend(bytes_to_bits(attacker.verifying_key().as_bytes()));
assert!(
self_signed_constraint().is_satisfied(&bits, &BTreeMap::new()),
"a peer can always satisfy a clause whose key it supplies"
);
}
#[test]
fn test_decoded_bits_is_exact_for_unpadded_base64() {
let b64 = Encoding::Base64UrlNoPad;
assert_eq!(b64.decoded_bits(8 * 86), Some(8 * 64));
assert_eq!(b64.decoded_bits(8 * 4), Some(8 * 3));
assert_eq!(b64.decoded_bits(8 * 2), Some(8));
assert_eq!(b64.decoded_bits(8 * 3), Some(8 * 2));
assert_eq!(b64.decoded_bits(8 * 5), None);
assert_eq!(b64.decoded_bits(4), None);
}
#[test]
fn test_decode_rejects_non_canonical_encodings() {
let b64 = Encoding::Base64UrlNoPad;
assert_eq!(b64.decode(b"QQ"), Some(vec![b'A']));
assert_eq!(b64.decode(b"QR"), None);
assert_eq!(b64.decode(b"QQ=="), None);
assert_eq!(b64.decode(b"QUJDRA"), Some(b"ABCD".to_vec()));
assert_eq!(
b64.decode(b"QUJDRAA"),
Some(vec![b'A', b'B', b'C', b'D', 0])
);
assert_eq!(b64.decode(b"QUJDR"), None);
assert_eq!(b64.decode(b"-_-_"), Some(vec![0xfb, 0xff, 0xbf]));
assert_eq!(b64.decode(b"+/+/"), None);
}
#[test]
fn test_static_len_of_a_decode() {
let expr = ConstraintExpr::Decode {
encoding: Encoding::Base64UrlNoPad,
of: Box::new(slice(0, 8 * 86)),
};
assert_eq!(expr.static_len(&|_| None), Some(512));
assert_eq!(expr.depth(), 2);
assert_eq!(expr.node_count(), 2);
assert_eq!(expr.slices().len(), 1);
}
mod circuit {
use num_bigint::BigUint;
use primitives::random::rng::test_rng;
use super::*;
use crate::{
circuit::{AlgebraicType, Circuit, Gate, Input},
config::DefaultConfig as C,
};
const MESSAGE_BYTES: u32 = 12;
const BATCH_SIZE: u32 = 8 * MESSAGE_BYTES + 512;
fn plaintext_bits(circuit: &mut Circuit<C>, batch_size: u32) -> u32 {
circuit
.add_gate(Gate::Input(Input::Plaintext {
algebraic_type: AlgebraicType::Bit,
batch_size,
}))
.unwrap()
}
fn build(clauses: Vec<ConstraintClause>) -> Result<Circuit<C>, String> {
build_sized(BATCH_SIZE, clauses)
}
fn build_sized(
batch_size: u32,
clauses: Vec<ConstraintClause>,
) -> Result<Circuit<C>, String> {
let mut circuit = Circuit::<C>::new();
plaintext_bits(&mut circuit, 256);
let x = plaintext_bits(&mut circuit, batch_size);
let gate = circuit
.add_gate(Gate::ConstrainPlaintextBits {
x,
clauses,
on_ambiguity: OnAmbiguity::Fail,
})
.map_err(|e| e.to_string())?;
circuit.add_output(gate).unwrap();
Ok(circuit)
}
fn one(constraint: PlaintextBitConstraint) -> Vec<ConstraintClause> {
vec![ConstraintClause::new(vec![constraint])]
}
fn expect(data: &[bool], ok: bool) -> Vec<BigUint> {
data.iter()
.chain(std::iter::once(&ok))
.map(|b| BigUint::from(*b))
.collect()
}
#[test]
fn test_gate_output_is_the_input_plus_a_success_bit() {
let circuit = build(one(signature_constraint(MESSAGE_BYTES))).unwrap();
let output = circuit.gate_output_unchecked(2);
assert_eq!(output.get_batch_size(), BATCH_SIZE + 1);
assert_eq!(output.get_type(), AlgebraicType::Bit);
assert_eq!(
output.get_form(),
crate::circuit::ShareOrPlaintext::Plaintext
);
}
#[test]
fn test_a_comparison_alone_does_not_cover_the_batch() {
let err = build(one(PlaintextBitConstraint::Comparison {
relation: Relation::AtLeast,
lhs: slice(0, 8 * 4),
rhs: ConstraintExpr::Constant(vec![0u8; 4]),
}))
.expect_err("a comparison covers nothing");
assert!(err.contains("must be covered"), "{err}");
}
#[test]
fn test_a_comparison_beside_a_pinning_constraint_validates() {
build(vec![ConstraintClause::new(vec![
signature_constraint(MESSAGE_BYTES),
PlaintextBitConstraint::Comparison {
relation: Relation::AtLeast,
lhs: slice(0, 8 * 4),
rhs: ConstraintExpr::Constant(vec![0u8; 4]),
},
])])
.expect("pinned by the signature");
}
#[test]
fn test_validation_refuses_a_comparison_of_unequal_widths() {
let err = build(vec![ConstraintClause::new(vec![
signature_constraint(MESSAGE_BYTES),
PlaintextBitConstraint::Comparison {
relation: Relation::AtMost,
lhs: slice(0, 8 * 4),
rhs: ConstraintExpr::Constant(vec![0u8; 5]),
},
])])
.expect_err("four bytes against five");
assert!(err.contains("same length"), "{err}");
}
const KEY_IN_RESPONSE_BITS: u32 = 512 + 8 * MESSAGE_BYTES + 256;
#[test]
fn test_a_self_signed_clause_is_refused() {
let err = build_sized(KEY_IN_RESPONSE_BITS, one(self_signed_constraint()))
.expect_err("a key sliced from the batch is anchored by nothing");
assert!(err.contains("must be covered"), "{err}");
}
#[test]
fn test_a_key_from_the_response_pinned_by_another_constraint_validates() {
let key_at = 512 + 8 * MESSAGE_BYTES;
build_sized(
KEY_IN_RESPONSE_BITS,
vec![ConstraintClause::new(vec![
self_signed_constraint(),
PlaintextBitConstraint::Equality {
bits: slice(key_at, 256),
expected: ConstraintExpr::Constant(vec![0u8; 32]),
},
])],
)
.expect("the key is pinned by the equality");
}
#[test]
fn test_mock_eval_passes_the_bits_through() {
let key = SigningKey::from_bytes(&[7u8; 32]);
let message = b"{\"price\":42}";
assert_eq!(message.len() as u32, MESSAGE_BYTES);
let (bits, public_key) = signed_batch(&key, message);
let circuit = build(one(signature_constraint(MESSAGE_BYTES))).unwrap();
let inputs = public_key
.iter()
.chain(bits.iter())
.map(|b| BigUint::from(*b))
.collect::<Vec<BigUint>>();
let output = circuit.mock_eval_big_uint(inputs, &mut test_rng());
assert_eq!(output, expect(&bits, true));
}
#[test]
fn test_mock_eval_reports_an_unsatisfied_constraint() {
let key = SigningKey::from_bytes(&[7u8; 32]);
let (mut bits, public_key) = signed_batch(&key, b"{\"price\":42}");
bits[0] = !bits[0];
let circuit = build(one(signature_constraint(MESSAGE_BYTES))).unwrap();
let inputs = public_key
.iter()
.chain(bits.iter())
.map(|b| BigUint::from(*b))
.collect::<Vec<BigUint>>();
let output = circuit.mock_eval_big_uint(inputs, &mut test_rng());
assert_eq!(output, expect(&vec![false; BATCH_SIZE as usize], false));
}
#[test]
fn test_validation_rejects_uncovered_bits() {
let err = build(one(PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: slice(8 * MESSAGE_BYTES, 512),
message: slice(0, 8 * (MESSAGE_BYTES - 1)),
public_key: ConstraintExpr::Wire(0),
}))
.unwrap_err();
assert!(
err.contains("clause 0") && err.contains("8 are not"),
"{err}"
);
}
#[test]
fn test_validation_rejects_no_clauses() {
let err = build(vec![]).unwrap_err();
assert!(err.contains("expected at least one clause"), "{err}");
}
#[test]
fn test_validation_rejects_an_empty_clause() {
let err = build(vec![
ConstraintClause::new(vec![signature_constraint(MESSAGE_BYTES)]),
ConstraintClause::new(vec![]),
])
.unwrap_err();
assert!(err.contains("clause 1 is empty"), "{err}");
}
#[test]
fn test_validation_rejects_out_of_range_slice() {
let err = build(one(signature_constraint(MESSAGE_BYTES + 1))).unwrap_err();
assert!(err.contains("out-of-range"), "{err}");
}
#[test]
fn test_validation_rejects_mis_sized_signature() {
let err = build(one(PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: slice(8 * MESSAGE_BYTES, 256),
message: slice(0, 8 * MESSAGE_BYTES),
public_key: ConstraintExpr::Wire(0),
}))
.unwrap_err();
assert!(
err.contains("expected a 512-bit Ed25519 signature"),
"{err}"
);
}
#[test]
fn test_validation_rejects_mis_sized_public_key() {
let mut circuit = Circuit::<C>::new();
plaintext_bits(&mut circuit, 128);
let x = plaintext_bits(&mut circuit, BATCH_SIZE);
let err = circuit
.add_gate(Gate::ConstrainPlaintextBits {
x,
on_ambiguity: OnAmbiguity::Fail,
clauses: one(signature_constraint(MESSAGE_BYTES)),
})
.unwrap_err()
.to_string();
assert!(
err.contains("expected a 256-bit Ed25519 public key"),
"{err}"
);
}
#[test]
fn test_validation_rejects_mis_sized_equality_value() {
let err = build(one(PlaintextBitConstraint::Equality {
bits: slice(0, BATCH_SIZE),
expected: ConstraintExpr::Wire(0),
}))
.unwrap_err();
assert!(err.contains("must be the same length"), "{err}");
}
fn build_two_keys() -> Circuit<C> {
let mut circuit = Circuit::<C>::new();
plaintext_bits(&mut circuit, 256);
plaintext_bits(&mut circuit, 256);
let x = plaintext_bits(&mut circuit, BATCH_SIZE);
let clause = |public_key| {
ConstraintClause::new(vec![PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: slice(8 * MESSAGE_BYTES, 512),
message: slice(0, 8 * MESSAGE_BYTES),
public_key: ConstraintExpr::Wire(public_key),
}])
};
let gate = circuit
.add_gate(Gate::ConstrainPlaintextBits {
x,
on_ambiguity: OnAmbiguity::Fail,
clauses: vec![clause(0), clause(1)],
})
.unwrap();
circuit.add_output(gate).unwrap();
circuit
}
#[test]
fn test_a_later_clause_can_satisfy_the_gate() {
let circuit = build_two_keys();
let first = SigningKey::from_bytes(&[7u8; 32]);
let second = SigningKey::from_bytes(&[9u8; 32]);
let first_key = bytes_to_bits(first.verifying_key().as_bytes());
let (bits, second_key) = signed_batch(&second, b"{\"price\":42}");
let inputs = first_key
.iter()
.chain(second_key.iter())
.chain(bits.iter())
.map(|b| BigUint::from(*b))
.collect::<Vec<BigUint>>();
let output = circuit.mock_eval_big_uint(inputs, &mut test_rng());
assert_eq!(output, expect(&bits, true));
}
#[test]
fn test_the_gate_reports_failure_only_when_all_clauses_fail() {
let circuit = build_two_keys();
let third = SigningKey::from_bytes(&[11u8; 32]);
let (bits, _) = signed_batch(&third, b"{\"price\":42}");
let first = bytes_to_bits(
SigningKey::from_bytes(&[7u8; 32])
.verifying_key()
.as_bytes(),
);
let second = bytes_to_bits(
SigningKey::from_bytes(&[9u8; 32])
.verifying_key()
.as_bytes(),
);
let inputs = first
.iter()
.chain(second.iter())
.chain(bits.iter())
.map(|b| BigUint::from(*b))
.collect::<Vec<BigUint>>();
let output = circuit.mock_eval_big_uint(inputs, &mut test_rng());
assert_eq!(output, expect(&vec![false; BATCH_SIZE as usize], false));
}
#[test]
fn test_random_bits_evaluate_to_a_clean_failure() {
use rand::Rng;
let circuit = build(one(signature_constraint(MESSAGE_BYTES))).unwrap();
let mut rng = test_rng();
let inputs = (0..256 + BATCH_SIZE)
.map(|_| BigUint::from(rng.gen::<bool>()))
.collect::<Vec<BigUint>>();
let output = circuit.mock_eval_big_uint(inputs, &mut test_rng());
assert_eq!(output, expect(&vec![false; BATCH_SIZE as usize], false));
}
#[test]
fn test_validation_requires_coverage_from_every_clause() {
let err = build(vec![
ConstraintClause::new(vec![signature_constraint(MESSAGE_BYTES)]),
ConstraintClause::new(vec![PlaintextBitConstraint::Equality {
bits: slice(0, 256),
expected: ConstraintExpr::Wire(0),
}]),
])
.unwrap_err();
assert!(err.contains("clause 1"), "{err}");
assert!(err.contains("must be covered"), "{err}");
}
#[test]
fn test_gate_inputs_are_listed_clause_by_clause() {
let circuit = build_two_keys();
assert_eq!(circuit.gate_unchecked(3).get_inputs(), vec![2, 0, 1]);
}
#[test]
fn test_validation_rejects_an_over_deep_expression() {
let mut expected = slice(0, 256);
for _ in 0..MAX_EXPR_DEPTH {
expected = ConstraintExpr::Digest {
algorithm: DigestAlgorithm::Sha256,
of: Box::new(expected),
};
}
let err = build(one(PlaintextBitConstraint::Equality {
bits: slice(0, 256),
expected,
}))
.unwrap_err();
assert!(err.contains("nests deeper than"), "{err}");
}
#[test]
fn test_validation_rejects_an_over_wide_expression() {
let err = build(one(PlaintextBitConstraint::Equality {
bits: ConstraintExpr::Concat(
(0..MAX_EXPR_NODES as u32 + 1)
.map(|i| slice(i, 1))
.collect(),
),
expected: ConstraintExpr::Wire(0),
}))
.unwrap_err();
assert!(err.contains("more than"), "{err}");
}
#[test]
fn test_validation_rejects_shared_input() {
let mut circuit = Circuit::<C>::new();
plaintext_bits(&mut circuit, 256);
let x = circuit
.add_gate(Gate::Input(Input::Share {
algebraic_type: AlgebraicType::Bit,
batch_size: BATCH_SIZE,
}))
.unwrap();
let err = circuit
.add_gate(Gate::ConstrainPlaintextBits {
x,
on_ambiguity: OnAmbiguity::Fail,
clauses: one(signature_constraint(MESSAGE_BYTES)),
})
.unwrap_err()
.to_string();
assert!(err.contains("is_plaintext"), "{err}");
}
}
}