Skip to content

Commit 7aba7ee

Browse files
committed
asn1: Add support for PrintableString
Signed-off-by: Facundo Tuesca <[email protected]>
1 parent 1a64c82 commit 7aba7ee

File tree

8 files changed

+103
-7
lines changed

8 files changed

+103
-7
lines changed

src/cryptography/hazmat/asn1/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
33
# for complete details.
44

5-
from cryptography.hazmat.asn1.asn1 import decode_der, encode_der, sequence
5+
from cryptography.hazmat.asn1.asn1 import (
6+
PrintableString,
7+
decode_der,
8+
encode_der,
9+
sequence,
10+
)
611

712
__all__ = [
13+
"PrintableString",
814
"decode_der",
915
"encode_der",
1016
"sequence",

src/cryptography/hazmat/asn1/asn1.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,6 @@ def sequence(cls: type[U]) -> type[U]:
115115
)(cls)
116116
_register_asn1_sequence(dataclass_cls)
117117
return dataclass_cls
118+
119+
120+
PrintableString = declarative_asn1.PrintableString

src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ class AnnotatedTypeObject:
3434
def __new__(
3535
cls, annotated_type: AnnotatedType, value: typing.Any
3636
) -> AnnotatedTypeObject: ...
37+
38+
class PrintableString:
39+
def __new__(cls, inner: str) -> PrintableString: ...
40+
def as_str(self) -> str: ...

src/rust/src/declarative_asn1/decode.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use asn1::Parser;
66
use pyo3::types::PyAnyMethods;
77

88
use crate::asn1::big_byte_slice_to_py_int;
9-
use crate::declarative_asn1::types::{AnnotatedType, Type};
9+
use crate::declarative_asn1::types::{AnnotatedType, PrintableString, Type};
1010
use crate::error::CryptographyError;
1111

1212
type ParseResult<T> = Result<T, CryptographyError>;
@@ -48,6 +48,15 @@ fn decode_pystr<'a>(
4848
Ok(pyo3::types::PyString::new(py, value.as_str()))
4949
}
5050

51+
fn decode_printable_string<'a>(
52+
py: pyo3::Python<'a>,
53+
parser: &mut Parser<'a>,
54+
) -> ParseResult<pyo3::Bound<'a, PrintableString>> {
55+
let value = parser.read_element::<asn1::PrintableString<'a>>()?.as_str();
56+
let inner = pyo3::types::PyString::new(py, value).unbind();
57+
Ok(pyo3::Bound::new(py, PrintableString { inner })?)
58+
}
59+
5160
pub(crate) fn decode_annotated_type<'a>(
5261
py: pyo3::Python<'a>,
5362
parser: &mut Parser<'a>,
@@ -78,5 +87,6 @@ pub(crate) fn decode_annotated_type<'a>(
7887
Type::PyInt() => Ok(decode_pyint(py, parser)?.into_any()),
7988
Type::PyBytes() => Ok(decode_pybytes(py, parser)?.into_any()),
8089
Type::PyStr() => Ok(decode_pystr(py, parser)?.into_any()),
90+
Type::PrintableString() => Ok(decode_printable_string(py, parser)?.into_any()),
8191
}
8292
}

src/rust/src/declarative_asn1/encode.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use asn1::{SimpleAsn1Writable, Writer};
66
use pyo3::types::PyAnyMethods;
77

8-
use crate::declarative_asn1::types::{AnnotatedType, AnnotatedTypeObject, Type};
8+
use crate::declarative_asn1::types::{AnnotatedType, AnnotatedTypeObject, PrintableString, Type};
99

1010
fn write_value<T: SimpleAsn1Writable>(
1111
writer: &mut Writer<'_>,
@@ -73,6 +73,20 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> {
7373
let asn1_string: asn1::Utf8String<'_> = asn1::Utf8String::new(&val);
7474
write_value(writer, &asn1_string)
7575
}
76+
Type::PrintableString() => {
77+
let val: &pyo3::Bound<'_, PrintableString> = value
78+
.downcast()
79+
.map_err(|_| asn1::WriteError::AllocationError)?;
80+
let inner_str = val
81+
.get()
82+
.inner
83+
.to_str(py)
84+
.map_err(|_| asn1::WriteError::AllocationError)?;
85+
let printable_string: asn1::PrintableString<'_> =
86+
asn1::PrintableString::new(inner_str)
87+
.ok_or(asn1::WriteError::AllocationError)?;
88+
write_value(writer, &printable_string)
89+
}
7690
}
7791
}
7892
}

src/rust/src/declarative_asn1/types.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ pub enum Type {
3232
/// `str` -> `UTF8String`
3333
#[pyo3(constructor = ())]
3434
PyStr(),
35+
/// PrintableString (`str`)
36+
#[pyo3(constructor = ())]
37+
PrintableString(),
3538
}
3639

