Skip to content

x509-cert/const-oid: a 100-character OID (RFC 5280 Appendix B) needs 40 octets, so a certificate carrying one as a non-critical extension id does not decode #2485

Open

Description

@Executioner1939

Hi,

RFC 5280 Appendix B requires support for OIDs whose dotted-decimal form is up to 100 characters, with up to 20 arcs, each below 2^28. ObjectIdentifier's default capacity is 39 octets. A 100-character OID that stays within those limits can need 40, and then it doesn't fit. When that OID is the id of a non-critical extension, Certificate::from_der refuses the whole certificate, although RFC 5280 §4.2 says a non-critical extension a relying party doesn't recognise MAY be ignored.

The OID used is 1.3 followed by nine arcs of 268435455 (2^28 − 1, the largest Appendix B mandates) and then 123456: 12 arcs, 100 characters, 40 contents octets (1 + 9 × 4 + 3). The control swaps the last arc for 16383 (99 characters, 39 octets) and decodes fine. The three test certificates differ only in that extension id, and both controls re-encode to the exact octets they were decoded from, so the DER is canonical and the 40th octet is what fails.

What the reproduction shows

const-oid 0.10.2 + x509-cert 0.3.0 const-oid 0.9.6 + x509-cert 0.2.5
OID, 100 chars / 40 octets new and from_bytes: Err(Length) from_bytes: Err(NotEnoughArcs)
OID, 99 chars / 39 octets (control) Ok Ok
certificate, extension id 1.3.6.1.4.1.99999.1 (control) Ok, re-encodes identically Ok, re-encodes identically
certificate, extension id = the 99-char OID (control) Ok, re-encodes identically Ok, re-encodes identically
certificate, extension id = the 100-char OID Err: incorrect length for OBJECT IDENTIFIER at DER byte 214 Err: incorrect length for OBJECT IDENTIFIER at DER byte 212

Where it comes from

  • const-oid 0.10.2: DEFAULT_MAX_SIZE is 39 (src/lib.rs:48, src/lib.rs:66). The string parser refuses an arc that would end past it (src/encoder.rs:96-102), and from_bytes does the same after checking the arcs (src/lib.rs:121-132 into src/lib.rs:234-254, Error::Length at :240-241).
  • der 0.8.2 decodes an OBJECT IDENTIFIER into a buffer of that capacity and fails when the contents are longer (src/asn1/oid.rs:12-28).
  • x509-cert 0.3.0 and 0.2.5 type Extension::extn_id as the default ObjectIdentifier (src/ext.rs:29-36), and the extensions are a field of the TBS certificate (src/certificate.rs:163-164 in 0.3.0), so one extension id that doesn't fit fails the decode, critical or not.
  • On 0.9.6, from_bytes reports an input that is too long as Error::NotEnoughArcs (src/lib.rs:143-147), which names the wrong problem.
flowchart TD
  cert["Certificate DER, extension id = 1.3 + 9 x 268435455 + 123456<br/>100 characters, 40 octets (RFC 5280 App. B: must be supported)"]
  cert --> a["x509-cert 0.3.0 Certificate::from_der"]
  cert --> b["x509-cert 0.2.5 Certificate::from_der"]
  a --> ext3["Extension.extn_id: ObjectIdentifier<br/>ext.rs:29-36"]
  b --> ext2["Extension.extn_id: ObjectIdentifier<br/>ext.rs:29-36"]
  ext3 --> d8["der 0.8.2 decode_value: buf [u8; 39]<br/>oid.rs:12-28 -> length error"]
  ext2 --> d7["der 0.7.10 decode_value: buf [u8; 39]<br/>oid.rs:12-23 -> length error"]
  d8 --> cap10["const-oid 0.10.2 DEFAULT_MAX_SIZE = 39<br/>lib.rs:48, 66"]
  d7 --> cap9["const-oid 0.9.6 MAX_SIZE = 39<br/>lib.rs:92"]
  cap10 --> fail["Err: the whole certificate is unreadable<br/>(RFC 5280 4.2: a non-critical extension MAY be ignored)"]
  cap9 --> fail
  ctl["control: the same certificate with 39 octets<br/>Ok, re-encodes identically"]
  classDef bad fill:#fde,stroke:#b35
  classDef good fill:#def,stroke:#37b
  class fail bad
  class ctl good
