use metamorphic_crypto::hash::sha3_512_with_context;
use crate::error::{Error, Result};
pub const COMMITMENT_OPENING_LEN: usize = 32;
pub const COMMITMENT_LEN: usize = 64;
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Commitment([u8; COMMITMENT_LEN]);
#[derive(Clone, PartialEq, Eq)]
pub struct Opening([u8; COMMITMENT_OPENING_LEN]);
impl Commitment {
#[must_use]
pub fn from_bytes(bytes: [u8; COMMITMENT_LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; COMMITMENT_LEN] {
&self.0
}
}
impl core::fmt::Debug for Commitment {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Commitment({:02x}{:02x}..)", self.0[0], self.0[1])
}
}
impl Opening {
#[must_use]
pub fn from_bytes(bytes: [u8; COMMITMENT_OPENING_LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; COMMITMENT_OPENING_LEN] {
&self.0
}
}
impl core::fmt::Debug for Opening {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("Opening(..)")
}
}
#[must_use]
pub fn commit_with_opening(context: &str, value: &[u8], opening: &Opening) -> Commitment {
let mut framed = Vec::with_capacity(COMMITMENT_OPENING_LEN + value.len());
framed.extend_from_slice(opening.as_bytes());
framed.extend_from_slice(value);
Commitment(sha3_512_with_context(context, &framed))
}
#[must_use]
pub fn commit(context: &str, value: &[u8]) -> (Commitment, Opening) {
let mut nonce = [0u8; COMMITMENT_OPENING_LEN];
getrandom::getrandom(&mut nonce).expect("OS CSPRNG unavailable");
let opening = Opening(nonce);
let commitment = commit_with_opening(context, value, &opening);
(commitment, opening)
}
pub fn verify_commitment(
context: &str,
commitment: &Commitment,
value: &[u8],
opening: &Opening,
) -> Result<()> {
if &commit_with_opening(context, value, opening) == commitment {
Ok(())
} else {
Err(Error::CommitmentMismatch)
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
const CTX: &str = "acme/coniks-commitment/v1";
#[test]
fn commit_then_verify_opens() {
let (c, o) = commit(CTX, b"public key bytes");
assert!(verify_commitment(CTX, &c, b"public key bytes", &o).is_ok());
}
#[test]
fn wrong_value_does_not_open() {
let (c, o) = commit(CTX, b"value-a");
assert_eq!(
verify_commitment(CTX, &c, b"value-b", &o),
Err(Error::CommitmentMismatch)
);
}
#[test]
fn wrong_opening_does_not_open() {
let (c, _o) = commit(CTX, b"value");
let other = Opening::from_bytes([0u8; COMMITMENT_OPENING_LEN]);
assert_eq!(
verify_commitment(CTX, &c, b"value", &other),
Err(Error::CommitmentMismatch)
);
}
#[test]
fn different_context_does_not_open() {
let (c, o) = commit(CTX, b"value");
assert_eq!(
verify_commitment("other/coniks-commitment/v1", &c, b"value", &o),
Err(Error::CommitmentMismatch)
);
}
#[test]
fn fresh_commitments_are_hiding_across_calls() {
let (c1, _) = commit(CTX, b"same");
let (c2, _) = commit(CTX, b"same");
assert_ne!(c1, c2);
}
#[test]
fn deterministic_for_fixed_opening() {
let o = Opening::from_bytes([5u8; COMMITMENT_OPENING_LEN]);
assert_eq!(
commit_with_opening(CTX, b"v", &o),
commit_with_opening(CTX, b"v", &o)
);
}
#[test]
fn matches_documented_framing() {
let o = Opening::from_bytes([3u8; COMMITMENT_OPENING_LEN]);
let value = b"explicit framing check";
let mut framed = Vec::new();
framed.extend_from_slice(o.as_bytes());
framed.extend_from_slice(value);
let expected = sha3_512_with_context(CTX, &framed);
assert_eq!(commit_with_opening(CTX, value, &o).as_bytes(), &expected);
}
use proptest::prelude::*;
proptest! {
#[test]
fn commit_verify_roundtrip(value: Vec<u8>, nonce: [u8; 32]) {
let opening = Opening::from_bytes(nonce);
let c = commit_with_opening(CTX, &value, &opening);
prop_assert!(verify_commitment(CTX, &c, &value, &opening).is_ok());
}
#[test]
fn distinct_values_distinct_commitments(a: Vec<u8>, b: Vec<u8>, nonce: [u8; 32]) {
prop_assume!(a != b);
let opening = Opening::from_bytes(nonce);
prop_assert_ne!(
commit_with_opening(CTX, &a, &opening),
commit_with_opening(CTX, &b, &opening)
);
}
}
}