metamorphic_log/
commitment.rs1use metamorphic_crypto::hash::sha3_512_with_context;
33
34use crate::error::{Error, Result};
35
36pub const COMMITMENT_OPENING_LEN: usize = 32;
38pub const COMMITMENT_LEN: usize = 64;
40
41#[derive(Clone, PartialEq, Eq, Hash)]
43pub struct Commitment([u8; COMMITMENT_LEN]);
44
45#[derive(Clone, PartialEq, Eq)]
48pub struct Opening([u8; COMMITMENT_OPENING_LEN]);
49
50impl Commitment {
51 #[must_use]
53 pub fn from_bytes(bytes: [u8; COMMITMENT_LEN]) -> Self {
54 Self(bytes)
55 }
56
57 #[must_use]
59 pub fn as_bytes(&self) -> &[u8; COMMITMENT_LEN] {
60 &self.0
61 }
62}
63
64impl core::fmt::Debug for Commitment {
65 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66 write!(f, "Commitment({:02x}{:02x}..)", self.0[0], self.0[1])
67 }
68}
69
70impl Opening {
71 #[must_use]
73 pub fn from_bytes(bytes: [u8; COMMITMENT_OPENING_LEN]) -> Self {
74 Self(bytes)
75 }
76
77 #[must_use]
79 pub fn as_bytes(&self) -> &[u8; COMMITMENT_OPENING_LEN] {
80 &self.0
81 }
82}
83
84impl core::fmt::Debug for Opening {
87 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88 f.write_str("Opening(..)")
89 }
90}
91
92#[must_use]
98pub fn commit_with_opening(context: &str, value: &[u8], opening: &Opening) -> Commitment {
99 let mut framed = Vec::with_capacity(COMMITMENT_OPENING_LEN + value.len());
100 framed.extend_from_slice(opening.as_bytes());
101 framed.extend_from_slice(value);
102 Commitment(sha3_512_with_context(context, &framed))
103}
104
105#[must_use]
109pub fn commit(context: &str, value: &[u8]) -> (Commitment, Opening) {
110 let mut nonce = [0u8; COMMITMENT_OPENING_LEN];
111 getrandom::getrandom(&mut nonce).expect("OS CSPRNG unavailable");
112 let opening = Opening(nonce);
113 let commitment = commit_with_opening(context, value, &opening);
114 (commitment, opening)
115}
116
117pub fn verify_commitment(
123 context: &str,
124 commitment: &Commitment,
125 value: &[u8],
126 opening: &Opening,
127) -> Result<()> {
128 if &commit_with_opening(context, value, opening) == commitment {
129 Ok(())
130 } else {
131 Err(Error::CommitmentMismatch)
132 }
133}
134
135#[cfg(all(test, not(target_arch = "wasm32")))]
136mod tests {
137 use super::*;
138
139 const CTX: &str = "acme/coniks-commitment/v1";
140
141 #[test]
142 fn commit_then_verify_opens() {
143 let (c, o) = commit(CTX, b"public key bytes");
144 assert!(verify_commitment(CTX, &c, b"public key bytes", &o).is_ok());
145 }
146
147 #[test]
148 fn wrong_value_does_not_open() {
149 let (c, o) = commit(CTX, b"value-a");
150 assert_eq!(
151 verify_commitment(CTX, &c, b"value-b", &o),
152 Err(Error::CommitmentMismatch)
153 );
154 }
155
156 #[test]
157 fn wrong_opening_does_not_open() {
158 let (c, _o) = commit(CTX, b"value");
159 let other = Opening::from_bytes([0u8; COMMITMENT_OPENING_LEN]);
160 assert_eq!(
161 verify_commitment(CTX, &c, b"value", &other),
162 Err(Error::CommitmentMismatch)
163 );
164 }
165
166 #[test]
167 fn different_context_does_not_open() {
168 let (c, o) = commit(CTX, b"value");
171 assert_eq!(
172 verify_commitment("other/coniks-commitment/v1", &c, b"value", &o),
173 Err(Error::CommitmentMismatch)
174 );
175 }
176
177 #[test]
178 fn fresh_commitments_are_hiding_across_calls() {
179 let (c1, _) = commit(CTX, b"same");
182 let (c2, _) = commit(CTX, b"same");
183 assert_ne!(c1, c2);
184 }
185
186 #[test]
187 fn deterministic_for_fixed_opening() {
188 let o = Opening::from_bytes([5u8; COMMITMENT_OPENING_LEN]);
189 assert_eq!(
190 commit_with_opening(CTX, b"v", &o),
191 commit_with_opening(CTX, b"v", &o)
192 );
193 }
194
195 #[test]
196 fn matches_documented_framing() {
197 let o = Opening::from_bytes([3u8; COMMITMENT_OPENING_LEN]);
198 let value = b"explicit framing check";
199 let mut framed = Vec::new();
200 framed.extend_from_slice(o.as_bytes());
201 framed.extend_from_slice(value);
202 let expected = sha3_512_with_context(CTX, &framed);
203 assert_eq!(commit_with_opening(CTX, value, &o).as_bytes(), &expected);
204 }
205
206 use proptest::prelude::*;
207
208 proptest! {
209 #[test]
210 fn commit_verify_roundtrip(value: Vec<u8>, nonce: [u8; 32]) {
211 let opening = Opening::from_bytes(nonce);
212 let c = commit_with_opening(CTX, &value, &opening);
213 prop_assert!(verify_commitment(CTX, &c, &value, &opening).is_ok());
214 }
215
216 #[test]
217 fn distinct_values_distinct_commitments(a: Vec<u8>, b: Vec<u8>, nonce: [u8; 32]) {
218 prop_assume!(a != b);
219 let opening = Opening::from_bytes(nonce);
220 prop_assert_ne!(
221 commit_with_opening(CTX, &a, &opening),
222 commit_with_opening(CTX, &b, &opening)
223 );
224 }
225 }
226}