Skip to main content

rust_ipns/
lib.rs

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
22/// libp2p inlines a public key into the PeerID (via an `identity` multihash) when its protobuf
23/// encoding is at most this many bytes; larger keys are referenced by a sha2-256 hash instead.
24const MAX_INLINE_KEY_LENGTH: usize = 42;
25
26/// Errors produced when creating, decoding, or validating an IPNS [`Record`].
27#[derive(Debug)]
28#[non_exhaustive]
29pub enum Error {
30    /// The record exceeds the 10 KiB IPNS size limit.
31    RecordTooLarge,
32    /// The record is missing its V2 signature.
33    MissingSignature,
34    /// The record is missing its data field.
35    EmptyData,
36    /// The signing key does not correspond to the IPNS name.
37    NameMismatch,
38    /// The IPNS name does not inline a public key and the record omits one.
39    MissingPublicKey,
40    /// The V2 signature failed verification.
41    InvalidSignature,
42    /// The record's EOL validity has elapsed.
43    Expired,
44    /// The dag-cbor data does not match the record's protobuf fields.
45    DataMismatch,
46    /// Unrecognized validity type.
47    InvalidValidityType,
48    /// Malformed protobuf.
49    Protobuf(quick_protobuf::Error),
50    /// Malformed dag-cbor data.
51    Cbor(Box<dyn std::error::Error + Send + Sync + 'static>),
52    /// Malformed EOL validity timestamp.
53    InvalidValidity(chrono::ParseError),
54    /// A key or signing operation failed.
55    SigningError(SigningError),
56    /// Invalid public key.
57    InvalidPublicKey(DecodingError),
58    /// Malformed multihash or peer id.
59    Multihash(multihash::Error),
60    /// A metadata key collides with a reserved IPNS field name.
61    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// Fields of the Bytes type are used here instead of Vec<u8> to ensure that
228// these fields are (de)serialized into "byte string" CBOR values instead of simple arrays.
229#[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    /// Additional non-standard metadata keys carried in the dag-cbor map. Empty for typical
247    /// records; preserved verbatim on decode/encode and covered by the V2 signature.
248    #[serde(flatten)]
249    pub metadata: BTreeMap<String, Ipld>,
250}
251
252impl Data {
253    pub fn value(&self) -> &[u8] {
254        &self.value
255    }
256
257    /// Non-standard metadata keys carried alongside the reserved IPNS fields.
258    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    /// Creates and signs an IPNS record pointing at `value`, valid until the absolute `eol`
281    /// (End-Of-Life) timestamp. `ttl` is a caching hint for resolvers.
282    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    /// Like [`Record::new`] but attaches additional dag-cbor `metadata` keys to the record. Keys
293    /// must not collide with the reserved fields (`Value`, `Validity`, `ValidityType`, `Sequence`,
294    /// `TTL`).
295    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    /// Whether the record carries a (legacy) V1 signature.
416    pub fn has_signature_v1(&self) -> bool {
417        !self.signature_v1.is_empty()
418    }
419
420    /// Whether the record carries a V2 signature.
421    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    /// The raw IPNS value.
442    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            // small keys are inlined in the name via an identity (code 0) multihash; anything
460            // else (e.g. an RSA name) carries no inlined key, so the record must embed one.
461            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    /// Fully validates the record against `peer_id`: name binding, V2 signature, and that the EOL
490    /// validity has not elapsed.
491    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    /// Orders this record against `other` by IPNS precedence: higher `sequence` wins, then the
502    /// later EOL `validity`.
503    /// Records should already be validated and refer to the same name.
504    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        // a genuine attacker record, with the attacker's pubKey spliced into the protobuf
562        // (field 7, tag 0x3a) to mimic a record that carries an embedded key.
563        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); // an ed25519 protobuf key is < 128 bytes
567        bytes.extend_from_slice(&pk);
568
569        let tampered = Record::decode(&bytes).unwrap();
570        tampered.verify_signature(attacker_peer).unwrap();
571        // the embedded key does not hash to the victim's name, so it must not validate for it.
572        assert!(tampered.verify_signature(victim_peer).is_err());
573    }
574
575    #[test]
576    fn create_and_verify_across_key_types() {
577        // Ed25519/Secp256k1 inline into the name; ECDSA does not, so its record must embed the
578        // public key. All must create and verify (and survive a wire round-trip).
579        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        // higher sequence wins even with an earlier EOL
621        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        // equal sequence: the later EOL wins
641        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); // later EOL, but no V2
651        v1_only.signature_v2.clear();
652        assert!(with_v2.has_signature_v2() && !v1_only.has_signature_v2());
653
654        // V2 presence outranks both sequence and the later validity
655        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        // survives a wire round-trip, still verifies, and exposes the metadata unchanged
679        let decoded = Record::decode(rec.encode().unwrap()).unwrap();
680        decoded.verify(peer).unwrap();
681        assert_eq!(decoded.data().unwrap().metadata(), &metadata);
682
683        // reserved keys are rejected
684        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}