Loading

This is related to #1984 but isn't the same thing: that asked for arcs beyond 2^32, which Appendix B doesn't require. This OID is inside Appendix B's limits and is one octet over the default capacity.

What would help, if you agree it's worth fixing: a default capacity that holds every OID Appendix B mandates, or an x509-cert decode path that accepts a larger MAX_SIZE, which const-oid's const parameter already makes possible. I'm happy to send a PR for whichever you prefer.

Reproduction script (self-contained; needs bash, rustup and network access for cargo to fetch the pinned crates)

Save it as repro.sh and run bash repro.sh. It writes a small Cargo project (locked by its own Cargo.lock) into a new temporary directory, builds it with Rust 1.98.0, and compares each observation with the table above. It exits 0 when everything matches. The script is yours to use under Apache-2.0 OR MIT.

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Protoworks Group (Pty) Ltd
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
# Self-contained reproduction (UP-CONSTOID-001). It writes the files below into a new
# temporary directory and runs repro.sh there; nothing outside that
# directory is written. Run it with: bash this-file.sh
set -euo pipefail
dir=$(mktemp -d "${TMPDIR:-/tmp}/repro.XXXXXX")
echo "files written to $dir"
cat >"$dir/rust-toolchain.toml" <<'__REPRO_FILE_1__'
[toolchain]
channel = "1.98.0"
__REPRO_FILE_1__
mkdir -p "$dir/harness"
cat >"$dir/harness/Cargo.lock" <<'__REPRO_FILE_2__'
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"