3740
/// A type that we know how to encode/decode, along with any
@@ -70,6 +73,26 @@ impl Annotation {
7073
}
7174
}
7275

76+
#[derive(pyo3::FromPyObject)]
77+
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.asn1")]
78+
pub struct PrintableString {
79+
pub(crate) inner: pyo3::Py<pyo3::types::PyString>,
80+
}
81+
82+
#[pyo3::pymethods]
83+
impl PrintableString {
84+
#[new]
85+
#[pyo3(signature = (inner,))]
86+
fn new(inner: pyo3::Py<pyo3::types::PyString>) -> Self {
87+
PrintableString { inner }
88+
}
89+
90+
#[pyo3(signature = ())]
91+
pub fn as_str(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyString>> {
92+
Ok(self.inner.clone_ref(py))
93+
}
94+
}
95+
7396
/// Utility function for converting builtin Python types
7497
/// to their Rust `Type` equivalent.
7598
#[pyo3::pyfunction]
@@ -85,6 +108,8 @@ pub fn non_root_python_to_rust<'p>(
85108
Type::PyStr().into_pyobject(py)
86109
} else if class.is(pyo3::types::PyBytes::type_object(py)) {
87110
Type::PyBytes().into_pyobject(py)
111+
} else if class.is(PrintableString::type_object(py)) {
112+
Type::PrintableString().into_pyobject(py)
88113
} else {
89114
Err(pyo3::exceptions::PyTypeError::new_err(format!(
90115
"cannot handle type: {class:?}"
@@ -131,5 +156,5 @@ pub(crate) fn python_class_to_annotated<'p>(
131156
#[pyo3::pymodule(gil_used = false)]
132157
pub(crate) mod types {
133158
#[pymodule_export]
134-
use super::{AnnotatedType, Annotation, Type};
159+
use super::{AnnotatedType, Annotation, PrintableString, Type};
135160
}

src/rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ mod _rust {
124124

125125
#[pymodule_export]
126126
use crate::declarative_asn1::types::{
127-
non_root_python_to_rust, AnnotatedType, Annotation, Type,
127+
non_root_python_to_rust, AnnotatedType, Annotation, PrintableString, Type,
128128
};
129129
}
130130

tests/hazmat/asn1/test_serialization.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sys
77
import typing
88

9+
import pytest
10+
911
import cryptography.hazmat.asn1 as asn1
1012

1113
U = typing.TypeVar("U")
@@ -34,16 +36,26 @@ def _comparable_dataclass(cls: typing.Type[U]) -> typing.Type[U]:
3436
)(cls)
3537

3638

39+
# Checks that the encoding-decoding roundtrip results
40+
# in the expected values and is consistent.
41+
#
42+
# The `decoded_eq` argument is the equality function to use
43+
# for the decoded values. It's useful for types that aren't
44+
# directly comparable, like `PrintableString`.
3745
def assert_roundtrips(
38-
test_cases: typing.List[typing.Tuple[typing.Any, bytes]],
46+
test_cases: typing.List[typing.Tuple[U, bytes]],
47+
decoded_eq: typing.Optional[typing.Callable[[U, U], bool]] = None,
3948
) -> None:
4049
for obj, obj_bytes in test_cases:
4150
encoded = asn1.encode_der(obj)
4251
assert encoded == obj_bytes
4352

4453
decoded = asn1.decode_der(type(obj), encoded)
4554
assert isinstance(decoded, type(obj))
46-
assert decoded == obj
55+
if decoded_eq:
56+
assert decoded_eq(decoded, obj)
57+
else:
58+
assert decoded == obj
4759

4860

4961
class TestBool:
@@ -105,6 +117,28 @@ def test_string(self) -> None:
105117
)
106118

107119

120+
class TestPrintableString:
121+
def test_ok_printable_string(self) -> None:
122+
def decoded_eq(a: asn1.PrintableString, b: asn1.PrintableString):
123+
return a.as_str() == b.as_str()
124+
125+
assert_roundtrips(
126+
[
127+
(asn1.PrintableString(""), b"\x13\x00"),
128+
(asn1.PrintableString("hello"), b"\x13\x05hello"),
129+
(asn1.PrintableString("Test User 1"), b"\x13\x0bTest User 1"),
130+
],
131+
decoded_eq,
132+
)
133+
134+
def test_invalid_printable_string(self) -> None:
135+
with pytest.raises(ValueError, match="allocation error"):
136+
asn1.encode_der(asn1.PrintableString("café"))
137+
138+
with pytest.raises(ValueError, match="error parsing asn1 value"):
139+
asn1.decode_der(asn1.PrintableString, b"\x0c\x05caf\xc3\xa9")
140+
141+
108142
class TestSequence:
109143
def test_ok_sequence_single_field(self) -> None:
110144
@asn1.sequence

0 commit comments

Comments
 (0)