Skip to main content

provenance_mark/
mark.rs

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// JSON Example:
24// {"chainID":"znwVmQ==","date":"2023-06-20T00:00:00Z","hash":"ZaTfvw==","key":"
25// znwVmQ==","res":0,"seq":0}
26
27#[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    /// The first four bytes of the mark's hash as a hex string.
275    pub fn identifier(&self) -> String { hex::encode(&self.hash[..4]) }
276
277    /// The first four bytes of the mark's hash as upper-case ByteWords.
278    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    /// The first four bytes of the mark's hash as Bytemoji.
285    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    /// A compact 8-letter identifier derived from the upper-case ByteWords
293    /// identifier by taking the first and last letter of each ByteWords word
294    /// (4 words × 2 letters = 8 letters).
295    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        // Conservative fallback: if the input wasn't in the expected
312        // space-separated 4-word format, remove whitespace and chunk the
313        // remaining letters.
314        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        // `next` can't be a genesis
342        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        // `next` must have the next highest sequence number
349        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        // `next` must have an equal or later date
357        if self.date > next.date {
358            return Err(ValidationIssue::DateOrdering {
359                previous: self.date,
360                next: next.date,
361            }
362            .into());
363        }
364        // `next` must reveal the key that was used to generate this mark's hash
365        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    // Example format:
432    // ur:provenance/lfaegdtokebznlahftbsnlaxpsdiwecswsrnlsdsdpghrp
433    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
533// Convert from an instance reference to an instance.
534impl 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}