Skip to main content

urma_runtime/
pinning.rs

1use crate::error::{Context, Error, ensure};
2use ring::signature::{
3    ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1, ED25519, RSA_PKCS1_2048_8192_SHA256,
4    RSA_PKCS1_2048_8192_SHA384, RSA_PKCS1_2048_8192_SHA512, RSA_PSS_2048_8192_SHA256,
5    RSA_PSS_2048_8192_SHA384, RSA_PSS_2048_8192_SHA512, UnparsedPublicKey, VerificationAlgorithm,
6};
7use rustls::{
8    Certificate, DigitallySignedStruct, ServerName, SignatureScheme,
9    client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
10};
11use sha2::{Digest, Sha256};
12use std::{
13    collections::HashMap,
14    path::{Path, PathBuf},
15    sync::{Arc, Mutex},
16    time::SystemTime,
17};
18
19pub struct Pins {
20    directory: PinDirectory,
21    memory: Mutex<HashMap<String, [u8; 32]>>,
22}
23
24enum PinDirectory {
25    Ephemeral,
26    Persistent(PathBuf),
27}
28
29impl Pins {
30    pub fn ephemeral() -> Self {
31        tracing::warn!(
32            "electrum certificate pins are not persisted; trust on first use lasts this session only"
33        );
34        Self {
35            directory: PinDirectory::Ephemeral,
36            memory: Mutex::new(HashMap::new()),
37        }
38    }
39
40    pub fn in_directory(cache_dir: &Path) -> Result<Self, Error> {
41        let directory = cache_dir.join("electrum-pins");
42        if !directory.try_exists()? {
43            std::fs::create_dir_all(cache_dir)?;
44            urma_io::create_private_directory(&directory)?;
45        }
46        Ok(Self {
47            directory: PinDirectory::Persistent(directory),
48            memory: Mutex::new(HashMap::new()),
49        })
50    }
51
52    pub fn check(&self, key: &str, digest: [u8; 32]) -> Result<(), Error> {
53        let mut memory = match self.memory.lock() {
54            Ok(memory) => memory,
55            Err(poisoned) => {
56                tracing::error!("electrum pin table lock poisoned");
57                poisoned.into_inner()
58            }
59        };
60        let known = match memory.get(key) {
61            Some(pinned) => Some(*pinned),
62            None => self.load(key)?,
63        };
64        match known {
65            Some(pinned) => {
66                ensure!(
67                    pinned == digest,
68                    "electrum server {key} presented a certificate that differs from its pinned certificate; refusing"
69                );
70                memory.insert(key.to_owned(), digest);
71                Ok(())
72            }
73            None => {
74                self.persist(key, digest)?;
75                memory.insert(key.to_owned(), digest);
76                tracing::warn!(
77                    server = key,
78                    pin = hex::encode(digest),
79                    "electrum certificate pinned on first use"
80                );
81                Ok(())
82            }
83        }
84    }
85
86    fn load(&self, key: &str) -> Result<Option<[u8; 32]>, Error> {
87        let PinDirectory::Persistent(directory) = &self.directory else {
88            return Ok(None);
89        };
90        let path = directory.join(format!("{key}.sha256"));
91        if !path.try_exists()? {
92            return Ok(None);
93        }
94        let text = String::from_utf8(urma_io::read_bounded(&path, 128)?)?;
95        let bytes = hex::decode(text.trim())?;
96        Ok(Some(<[u8; 32]>::try_from(bytes.as_slice())?))
97    }
98
99    fn persist(&self, key: &str, digest: [u8; 32]) -> Result<(), Error> {
100        let PinDirectory::Persistent(directory) = &self.directory else {
101            return Ok(());
102        };
103        let path = directory.join(format!("{key}.sha256"));
104        urma_io::write_new(&path, format!("{}\n", hex::encode(digest)).as_bytes())?;
105        Ok(())
106    }
107}
108
109pub(crate) struct Pinned {
110    pub(crate) pins: Arc<Pins>,
111    pub(crate) key: String,
112}
113
114impl ServerCertVerifier for Pinned {
115    fn verify_server_cert(
116        &self,
117        end_entity: &Certificate,
118        _intermediates: &[Certificate],
119        _server_name: &ServerName,
120        _scts: &mut dyn Iterator<Item = &[u8]>,
121        _ocsp_response: &[u8],
122        _now: SystemTime,
123    ) -> Result<ServerCertVerified, rustls::Error> {
124        let digest: [u8; 32] = Sha256::digest(&end_entity.0).into();
125        match self.pins.check(&self.key, digest) {
126            Ok(()) => Ok(ServerCertVerified::assertion()),
127            Err(error) => {
128                tracing::error!(server = %self.key, %error, "electrum certificate rejected");
129                Err(rustls::Error::General(error.to_string()))
130            }
131        }
132    }
133
134    fn verify_tls12_signature(
135        &self,
136        message: &[u8],
137        cert: &Certificate,
138        dss: &DigitallySignedStruct,
139    ) -> Result<HandshakeSignatureValid, rustls::Error> {
140        self.verify_signature(message, cert, dss)
141    }
142
143    fn verify_tls13_signature(
144        &self,
145        message: &[u8],
146        cert: &Certificate,
147        dss: &DigitallySignedStruct,
148    ) -> Result<HandshakeSignatureValid, rustls::Error> {
149        self.verify_signature(message, cert, dss)
150    }
151
152    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
153        vec![
154            SignatureScheme::ECDSA_NISTP384_SHA384,
155            SignatureScheme::ECDSA_NISTP256_SHA256,
156            SignatureScheme::ED25519,
157            SignatureScheme::RSA_PSS_SHA512,
158            SignatureScheme::RSA_PSS_SHA384,
159            SignatureScheme::RSA_PSS_SHA256,
160            SignatureScheme::RSA_PKCS1_SHA512,
161            SignatureScheme::RSA_PKCS1_SHA384,
162            SignatureScheme::RSA_PKCS1_SHA256,
163        ]
164    }
165
166    fn request_scts(&self) -> bool {
167        false
168    }
169}
170
171impl Pinned {
172    fn verify_signature(
173        &self,
174        message: &[u8],
175        cert: &Certificate,
176        dss: &DigitallySignedStruct,
177    ) -> Result<HandshakeSignatureValid, rustls::Error> {
178        match verify_handshake(message, &cert.0, dss) {
179            Ok(()) => Ok(HandshakeSignatureValid::assertion()),
180            Err(error) => {
181                tracing::error!(server = %self.key, %error, "electrum handshake signature rejected");
182                Err(rustls::Error::General(error.to_string()))
183            }
184        }
185    }
186}
187
188enum PublicKey<'a> {
189    Rsa(&'a [u8]),
190    P256(&'a [u8]),
191    P384(&'a [u8]),
192    Ed25519(&'a [u8]),
193}
194
195fn verify_handshake(
196    message: &[u8],
197    certificate: &[u8],
198    dss: &DigitallySignedStruct,
199) -> Result<(), Error> {
200    let key = public_key(certificate)?;
201    let (algorithm, bytes): (&'static dyn VerificationAlgorithm, &[u8]) = match (dss.scheme, key) {
202        (SignatureScheme::RSA_PKCS1_SHA256, PublicKey::Rsa(k)) => (&RSA_PKCS1_2048_8192_SHA256, k),
203        (SignatureScheme::RSA_PKCS1_SHA384, PublicKey::Rsa(k)) => (&RSA_PKCS1_2048_8192_SHA384, k),
204        (SignatureScheme::RSA_PKCS1_SHA512, PublicKey::Rsa(k)) => (&RSA_PKCS1_2048_8192_SHA512, k),
205        (SignatureScheme::RSA_PSS_SHA256, PublicKey::Rsa(k)) => (&RSA_PSS_2048_8192_SHA256, k),
206        (SignatureScheme::RSA_PSS_SHA384, PublicKey::Rsa(k)) => (&RSA_PSS_2048_8192_SHA384, k),
207        (SignatureScheme::RSA_PSS_SHA512, PublicKey::Rsa(k)) => (&RSA_PSS_2048_8192_SHA512, k),
208        (SignatureScheme::ECDSA_NISTP256_SHA256, PublicKey::P256(k)) => {
209            (&ECDSA_P256_SHA256_ASN1, k)
210        }
211        (SignatureScheme::ECDSA_NISTP384_SHA384, PublicKey::P384(k)) => {
212            (&ECDSA_P384_SHA384_ASN1, k)
213        }
214        (SignatureScheme::ED25519, PublicKey::Ed25519(k)) => (&ED25519, k),
215        (scheme, _) => {
216            return Err(Error::Unsupported(format!(
217                "handshake signature scheme {scheme:?} does not match the pinned certificate key"
218            )));
219        }
220    };
221    UnparsedPublicKey::new(algorithm, bytes)
222        .verify(message, dss.signature())
223        .map_err(|cause| Error::Invalid(format!("handshake signature invalid: {cause}")))
224}
225
226fn element(bytes: &[u8]) -> Result<(u8, &[u8], &[u8]), Error> {
227    let tag = *bytes.first().context("certificate DER truncated")?;
228    let first = *bytes.get(1).context("certificate DER truncated")?;
229    let (length, header) = if first < 0x80 {
230        (usize::from(first), 2)
231    } else {
232        let count = usize::from(first & 0x7f);
233        ensure!(
234            (1..=3).contains(&count),
235            "certificate DER length form unsupported"
236        );
237        let mut length = 0_usize;
238        for offset in 0..count {
239            let byte = *bytes.get(2 + offset).context("certificate DER truncated")?;
240            length = (length << 8) | usize::from(byte);
241        }
242        (length, 2 + count)
243    };
244    let end = header
245        .checked_add(length)
246        .context("certificate DER length overflow")?;
247    let content = bytes
248        .get(header..end)
249        .context("certificate DER truncated")?;
250    Ok((tag, content, &bytes[end..]))
251}
252
253fn sequence(bytes: &[u8]) -> Result<(&[u8], &[u8]), Error> {
254    let (tag, content, rest) = element(bytes)?;
255    ensure!(tag == 0x30, "certificate DER expected a SEQUENCE");
256    Ok((content, rest))
257}
258
259fn public_key(certificate: &[u8]) -> Result<PublicKey<'_>, Error> {
260    let (body, _trailer) = sequence(certificate)?;
261    let (tbs, _signature) = sequence(body)?;
262    let (tag, _content, after_first) = element(tbs)?;
263    let mut rest = if tag == 0xA0 { after_first } else { tbs };
264    for _field in 0..5 {
265        let (_tag, _content, after) = element(rest)?;
266        rest = after;
267    }
268    let (spki, _extensions) = sequence(rest)?;
269    let (algorithm, key_rest) = sequence(spki)?;
270    let (tag, oid, params) = element(algorithm)?;
271    ensure!(tag == 0x06, "certificate key algorithm is not an OID");
272    let (tag, bits, _after_key) = element(key_rest)?;
273    ensure!(
274        tag == 0x03 && bits.first() == Some(&0),
275        "certificate key is not a byte-aligned BIT STRING"
276    );
277    let key = &bits[1..];
278    match oid {
279        [0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01] => Ok(PublicKey::Rsa(key)),
280        [0x2B, 0x65, 0x70] => Ok(PublicKey::Ed25519(key)),
281        [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01] => {
282            let (tag, curve, _after_curve) = element(params)?;
283            ensure!(tag == 0x06, "certificate EC curve is not an OID");
284            match curve {
285                [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07] => Ok(PublicKey::P256(key)),
286                [0x2B, 0x81, 0x04, 0x00, 0x22] => Ok(PublicKey::P384(key)),
287                other => Err(Error::Unsupported(format!(
288                    "certificate EC curve {} unsupported",
289                    hex::encode(other)
290                ))),
291            }
292        }
293        other => Err(Error::Unsupported(format!(
294            "certificate key algorithm {} unsupported",
295            hex::encode(other)
296        ))),
297    }
298}