From 97284ddb8f118aa2a1df85fdb0a4024055c9df48 Mon Sep 17 00:00:00 2001 From: choksi2212 Date: Mon, 26 Oct 2026 05:02:21 +0530 Subject: [PATCH] const-oid: accept the RFC 5280 Appendix B worst case in the default capacity DEFAULT_MAX_SIZE was 39 so the struct came out a round 40 bytes total, but RFC 5280 Appendix B requires implementations to accept OIDs with at most 20 arcs, every arc below 2^28, whose dotted-decimal string is at most 100 bytes. The 100-byte string is the binding limit: after the leading two arcs (one octet), each 4-octet arc costs at least 8 string bytes to spell, so a 100-byte string needs at most 1 + 12 * 4 = 49 contents octets - reached by twelve arcs of 9999999 below 1.2, which is exactly what the new worst-case test encodes. Raise the default to 49 so every Appendix B-conformant OID fits, fix the validity docs (the encoding may be exactly MAX_SIZE long, not shorter), and pin the reported 100-byte/40-octet OID, the 99-byte/49-octet ceiling, and a custom 39 capacity still rejecting what it cannot hold. der and x509-cert take the capacity from the default, so certificates carrying such OIDs as extension ids decode again (#2485). Note: ObjectIdentifier grows from 40 to 50 bytes total, so the release will need a minor version bump rather than a patch. Signed-off-by: choksi2212 --- const-oid/src/lib.rs | 18 ++++++++++++++---- const-oid/tests/oid.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/const-oid/src/lib.rs b/const-oid/src/lib.rs index 679389eec..373d92f89 100644 --- a/const-oid/src/lib.rs +++ b/const-oid/src/lib.rs @@ -42,10 +42,20 @@ pub use crate::{ use crate::encoder::Encoder; use core::{borrow::Borrow, fmt, ops::Deref, str::FromStr}; -/// Default maximum size. +/// Default maximum size: the largest BER/DER contents RFC 5280 Appendix B +/// requires implementations to accept. /// -/// Makes `ObjectIdentifier` 40-bytes total w\ 1-byte length. -const DEFAULT_MAX_SIZE: usize = 39; +/// Appendix B mandates support for OIDs with at most 20 arcs, every arc below +/// 2^28, whose dotted-decimal string is at most 100 bytes. The 100-byte string +/// is the binding limit: the leading `1.2.` packs the first two arcs into a +/// single octet (the second arc is 0..=39 under X.660), and every further +/// 4-octet arc costs at least 8 string bytes to spell (values in +/// 2^21..=2^28-1 start at 7 digits), so 96 remaining bytes buy at most +/// twelve of them: 1 + 12 * 4 = 49 contents octets, reached by twelve arcs +/// of 9999999 below 1.2 (a 99-byte string). +/// +/// Makes `ObjectIdentifier` 50-bytes total w\ 1-byte length. +const DEFAULT_MAX_SIZE: usize = 49; /// Object identifier (OID). /// @@ -60,7 +70,7 @@ const DEFAULT_MAX_SIZE: usize = 39; /// - The OID MUST have at least 3 arcs /// - The first arc MUST be within the range 0-2 /// - The second arc MUST be within the range 0-39 -/// - The BER/DER encoding of the OID MUST be shorter than +/// - The BER/DER encoding of the OID MUST be no longer than /// [`ObjectIdentifier::MAX_SIZE`] #[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct ObjectIdentifier { diff --git a/const-oid/tests/oid.rs b/const-oid/tests/oid.rs index 5e6005376..97754ddfc 100644 --- a/const-oid/tests/oid.rs +++ b/const-oid/tests/oid.rs @@ -316,3 +316,45 @@ fn starts_with() { assert!(!child.starts_with(oid("2.2.3"))); assert!(!child.starts_with(oid("1.2.3.4.5.6"))); } + +/// Worst case RFC 5280 Appendix B mandates support for: at most 20 arcs, +/// every arc below 2^28, dotted-decimal string at most 100 bytes. +/// Twelve 7-digit arcs below 1.2 spell a 99-byte string that encodes to +/// 49 contents octets, the ceiling for any Appendix B-conformant OID. +#[test] +fn rfc5280_appendix_b_worst_case_fits() { + let oid_str = "1.2.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999"; + assert_eq!(oid_str.len(), 99); + + let oid = ObjectIdentifier::new(oid_str).unwrap(); + assert_eq!(oid.as_bytes().len(), 49); + assert_eq!(oid.to_string(), oid_str); + + let round_trip = ObjectIdentifier::from_bytes(oid.as_bytes()).unwrap(); + assert_eq!(round_trip, oid); +} + +/// 100-byte dotted string from #2485 encoding to 40 contents octets. +#[test] +fn rfc5280_appendix_b_100_byte_40_octet_oid_fits() { + let oid_str = "1.3.268435455.268435455.268435455.268435455.268435455.268435455.268435455.268435455.268435455.123456"; + assert_eq!(oid_str.len(), 100); + + let oid = ObjectIdentifier::new(oid_str).unwrap(); + assert_eq!(oid.as_bytes().len(), 40); + assert_eq!(oid.to_string(), oid_str); + + let round_trip = ObjectIdentifier::from_bytes(oid.as_bytes()).unwrap(); + assert_eq!(round_trip, oid); +} + +/// The default capacity is what grows with this change: an explicitly +/// smaller capacity still rejects OIDs that do not fit it. +#[test] +fn explicit_smaller_capacity_still_rejects() { + let oid_str = "1.2.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999.9999999"; + let oid = ObjectIdentifier::new(oid_str).unwrap(); + let ber = oid.as_bytes(); + assert_eq!(ber.len(), 49); + assert!(ObjectIdentifier::<39>::from_bytes_sized(ber).is_err()); +}