Skip to main content

dig_tls/
identity.rs

1//! Peer identity — `peer_id = SHA-256(TLS SubjectPublicKeyInfo DER)`.
2//!
3//! The transport identity of a DIG peer is the SHA-256 digest of the ASN.1 `SubjectPublicKeyInfo`
4//! sequence (algorithm id + subjectPublicKey bit string) lifted from its leaf X.509 certificate.
5//! This is the SAME derivation Chia uses for its node id and byte-identical to `dig-gossip`'s and
6//! `dig-nat`'s `peer_id_from_tls_spki_der` — dig-tls is now the CANONICAL home so no consumer
7//! re-implements it. Because every DIG connection is mutual TLS, each side derives the other's
8//! [`PeerId`] from the certificate presented during the handshake.
9
10use std::fmt;
11
12use sha2::{Digest, Sha256};
13
14/// A peer's stable network identity: the 32-byte SHA-256 of its TLS SPKI DER.
15#[derive(Clone, Copy, PartialEq, Eq, Hash)]
16pub struct PeerId([u8; 32]);
17
18impl PeerId {
19    /// Construct from raw 32 bytes.
20    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
21        PeerId(bytes)
22    }
23
24    /// The raw 32 bytes.
25    pub const fn as_bytes(&self) -> &[u8; 32] {
26        &self.0
27    }
28
29    /// Lowercase hex encoding (the canonical string form used on the relay wire and in status APIs).
30    pub fn to_hex(&self) -> String {
31        let mut s = String::with_capacity(64);
32        for b in self.0 {
33            s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
34            s.push(char::from_digit((b & 0x0f) as u32, 16).unwrap());
35        }
36        s
37    }
38
39    /// Parse a 64-char hex string. Returns `None` for a wrong length or a non-hex character.
40    pub fn from_hex(hex: &str) -> Option<Self> {
41        if hex.len() != 64 {
42            return None;
43        }
44        let mut out = [0u8; 32];
45        for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
46            let hi = (chunk[0] as char).to_digit(16)?;
47            let lo = (chunk[1] as char).to_digit(16)?;
48            out[i] = ((hi << 4) | lo) as u8;
49        }
50        Some(PeerId(out))
51    }
52}
53
54impl fmt::Debug for PeerId {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "PeerId({})", self.to_hex())
57    }
58}
59
60impl fmt::Display for PeerId {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.write_str(&self.to_hex())
63    }
64}
65
66/// Derive a [`PeerId`] from a TLS **SubjectPublicKeyInfo** block in PKIX DER form.
67///
68/// `spki_der` must be the full ASN.1 `SubjectPublicKeyInfo` sequence (algorithm id + subjectPublicKey
69/// bit string) — **not** the bare public-key bit string. [`peer_id_from_leaf_cert_der`] extracts it
70/// from a whole leaf certificate for you.
71pub fn peer_id_from_tls_spki_der(spki_der: &[u8]) -> PeerId {
72    PeerId(Sha256::digest(spki_der).into())
73}
74
75/// Extract the SubjectPublicKeyInfo DER from a leaf X.509 certificate (DER-encoded) and derive the
76/// [`PeerId`]. Returns `None` if the certificate cannot be parsed as X.509.
77pub fn peer_id_from_leaf_cert_der(cert_der: &[u8]) -> Option<PeerId> {
78    let (_, x509) = x509_parser::parse_x509_certificate(cert_der).ok()?;
79    Some(peer_id_from_tls_spki_der(
80        x509.tbs_certificate.subject_pki.raw,
81    ))
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn hex_round_trips() {
90        let id = PeerId::from_bytes([0xABu8; 32]);
91        let hex = id.to_hex();
92        assert_eq!(hex.len(), 64);
93        assert_eq!(PeerId::from_hex(&hex), Some(id));
94    }
95
96    #[test]
97    fn from_hex_rejects_bad_input() {
98        assert_eq!(PeerId::from_hex("tooshort"), None);
99        assert_eq!(PeerId::from_hex(&"z".repeat(64)), None);
100    }
101
102    #[test]
103    fn peer_id_is_sha256_of_spki() {
104        let spki = b"a fake SPKI DER blob";
105        let expected: [u8; 32] = Sha256::digest(spki).into();
106        assert_eq!(peer_id_from_tls_spki_der(spki).as_bytes(), &expected);
107    }
108
109    #[test]
110    fn peer_id_from_unparseable_cert_is_none() {
111        assert_eq!(peer_id_from_leaf_cert_der(b"not a certificate"), None);
112    }
113
114    #[test]
115    fn display_equals_hex() {
116        let id = PeerId::from_bytes([0x01u8; 32]);
117        assert_eq!(format!("{id}"), id.to_hex());
118    }
119}