Skip to main content

native_ossl/
obj.rs

1//! Object Identifiers — [`Oid`] wraps `ASN1_OBJECT` for arbitrary OID comparison.
2//!
3//! Unlike [`crate::x509::nid_from_text`], which only recognises OIDs registered in
4//! OpenSSL's built-in OBJ table, [`Oid::from_text`] creates an object for any dotted
5//! decimal OID string, including vendor- and application-specific OIDs that have no
6//! registered NID.
7
8use crate::error::ErrorStack;
9use native_ossl_sys as sys;
10use std::ffi::CString;
11use std::fmt;
12
13// ── Oid ───────────────────────────────────────────────────────────────────────
14
15/// An owned Object Identifier wrapping an OpenSSL `ASN1_OBJECT*`.
16///
17/// [`Oid::from_text`] accepts any dotted decimal OID string (`"1.3.6.1.5.2.3.5"`)
18/// or registered short name (`"sha256"`), returning an `Oid` even for OIDs that are
19/// not in OpenSSL's built-in NID table.  For registered OIDs, [`Oid::nid`] returns
20/// the corresponding NID; for unregistered OIDs it returns 0 (`NID_undef`).
21///
22/// Two `Oid` values compare equal when their ASN.1 encodings are identical,
23/// regardless of whether either has a registered NID.
24pub struct Oid {
25    ptr: *mut sys::ASN1_OBJECT,
26}
27
28// SAFETY: ASN1_OBJECT is a read-only value object after construction; no
29// thread-local state is mutated through a shared reference.
30unsafe impl Send for Oid {}
31unsafe impl Sync for Oid {}
32
33impl Clone for Oid {
34    fn clone(&self) -> Self {
35        // SAFETY: self.ptr is a valid, non-null ASN1_OBJECT*; OBJ_dup allocates
36        // a new copy on the heap and returns it (or NULL on OOM, which would
37        // cause a panic in the unwrap — acceptable since this matches Rust's
38        // global allocator contract).
39        let dup = unsafe { sys::OBJ_dup(self.ptr) };
40        assert!(!dup.is_null(), "OBJ_dup: allocation failed");
41        Oid { ptr: dup }
42    }
43}
44
45impl Drop for Oid {
46    fn drop(&mut self) {
47        // SAFETY: ptr is a valid, non-null, owned ASN1_OBJECT* allocated by
48        // OBJ_txt2obj(..., 1) (copy=1 → heap-allocated).
49        unsafe { sys::ASN1_OBJECT_free(self.ptr) };
50    }
51}
52
53impl Oid {
54    /// Wrap a raw, owned `ASN1_OBJECT*`.
55    ///
56    /// # Safety
57    ///
58    /// `ptr` must be a valid, non-null `ASN1_OBJECT*` allocated on the heap
59    /// (e.g. by `OBJ_dup` or `OBJ_txt2obj` with copy=1).  Ownership is
60    /// transferred to the returned `Oid`; the caller must not free `ptr`.
61    pub(crate) unsafe fn from_ptr(ptr: *mut sys::ASN1_OBJECT) -> Self {
62        Oid { ptr }
63    }
64
65    /// Construct from a dotted decimal OID string (`"1.3.6.1.5.2.3.5"`) or a
66    /// registered short name (`"sha256"`).
67    ///
68    /// Unlike [`crate::x509::nid_from_text`], this succeeds for OIDs that are
69    /// not registered in OpenSSL's built-in NID table.
70    ///
71    /// # Errors
72    ///
73    /// Returns an [`ErrorStack`] if `s` is not a valid OID string or name, or
74    /// if `s` contains an interior NUL byte.
75    pub fn from_text(s: &str) -> Result<Self, ErrorStack> {
76        let cs = CString::new(s).map_err(|_| ErrorStack::drain())?;
77        // SAFETY:
78        // - cs.as_ptr() is valid, NUL-terminated, for the duration of this call
79        // - copy=1 → OBJ_txt2obj allocates a new ASN1_OBJECT on the heap; we own it
80        // - no mutable global state is accessed (the OBJ table may be read under
81        //   a shared lock in threaded builds, which OpenSSL itself guarantees)
82        let ptr = unsafe { sys::OBJ_txt2obj(cs.as_ptr(), 0) };
83        if ptr.is_null() {
84            Err(ErrorStack::drain())
85        } else {
86            Ok(Oid { ptr })
87        }
88    }
89
90    /// Return the NID for this OID, or `0` (`NID_undef`) if the OID is not
91    /// registered in OpenSSL's built-in NID table.
92    #[must_use]
93    pub fn nid(&self) -> i32 {
94        // SAFETY: self.ptr is a valid, non-null ASN1_OBJECT* for the lifetime of self.
95        unsafe { sys::OBJ_obj2nid(self.ptr) }
96    }
97}
98
99impl PartialEq for Oid {
100    fn eq(&self, other: &Self) -> bool {
101        // SAFETY: both ptrs are valid, non-null, owned ASN1_OBJECT* values.
102        // OBJ_cmp compares the DER encoding; negative/zero/positive like memcmp.
103        unsafe { sys::OBJ_cmp(self.ptr, other.ptr) == 0 }
104    }
105}
106
107impl Eq for Oid {}
108
109impl fmt::Debug for Oid {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "Oid({self})")
112    }
113}
114
115impl fmt::Display for Oid {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        // First call with a zero-length buffer to learn the required length.
118        // SAFETY: buf=NULL, buf_len=0 is a documented way to query length in OpenSSL.
119        let len = unsafe { sys::OBJ_obj2txt(std::ptr::null_mut(), 0, self.ptr, 1) };
120        // len <= 0 means error or empty; bail out early.
121        let Ok(len_usize) = usize::try_from(len) else {
122            return f.write_str("<invalid-oid>");
123        };
124        let mut buf = vec![0u8; len_usize + 1];
125        // SAFETY: buf is allocated to len+1 bytes; OBJ_obj2txt writes at most buf_len
126        // bytes including the NUL terminator; no_name=1 forces dotted decimal output.
127        let written = unsafe { sys::OBJ_obj2txt(buf.as_mut_ptr().cast(), len + 1, self.ptr, 1) };
128        let Ok(written_usize) = usize::try_from(written) else {
129            return f.write_str("<invalid-oid>");
130        };
131        buf.truncate(written_usize);
132        f.write_str(std::str::from_utf8(&buf).unwrap_or("<invalid-oid>"))
133    }
134}
135
136// ── Tests ─────────────────────────────────────────────────────────────────────
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn oid_from_registered_name() {
144        // "sha256" is a registered short name; nid() must return a known NID
145        let oid = Oid::from_text("sha256").expect("sha256 must be recognised");
146        assert_ne!(oid.nid(), 0, "registered OID must have a NID");
147    }
148
149    #[test]
150    fn oid_from_dotted_decimal() {
151        // 2.5.4.3 is commonName — registered, but supplied as dotted decimal
152        let oid = Oid::from_text("2.5.4.3").expect("commonName OID must parse");
153        assert_ne!(oid.nid(), 0);
154    }
155
156    #[test]
157    fn oid_unregistered_dotted_decimal() {
158        // A made-up OID that is not in the OpenSSL NID table.
159        // Using a private-use arc (2.25.*) with a random UUID-based component
160        // that is guaranteed not to be registered.
161        let oid = Oid::from_text("2.25.999999999999999999999999")
162            .expect("arbitrary dotted OID must parse with OBJ_txt2obj");
163        // nid() returns 0 (NID_undef) for unregistered OIDs
164        assert_eq!(oid.nid(), 0);
165    }
166
167    #[test]
168    fn oid_equality_same_oid() {
169        let a = Oid::from_text("2.5.4.3").unwrap();
170        let b = Oid::from_text("2.5.4.3").unwrap();
171        assert_eq!(a, b);
172    }
173
174    #[test]
175    fn oid_equality_different_oids() {
176        let a = Oid::from_text("2.5.4.3").unwrap();
177        let b = Oid::from_text("2.5.4.6").unwrap();
178        assert_ne!(a, b);
179    }
180
181    #[test]
182    fn oid_display_dotted_decimal() {
183        let oid = Oid::from_text("2.5.4.3").unwrap();
184        let s = oid.to_string();
185        assert_eq!(s, "2.5.4.3");
186    }
187
188    #[test]
189    fn oid_display_unregistered() {
190        // Unregistered OID must round-trip through Display
191        let dotted = "1.3.6.1.5.2.3.5";
192        let oid = Oid::from_text(dotted).unwrap();
193        assert_eq!(oid.to_string(), dotted);
194    }
195
196    #[test]
197    fn oid_registered_name_display_is_dotted() {
198        // When no_name=1 is set, Display always produces dotted decimal, never a name.
199        let oid = Oid::from_text("sha256").unwrap();
200        let s = oid.to_string();
201        // Must be dotted decimal, not "sha256"
202        assert!(
203            s.contains('.'),
204            "Display must produce dotted decimal, got: {s}"
205        );
206        assert!(
207            !s.contains("sha"),
208            "Display must not contain short name: {s}"
209        );
210    }
211
212    #[test]
213    fn oid_interior_nul_returns_error() {
214        assert!(Oid::from_text("1.2\x003.4").is_err());
215    }
216}