1#[cfg(feature = "envelope")]
2use std::sync::Arc;
3
4#[cfg(feature = "envelope")]
5use bc_envelope::prelude::*;
6#[cfg(feature = "envelope")]
7use bc_envelope::{FormatContext, with_format_context_mut};
8use bc_ur::bytewords;
9#[cfg(not(feature = "envelope"))]
10use dcbor::{Date, prelude::*};
11use serde::{Deserialize, Serialize};
12use url::Url;
13
14use crate::{
15 Error, ProvenanceMarkResolution, Result,
16 crypto_utils::{SHA256_SIZE, obfuscate, sha256, sha256_prefix},
17 util::{
18 deserialize_base64, deserialize_cbor, deserialize_iso8601,
19 serialize_base64, serialize_cbor, serialize_iso8601,
20 },
21};
22
23#[derive(Serialize, Clone)]
28pub struct ProvenanceMark {
29 seq: u32,
30
31 #[serde(serialize_with = "serialize_iso8601")]
32 date: Date,
33
34 res: ProvenanceMarkResolution,
35
36 #[serde(serialize_with = "serialize_base64")]
37 chain_id: Vec<u8>,
38
39 #[serde(serialize_with = "serialize_base64")]
40 key: Vec<u8>,
41
42 #[serde(serialize_with = "serialize_base64")]
43 hash: Vec<u8>,
44
45 #[serde(
46 default,
47 skip_serializing_if = "Vec::is_empty",
48 serialize_with = "serialize_cbor"
49 )]
50 info_bytes: Vec<u8>,
51
52 #[serde(skip)]
53 seq_bytes: Vec<u8>,
54
55 #[serde(skip)]
56 date_bytes: Vec<u8>,
57}
58
59impl<'de> Deserialize<'de> for ProvenanceMark {
60 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
61 where
62 D: serde::Deserializer<'de>,
63 {
64 #[derive(Deserialize)]
65 struct ProvenanceMarkHelper {
66 res: ProvenanceMarkResolution,
67 #[serde(deserialize_with = "deserialize_base64")]
68 key: Vec<u8>,
69 #[serde(deserialize_with = "deserialize_base64")]
70 hash: Vec<u8>,
71 #[serde(deserialize_with = "deserialize_base64")]
72 chain_id: Vec<u8>,
73 #[serde(default, deserialize_with = "deserialize_cbor")]
74 info_bytes: Vec<u8>,
75 seq: u32,
76 #[serde(deserialize_with = "deserialize_iso8601")]
77 date: Date,
78 }
79
80 let helper = ProvenanceMarkHelper::deserialize(deserializer)?;
81 let seq_bytes = helper
82 .res
83 .serialize_seq(helper.seq)
84 .map_err(serde::de::Error::custom)?;
85 let date_bytes = helper
86 .res
87 .serialize_date(helper.date)
88 .map_err(serde::de::Error::custom)?;
89
90 Ok(ProvenanceMark {
91 res: helper.res,
92 key: helper.key,
93 hash: helper.hash,
94 chain_id: helper.chain_id,
95 seq_bytes,
96 date_bytes,
97 info_bytes: helper.info_bytes,
98 seq: helper.seq,
99 date: helper.date,
100 })
101 }
102}
103
104impl PartialEq for ProvenanceMark {
105 fn eq(&self, other: &Self) -> bool {
106 self.res == other.res && self.message() == other.message()
107 }
108}
109
110impl Eq for ProvenanceMark {}
111
112impl std::hash::Hash for ProvenanceMark {
113 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
114 self.res.hash(state);
115 self.message().hash(state);
116 }
117}
118
119impl ProvenanceMark {
120 pub fn res(&self) -> ProvenanceMarkResolution { self.res }
121 pub fn key(&self) -> &[u8] { &self.key }
122 pub fn hash(&self) -> &[u8] { &self.hash }
123 pub fn chain_id(&self) -> &[u8] { &self.chain_id }
124 pub fn seq_bytes(&self) -> &[u8] { &self.seq_bytes }
125 pub fn date_bytes(&self) -> &[u8] { &self.date_bytes }
126
127 pub fn seq(&self) -> u32 { self.seq }
128 pub fn date(&self) -> Date { self.date }
129
130 pub fn message(&self) -> Vec<u8> {
131 let payload = [
132 self.chain_id.clone(),
133 self.hash.clone(),
134 self.seq_bytes.clone(),
135 self.date_bytes.clone(),
136 self.info_bytes.clone(),
137 ]
138 .concat();
139 [self.key.clone(), obfuscate(&self.key, payload)].concat()
140 }
141
142 pub fn info(&self) -> Option<CBOR> {
143 if self.info_bytes.is_empty() {
144 None
145 } else {
146 CBOR::try_from_data(&self.info_bytes).unwrap().into()
147 }
148 }
149}
150
151impl ProvenanceMark {
152 pub fn new(
153 res: ProvenanceMarkResolution,
154 key: Vec<u8>,
155 next_key: Vec<u8>,
156 chain_id: Vec<u8>,
157 seq: u32,
158 date: Date,
159 info: Option<impl CBOREncodable>,
160 ) -> Result<Self> {
161 if key.len() != res.link_length() {
162 return Err(Error::InvalidKeyLength {
163 expected: res.link_length(),
164 actual: key.len(),
165 });
166 }
167 if next_key.len() != res.link_length() {
168 return Err(Error::InvalidNextKeyLength {
169 expected: res.link_length(),
170 actual: next_key.len(),
171 });
172 }
173 if chain_id.len() != res.link_length() {
174 return Err(Error::InvalidChainIdLength {
175 expected: res.link_length(),
176 actual: chain_id.len(),
177 });
178 }
179
180 let date_bytes = res.serialize_date(date)?;
181 let seq_bytes = res.serialize_seq(seq)?;
182
183 let date = res.deserialize_date(&date_bytes)?;
184
185 let info_bytes = match info {
186 Some(info) => info.to_cbor_data(),
187 None => Vec::new(),
188 };
189
190 let hash = Self::make_hash(
191 res,
192 &key,
193 next_key,
194 &chain_id,
195 &seq_bytes,
196 &date_bytes,
197 &info_bytes,
198 );
199
200 Ok(Self {
201 res,
202 key,
203 hash,
204 chain_id,
205 seq_bytes,
206 date_bytes,
207 info_bytes,
208
209 seq,
210 date,
211 })
212 }
213
214 pub fn from_message(
215 res: ProvenanceMarkResolution,
216 message: Vec<u8>,
217 ) -> Result<Self> {
218 if message.len() < res.fixed_length() {
219 return Err(Error::InvalidMessageLength {
220 expected: res.fixed_length(),
221 actual: message.len(),
222 });
223 }
224
225 let key = message[res.key_range()].to_vec();
226 let payload = obfuscate(&key, &message[res.link_length()..]);
227 let hash = payload[res.hash_range()].to_vec();
228 let chain_id = payload[res.chain_id_range()].to_vec();
229 let seq_bytes = payload[res.seq_bytes_range()].to_vec();
230 let seq = res.deserialize_seq(&seq_bytes)?;
231 let date_bytes = payload[res.date_bytes_range()].to_vec();
232 let date = res.deserialize_date(&date_bytes)?;
233
234 let info_bytes = payload[res.info_range()].to_vec();
235 if !info_bytes.is_empty() && CBOR::try_from_data(&info_bytes).is_err() {
236 return Err(Error::InvalidInfoCbor);
237 }
238 Ok(Self {
239 res,
240 key,
241 hash,
242 chain_id,
243 seq_bytes,
244 date_bytes,
245 info_bytes,
246
247 seq,
248 date,
249 })
250 }
251
252 fn make_hash(
253 res: ProvenanceMarkResolution,
254 key: impl AsRef<[u8]>,
255 next_key: impl AsRef<[u8]>,
256 chain_id: impl AsRef<[u8]>,
257 seq_bytes: impl AsRef<[u8]>,
258 date_bytes: impl AsRef<[u8]>,
259 info_bytes: impl AsRef<[u8]>,
260 ) -> Vec<u8> {
261 let mut buf = Vec::new();
262 buf.extend_from_slice(key.as_ref());
263 buf.extend_from_slice(next_key.as_ref());
264 buf.extend_from_slice(chain_id.as_ref());
265 buf.extend_from_slice(seq_bytes.as_ref());
266 buf.extend_from_slice(date_bytes.as_ref());
267 buf.extend_from_slice(info_bytes.as_ref());
268
269 sha256_prefix(&buf, res.link_length())
270 }
271}
272
273impl ProvenanceMark {
274 pub fn identifier(&self) -> String { hex::encode(&self.hash[..4]) }
276
277 pub fn bytewords_identifier(&self, prefix: bool) -> String {
279 let s = bytewords::identifier(&self.hash[..4].try_into().unwrap())
280 .to_uppercase();
281 if prefix { format!("🅟 {}", s) } else { s }
282 }
283
284 pub fn bytemoji_identifier(&self, prefix: bool) -> String {
286 let s =
287 bytewords::bytemoji_identifier(&self.hash[..4].try_into().unwrap())
288 .to_uppercase();
289 if prefix { format!("🅟 {}", s) } else { s }
290 }
291
292 pub fn bytewords_minimal_identifier(&self, prefix: bool) -> String {
296 let full = bytewords::identifier(&self.hash[..4].try_into().unwrap());
297
298 let words: Vec<&str> = full.split_whitespace().collect();
299 let mut out = String::with_capacity(8);
300 if words.len() == 4 {
301 for w in words {
302 let b = w.as_bytes();
303 if b.is_empty() {
304 continue;
305 }
306 out.push((b[0] as char).to_ascii_uppercase());
307 out.push((b[b.len() - 1] as char).to_ascii_uppercase());
308 }
309 }
310
311 if out.len() != 8 {
315 out.clear();
316 let compact: String = full
317 .chars()
318 .filter(|c| c.is_ascii_alphabetic())
319 .map(|c| c.to_ascii_uppercase())
320 .collect();
321 for chunk in compact.as_bytes().chunks(4) {
322 if chunk.len() != 4 {
323 continue;
324 }
325 out.push(chunk[0] as char);
326 out.push(chunk[3] as char);
327 }
328 }
329 if prefix { format!("🅟 {}", out) } else { out }
330 }
331}
332
333impl ProvenanceMark {
334 pub fn precedes(&self, next: &ProvenanceMark) -> bool {
335 self.precedes_opt(next).is_ok()
336 }
337
338 pub fn precedes_opt(&self, next: &ProvenanceMark) -> Result<()> {
339 use crate::ValidationIssue;
340
341 if next.seq == 0 {
343 return Err(ValidationIssue::NonGenesisAtZero.into());
344 }
345 if next.key == next.chain_id {
346 return Err(ValidationIssue::InvalidGenesisKey.into());
347 }
348 if self.seq != next.seq - 1 {
350 return Err(ValidationIssue::SequenceGap {
351 expected: self.seq + 1,
352 actual: next.seq,
353 }
354 .into());
355 }
356 if self.date > next.date {
358 return Err(ValidationIssue::DateOrdering {
359 previous: self.date,
360 next: next.date,
361 }
362 .into());
363 }
364 let expected_hash = Self::make_hash(
366 self.res,
367 &self.key,
368 &next.key,
369 &self.chain_id,
370 &self.seq_bytes,
371 &self.date_bytes,
372 &self.info_bytes,
373 );
374 if self.hash != expected_hash {
375 return Err(ValidationIssue::HashMismatch {
376 expected: expected_hash,
377 actual: self.hash.clone(),
378 }
379 .into());
380 }
381 Ok(())
382 }
383
384 pub fn is_sequence_valid(marks: &[ProvenanceMark]) -> bool {
385 if marks.len() < 2 {
386 return false;
387 }
388 if marks[0].seq == 0 && !marks[0].is_genesis() {
389 return false;
390 }
391 marks.windows(2).all(|pair| pair[0].precedes(&pair[1]))
392 }
393
394 pub fn is_genesis(&self) -> bool {
395 self.seq == 0 && self.key == self.chain_id
396 }
397}
398
399impl ProvenanceMark {
400 pub fn to_bytewords_with_style(&self, style: bytewords::Style) -> String {
401 bytewords::encode(self.message(), style)
402 }
403
404 pub fn to_bytewords(&self) -> String {
405 self.to_bytewords_with_style(bytewords::Style::Standard)
406 }
407
408 pub fn from_bytewords(
409 res: ProvenanceMarkResolution,
410 bytewords: &str,
411 ) -> Result<Self> {
412 let message = bytewords::decode(bytewords, bytewords::Style::Standard)?;
413 Self::from_message(res, message)
414 }
415}
416
417impl ProvenanceMark {
418 pub fn to_url_encoding(&self) -> String {
419 bytewords::encode(self.to_cbor_data(), bytewords::Style::Minimal)
420 }
421
422 pub fn from_url_encoding(url_encoding: &str) -> Result<Self> {
423 let cbor_data =
424 bytewords::decode(url_encoding, bytewords::Style::Minimal)?;
425 let cbor = CBOR::try_from_data(cbor_data)?;
426 Ok(Self::try_from(cbor)?)
427 }
428}
429
430impl ProvenanceMark {
431 pub fn to_url(&self, base: &str) -> Url {
434 let mut url = Url::parse(base).unwrap();
435 url.query_pairs_mut()
436 .append_pair("provenance", &self.to_url_encoding());
437 url
438 }
439
440 pub fn from_url(url: &Url) -> Result<Self> {
441 let query = url.query_pairs().find(|(key, _)| key == "provenance");
442 if let Some((_, value)) = query {
443 Self::from_url_encoding(&value)
444 } else {
445 Err(Error::MissingUrlParameter {
446 parameter: "provenance".to_string(),
447 })
448 }
449 }
450}
451
452impl std::fmt::Debug for ProvenanceMark {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 let mut components = vec![
455 format!("key: {}", hex::encode(&self.key)),
456 format!("hash: {}", hex::encode(&self.hash)),
457 format!("chainID: {}", hex::encode(&self.chain_id)),
458 format!("seq: {}", self.seq),
459 format!("date: {}", self.date.to_string()),
460 ];
461
462 if let Some(info) = self.info() {
463 components.push(format!("info: {}", info.diagnostic()));
464 }
465
466 write!(f, "ProvenanceMark({})", components.join(", "))
467 }
468}
469
470impl std::fmt::Display for ProvenanceMark {
471 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472 write!(f, "ProvenanceMark({})", self.identifier())
473 }
474}
475
476#[cfg(feature = "envelope")]
477pub fn register_tags_in(context: &mut FormatContext) {
478 bc_envelope::register_tags_in(context);
479
480 context.tags_mut().set_summarizer(
481 bc_tags::TAG_PROVENANCE_MARK,
482 Arc::new(move |untagged_cbor: CBOR, _flat: bool| {
483 let provenance_mark =
484 ProvenanceMark::from_untagged_cbor(untagged_cbor)?;
485 Ok(provenance_mark.to_string())
486 }),
487 );
488}
489
490#[cfg(feature = "envelope")]
491pub fn register_tags() {
492 with_format_context_mut!(|context: &mut FormatContext| {
493 register_tags_in(context);
494 });
495}
496
497impl CBORTagged for ProvenanceMark {
498 fn cbor_tags() -> Vec<Tag> {
499 tags_for_values(&[bc_tags::TAG_PROVENANCE_MARK])
500 }
501}
502
503impl From<ProvenanceMark> for CBOR {
504 fn from(value: ProvenanceMark) -> Self { value.tagged_cbor() }
505}
506
507impl CBORTaggedEncodable for ProvenanceMark {
508 fn untagged_cbor(&self) -> CBOR {
509 vec![self.res.to_cbor(), CBOR::to_byte_string(self.message())].to_cbor()
510 }
511}
512
513impl TryFrom<CBOR> for ProvenanceMark {
514 type Error = dcbor::Error;
515
516 fn try_from(cbor: CBOR) -> dcbor::Result<Self> {
517 Self::from_tagged_cbor(cbor)
518 }
519}
520
521impl CBORTaggedDecodable for ProvenanceMark {
522 fn from_untagged_cbor(cbor: CBOR) -> dcbor::Result<Self> {
523 let v = CBOR::try_into_array(cbor)?;
524 if v.len() != 2 {
525 return Err("Invalid provenance mark length".into());
526 }
527 let res = ProvenanceMarkResolution::try_from(v[0].clone())?;
528 let message = CBOR::try_into_byte_string(v[1].clone())?;
529 Self::from_message(res, message).map_err(dcbor::Error::from)
530 }
531}
532
533impl From<&ProvenanceMark> for ProvenanceMark {
535 fn from(mark: &ProvenanceMark) -> Self { mark.clone() }
536}
537
538impl ProvenanceMark {
539 pub fn fingerprint(&self) -> [u8; SHA256_SIZE] {
540 sha256(self.to_cbor_data())
541 }
542}
543
544#[cfg(feature = "envelope")]
545impl From<ProvenanceMark> for Envelope {
546 fn from(mark: ProvenanceMark) -> Self { Envelope::new(mark.to_cbor()) }
547}
548
549#[cfg(feature = "envelope")]
550impl TryFrom<Envelope> for ProvenanceMark {
551 type Error = Error;
552
553 fn try_from(envelope: Envelope) -> Result<Self> {
554 let leaf = envelope.subject().try_leaf().map_err(|e| {
555 Error::Cbor(dcbor::Error::Custom(format!("envelope error: {}", e)))
556 })?;
557 let cbor_result: std::result::Result<Self, dcbor::Error> =
558 leaf.try_into();
559 cbor_result.map_err(Error::Cbor)
560 }
561}