镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

x509-cert 0.3.0 declares RFC 5914's TrustAnchorInfo v1 as 0, so it encodes v1 as INTEGER 0 and reads a written version 0 as v1 #2487

Open

Description

@Executioner1939

Hi,

Details, the source lines, and a self-contained reproduction are below.

Versions pinned

Thing Pin
x509-cert 0.3.0, the newest release on crates.io (read 2026-10-21), over der 0.8; the .crate checksums are in harness/Cargo.lock
x509-cert, as we use it 0.2.5, the same lines (src/anchor.rs:17-21, :34@x509-cert-0.2.5); cited, not run
x509-cert master 13a0390d (2026-10-13), the same declaration (x509-cert/src/anchor.rs:19-22, :36, :43@13a0390d); read 2026-10-21, not run
the input one anchor (an Ed25519 key, a one-octet keyId) with its version left out, and written as 0, 1 and 2, in harness/src/main.rs
toolchain rustc 1.98.0 (the bundled rust-toolchain.toml)

The rule

RFC 5914 section 2:

  • version TrustAnchorInfoVersion DEFAULT v1,
  • TrustAnchorInfoVersion ::= INTEGER { v1(1) }
  • §2.1: "version identifies the version of TrustAnchorInfo. ... However,
    the default value, v1, cannot be changed."

So v1 is the integer 1. A DER encoding of a v1 anchor leaves version out,
because it is the default.

What x509-cert 0.3.0 does

  • anchor::Version is an Enumerated INTEGER with one value, V1 = 0
    (src/anchor.rs:15-23@x509-cert-0.3.0).
  • The same file's doc comment quotes the RFC's
    TrustAnchorInfoVersion ::= INTEGER { v1(1) }
    (src/anchor.rs:36@x509-cert-0.3.0).
  • TrustAnchorInfo.version takes that type with
    #[asn1(default = "Default::default")]
    (src/anchor.rs:43-44@x509-cert-0.3.0). An anchor without the field
    decodes as V1 and is written back without it, which is right. The value
    V1 stands for is not.

What the run shows

repro.sh builds harness/ against x509-cert 0.3.0 at its locked checksum
and compares each OBS line with the claim table in its EXPECT blocks:

Input or call x509-cert 0.3.0 RFC 5914
Version::V1, encoded 02 01 00 (INTEGER 0) 02 01 01 (INTEGER 1)
an anchor that writes version 0 accepted as V1, and written back without the field 0 is not v1
an anchor that writes version 1 refused: "malformed ASN.1 DER value for INTEGER" v1, written out where DER leaves it out
an anchor that writes version 2 refused, the same way a version this RFC does not define
an anchor without version V1, written back the same v1

The controls are the last two rows. The claim is the first two rows: v1 is
encoded as 0, and a written 0 is read as v1 and then lost on re-encoding.

Not claimed

  • That an anchor with version 1 written out must be accepted. Under
    DER it must not be, since DER leaves a default out. The run records only
    that x509-cert refuses it for being outside its INTEGER values, not for
    being a written-out default.
  • Any effect on certificates. TrustAnchorInfo is the only type
    involved. x509-cert's certificate::Version is not part of this entry.
  • Anything about other x509-cert releases than 0.2.5 and 0.3.0. 0.2.5
    and master at 13a0390d have the same declaration and doc comment, cited
    above but not run.

The path

flowchart TD
  spec["RFC 5914 section 2<br/>TrustAnchorInfoVersion ::= INTEGER { v1(1) }<br/>version ... DEFAULT v1<br/>rfc5914...txt:149, 158, 162-165"]
  decl["anchor::Version { V1 = 0 }<br/>anchor.rs:15-23@x509-cert-0.3.0<br/>its doc comment quotes v1(1), anchor.rs:36"]
  spec -. "v1 is 1" .-> decl
  decl --> enc["Version::V1 encoded: 02 01 00"]
  w0["anchor writing version 0"] --> dec["TrustAnchorInfo::from_der<br/>anchor.rs:43-44, default = V1"]
  dec --> acc["accepted as V1<br/>written back without the field: the 0 is lost"]
  w1["anchor writing version 1"] --> dec
  dec --> ref["refused: malformed INTEGER<br/>(right under DER, for another reason)"]
  absent["anchor without version"] --> dec
  dec --> ok["V1, written back the same"]
  classDef bad fill:#fde,stroke:#b35
  classDef good fill:#def,stroke:#37b
  class enc,acc bad
  class ok good
Loading
Reproduction script (self-contained)

Save it as repro.sh and run bash repro.sh. It writes the files it needs into a new temporary directory and runs the check there; it exits 0 when every observation matches this report. The script is yours to use under MIT OR Apache-2.0.

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Protoworks Group (Pty) Ltd
# SPDX-License-Identifier: MIT OR Apache-2.0
#
# Self-contained reproduction (UP-X509CERT-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.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"

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

