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}