foctet_net/tls/
cert.rs

1//! X.509 certificate handling for foctet
2//!
3//! This module handles generation, signing, and verification of certificates.
4
5use foctet_core::id::NodeId;
6use foctet_core::key;
7use std::sync::Arc;
8use x509_parser::{prelude::*, signature_algorithm::SignatureAlgorithm};
9/// The foctet Public Key Extension is a X.509 extension
10/// with the Object Identifier 1.3.6.1.4.1.32473.1.1
11const FOCTET_EXT_OID: [u64; 9] = [1, 3, 6, 1, 4, 1, 32473, 1, 1];
12
13/// The node signs the concatenation of the string `foctet-tls-handshake:`
14/// and the public key that it used to generate the certificate carrying
15/// the foctet Public Key Extension, using its private host key.
16/// This signature provides cryptographic proof that the node was
17/// in possession of the private host key at the time the certificate was signed.
18const FOCTET_SIGNING_PREFIX: [u8; 21] = *b"foctet-tls-handshake:";
19
20// Certificates MUST use the NamedCurve encoding for elliptic curve parameters.
21// Similarly, hash functions with an output length less than 256 bits MUST NOT be used.
22static FOCTET_SIGNATURE_ALGORITHM: &rcgen::SignatureAlgorithm = &rcgen::PKCS_ECDSA_P256_SHA256;
23
24#[derive(Debug)]
25pub(crate) struct AlwaysResolvesCert(Arc<rustls::sign::CertifiedKey>);
26
27impl AlwaysResolvesCert {
28    pub(crate) fn new(
29        cert: rustls::pki_types::CertificateDer<'static>,
30        key: &rustls::pki_types::PrivateKeyDer<'_>,
31    ) -> Result<Self, rustls::Error> {
32        let certified_key = rustls::sign::CertifiedKey::new(
33            vec![cert],
34            rustls::crypto::ring::sign::any_ecdsa_type(key)?,
35        );
36        Ok(Self(Arc::new(certified_key)))
37    }
38}
39
40impl rustls::client::ResolvesClientCert for AlwaysResolvesCert {
41    fn resolve(
42        &self,
43        _root_hint_subjects: &[&[u8]],
44        _sigschemes: &[rustls::SignatureScheme],
45    ) -> Option<Arc<rustls::sign::CertifiedKey>> {
46        Some(Arc::clone(&self.0))
47    }
48
49    fn has_certs(&self) -> bool {
50        true
51    }
52}
53
54impl rustls::server::ResolvesServerCert for AlwaysResolvesCert {
55    fn resolve(
56        &self,
57        _client_hello: rustls::server::ClientHello<'_>,
58    ) -> Option<Arc<rustls::sign::CertifiedKey>> {
59        Some(Arc::clone(&self.0))
60    }
61}
62
63/// Generates a self-signed TLS certificate that includes a foctet-specific
64/// certificate extension containing the public key of the given keypair.
65pub fn generate(
66    identity_keypair: &key::Keypair,
67) -> Result<
68    (
69        rustls::pki_types::CertificateDer<'static>,
70        rustls::pki_types::PrivateKeyDer<'static>,
71    ),
72    GenError,
73> {
74    let node_id = identity_keypair.public();
75    let node_id_base32 = node_id.to_base32();
76    let node_id_uri = format!("foctet://{}", node_id_base32);
77    let certificate_keypair = rcgen::KeyPair::generate_for(FOCTET_SIGNATURE_ALGORITHM)?;
78    let rustls_key = rustls::pki_types::PrivateKeyDer::from(
79        rustls::pki_types::PrivatePkcs8KeyDer::from(certificate_keypair.serialize_der()),
80    );
81    let mut params = rcgen::CertificateParams::new(vec![])?;
82    params
83        .subject_alt_names
84        .push(rcgen::SanType::URI(rcgen::Ia5String::try_from(
85            node_id_uri,
86        )?));
87    params.distinguished_name = rcgen::DistinguishedName::new();
88
89    // Signature start
90    //
91    let mut signing_msg = Vec::new();
92    signing_msg.extend(FOCTET_SIGNING_PREFIX);
93    signing_msg.extend(&certificate_keypair.public_key_der());
94
95    let signature = identity_keypair.sign(&signing_msg);
96
97    // ASN.1 encoding
98    let signed_extension = yasna::construct_der(|writer| {
99        writer.write_sequence(|writer| {
100            writer.next().write_bytes(&node_id.to_bytes());
101            writer.next().write_bytes(&signature);
102        })
103    });
104
105    // Add signed extension to certificate
106    params
107        .custom_extensions
108        .push(rcgen::CustomExtension::from_oid_content(
109            &FOCTET_EXT_OID,
110            signed_extension,
111        ));
112
113    let certificate = params.self_signed(&certificate_keypair)?;
114
115    let rustls_certificate = rustls::pki_types::CertificateDer::from(certificate);
116
117    Ok((rustls_certificate, rustls_key))
118}
119
120/// Attempts to parse the provided bytes as a [`FoctetCertificate`].
121///
122/// For this to succeed, the certificate must contain the specified extension and the signature must
123/// match the embedded public key.
124pub fn parse<'a>(
125    cert: &'a rustls::pki_types::CertificateDer<'a>,
126) -> Result<FoctetCertificate<'a>, ParseError> {
127    let cert = parse_unverified(cert.as_ref())?;
128    cert.verify()?;
129    Ok(cert)
130}
131
132/// An X.509 certificate with a foctet-specific extension
133/// is used to secure foctet connections.
134#[derive(Debug)]
135pub struct FoctetCertificate<'a> {
136    certificate: X509Certificate<'a>,
137    extension: FoctetExtension,
138}
139
140/// The contents of the specific foctet extension, containing the public host key
141/// and a signature performed using the private host key.
142#[derive(Debug)]
143pub struct FoctetExtension {
144    public_key: key::PublicKey,
145    /// This signature provides cryptographic proof that the node was
146    /// in possession of the private host key at the time the certificate was signed.
147    signature: Vec<u8>,
148}
149
150#[derive(Debug, thiserror::Error)]
151#[error(transparent)]
152pub struct GenError(#[from] rcgen::Error);
153
154#[derive(Debug, thiserror::Error)]
155#[error(transparent)]
156pub struct ParseError(#[from] pub(crate) webpki::Error);
157
158#[derive(Debug, thiserror::Error)]
159#[error(transparent)]
160pub struct VerificationError(#[from] pub(crate) webpki::Error);
161
162/// Internal function that only parses but does not verify the certificate.
163///
164/// Useful for testing but unsuitable for production.
165fn parse_unverified(der_input: &[u8]) -> Result<FoctetCertificate, webpki::Error> {
166    let x509_cert = x509_parser::parse_x509_certificate(der_input)
167        .map_err(|_| webpki::Error::BadDer)?
168        .1;
169
170    let san_node_id = match x509_cert.subject_alternative_name() {
171        Ok(san_opt) => {
172            let san = san_opt.ok_or(webpki::Error::BadDer)?;
173            let uri = san
174                .value
175                .general_names
176                .iter()
177                .find_map(|gn| {
178                    if let GeneralName::URI(uri) = gn {
179                        Some(uri)
180                    } else {
181                        None
182                    }
183                })
184                .ok_or(webpki::Error::UnknownIssuer)?;
185            let node_id = NodeId::from_base32(&uri["foctet://".len()..])
186                .map_err(|_| webpki::Error::UnknownIssuer)?;
187            node_id
188        }
189        Err(e) => {
190            tracing::error!("Failed to parse SAN: {:?}", e);
191            return Err(webpki::Error::UnknownIssuer);
192        }
193    };
194
195    let custom_oid = der_parser::oid::Oid::from(&FOCTET_EXT_OID)
196        .expect("This is a valid OID of foctet extension");
197
198    let mut extracted_node_id = None;
199    let mut extracted_signature = None;
200
201    for ext in x509_cert.extensions() {
202        if ext.oid == custom_oid {
203            let (node_id_bytes, signature_bytes): (Vec<u8>, Vec<u8>) =
204                yasna::decode_der(&ext.value).map_err(|_| webpki::Error::ExtensionValueInvalid)?;
205            extracted_node_id = Some(node_id_bytes);
206            extracted_signature = Some(signature_bytes);
207        }
208    }
209    let node_id_bytes = extracted_node_id.ok_or(webpki::Error::UnknownIssuer)?;
210    let signature = extracted_signature.ok_or(webpki::Error::UnknownIssuer)?;
211
212    let ext_node_id =
213        key::PublicKey::try_from_bytes(&node_id_bytes).map_err(|_| webpki::Error::UnknownIssuer)?;
214    if san_node_id != ext_node_id {
215        return Err(webpki::Error::UnknownIssuer);
216    }
217    let ext = FoctetExtension {
218        public_key: ext_node_id,
219        signature,
220    };
221
222    Ok(FoctetCertificate {
223        certificate: x509_cert,
224        extension: ext,
225    })
226}
227
228impl FoctetCertificate<'_> {
229    /// The [`NodeId`] of the remote node.
230    pub fn node_id(&self) -> NodeId {
231        self.extension.public_key
232    }
233
234    /// Verify the `signature` of the `message` signed by the private key corresponding to the
235    /// public key stored in the certificate.
236    pub fn verify_signature(
237        &self,
238        signature_scheme: rustls::SignatureScheme,
239        message: &[u8],
240        signature: &[u8],
241    ) -> Result<(), VerificationError> {
242        let pk = self.public_key(signature_scheme)?;
243        pk.verify(message, signature)
244            .map_err(|_| webpki::Error::InvalidSignatureForPublicKey)?;
245
246        Ok(())
247    }
248    /// Get a [`ring::signature::UnparsedPublicKey`] for this `signature_scheme`.
249    /// Return `Error` if the `signature_scheme` does not match the public key signature
250    /// and hashing algorithm or if the `signature_scheme` is not supported.
251    fn public_key(
252        &self,
253        signature_scheme: rustls::SignatureScheme,
254    ) -> Result<ring::signature::UnparsedPublicKey<&[u8]>, webpki::Error> {
255        use ring::signature;
256        use rustls::SignatureScheme::*;
257
258        let current_signature_scheme = self.signature_scheme()?;
259        if signature_scheme != current_signature_scheme {
260            // This certificate was signed with a different signature scheme
261            return Err(webpki::Error::UnsupportedSignatureAlgorithmForPublicKey);
262        }
263
264        let verification_algorithm: &dyn signature::VerificationAlgorithm = match signature_scheme {
265            RSA_PKCS1_SHA256 => &signature::RSA_PKCS1_2048_8192_SHA256,
266            RSA_PKCS1_SHA384 => &signature::RSA_PKCS1_2048_8192_SHA384,
267            RSA_PKCS1_SHA512 => &signature::RSA_PKCS1_2048_8192_SHA512,
268            ECDSA_NISTP256_SHA256 => &signature::ECDSA_P256_SHA256_ASN1,
269            ECDSA_NISTP384_SHA384 => &signature::ECDSA_P384_SHA384_ASN1,
270            ECDSA_NISTP521_SHA512 => {
271                // See https://github.com/briansmith/ring/issues/824
272                return Err(webpki::Error::UnsupportedSignatureAlgorithm);
273            }
274            RSA_PSS_SHA256 => &signature::RSA_PSS_2048_8192_SHA256,
275            RSA_PSS_SHA384 => &signature::RSA_PSS_2048_8192_SHA384,
276            RSA_PSS_SHA512 => &signature::RSA_PSS_2048_8192_SHA512,
277            ED25519 => &signature::ED25519,
278            ED448 => {
279                // See https://github.com/briansmith/ring/issues/463
280                return Err(webpki::Error::UnsupportedSignatureAlgorithm);
281            }
282            // Similarly, hash functions with an output length less than 256 bits
283            // MUST NOT be used, due to the possibility of collision attacks.
284            // In particular, MD5 and SHA1 MUST NOT be used.
285            RSA_PKCS1_SHA1 => return Err(webpki::Error::UnsupportedSignatureAlgorithm),
286            ECDSA_SHA1_Legacy => return Err(webpki::Error::UnsupportedSignatureAlgorithm),
287            _ => return Err(webpki::Error::UnsupportedSignatureAlgorithm),
288        };
289        let spki = &self.certificate.tbs_certificate.subject_pki;
290        let key = signature::UnparsedPublicKey::new(
291            verification_algorithm,
292            spki.subject_public_key.as_ref(),
293        );
294
295        Ok(key)
296    }
297
298    /// This method validates the certificate according to foctet TLS 1.3 specs.
299    /// The certificate MUST:
300    /// 1. be valid at the time it is received by the node;
301    /// 2. use the NamedCurve encoding;
302    /// 3. use hash functions with an output length not less than 256 bits;
303    /// 4. be self signed;
304    /// 5. contain a valid signature in the specific foctet extension.
305    fn verify(&self) -> Result<(), webpki::Error> {
306        use webpki::Error;
307        // The certificate MUST have NotBefore and NotAfter fields set
308        // such that the certificate is valid at the time it is received by the node.
309        if !self.certificate.validity().is_valid() {
310            return Err(Error::InvalidCertValidity);
311        }
312
313        // Certificates MUST use the NamedCurve encoding for elliptic curve parameters.
314        // Similarly, hash functions with an output length less than 256 bits
315        // MUST NOT be used, due to the possibility of collision attacks.
316        // In particular, MD5 and SHA1 MUST NOT be used.
317        // Endpoints MUST abort the connection attempt if it is not used.
318        let signature_scheme = self.signature_scheme()?;
319        // Endpoints MUST abort the connection attempt if the certificate’s
320        // self-signature is not valid.
321        let raw_certificate = self.certificate.tbs_certificate.as_ref();
322        let signature = self.certificate.signature_value.as_ref();
323        // check if self signed
324        self.verify_signature(signature_scheme, raw_certificate, signature)
325            .map_err(|_| Error::SignatureAlgorithmMismatch)?;
326
327        // The certificate MUST contain a valid signature in the specific foctet extension.
328        let subject_pki = self.certificate.public_key().raw;
329        let mut signing_msg = Vec::new();
330        signing_msg.extend(FOCTET_SIGNING_PREFIX);
331        signing_msg.extend(subject_pki);
332        if !self
333            .extension
334            .public_key
335            .verify(&signing_msg, &self.extension.signature)
336        {
337            return Err(Error::InvalidSignatureForPublicKey);
338        }
339        Ok(())
340    }
341
342    /// Return the signature scheme corresponding to [`AlgorithmIdentifier`]s
343    /// of `subject_pki` and `signature_algorithm`
344    /// according to <https://www.rfc-editor.org/rfc/rfc8446.html#section-4.2.3>.
345    fn signature_scheme(&self) -> Result<rustls::SignatureScheme, webpki::Error> {
346        // Certificates MUST use the NamedCurve encoding for elliptic curve parameters.
347        // Endpoints MUST abort the connection attempt if it is not used.
348        use oid_registry::*;
349        use rustls::SignatureScheme::*;
350
351        let signature_algorithm = &self.certificate.signature_algorithm;
352        let pki_algorithm = &self.certificate.tbs_certificate.subject_pki.algorithm;
353
354        if pki_algorithm.algorithm == OID_PKCS1_RSAENCRYPTION {
355            if signature_algorithm.algorithm == OID_PKCS1_SHA256WITHRSA {
356                return Ok(RSA_PKCS1_SHA256);
357            }
358            if signature_algorithm.algorithm == OID_PKCS1_SHA384WITHRSA {
359                return Ok(RSA_PKCS1_SHA384);
360            }
361            if signature_algorithm.algorithm == OID_PKCS1_SHA512WITHRSA {
362                return Ok(RSA_PKCS1_SHA512);
363            }
364            if signature_algorithm.algorithm == OID_PKCS1_RSASSAPSS {
365                // According to https://datatracker.ietf.org/doc/html/rfc4055#section-3.1:
366                // Inside of params there should be a sequence of:
367                // - Hash Algorithm
368                // - Mask Algorithm
369                // - Salt Length
370                // - Trailer Field
371
372                // We are interested in Hash Algorithm only
373
374                if let Ok(SignatureAlgorithm::RSASSA_PSS(params)) =
375                    SignatureAlgorithm::try_from(signature_algorithm)
376                {
377                    let hash_oid = params.hash_algorithm_oid();
378                    if hash_oid == &OID_NIST_HASH_SHA256 {
379                        return Ok(RSA_PSS_SHA256);
380                    }
381                    if hash_oid == &OID_NIST_HASH_SHA384 {
382                        return Ok(RSA_PSS_SHA384);
383                    }
384                    if hash_oid == &OID_NIST_HASH_SHA512 {
385                        return Ok(RSA_PSS_SHA512);
386                    }
387                }
388
389                // Default hash algo is SHA-1, however:
390                // In particular, MD5 and SHA1 MUST NOT be used.
391                return Err(webpki::Error::UnsupportedSignatureAlgorithm);
392            }
393        }
394
395        if pki_algorithm.algorithm == OID_KEY_TYPE_EC_PUBLIC_KEY {
396            let signature_param = pki_algorithm
397                .parameters
398                .as_ref()
399                .ok_or(webpki::Error::BadDer)?
400                .as_oid()
401                .map_err(|_| webpki::Error::BadDer)?;
402            if signature_param == OID_EC_P256
403                && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA256
404            {
405                return Ok(ECDSA_NISTP256_SHA256);
406            }
407            if signature_param == OID_NIST_EC_P384
408                && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA384
409            {
410                return Ok(ECDSA_NISTP384_SHA384);
411            }
412            if signature_param == OID_NIST_EC_P521
413                && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA512
414            {
415                return Ok(ECDSA_NISTP521_SHA512);
416            }
417            return Err(webpki::Error::UnsupportedSignatureAlgorithm);
418        }
419
420        if signature_algorithm.algorithm == OID_SIG_ED25519 {
421            return Ok(ED25519);
422        }
423        if signature_algorithm.algorithm == OID_SIG_ED448 {
424            return Ok(ED448);
425        }
426
427        Err(webpki::Error::UnsupportedSignatureAlgorithm)
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn generate_parse_verify() {
437        let keypair = key::Keypair::generate();
438
439        let (cert, _) = generate(&keypair).unwrap();
440        let parsed_cert = parse(&cert).unwrap();
441
442        assert!(parsed_cert.verify().is_ok());
443        assert_eq!(keypair.public(), parsed_cert.node_id());
444    }
445}