[[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.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9"
dependencies = [
 "base64ct",
 "der",
]

[[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-x509cert-001"
version = "0.0.0"
dependencies = [
 "der",
 "x509-cert",
]

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

[[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: MIT OR Apache-2.0
#

# UP-X509CERT-001's harness: x509-cert 0.3.0 (the newest release) decoding
# one RFC 5914 TrustAnchorInfo with its version left out and written as 0, 1
# and 2, and encoding anchor::Version::V1, through its public API only.
[package]
name = "up-x509cert-001"
version = "0.0.0"
edition = "2021"
publish = false

[dependencies]
x509-cert = { version = "=0.3.0", default-features = false }
der = { version = "0.8", features = ["alloc"] }

[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: MIT OR Apache-2.0
//

//! UP-X509CERT-001: RFC 5914 has `TrustAnchorInfoVersion ::= INTEGER { v1(1) }`
//! and `version TrustAnchorInfoVersion DEFAULT v1`. One anchor (an Ed25519
//! key, a one-octet keyId) is written here with no version and with a version
//! of 0, 1 and 2, and decoded by x509-cert; `Version::V1` is encoded on its
//! own. Prints `OBS key=value`. Nothing is fetched.

use der::{Decode, Encode};
use x509_cert::anchor::{TrustAnchorInfo, Version};
use x509_cert::certificate::Rfc5280;

fn hex(octets: &[u8]) -> String {
    octets.iter().map(|o| format!("{o:02x}")).collect()
}

/// The anchor's DER, with `version` written as `INTEGER version` when given.
fn anchor(version: Option<u8>) -> Vec<u8> {
    let mut fields = Vec::new();
    if let Some(v) = version {
        fields.extend_from_slice(&[0x02, 0x01, v]);
    }
    // SubjectPublicKeyInfo: id-Ed25519 (RFC 8410) and a 32-octet key.
    fields.extend_from_slice(&[0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00]);
    fields.extend_from_slice(&[0x11; 32]);
    // keyId: OCTET STRING of one octet.
    fields.extend_from_slice(&[0x04, 0x01, 0xaa]);
    let mut der = vec![0x30, u8::try_from(fields.len()).expect("short form")];
    der.extend_from_slice(&fields);
    der
}

fn main() {
    for (name, version) in [("absent", None), ("written-0", Some(0)), ("written-1", Some(1)), ("written-2", Some(2))] {
        match TrustAnchorInfo::<Rfc5280>::from_der(&anchor(version)) {
            Ok(info) => {
                println!("OBS decode.{name}=accepted as {:?}", info.version);
                let again = info.to_der().expect("DER");
                println!("OBS decode.{name}.reencoded-without-version={}", again == anchor(None));
            }
            Err(error) => println!("OBS decode.{name}=refused: {error}"),
        }
    }
    println!("OBS encode.Version::V1={}", hex(&Version::V1.to_der().expect("DER")));
}
__REPRO_FILE_4__
cat >"$dir/repro.sh" <<'__REPRO_FILE_5__'
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Protoworks Group (Pty) Ltd
# SPDX-License-Identifier: MIT OR Apache-2.0
#
set -euo pipefail

# repro.sh — UP-X509CERT-001: x509-cert 0.3.0 (the newest release) declares
# the RFC 5914 TrustAnchorInfoVersion v1 as 0, where the RFC's module has
# INTEGER { v1(1) }. It encodes Version::V1 as INTEGER 0, and it accepts an
# anchor that writes version 0, which is not v1, as v1 and drops the field
# when it writes the anchor again.
#
#   ./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.
#
# 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/ (x509-cert 0.3.0)"
out=$(run_harness) || { note "the harness did not build or run"; exit 2; }
printf '%s\n' "$out" | sed 's/^/    /'

# The controls: an anchor that leaves the version out is v1 and is written
# back as it came, and versions RFC 5914 does not define are refused.
compare control "$out" <<'EXPECT'
decode.absent=accepted as V1
decode.absent.reencoded-without-version=true
decode.written-2=refused: malformed ASN.1 DER value for INTEGER
EXPECT

# The claim: v1 is encoded as INTEGER 0 (RFC 5914: v1(1), so 020101), and an
# anchor that writes version 0 is accepted as v1 and written back without
# the field, so the 0 it carried is lost.
compare claim "$out" <<'EXPECT'
encode.Version::V1=020100
decode.written-0=accepted as V1
decode.written-0.reencoded-without-version=true
EXPECT

finish UP-X509CERT-001
__REPRO_FILE_5__
chmod +x "$dir/repro.sh"
cd "$dir" && ./repro.sh
Recorded run
date: 2026-10-21T20:32:36Z
arch: Darwin arm64
tools: bash 5.3.20(1)-release; git version 2.56.0; docker client 29.9.0; shellcheck 0.11.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/ (x509-cert 0.3.0)
    OBS decode.absent=accepted as V1
    OBS decode.absent.reencoded-without-version=true
    OBS decode.written-0=accepted as V1
    OBS decode.written-0.reencoded-without-version=true
    OBS decode.written-1=refused: malformed ASN.1 DER value for INTEGER
    OBS decode.written-2=refused: malformed ASN.1 DER value for INTEGER
    OBS encode.Version::V1=020100
  ok   control: decode.absent                                       accepted as V1
  ok   control: decode.absent.reencoded-without-version             true
  ok   control: decode.written-2                                    refused: malformed ASN.1 DER value for INTEGER
  ok   claim: encode.Version::V1                                    020100
  ok   claim: decode.written-0                                      accepted as V1
  ok   claim: decode.written-0.reencoded-without-version            true

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

exit: 0

Activity

  1. carl-wallace commented on Oct 23, 2026

    @carl-wallace
    Contributor

    I think you are suggesting that:

    pub enum Version {
        /// Version 1 (default)
        #[default]
        V1 = 0,
    }

    in anchor.rs be changed to

    pub enum Version {
        /// Version 1 (default)
        #[default]
        V1 = 1,
    }

    That looks right to me. A PR with that change and a test (V1 encodes as 02 01 01; a written version 0 is refused) would be welcome.

  2. choksi2212 commented on Oct 24, 2026

    @choksi2212
    Contributor

    Taking this one as well, if that's alright. RFC 5914's v1(1) reads clearly and the current V1 = 0 contradicts the doc comment right above it. Plan: set the discriminant to 1, pin the absent-version default (still omits on encode, still decodes as V1), and reject a written 0 or 1 the way DER requires. Tests on both sides of each behavior.

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