From d62490d8c2b2873f8347c7e43d014b21eb40f14a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 18:22:26 +0000 Subject: [PATCH 1/2] Support encode_der for asn1.TLV values Previously, calling `asn1.encode_der` on a value decoded with the `asn1.TLV` specifier raised `NotImplementedError`. This made it impossible to parse an arbitrary ASN.1 element as a `TLV` and then serialize just that element back to DER. A `TLV`'s `full_data` is a complete, already-validated DER element, so encoding re-parses it and writes it back out verbatim, honoring an EXPLICIT annotation when present (IMPLICIT annotations on TLV fields remain rejected at definition time). Closes #15109 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016iPBDfr6FxfNSm6nzakgwh --- src/rust/src/declarative_asn1/encode.rs | 77 +++++++++++++++++++++++-- tests/hazmat/asn1/test_serialization.py | 27 ++++++--- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/rust/src/declarative_asn1/encode.rs b/src/rust/src/declarative_asn1/encode.rs index f3e0a8f6630b..6481952c3c59 100644 --- a/src/rust/src/declarative_asn1/encode.rs +++ b/src/rust/src/declarative_asn1/encode.rs @@ -277,11 +277,33 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> { check_size_constraint(&annotation.size, || n_bits, "BIT STRING")?; Ok(write_value(writer, &bitstring, encoding)?) } - Type::Tlv() => Err(CryptographyError::Py( - pyo3::exceptions::PyNotImplementedError::new_err( - "TLV encoding currently not supported", - ), - )), + Type::Tlv() => { + let tlv = value.cast::()?; + // The `Tlv` Python object stores the element's raw DER bytes + // (`full_data`) rather than a live `asn1::Tlv<'_>`, since the + // latter borrows its input and can't be held in a pyclass. To + // emit it we need an `asn1::Asn1Writable`, but `asn1::Tlv` has + // no public constructor, so we recover one by re-parsing + // `full_data`. This only reads the tag/length header (no + // allocation, no re-validation of the value) and is infallible + // in practice, since the bytes came from a successful decode. + let asn1_tlv = + asn1::parse_single::>(tlv.get().full_data.as_bytes(py))?; + match encoding { + Some(e) => match e.get() { + // TLVs with implicit annotations are not supported + // (they are caught first at the Python level). + Encoding::Implicit(_) => Err(CryptographyError::Py( + pyo3::exceptions::PyValueError::new_err( + "invalid type definition: TLV/ANY fields cannot \ + be implicitly encoded", + ), + )), + Encoding::Explicit(n) => Ok(writer.write_explicit_element(&asn1_tlv, *n)?), + }, + None => Ok(writer.write_element(&asn1_tlv)?), + } + } Type::Null() => Ok(write_value(writer, &(), encoding)?), Type::Certificate() => { let val = value.cast::()?; @@ -327,10 +349,53 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> { #[cfg(test)] mod tests { use crate::declarative_asn1::types::{ - AnnotatedType, AnnotatedTypeObject, Annotation, Encoding, Type, Variant, + AnnotatedType, AnnotatedTypeObject, Annotation, Encoding, Tlv, Type, Variant, }; use asn1::Asn1Writable; use pyo3::PyTypeInfo; + + #[test] + // Needed for coverage of the `Encoding::Implicit` arm of the + // `Type::Tlv()` encoding case, since implicit TLV annotations are + // rejected first at the Python level and so never reach it. + fn test_encode_implicit_tlv() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + // The DER encoding of the INTEGER `1`. + let full_data = pyo3::types::PyBytes::new(py, b"\x02\x01\x01").unbind(); + let tag_bytes = pyo3::types::PyBytes::new(py, b"\x02").unbind(); + let tlv = pyo3::Py::new( + py, + Tlv { + tag_bytes, + data_index: 2, + full_data, + }, + ) + .unwrap(); + + let annotation = Annotation { + default: None, + encoding: Some(pyo3::Py::new(py, Encoding::Implicit(0)).unwrap()), + size: None, + }; + let ann_type = AnnotatedType { + inner: pyo3::Py::new(py, Type::Tlv()).unwrap(), + annotation: pyo3::Py::new(py, annotation).unwrap(), + }; + let object = AnnotatedTypeObject { + annotated_type: &ann_type, + value: tlv.bind(py).clone().into_any(), + }; + + let result = asn1::write(|writer| object.write(writer)); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(format!("{error}") + .contains("invalid type definition: TLV/ANY fields cannot be implicitly encoded")); + }); + } + #[test] fn test_encode_implicit_choice() { pyo3::Python::initialize(); diff --git a/tests/hazmat/asn1/test_serialization.py b/tests/hazmat/asn1/test_serialization.py index 4e2e2a6afb6c..93614aa6bfef 100644 --- a/tests/hazmat/asn1/test_serialization.py +++ b/tests/hazmat/asn1/test_serialization.py @@ -328,14 +328,23 @@ class Example: assert isinstance(decoded_example, Example) assert decoded_example.foo == 9 - def test_fail_encode_tlv(self) -> None: - tlv = asn1.decode_der(asn1.TLV, b"\x03\x02\x07\x40") + def test_ok_encode_tlv(self) -> None: + for original in [ + b"\x03\x02\x07\x40", + b"\x02\x01\x01", + b"\x30\x03\x02\x01\x09", + b"\x05\x00", + ]: + tlv = asn1.decode_der(asn1.TLV, original) + assert isinstance(tlv, asn1.TLV) + assert asn1.encode_der(tlv) == original + + def test_ok_encode_tlv_issue_example(self) -> None: + # The example from the original feature request: parse an + # arbitrary part with the TLV specifier, then serialize it back. + tlv = asn1.decode_der(asn1.TLV, asn1.encode_der(1)) assert isinstance(tlv, asn1.TLV) - - with pytest.raises( - NotImplementedError, match="TLV encoding currently not supported" - ): - asn1.encode_der(tlv) + assert asn1.encode_der(tlv) == asn1.encode_der(1) class TestNull: @@ -1054,6 +1063,10 @@ class Example: assert decoded.bar.tag_bytes == b"\x02" assert bytes(decoded.bar.data) == b"\x09" + # Re-encoding the EXPLICIT-tagged TLV fields produces the + # original DER. + assert asn1.encode_der(decoded) == encoded + def test_fail_sequence_with_tlv_with_explicit_annotation( self, ) -> None: From edcfa58c5a412c0fd5d9ffb8593af8a330da3326 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 01:29:00 +0000 Subject: [PATCH 2/2] Rename TLV encode test to describe behavior Renames `test_ok_encode_tlv_issue_example` to `test_ok_encode_tlv_roundtrip_from_value` and rewords its comment to describe the behavior under test rather than referencing the original feature request, which won't age well. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016iPBDfr6FxfNSm6nzakgwh --- tests/hazmat/asn1/test_serialization.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/hazmat/asn1/test_serialization.py b/tests/hazmat/asn1/test_serialization.py index 93614aa6bfef..06b147deb217 100644 --- a/tests/hazmat/asn1/test_serialization.py +++ b/tests/hazmat/asn1/test_serialization.py @@ -339,12 +339,13 @@ def test_ok_encode_tlv(self) -> None: assert isinstance(tlv, asn1.TLV) assert asn1.encode_der(tlv) == original - def test_ok_encode_tlv_issue_example(self) -> None: - # The example from the original feature request: parse an - # arbitrary part with the TLV specifier, then serialize it back. - tlv = asn1.decode_der(asn1.TLV, asn1.encode_der(1)) + def test_ok_encode_tlv_roundtrip_from_value(self) -> None: + # Parse an arbitrary element with the TLV specifier, then + # serialize just that element back to its original DER. + original = asn1.encode_der(1) + tlv = asn1.decode_der(asn1.TLV, original) assert isinstance(tlv, asn1.TLV) - assert asn1.encode_der(tlv) == asn1.encode_der(1) + assert asn1.encode_der(tlv) == original class TestNull: