rc-crypto 0.1.4

Crypto library for the RC X509 platform
// Copyright 2026-Present Datadog, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::fmt::Display;

use smallvec::SmallVec;
use thiserror::Error;
use valuable::Valuable;
use x509_parser::prelude::{ParsedExtension, X509Certificate};

use crate::{
    cached_string_repr::CachedStringRepr, certificate::id::DangerousComparableId, hex::colon_string,
};

/// No Subject Key Identifier extension was found in the certificate.
#[derive(Debug, Error)]
#[error("no Subject Key Identifier found")]
pub struct ErrorNoSKI;

/// Certificate ID was an invalid length.
#[derive(Debug, Error)]
#[error("certificate ID is an invalid length")]
pub struct InvalidLength {
    actual_len: usize,
}

/// Error extracting a [`CertId`] from an [`X509Certificate`].
#[derive(Debug, Error)]
pub enum InvalidCertId {
    /// No Subject Key Identifier extension was found in the certificate.
    #[error(transparent)]
    NoSKI(#[from] ErrorNoSKI),

    /// Certificate ID was an invalid length.
    #[error(transparent)]
    InvalidLength(#[from] InvalidLength),
}

/// An opaque identifier for the [`Certificate`] this value was extracted from.
///
/// This is an untrusted value, and can be set to anything the cert issuer
/// wishes. Derived values such as a [`KeyId`] or certificate [`Fingerprint`])
/// SHOULD be preferred for general use. The
/// [`CertId::into_dangerous_comparable()`] method can be used to obtain a
/// handle that implements [`PartialEq`].
///
/// The [`CertId`] is a user friendly rename of the [Subject Key Identifier]
/// (commonly abbreviated SKI) within an X509 certificate. While the SKI claims
/// to be an identifier of the key in the cert, it does not always identify the
/// key material specifically (`hash(cert_dn + cert_serial)` is not uncommon).
///
/// [`KeyId`]: crate::keys::KeyId
/// [`Certificate`]: crate::certificate::Certificate
/// [`Fingerprint`]: crate::certificate::Fingerprint
/// [Subject Key Identifier]:
///     https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2
#[derive(Debug, Hash, Clone)] // NOTE: no PartialEq - not trusted, do not compare.
pub struct CertId {
    bytes: SmallVec<[u8; 20]>,

    /// A lazily-rendered string representation of `bytes`.
    ///
    /// See [`Self::as_hex_str()`] for initialisation.
    rendered: CachedStringRepr,
}

impl CertId {
    /// Minimum bytes CertId MUST be.
    const MIN_LENGTH: usize = 16;

    /// Maximum bytes CertId MUST be.
    const MAX_LENGTH: usize = 64;

    /// Render this value following the conventions of OpenSSL's colon-delimited
    /// string representation.
    pub fn as_hex_str(&self) -> &str {
        self.rendered.get_or_init(|| colon_string(&self.bytes))
    }

    /// Return the raw bytes for this ID (private to this module).
    pub(super) fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// Obtain a borrowed wrapper type that has a [`PartialEq`] implementation,
    /// allowing this value to be compared to other values with the correctness
    /// caveats documented for this type.
    pub fn as_dangerous_comparable(&self) -> DangerousComparableId<'_, Self> {
        DangerousComparableId::from(self)
    }
}

impl Display for CertId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_hex_str())
    }
}

impl From<CertId> for Vec<u8> {
    fn from(id: CertId) -> Vec<u8> {
        id.bytes.into_vec()
    }
}

impl TryFrom<&[u8]> for CertId {
    type Error = InvalidLength;

    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
        if value.len() < Self::MIN_LENGTH || value.len() > Self::MAX_LENGTH {
            return Err(InvalidLength {
                actual_len: value.len(),
            });
        }
        Ok(Self {
            bytes: SmallVec::from_slice(value),
            rendered: Default::default(),
        })
    }
}

impl TryFrom<Vec<u8>> for CertId {
    type Error = InvalidLength;

    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
        if value.len() < Self::MIN_LENGTH || value.len() > Self::MAX_LENGTH {
            return Err(InvalidLength {
                actual_len: value.len(),
            });
        }
        Ok(Self {
            bytes: SmallVec::from_vec(value),
            rendered: Default::default(),
        })
    }
}

impl<'a> TryFrom<&X509Certificate<'a>> for CertId {
    type Error = InvalidCertId;

    fn try_from(cert: &X509Certificate<'a>) -> Result<Self, Self::Error> {
        let bytes = cert
            .iter_extensions()
            .find_map(|v| match v.parsed_extension() {
                ParsedExtension::SubjectKeyIdentifier(ski) => Some(ski.0),
                _ => None,
            })
            .ok_or(ErrorNoSKI)?;
        Ok(CertId::try_from(bytes)?)
    }
}

impl Valuable for CertId {
    fn as_value(&self) -> valuable::Value<'_> {
        valuable::Value::String(self.as_hex_str())
    }

    fn visit(&self, visit: &mut dyn valuable::Visit) {
        visit.visit_value(self.as_value());
    }
}

#[cfg(test)]
mod tests {
    use proptest::prelude::*;
    use rc_x509_test_helpers::assert_valuable_repr;
    use static_assertions::assert_not_impl_any;
    use x509_parser::prelude::FromDer;

    use super::*;

    use crate::certificate::tests::cert_fixture;

    const FIXTURE_SKI_STR: &str = "dc:8d:b6:27:52:78:58:4c:fd:a2:43:db:cb:2b:e0:57:68:6e:2b:8e";

    // Why: a CertId can be set to anything by the issuer, making it unreliable
    // as a unique identifier, and should not be used to compare two
    // certificates for equality (outside of chain building which is then
    // cryptographically verified).
    assert_not_impl_any!(CertId: PartialEq, Eq);

    fn fixture_ski() -> CertId {
        let der = cert_fixture().as_der();
        let cert = X509Certificate::from_der(&der).expect("valid DER").1;

        CertId::try_from(&cert).expect("extract SKI")
    }

    #[test]
    fn test_fixture() {
        let aki = fixture_ski();

        assert_eq!(aki.as_hex_str(), FIXTURE_SKI_STR,);
    }

    #[test]
    fn test_valuable_repr() {
        let aki = fixture_ski();

        assert_valuable_repr(&aki, FIXTURE_SKI_STR);
    }

    #[test]
    fn test_danger_eq() {
        let ski = fixture_ski();

        assert_eq!(ski.as_dangerous_comparable(), ski);
    }

    proptest! {
        #[test]
        fn prop_length_bounds_enforced(
            ski in prop::collection::vec(any::<u8>(), 0..(CertId::MAX_LENGTH + 20)),
        ) {
            let in_bounds = (CertId::MIN_LENGTH..=CertId::MAX_LENGTH).contains(&ski.len());

            // Invariant: both TryFrom impls accept iff the length is within
            // [MIN_LENGTH, MAX_LENGTH], and reject otherwise.
            assert_eq!(CertId::try_from(ski.as_slice()).is_ok(), in_bounds);
            assert_eq!(CertId::try_from(ski).is_ok(), in_bounds);
        }
    }
}