1use bytes::Bytes;
2use chrono::DateTime;
3use chrono::FixedOffset;
4use chrono::SecondsFormat;
5use chrono::Utc;
6use ipld_core::ipld::Ipld;
7use libp2p_identity::PeerId;
8use libp2p_identity::PublicKey;
9use libp2p_identity::{DecodingError, Keypair, SigningError};
10use quick_protobuf::MessageWrite;
11use quick_protobuf::Writer;
12use quick_protobuf::{BytesReader, MessageRead};
13use serde::{Deserialize, Serialize, Serializer};
14use std::collections::BTreeMap;
15
16mod generate;
17
18const SIGNATURE_V2_BASE: &[u8] = &[
19 0x69, 0x70, 0x6e, 0x73, 0x2d, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x3a,
20];
21
22const MAX_INLINE_KEY_LENGTH: usize = 42;
25
26#[derive(Debug)]
28#[non_exhaustive]
29pub enum Error {
30 RecordTooLarge,
32 MissingSignature,
34 EmptyData,
36 NameMismatch,
38 MissingPublicKey,
40 InvalidSignature,
42 Expired,
44 DataMismatch,
46 InvalidValidityType,
48 Protobuf(quick_protobuf::Error),
50 Cbor(Box<dyn std::error::Error + Send + Sync + 'static>),
52 InvalidValidity(chrono::ParseError),
54 SigningError(SigningError),
56 InvalidPublicKey(DecodingError),
58 Multihash(multihash::Error),
60 ReservedMetadataKey(String),
62}
63
64impl std::fmt::Display for Error {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 Error::RecordTooLarge => write!(f, "record exceeds the 10 KiB limit"),
68 Error::MissingSignature => write!(f, "record is missing a V2 signature"),
69 Error::EmptyData => write!(f, "record is missing its data field"),
70 Error::NameMismatch => write!(f, "public key does not match the IPNS name"),
71 Error::MissingPublicKey => {
72 write!(
73 f,
74 "record omits pubKey but the IPNS name does not inline one"
75 )
76 }
77 Error::InvalidSignature => write!(f, "signature is invalid"),
78 Error::Expired => write!(f, "record has expired"),
79 Error::DataMismatch => write!(f, "dag-cbor data does not match the protobuf fields"),
80 Error::InvalidValidityType => write!(f, "invalid validity type"),
81 Error::Protobuf(e) => write!(f, "protobuf error: {e}"),
82 Error::Cbor(e) => write!(f, "dag-cbor error: {e}"),
83 Error::InvalidValidity(e) => write!(f, "invalid validity timestamp: {e}"),
84 Error::SigningError(e) => write!(f, "signing error: {e}"),
85 Error::InvalidPublicKey(e) => write!(f, "invalid public key: {e}"),
86 Error::Multihash(e) => write!(f, "invalid multihash: {e}"),
87 Error::ReservedMetadataKey(k) => write!(f, "metadata key `{k}` is reserved"),
88 }
89 }
90}
91
92impl std::error::Error for Error {
93 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
94 match self {
95 Error::Protobuf(e) => Some(e),
96 Error::SigningError(e) => Some(e),
97 Error::InvalidPublicKey(e) => Some(e),
98 Error::Multihash(e) => Some(e),
99 Error::Cbor(e) => Some(&**e),
100 Error::InvalidValidity(e) => Some(e),
101 _ => None,
102 }
103 }
104}
105
106impl From<quick_protobuf::Error> for Error {
107 fn from(e: quick_protobuf::Error) -> Self {
108 Error::Protobuf(e)
109 }
110}
111
112impl From<chrono::ParseError> for Error {
113 fn from(e: chrono::ParseError) -> Self {
114 Error::InvalidValidity(e)
115 }
116}
117
118impl From<Error> for std::io::Error {
119 fn from(e: Error) -> Self {
120 std::io::Error::new(std::io::ErrorKind::InvalidData, e)
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
125#[repr(i32)]
126pub enum ValidityType {
127 EOL = 0,
128}
129
130impl std::fmt::Display for ValidityType {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 write!(f, "EOL")
133 }
134}
135
136impl Serialize for ValidityType {
137 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138 where
139 S: Serializer,
140 {
141 serializer.serialize_i32(*self as i32)
142 }
143}
144
145impl<'de> Deserialize<'de> for ValidityType {
146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147 where
148 D: serde::Deserializer<'de>,
149 {
150 let i = i32::deserialize(deserializer)?;
151 ValidityType::try_from(i).map_err(serde::de::Error::custom)
152 }
153}
154
155impl TryFrom<i32> for ValidityType {
156 type Error = Error;
157 fn try_from(i: i32) -> Result<Self, Self::Error> {
158 match i {
159 0 => Ok(ValidityType::EOL),
160 _ => Err(Error::InvalidValidityType),
161 }
162 }
163}
164
165impl From<ValidityType> for i32 {
166 fn from(ty: ValidityType) -> Self {
167 ty as i32
168 }
169}
170
171impl From<generate::ipns_pb::mod_IpnsEntry::ValidityType> for ValidityType {
172 fn from(v_ty: generate::ipns_pb::mod_IpnsEntry::ValidityType) -> Self {
173 match v_ty {
174 generate::ipns_pb::mod_IpnsEntry::ValidityType::EOL => ValidityType::EOL,
175 }
176 }
177}
178
179#[derive(Clone, Debug)]
180pub struct Record {
181 data: Vec<u8>,
182
183 value: Vec<u8>,
184 validity_type: ValidityType,
185 validity: Vec<u8>,
186 sequence: u64,
187 ttl: u64,
188
189 public_key: Vec<u8>,
190
191 signature_v1: Vec<u8>,
192 signature_v2: Vec<u8>,
193}
194
195impl From<generate::ipns_pb::IpnsEntry<'_>> for Record {
196 fn from(entry: generate::ipns_pb::IpnsEntry<'_>) -> Self {
197 Record {
198 data: entry.data.into(),
199 value: entry.value.into(),
200 validity_type: entry.validityType.into(),
201 validity: entry.validity.into(),
202 sequence: entry.sequence,
203 ttl: entry.ttl,
204 public_key: entry.pubKey.into(),
205 signature_v1: entry.signatureV1.into(),
206 signature_v2: entry.signatureV2.into(),
207 }
208 }
209}
210
211impl<'a> From<&'a Record> for generate::ipns_pb::IpnsEntry<'a> {
212 fn from(record: &'a Record) -> Self {
213 generate::ipns_pb::IpnsEntry {
214 validity: (&record.validity).into(),
215 validityType: generate::ipns_pb::mod_IpnsEntry::ValidityType::EOL,
216 value: (&record.value).into(),
217 signatureV1: (&record.signature_v1).into(),
218 signatureV2: (&record.signature_v2).into(),
219 sequence: record.sequence,
220 pubKey: (&record.public_key).into(),
221 ttl: record.ttl,
222 data: (&record.data).into(),
223 }
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct Data {
231 #[serde(rename = "Value")]
232 pub value: Bytes,
233
234 #[serde(rename = "ValidityType")]
235 pub validity_type: ValidityType,
236
237 #[serde(rename = "Validity")]
238 pub validity: Bytes,
239
240 #[serde(rename = "Sequence")]
241 pub sequence: u64,
242
243 #[serde(rename = "TTL")]
244 pub ttl: u64,
245
246 #[serde(flatten)]
249 pub metadata: BTreeMap<String, Ipld>,
250}
251
252impl Data {
253 pub fn value(&self) -> &[u8] {
254 &self.value
255 }
256
257 pub fn metadata(&self) -> &BTreeMap<String, Ipld> {
259 &self.metadata
260 }
261
262 pub fn validity_type(&self) -> ValidityType {
263 self.validity_type
264 }
265
266 pub fn validity(&self) -> &[u8] {
267 &self.validity
268 }
269
270 pub fn sequence(&self) -> u64 {
271 self.sequence
272 }
273
274 pub fn ttl(&self) -> u64 {
275 self.ttl
276 }
277}
278
279impl Record {
280 pub fn new(
283 keypair: &Keypair,
284 value: impl AsRef<[u8]>,
285 eol: DateTime<Utc>,
286 seq: u64,
287 ttl: std::time::Duration,
288 ) -> Result<Self, Error> {
289 Self::new_with_metadata(keypair, value, eol, seq, ttl, BTreeMap::new())
290 }
291
292 pub fn new_with_metadata(
296 keypair: &Keypair,
297 value: impl AsRef<[u8]>,
298 eol: DateTime<Utc>,
299 seq: u64,
300 ttl: std::time::Duration,
301 metadata: BTreeMap<String, Ipld>,
302 ) -> Result<Self, Error> {
303 for reserved in ["Value", "Validity", "ValidityType", "Sequence", "TTL"] {
304 if metadata.contains_key(reserved) {
305 return Err(Error::ReservedMetadataKey(reserved.to_string()));
306 }
307 }
308
309 let value = value.as_ref().to_vec();
310
311 let ttl = u64::try_from(ttl.as_nanos()).unwrap_or(u64::MAX);
312
313 let validity = eol.to_rfc3339_opts(SecondsFormat::Nanos, true).into_bytes();
314
315 let validity_type = ValidityType::EOL;
316
317 let signature_v1_construct = {
318 let mut data = Vec::with_capacity(value.len() + validity.len() + 3);
319
320 data.extend(value.iter());
321 data.extend(validity.iter());
322 data.extend(validity_type.to_string().as_bytes());
323
324 data
325 };
326
327 let signature_v1 = keypair
328 .sign(&signature_v1_construct)
329 .map_err(Error::SigningError)?;
330
331 let document = Data {
332 value: Bytes::from(value.clone()),
333 validity_type,
334 validity: Bytes::from(validity.clone()),
335 sequence: seq,
336 ttl,
337 metadata,
338 };
339
340 let data = serde_ipld_dagcbor::to_vec(&document).map_err(|e| Error::Cbor(Box::new(e)))?;
341
342 let signature_v2_construct = SIGNATURE_V2_BASE
343 .iter()
344 .chain(data.iter())
345 .copied()
346 .collect::<Vec<_>>();
347
348 let signature_v2 = keypair
349 .sign(&signature_v2_construct)
350 .map_err(Error::SigningError)?;
351
352 let encoded_public_key = keypair.public().encode_protobuf();
353 let public_key = if encoded_public_key.len() > MAX_INLINE_KEY_LENGTH {
354 encoded_public_key
355 } else {
356 Vec::new()
357 };
358
359 Ok(Record {
360 data,
361 value,
362 validity_type,
363 validity,
364 sequence: seq,
365 ttl,
366 public_key,
367 signature_v1,
368 signature_v2,
369 })
370 }
371
372 pub fn decode(data: impl AsRef<[u8]>) -> Result<Self, Error> {
373 let data = data.as_ref();
374
375 if data.len() > 10 * 1024 {
376 return Err(Error::RecordTooLarge);
377 }
378
379 let mut reader = BytesReader::from_bytes(data);
380 let entry = generate::ipns_pb::IpnsEntry::from_reader(&mut reader, data)?;
381 let record = entry.into();
382 Ok(record)
383 }
384
385 pub fn encode(&self) -> Result<Vec<u8>, Error> {
386 let entry: generate::ipns_pb::IpnsEntry = self.into();
387
388 let mut buf = Vec::with_capacity(entry.get_size());
389 let mut writer = Writer::new(&mut buf);
390
391 entry.write_message(&mut writer)?;
392
393 Ok(buf)
394 }
395}
396
397impl Record {
398 pub fn sequence(&self) -> u64 {
399 self.sequence
400 }
401
402 pub fn validity_type(&self) -> ValidityType {
403 self.validity_type
404 }
405
406 pub fn validity(&self) -> Result<DateTime<FixedOffset>, Error> {
407 let time = String::from_utf8_lossy(&self.validity);
408 Ok(chrono::DateTime::parse_from_rfc3339(&time)?)
409 }
410
411 pub fn ttl(&self) -> u64 {
412 self.ttl
413 }
414
415 pub fn has_signature_v1(&self) -> bool {
417 !self.signature_v1.is_empty()
418 }
419
420 pub fn has_signature_v2(&self) -> bool {
422 !self.signature_v2.is_empty()
423 }
424
425 pub fn data(&self) -> Result<Data, Error> {
426 let data: Data =
427 serde_ipld_dagcbor::from_slice(&self.data).map_err(|e| Error::Cbor(Box::new(e)))?;
428
429 if data.value != self.value
430 || data.validity != self.validity
431 || data.validity_type != self.validity_type
432 || data.sequence != self.sequence
433 || data.ttl != self.ttl
434 {
435 return Err(Error::DataMismatch);
436 }
437
438 Ok(data)
439 }
440
441 pub fn value(&self) -> &[u8] {
443 &self.value
444 }
445
446 pub fn verify_signature(&self, peer_id: PeerId) -> Result<(), Error> {
447 use multihash::Multihash;
448
449 if self.signature_v2.is_empty() {
450 return Err(Error::MissingSignature);
451 }
452
453 if self.data.is_empty() {
454 return Err(Error::EmptyData);
455 }
456
457 let public_key = if self.public_key.is_empty() {
458 let mh = Multihash::<64>::from_bytes(&peer_id.to_bytes()).map_err(Error::Multihash)?;
459 if mh.code() != 0 {
462 return Err(Error::MissingPublicKey);
463 }
464 PublicKey::try_decode_protobuf(mh.digest())
465 } else {
466 PublicKey::try_decode_protobuf(&self.public_key)
467 }
468 .map_err(Error::InvalidPublicKey)?;
469
470 if PeerId::from_public_key(&public_key) != peer_id {
471 return Err(Error::NameMismatch);
472 }
473
474 self.data()?;
475
476 let signature_v2 = SIGNATURE_V2_BASE
477 .iter()
478 .chain(self.data.iter())
479 .copied()
480 .collect::<Vec<_>>();
481
482 if !public_key.verify(&signature_v2, &self.signature_v2) {
483 return Err(Error::InvalidSignature);
484 }
485
486 Ok(())
487 }
488
489 pub fn verify(&self, peer_id: PeerId) -> Result<(), Error> {
492 self.verify_signature(peer_id)?;
493
494 if self.validity()?.with_timezone(&Utc) < Utc::now() {
495 return Err(Error::Expired);
496 }
497
498 Ok(())
499 }
500
501 pub fn compare(&self, other: &Record) -> Result<std::cmp::Ordering, Error> {
505 use std::cmp::Ordering;
506
507 match self
508 .has_signature_v2()
509 .cmp(&other.has_signature_v2())
510 .then_with(|| self.sequence.cmp(&other.sequence))
511 {
512 Ordering::Equal => Ok(self.validity()?.cmp(&other.validity()?)),
513 ord => Ok(ord),
514 }
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use chrono::Duration;
522
523 fn record_for(kp: &Keypair, hours: i64) -> Record {
524 Record::new(
525 kp,
526 b"/ipfs/bafkqaaa",
527 Utc::now() + Duration::hours(hours),
528 0,
529 std::time::Duration::ZERO,
530 )
531 .unwrap()
532 }
533
534 #[test]
535 fn valid_record_roundtrips_and_verifies() {
536 let kp = Keypair::generate_ed25519();
537 let peer = PeerId::from_public_key(&kp.public());
538 let rec = record_for(&kp, 24);
539 rec.verify(peer).unwrap();
540
541 let decoded = Record::decode(rec.encode().unwrap()).unwrap();
542 decoded.verify(peer).unwrap();
543 }
544
545 #[test]
546 fn verify_rejects_expired_record_but_signature_still_checks() {
547 let kp = Keypair::generate_ed25519();
548 let peer = PeerId::from_public_key(&kp.public());
549 let rec = record_for(&kp, -1);
550 rec.verify_signature(peer).unwrap();
551 assert!(rec.verify(peer).is_err());
552 }
553
554 #[test]
555 fn embedded_pubkey_must_match_the_name() {
556 let attacker = Keypair::generate_ed25519();
557 let victim = Keypair::generate_ed25519();
558 let attacker_peer = PeerId::from_public_key(&attacker.public());
559 let victim_peer = PeerId::from_public_key(&victim.public());
560
561 let mut bytes = record_for(&attacker, 24).encode().unwrap();
564 let pk = attacker.public().encode_protobuf();
565 bytes.push(0x3a);
566 bytes.push(pk.len() as u8); bytes.extend_from_slice(&pk);
568
569 let tampered = Record::decode(&bytes).unwrap();
570 tampered.verify_signature(attacker_peer).unwrap();
571 assert!(tampered.verify_signature(victim_peer).is_err());
573 }
574
575 #[test]
576 fn create_and_verify_across_key_types() {
577 for kp in [
580 Keypair::generate_ed25519(),
581 Keypair::generate_secp256k1(),
582 Keypair::generate_ecdsa(),
583 ] {
584 let peer = PeerId::from_public_key(&kp.public());
585 let rec = Record::new(
586 &kp,
587 b"/ipfs/bafkqaaa",
588 Utc::now() + Duration::hours(24),
589 0,
590 std::time::Duration::ZERO,
591 )
592 .unwrap();
593 rec.verify(peer).unwrap();
594 let decoded = Record::decode(rec.encode().unwrap()).unwrap();
595 decoded.verify(peer).unwrap();
596 }
597 }
598
599 #[test]
600 fn compare_prefers_higher_sequence_then_later_validity() {
601 use std::cmp::Ordering;
602 let kp = Keypair::generate_ed25519();
603
604 let seq0 = Record::new(
605 &kp,
606 b"/ipfs/bafkqaaa",
607 Utc::now() + Duration::hours(24),
608 0,
609 std::time::Duration::ZERO,
610 )
611 .unwrap();
612 let seq1 = Record::new(
613 &kp,
614 b"/ipfs/bafkqaaa",
615 Utc::now() + Duration::hours(1),
616 1,
617 std::time::Duration::ZERO,
618 )
619 .unwrap();
620 assert_eq!(seq1.compare(&seq0).unwrap(), Ordering::Greater);
622 assert_eq!(seq0.compare(&seq1).unwrap(), Ordering::Less);
623
624 let near = Record::new(
625 &kp,
626 b"/ipfs/bafkqaaa",
627 Utc::now() + Duration::hours(1),
628 5,
629 std::time::Duration::ZERO,
630 )
631 .unwrap();
632 let far = Record::new(
633 &kp,
634 b"/ipfs/bafkqaaa",
635 Utc::now() + Duration::hours(48),
636 5,
637 std::time::Duration::ZERO,
638 )
639 .unwrap();
640 assert_eq!(far.compare(&near).unwrap(), Ordering::Greater);
642 }
643
644 #[test]
645 fn compare_prefers_v2_over_v1_only() {
646 use std::cmp::Ordering;
647 let kp = Keypair::generate_ed25519();
648
649 let with_v2 = record_for(&kp, 1);
650 let mut v1_only = record_for(&kp, 48); v1_only.signature_v2.clear();
652 assert!(with_v2.has_signature_v2() && !v1_only.has_signature_v2());
653
654 assert_eq!(with_v2.compare(&v1_only).unwrap(), Ordering::Greater);
656 assert_eq!(v1_only.compare(&with_v2).unwrap(), Ordering::Less);
657 }
658
659 #[test]
660 fn metadata_roundtrips_and_is_signed() {
661 let kp = Keypair::generate_ed25519();
662 let peer = PeerId::from_public_key(&kp.public());
663 let mut metadata = BTreeMap::new();
664 metadata.insert("Foo".to_string(), Ipld::String("bar".into()));
665 metadata.insert("Count".to_string(), Ipld::Integer(7));
666
667 let rec = Record::new_with_metadata(
668 &kp,
669 b"/ipfs/bafkqaaa",
670 Utc::now() + Duration::hours(24),
671 0,
672 std::time::Duration::ZERO,
673 metadata.clone(),
674 )
675 .unwrap();
676 rec.verify(peer).unwrap();
677
678 let decoded = Record::decode(rec.encode().unwrap()).unwrap();
680 decoded.verify(peer).unwrap();
681 assert_eq!(decoded.data().unwrap().metadata(), &metadata);
682
683 let mut bad = BTreeMap::new();
685 bad.insert("TTL".to_string(), Ipld::Integer(1));
686 assert!(matches!(
687 Record::new_with_metadata(
688 &kp,
689 b"/x",
690 Utc::now() + Duration::hours(1),
691 0,
692 std::time::Duration::ZERO,
693 bad,
694 ),
695 Err(Error::ReservedMetadataKey(_))
696 ));
697 }
698}