Skip to content

Commit ed8c355

Browse files
authored
asn1: Add support for BIT STRING (#13908)
* asn1: Add support for `BIT STRING` Signed-off-by: Facundo Tuesca <[email protected]> * fix __repr__ Signed-off-by: Facundo Tuesca <[email protected]> * remove unnecessary signature option Signed-off-by: Facundo Tuesca <[email protected]> --------- Signed-off-by: Facundo Tuesca <[email protected]>
1 parent 3178a80 commit ed8c355

File tree

9 files changed

+178
-10
lines changed

9 files changed

+178
-10
lines changed

src/cryptography/hazmat/asn1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# for complete details.
44

55
from cryptography.hazmat.asn1.asn1 import (
6+
BitString,
67
Default,
78
Explicit,
89
GeneralizedTime,
@@ -16,6 +17,7 @@
1617
)
1718

1819
__all__ = [
20+
"BitString",
1921
"Default",
2022
"Explicit",
2123
"GeneralizedTime",

src/cryptography/hazmat/asn1/asn1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,4 @@ class Default(typing.Generic[U]):
240240
PrintableString = declarative_asn1.PrintableString
241241
UtcTime = declarative_asn1.UtcTime
242242
GeneralizedTime = declarative_asn1.GeneralizedTime
243+
BitString = declarative_asn1.BitString

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,10 @@ class GeneralizedTime:
7777
def __repr__(self) -> str: ...
7878
def __eq__(self, other: object) -> bool: ...
7979
def as_datetime(self) -> datetime.datetime: ...
80+
81+
class BitString:
82+
def __new__(cls, data: bytes, padding_bits: int) -> BitString: ...
83+
def __repr__(self) -> str: ...
84+
def __eq__(self, other: object) -> bool: ...
85+
def as_bytes(self) -> bytes: ...
86+
def padding_bits(self) -> int: ...

src/rust/src/declarative_asn1/decode.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use pyo3::types::PyListMethods;
88

99
use crate::asn1::big_byte_slice_to_py_int;
1010
use crate::declarative_asn1::types::{
11-
type_to_tag, AnnotatedType, Encoding, GeneralizedTime, PrintableString, Type, UtcTime,
11+
type_to_tag, AnnotatedType, BitString, Encoding, GeneralizedTime, PrintableString, Type,
12+
UtcTime,
1213
};
1314
use crate::error::CryptographyError;
1415

@@ -118,6 +119,22 @@ fn decode_generalized_time<'a>(
118119
Ok(pyo3::Bound::new(py, GeneralizedTime { inner })?)
119120
}
120121

122+
fn decode_bitstring<'a>(
123+
py: pyo3::Python<'a>,
124+
parser: &mut Parser<'a>,
125+
encoding: &Option<pyo3::Py<Encoding>>,
126+
) -> ParseResult<pyo3::Bound<'a, BitString>> {
127+
let value = read_value::<asn1::BitString<'a>>(parser, encoding)?;
128+
let data = pyo3::types::PyBytes::new(py, value.as_bytes()).unbind();
129+
Ok(pyo3::Bound::new(
130+
py,
131+
BitString {
132+
data,
133+
padding_bits: value.padding_bits(),
134+
},
135+
)?)
136+
}
137+
121138
pub(crate) fn decode_annotated_type<'a>(
122139
py: pyo3::Python<'a>,
123140
parser: &mut Parser<'a>,
@@ -201,6 +218,7 @@ pub(crate) fn decode_annotated_type<'a>(
201218
Type::PrintableString() => decode_printable_string(py, parser, encoding)?.into_any(),
202219
Type::UtcTime() => decode_utc_time(py, parser, encoding)?.into_any(),
203220
Type::GeneralizedTime() => decode_generalized_time(py, parser, encoding)?.into_any(),
221+
Type::BitString() => decode_bitstring(py, parser, encoding)?.into_any(),
204222
};
205223

206224
match &ann_type.annotation.get().default {

src/rust/src/declarative_asn1/encode.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use pyo3::types::PyAnyMethods;
77
use pyo3::types::PyListMethods;
88

99
use crate::declarative_asn1::types::{
10-
AnnotatedType, AnnotatedTypeObject, Encoding, GeneralizedTime, PrintableString, Type, UtcTime,
10+
AnnotatedType, AnnotatedTypeObject, BitString, Encoding, GeneralizedTime, PrintableString,
11+
Type, UtcTime,
1112
};
1213

1314
fn write_value<T: SimpleAsn1Writable>(
@@ -171,6 +172,16 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> {
171172
.map_err(|_| asn1::WriteError::AllocationError)?;
172173
write_value(writer, &generalized_time, encoding)
173174
}
175+
Type::BitString() => {
176+
let val: &pyo3::Bound<'_, BitString> = value
177+
.cast()
178+
.map_err(|_| asn1::WriteError::AllocationError)?;
179+
180+
let bitstring: asn1::BitString<'_> =
181+
asn1::BitString::new(val.get().data.as_bytes(py), val.get().padding_bits)
182+
.ok_or(asn1::WriteError::AllocationError)?;
183+
write_value(writer, &bitstring, encoding)
184+
}
174185
}
175186
}
176187
}

src/rust/src/declarative_asn1/types.rs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub enum Type {
1919
/// The first element is the Python class that represents the sequence,
2020
/// the second element is a dict of the (already converted) fields of the class.
2121
Sequence(pyo3::Py<pyo3::types::PyType>, pyo3::Py<pyo3::types::PyDict>),
22-
/// SEQUENCEOF (`list[`T`]`)
22+
/// SEQUENCE OF (`list[`T`]`)
2323
SequenceOf(pyo3::Py<AnnotatedType>),
2424
/// OPTIONAL (`T | None`)
2525
Option(pyo3::Py<AnnotatedType>),
@@ -40,6 +40,8 @@ pub enum Type {
4040
UtcTime(),
4141
/// GeneralizedTime (`datetime`)
4242
GeneralizedTime(),
43+
/// BIT STRING (`bytes`)
44+
BitString(),
4345
}
4446

4547
/// A type that we know how to encode/decode, along with any
@@ -94,7 +96,6 @@ impl Annotation {
9496
}
9597
}
9698

97-
#[pyo3(signature = ())]
9899
fn is_empty(&self) -> bool {
99100
self.default.is_none() && self.encoding.is_none() && self.size.is_none()
100101
}
@@ -148,7 +149,6 @@ impl PrintableString {
148149
Ok(PrintableString { inner })
149150
}
150151

151-
#[pyo3(signature = ())]
152152
pub fn as_str(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyString>> {
153153
Ok(self.inner.clone_ref(py))
154154
}
@@ -192,7 +192,6 @@ impl UtcTime {
192192
Ok(UtcTime { inner })
193193
}
194194

195-
#[pyo3(signature = ())]
196195
pub fn as_datetime(
197196
&self,
198197
py: pyo3::Python<'_>,
@@ -232,7 +231,6 @@ impl GeneralizedTime {
232231
Ok(GeneralizedTime { inner })
233232
}
234233

235-
#[pyo3(signature = ())]
236234
pub fn as_datetime(
237235
&self,
238236
py: pyo3::Python<'_>,
@@ -249,6 +247,52 @@ impl GeneralizedTime {
249247
}
250248
}
251249

250+
#[derive(pyo3::FromPyObject)]
251+
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.asn1")]
252+
pub struct BitString {
253+
pub(crate) data: pyo3::Py<pyo3::types::PyBytes>,
254+
pub(crate) padding_bits: u8,
255+
}
256+
257+
#[pyo3::pymethods]
258+
impl BitString {
259+
#[new]
260+
#[pyo3(signature = (data, padding_bits,))]
261+
fn new(
262+
py: pyo3::Python<'_>,
263+
data: pyo3::Py<pyo3::types::PyBytes>,
264+
padding_bits: u8,
265+
) -> pyo3::PyResult<Self> {
266+
if asn1::BitString::new(data.as_bytes(py), padding_bits).is_none() {
267+
return Err(pyo3::exceptions::PyValueError::new_err(format!(
268+
"invalid BIT STRING: data: {data}, padding_bits: {padding_bits}"
269+
)));
270+
}
271+
272+
Ok(BitString { data, padding_bits })
273+
}
274+
275+
pub fn as_bytes(&self, py: pyo3::Python<'_>) -> pyo3::Py<pyo3::types::PyBytes> {
276+
self.data.clone_ref(py)
277+
}
278+
279+
pub fn padding_bits(&self) -> u8 {
280+
self.padding_bits
281+
}
282+
283+
fn __eq__(&self, py: pyo3::Python<'_>, other: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<bool> {
284+
Ok((**self.data.bind(py)).eq(other.data.bind(py))?
285+
&& self.padding_bits == other.padding_bits)
286+
}
287+
288+
pub fn __repr__(&self) -> pyo3::PyResult<String> {
289+
Ok(format!(
290+
"BitString(data={}, padding_bits={})",
291+
self.data, self.padding_bits,
292+
))
293+
}
294+
}
295+
252296
/// Utility function for converting builtin Python types
253297
/// to their Rust `Type` equivalent.
254298
#[pyo3::pyfunction]
@@ -270,6 +314,8 @@ pub fn non_root_python_to_rust<'p>(
270314
Type::UtcTime().into_pyobject(py)
271315
} else if class.is(GeneralizedTime::type_object(py)) {
272316
Type::GeneralizedTime().into_pyobject(py)
317+
} else if class.is(BitString::type_object(py)) {
318+
Type::BitString().into_pyobject(py)
273319
} else {
274320
Err(pyo3::exceptions::PyTypeError::new_err(format!(
275321
"cannot handle type: {class:?}"
@@ -326,6 +372,7 @@ pub(crate) fn type_to_tag(t: &Type, encoding: &Option<pyo3::Py<Encoding>>) -> as
326372
Type::PrintableString() => asn1::PrintableString::TAG,
327373
Type::UtcTime() => asn1::UtcTime::TAG,
328374
Type::GeneralizedTime() => asn1::GeneralizedTime::TAG,
375+
Type::BitString() => asn1::BitString::TAG,
329376
};
330377

331378
match encoding {

src/rust/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ mod _rust {
151151

152152
#[pymodule_export]
153153
use crate::declarative_asn1::types::{
154-
non_root_python_to_rust, AnnotatedType, Annotation, Encoding, GeneralizedTime,
155-
PrintableString, Size, Type, UtcTime,
154+
non_root_python_to_rust, AnnotatedType, Annotation, BitString, Encoding,
155+
GeneralizedTime, PrintableString, Size, Type, UtcTime,
156156
};
157157
}
158158

tests/hazmat/asn1/test_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,35 @@ def test_invalid_generalized_time(self) -> None:
107107
# We don't allow naive datetime objects
108108
asn1.GeneralizedTime(datetime.datetime(2000, 1, 1, 10, 10, 10))
109109

110+
def test_bitstring_getters(self) -> None:
111+
data = b"\x01\x02\x30"
112+
bt = asn1.BitString(data=data, padding_bits=2)
113+
114+
assert bt.as_bytes() == data
115+
assert bt.padding_bits() == 2
116+
117+
def test_repr_bitstring(self) -> None:
118+
data = b"\x01\x02\x30"
119+
assert (
120+
repr(asn1.BitString(data, 2))
121+
== f"BitString(data={data!r}, padding_bits=2)"
122+
)
123+
124+
def test_invalid_bitstring(self) -> None:
125+
with pytest.raises(
126+
ValueError,
127+
match="invalid BIT STRING",
128+
):
129+
# Padding bits cannot be > 7
130+
asn1.BitString(data=b"\x01\x02\x03", padding_bits=8)
131+
132+
with pytest.raises(
133+
ValueError,
134+
match="invalid BIT STRING",
135+
):
136+
# Padding bits have to be zero
137+
asn1.BitString(data=b"\x01\x02\x03", padding_bits=2)
138+
110139

111140
class TestSequenceAPI:
112141
def test_fail_unsupported_field(self) -> None:

tests/hazmat/asn1/test_serialization.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,51 @@ def test_generalized_time(self) -> None:
224224
)
225225

226226

227+
class TestBitString:
228+
def test_ok_bitstring(self) -> None:
229+
assert_roundtrips(
230+
[
231+
(
232+
asn1.BitString(data=b"\x6e\x5d\xc0", padding_bits=6),
233+
b"\x03\x04\x06\x6e\x5d\xc0",
234+
),
235+
(
236+
asn1.BitString(data=b"", padding_bits=0),
237+
b"\x03\x01\x00",
238+
),
239+
(
240+
asn1.BitString(data=b"\x00", padding_bits=7),
241+
b"\x03\x02\x07\x00",
242+
),
243+
(
244+
asn1.BitString(data=b"\x80", padding_bits=7),
245+
b"\x03\x02\x07\x80",
246+
),
247+
(
248+
asn1.BitString(data=b"\x81\xf0", padding_bits=4),
249+
b"\x03\x03\x04\x81\xf0",
250+
),
251+
]
252+
)
253+
254+
def test_fail_bitstring(self) -> None:
255+
with pytest.raises(ValueError, match="error parsing asn1 value"):
256+
# Prefix with number of padding bits missing
257+
asn1.decode_der(asn1.BitString, b"\x03\x00")
258+
259+
with pytest.raises(ValueError, match="error parsing asn1 value"):
260+
# Non-zero padding bits
261+
asn1.decode_der(asn1.BitString, b"\x03\x02\x07\x01")
262+
263+
with pytest.raises(ValueError, match="error parsing asn1 value"):
264+
# Non-zero padding bits
265+
asn1.decode_der(asn1.BitString, b"\x03\x02\x07\x40")
266+
267+
with pytest.raises(ValueError, match="error parsing asn1 value"):
268+
# Padding bits > 7
269+
asn1.decode_der(asn1.BitString, b"\x03\x02\x08\x00")
270+
271+
227272
class TestSequence:
228273
def test_ok_sequence_single_field(self) -> None:
229274
@asn1.sequence
@@ -492,12 +537,20 @@ class Example:
492537
e: typing.Union[asn1.UtcTime, None]
493538
f: typing.Union[asn1.GeneralizedTime, None]
494539
g: typing.Union[typing.List[int], None]
540+
h: typing.Union[asn1.BitString, None]
495541

496542
assert_roundtrips(
497543
[
498544
(
499545
Example(
500-
a=None, b=None, c=None, d=None, e=None, f=None, g=None
546+
a=None,
547+
b=None,
548+
c=None,
549+
d=None,
550+
e=None,
551+
f=None,
552+
g=None,
553+
h=None,
501554
),
502555
b"\x30\x00",
503556
)

0 commit comments

Comments
 (0)