[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"

[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"

[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
 "const-oid 0.9.6",
 "der_derive 0.7.3",
 "flagset",
 "zeroize",
]

[[package]]
name = "der"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a"
dependencies = [
 "const-oid 0.10.2",
 "der_derive 0.8.0",
 "flagset",
 "zeroize",
]

[[package]]
name = "der_derive"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "der_derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "flagset"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"

[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
 "base64ct",
 "der 0.7.10",
]

[[package]]
name = "spki"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9"
dependencies = [
 "base64ct",
 "der 0.8.2",
]

[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"

[[package]]
name = "up-constoid-001"
version = "0.0.0"
dependencies = [
 "const-oid 0.10.2",
 "const-oid 0.9.6",
 "x509-cert 0.2.5",
 "x509-cert 0.3.0",
]

[[package]]
name = "x509-cert"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
dependencies = [
 "const-oid 0.9.6",
 "der 0.7.10",
 "spki 0.7.3",
]

[[package]]
name = "x509-cert"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b"
dependencies = [
 "const-oid 0.10.2",
 "der 0.8.2",
 "spki 0.8.1",
]

[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
__REPRO_FILE_2__
mkdir -p "$dir/harness"
cat >"$dir/harness/Cargo.toml" <<'__REPRO_FILE_3__'
# SPDX-FileCopyrightText: Protoworks Group (Pty) Ltd
# SPDX-License-Identifier: Apache-2.0 OR MIT
#

# UP-CONSTOID-001's harness: const-oid 0.10.2 under x509-cert 0.3.0 (the
# newest of each), and const-oid 0.9.6 under x509-cert 0.2.5 (the 0.2
# line).
[package]
name = "up-constoid-001"
version = "0.0.0"
edition = "2021"
publish = false

[dependencies]
const-oid = "=0.10.2"
x509-cert = { version = "=0.3.0", default-features = false }
const-oid-09 = { package = "const-oid", version = "=0.9.6" }
x509-cert-02 = { package = "x509-cert", version = "=0.2.5", default-features = false }

[workspace]
__REPRO_FILE_3__
mkdir -p "$dir/harness/src"
cat >"$dir/harness/src/main.rs" <<'__REPRO_FILE_4__'
// SPDX-FileCopyrightText: Protoworks Group (Pty) Ltd
// SPDX-License-Identifier: Apache-2.0 OR MIT
//

//! UP-CONSTOID-001: an OID whose dotted-decimal form is 100 characters, as
//! the id of a non-critical extension, through const-oid 0.10.2 / x509-cert
//! 0.3.0 and const-oid 0.9.6 / x509-cert 0.2.5. RFC 5280 Appendix B:
//! "Implementations MUST also support OIDs where the length of the dotted
//! decimal ... string representation can be up to 100 bytes (inclusive)."
//! The certificate is assembled here octet by octet, so nothing of either
//! crate builds it; the control decodes and re-encodes to the same octets.
//! Prints `OBS key=value`.

use x509_cert::der::{Decode as _, Encode as _};
use x509_cert_02::der::{Decode as _, Encode as _};

/// The dotted form: `1.3`, nine `.268435455` (2^28 - 1, the largest arc RFC
/// 5280 Appendix B mandates) and a last arc.
fn dotted(last: u32) -> String {
    let mut s = String::from("1.3");
    for _ in 0..9 {
        s.push_str(".268435455");
    }
    s.push_str(&format!(".{last}"));
    s
}

/// X.690 §8.19 contents octets, written here rather than by either crate.
fn oid_contents(dotted: &str) -> Vec<u8> {
    let arcs: Vec<u64> = dotted.split('.').map(|a| a.parse().expect("digits")).collect();
    let mut out = vec![(arcs[0] * 40 + arcs[1]) as u8];
    for &arc in &arcs[2..] {
        let mut groups = vec![(arc & 0x7f) as u8];
        let mut rest = arc >> 7;
        while rest > 0 {
            groups.push(0x80 | (rest & 0x7f) as u8);
            rest >>= 7;
        }
        groups.reverse();
        out.extend(groups);
    }
    out
}

fn tlv(tag: u8, value: &[u8]) -> Vec<u8> {
    let mut out = vec![tag];
    let n = value.len();
    if n < 0x80 {
        out.push(n as u8);
    } else if n <= 0xff {
        out.extend([0x81, n as u8]);
    } else {
        out.extend([0x82, (n >> 8) as u8, n as u8]);
    }
    out.extend_from_slice(value);
    out
}

fn seq(parts: &[Vec<u8>]) -> Vec<u8> {
    tlv(0x30, &parts.concat())
}

fn oid(dotted: &str) -> Vec<u8> {
    tlv(0x06, &oid_contents(dotted))
}

/// A v3 certificate with one non-critical extension whose id is `ext_id`.
fn certificate(ext_id: &str) -> Vec<u8> {
    let ecdsa_sha256 = seq(&[oid("1.2.840.10045.4.3.2")]);
    let name = seq(&[tlv(0x31, &seq(&[oid("2.5.4.3"), tlv(0x0c, b"up-constoid-001")]))]);
    let validity = seq(&[tlv(0x17, b"250101000000Z"), tlv(0x17, b"350101000000Z")]);
    let mut point = vec![0x00, 0x04];
    point.extend([0x11; 64]);
    let spki = seq(&[
        seq(&[oid("1.2.840.10045.2.1"), oid("1.2.840.10045.3.1.7")]),
        tlv(0x03, &point),
    ]);
    let extension = seq(&[oid(ext_id), tlv(0x04, &[0x05, 0x00])]);
    let tbs = seq(&[
        tlv(0xa0, &tlv(0x02, &[0x02])),
        tlv(0x02, &[0x01]),
        ecdsa_sha256.clone(),
        name.clone(),
        validity,
        name,
        spki,
        tlv(0xa3, &seq(&[extension])),
    ]);
    seq(&[tbs, ecdsa_sha256, tlv(0x03, &[0x00, 0x01, 0x02, 0x03])])
}

fn obs(key: &str, value: impl std::fmt::Display) {
    println!("OBS {key}={value}");
}

fn verdict<T, E: std::fmt::Debug>(r: &Result<T, E>) -> String {
    match r {
        Ok(_) => "Ok".to_owned(),
        Err(e) => format!("Err({e:?})"),
    }
}

/// Decode with both x509-cert lines; on Ok, say whether re-encoding gives
/// back the same octets (it proves the assembled DER is the canonical form).
fn decode_both(label: &str, der: &[u8]) {
    match x509_cert::Certificate::from_der(der) {
        Ok(cert) => {
            obs(&format!("{label}.x509-cert-0.3.0"), "Ok");
            obs(
                &format!("{label}.x509-cert-0.3.0.reencodes-identically"),
                cert.to_der().map(|d| d == der).unwrap_or(false),
            );
        }
        Err(e) => {
            obs(&format!("{label}.x509-cert-0.3.0"), "Err");
            obs(&format!("info.{label}.x509-cert-0.3.0.error"), e);
        }
    }
    match x509_cert_02::Certificate::from_der(der) {
        Ok(cert) => {
            obs(&format!("{label}.x509-cert-0.2.5"), "Ok");
            obs(
                &format!("{label}.x509-cert-0.2.5.reencodes-identically"),
                cert.to_der().map(|d| d == der).unwrap_or(false),
            );
        }
        Err(e) => {
            obs(&format!("{label}.x509-cert-0.2.5"), "Err");
            obs(&format!("info.{label}.x509-cert-0.2.5.error"), e);
        }
    }
}

fn main() {
    // 100 characters, 40 BER octets: the claim.
    let long = dotted(123456);
    // 99 characters, 39 BER octets: the largest the crates hold.
    let fits = dotted(16383);

    obs("oid100.chars", long.len());
    obs("oid100.ber-octets", oid_contents(&long).len());
    obs("oid99.chars", fits.len());
    obs("oid99.ber-octets", oid_contents(&fits).len());

    obs("const-oid-0.10.2.new(oid100)", verdict(&const_oid::ObjectIdentifier::new(&long)));
    obs(
        "const-oid-0.10.2.from_bytes(oid100)",
        verdict(&const_oid::ObjectIdentifier::from_bytes(&oid_contents(&long))),
    );
    obs("const-oid-0.10.2.new(oid99)", verdict(&const_oid::ObjectIdentifier::new(&fits)));
    obs(
        "const-oid-0.9.6.from_bytes(oid100)",
        verdict(&const_oid_09::ObjectIdentifier::from_bytes(&oid_contents(&long))),
    );
    obs(
        "const-oid-0.9.6.from_bytes(oid99)",
        verdict(&const_oid_09::ObjectIdentifier::from_bytes(&oid_contents(&fits))),
    );
    obs("info.const-oid-0.9.6.new(oid100)", verdict(&const_oid_09::ObjectIdentifier::new(&long)));
    obs("info.const-oid-0.9.6.new(oid99)", verdict(&const_oid_09::ObjectIdentifier::new(&fits)));

    decode_both("control", &certificate("1.3.6.1.4.1.99999.1"));
    decode_both("cert-oid99", &certificate(&fits));
    decode_both("cert-oid100", &certificate(&long));
}
__REPRO_FILE_4__
cat >"$dir/repro.sh" <<'__REPRO_FILE_5__'
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Protoworks Group (Pty) Ltd
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
set -euo pipefail

# repro.sh — UP-CONSTOID-001: an OID whose dotted-decimal form is 100
# characters (RFC 5280 Appendix B: "MUST also support OIDs where the length
# of the dotted decimal ... string representation can be up to 100 bytes")
# does not fit const-oid's ObjectIdentifier, in 0.10.2 (the newest, under
# x509-cert 0.3.0) or 0.9.6 (under x509-cert 0.2.5); as the id of a
# non-critical extension it makes the whole certificate undecodable. A
# defensive check over a certificate the harness assembles octet by octet.
#
#   ./repro.sh
#
# What it does: builds harness/ (locked by harness/Cargo.lock) with the
# toolchain rust-toolchain.toml pins, runs it, and compares each
# `OBS key=value` with EXPECT, the claim table of the report. Lines named
# info.* are printed for the reader and not checked.
#
# Network: cargo's fetch of the crates harness/Cargo.lock pins. Nothing else.
#
# Exit status: 0 when every check holds (the defect is shown); otherwise the
# number of checks that disagreed; 2 if the harness could not be built or run.
#
# Dependencies: bash, awk, cargo and rustup (1.98.0).

here=$(cd "$(dirname "$0")" && pwd)
cd "$here"

fail=0
note() { echo "repro: $*"; }
check() {  # check <label> <want> <got>
  if [[ "$2" == "$3" ]]; then
    printf '  ok   %-60s %s\n' "$1" "$3"
  else
    printf '  FAIL %-60s got %s, want %s\n' "$1" "$3" "$2"
    fail=$((fail + 1))
  fi
}

if ! command -v cargo >/dev/null 2>&1; then
  note "cargo is not installed; cannot run"
  exit 2
fi
target=$(mktemp -d "${TMPDIR:-/tmp}/up-ev.XXXXXX")
trap 'rm -rf "$target"' EXIT

# The toolchain is the one the repository's rust-toolchain.toml pins.
rustc_version=$(cd "$here/harness" && rustc --version)
note "toolchain: $rustc_version"
check "rustc is the pinned 1.98.0" "rustc 1.98.0" "$(cut -d' ' -f1-2 <<<"$rustc_version")"

run_harness() {  # run_harness -> the harness's stdout
  (cd "$here/harness" && CARGO_TARGET_DIR="$target" cargo run --locked --quiet --release)
}

compare() {  # compare <label> <harness output> ; reads key=value lines on stdin
  local label="$1" out="$2" key want got
  while IFS='=' read -r key want; do
    [[ -z "$key" || "$key" == \#* ]] && continue
    got=$(awk -v k="OBS ${key}=" 'index($0, k) == 1 { print substr($0, length(k) + 1); exit }' <<<"$out")
    [[ -n "$got" ]] || got="(not printed)"
    check "$label: $key" "$want" "$got"
  done
}

finish() {  # finish <id>
  echo
  if [[ "$fail" -eq 0 ]]; then
    note "all checks held: $1 is shown as the report states it"
    exit 0
  fi
  note "$fail checks disagreed with the claim"
  exit "$fail"
}

note "building and running harness/ (const-oid 0.10.2 + x509-cert 0.3.0, const-oid 0.9.6 + x509-cert 0.2.5)"
out=$(run_harness) || { note "the harness did not build or run"; exit 2; }
printf '%s\n' "$out" | sed 's/^/    /'

compare oid "$out" <<'EXPECT'
oid100.chars=100
oid100.ber-octets=40
oid99.chars=99
oid99.ber-octets=39
EXPECT

compare const-oid "$out" <<'EXPECT'
const-oid-0.10.2.new(oid100)=Err(Length)
const-oid-0.10.2.from_bytes(oid100)=Err(Length)
const-oid-0.10.2.new(oid99)=Ok
const-oid-0.9.6.from_bytes(oid100)=Err(NotEnoughArcs)
const-oid-0.9.6.from_bytes(oid99)=Ok
EXPECT

# The controls: the assembled certificate is canonical DER, and the
# extension id is the only thing that changes between the three.
compare control "$out" <<'EXPECT'
control.x509-cert-0.3.0=Ok
control.x509-cert-0.3.0.reencodes-identically=true
control.x509-cert-0.2.5=Ok
control.x509-cert-0.2.5.reencodes-identically=true
cert-oid99.x509-cert-0.3.0=Ok
cert-oid99.x509-cert-0.3.0.reencodes-identically=true
cert-oid99.x509-cert-0.2.5=Ok
cert-oid99.x509-cert-0.2.5.reencodes-identically=true
EXPECT

compare claim "$out" <<'EXPECT'
cert-oid100.x509-cert-0.3.0=Err
cert-oid100.x509-cert-0.2.5=Err
EXPECT

finish UP-CONSTOID-001
__REPRO_FILE_5__
chmod +x "$dir/repro.sh"
cd "$dir" && ./repro.sh
Recorded run (macOS arm64, rustc 1.98.0)
repro: toolchain: rustc 1.98.0 (88d9e12ae 2026-08-18)
  ok   rustc is the pinned 1.98.0                                   rustc 1.98.0
repro: building and running harness/ (const-oid 0.10.2 + x509-cert 0.3.0, const-oid 0.9.6 + x509-cert 0.2.5)
  ok   oid: oid100.chars                                            100
  ok   oid: oid100.ber-octets                                       40
  ok   oid: oid99.chars                                             99
  ok   oid: oid99.ber-octets                                        39
  ok   const-oid: const-oid-0.10.2.new(oid100)                      Err(Length)
  ok   const-oid: const-oid-0.10.2.from_bytes(oid100)               Err(Length)
  ok   const-oid: const-oid-0.10.2.new(oid99)                       Ok
  ok   const-oid: const-oid-0.9.6.from_bytes(oid100)                Err(NotEnoughArcs)
  ok   const-oid: const-oid-0.9.6.from_bytes(oid99)                 Ok
  ok   control: control.x509-cert-0.3.0                             Ok
  ok   control: control.x509-cert-0.3.0.reencodes-identically       true
  ok   control: control.x509-cert-0.2.5                             Ok
  ok   control: control.x509-cert-0.2.5.reencodes-identically       true
  ok   control: cert-oid99.x509-cert-0.3.0                          Ok
  ok   control: cert-oid99.x509-cert-0.3.0.reencodes-identically    true
  ok   control: cert-oid99.x509-cert-0.2.5                          Ok
  ok   control: cert-oid99.x509-cert-0.2.5.reencodes-identically    true
  ok   claim: cert-oid100.x509-cert-0.3.0                           Err
  ok   claim: cert-oid100.x509-cert-0.2.5                           Err

repro: all checks held: UP-CONSTOID-001 is shown as the report states it

Activity

  1. choksi2212 commented on Oct 25, 2026

    @choksi2212
    Contributor

    Reproduced on current master (4c5fadb): ObjectIdentifier::new("1.3.268435455.268435455.268435455.268435455.268435455.268435455.268435455.268435455.268435455.268435455.123456") returns Err(Length), and the 39-octet control (last arc 16383) parses fine — exactly as in the report.

    The one-octet shortfall traces to DEFAULT_MAX_SIZE = 39 in const-oid/src/lib.rs:48, whose comment says the choice just makes the struct 40 bytes total with the 1-byte length. RFC 5280 Appendix B requires accepting OIDs whose dotted form is up to 100 characters, and the worst case under those limits (1.3 followed by nine arcs of 2^28 - 1 and one more arc) needs 40 content octets.

    I will open a PR that raises DEFAULT_MAX_SIZE to 40 so the Appendix B worst case fits, with the derivation in the doc comment, plus tests pinning the 100-character/40-octet OID through new, from_bytes, and as_bytes (these fail on the current 39-octet capacity). Since der and x509-cert take the capacity from ObjectIdentifier's default, the certificate case from the report comes along for the ride; the size of ObjectIdentifier grows by one byte, so the release needs a minor-version bump rather than a patch.

  2. choksi2212 commented on Oct 25, 2026

    @choksi2212
    Contributor

    Opened #2497 with this: the default capacity goes to 49, the full Appendix B ceiling (the 40-octet OID from this report is pinned by a test too, alongside the derivation of why 49 is the true worst case and not 40). RED/GREEN on the capacity change and the full const-oid/der/x509-cert verification are in the PR.

  3. tarcieri commented on Oct 25, 2026

    @tarcieri
    Member

    There's not much we can do about the default ObjectIdentifier size being 39 at this point: it would be a breaking change to both const-oid and x509-cert to change it.

    We can change it in the next breaking release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions