Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/cryptography/hazmat/asn1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# for complete details.

from cryptography.hazmat.asn1.asn1 import (
BitString,
Default,
Explicit,
GeneralizedTime,
Expand All @@ -16,6 +17,7 @@
)

__all__ = [
"BitString",
"Default",
"Explicit",
"GeneralizedTime",
Expand Down
1 change: 1 addition & 0 deletions src/cryptography/hazmat/asn1/asn1.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,4 @@ class Default(typing.Generic[U]):
PrintableString = declarative_asn1.PrintableString
UtcTime = declarative_asn1.UtcTime
GeneralizedTime = declarative_asn1.GeneralizedTime
BitString = declarative_asn1.BitString
7 changes: 7 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ class GeneralizedTime:
def __repr__(self) -> str: ...
def __eq__(self, other: object) -> bool: ...
def as_datetime(self) -> datetime.datetime: ...

class BitString:
def __new__(cls, data: bytes, padding_bits: int) -> BitString: ...
def __repr__(self) -> str: ...
def __eq__(self, other: object) -> bool: ...
def as_bytes(self) -> bytes: ...
def padding_bits(self) -> int: ...
20 changes: 19 additions & 1 deletion src/rust/src/declarative_asn1/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use pyo3::types::PyListMethods;

use crate::asn1::big_byte_slice_to_py_int;
use crate::declarative_asn1::types::{
type_to_tag, AnnotatedType, Encoding, GeneralizedTime, PrintableString, Type, UtcTime,
type_to_tag, AnnotatedType, BitString, Encoding, GeneralizedTime, PrintableString, Type,
UtcTime,
};
use crate::error::CryptographyError;

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

fn decode_bitstring<'a>(
py: pyo3::Python<'a>,
parser: &mut Parser<'a>,
encoding: &Option<pyo3::Py<Encoding>>,
) -> ParseResult<pyo3::Bound<'a, BitString>> {
let value = read_value::<asn1::BitString<'a>>(parser, encoding)?;
let data = pyo3::types::PyBytes::new(py, value.as_bytes()).unbind();
Ok(pyo3::Bound::new(
py,
BitString {
data,
padding_bits: value.padding_bits(),
},
)?)
}

pub(crate) fn decode_annotated_type<'a>(
py: pyo3::Python<'a>,
parser: &mut Parser<'a>,
Expand Down Expand Up @@ -201,6 +218,7 @@ pub(crate) fn decode_annotated_type<'a>(
Type::PrintableString() => decode_printable_string(py, parser, encoding)?.into_any(),
Type::UtcTime() => decode_utc_time(py, parser, encoding)?.into_any(),
Type::GeneralizedTime() => decode_generalized_time(py, parser, encoding)?.into_any(),
Type::BitString() => decode_bitstring(py, parser, encoding)?.into_any(),
};

match &ann_type.annotation.get().default {
Expand Down
13 changes: 12 additions & 1 deletion src/rust/src/declarative_asn1/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use pyo3::types::PyAnyMethods;
use pyo3::types::PyListMethods;

use crate::declarative_asn1::types::{
AnnotatedType, AnnotatedTypeObject, Encoding, GeneralizedTime, PrintableString, Type, UtcTime,
AnnotatedType, AnnotatedTypeObject, BitString, Encoding, GeneralizedTime, PrintableString,
Type, UtcTime,
};

fn write_value<T: SimpleAsn1Writable>(
Expand Down Expand Up @@ -171,6 +172,16 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> {
.map_err(|_| asn1::WriteError::AllocationError)?;
write_value(writer, &generalized_time, encoding)
}
Type::BitString() => {
let val: &pyo3::Bound<'_, BitString> = value
.cast()
.map_err(|_| asn1::WriteError::AllocationError)?;

let bitstring: asn1::BitString<'_> =
asn1::BitString::new(val.get().data.as_bytes(py), val.get().padding_bits)
.ok_or(asn1::WriteError::AllocationError)?;
write_value(writer, &bitstring, encoding)
}
}
}
}
55 changes: 54 additions & 1 deletion src/rust/src/declarative_asn1/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub enum Type {
/// The first element is the Python class that represents the sequence,
/// the second element is a dict of the (already converted) fields of the class.
Sequence(pyo3::Py<pyo3::types::PyType>, pyo3::Py<pyo3::types::PyDict>),
/// SEQUENCEOF (`list[`T`]`)
/// SEQUENCE OF (`list[`T`]`)
SequenceOf(pyo3::Py<AnnotatedType>),
/// OPTIONAL (`T | None`)
Option(pyo3::Py<AnnotatedType>),
Expand All @@ -40,6 +40,8 @@ pub enum Type {
UtcTime(),
/// GeneralizedTime (`datetime`)
GeneralizedTime(),
/// BIT STRING (`bytes`)
BitString(),
}

/// A type that we know how to encode/decode, along with any
Expand Down Expand Up @@ -249,6 +251,54 @@ impl GeneralizedTime {
}
}

#[derive(pyo3::FromPyObject)]
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.asn1")]
pub struct BitString {
pub(crate) data: pyo3::Py<pyo3::types::PyBytes>,
pub(crate) padding_bits: u8,
}

#[pyo3::pymethods]
impl BitString {
#[new]
#[pyo3(signature = (data, padding_bits,))]
fn new(
py: pyo3::Python<'_>,
data: pyo3::Py<pyo3::types::PyBytes>,
padding_bits: u8,
) -> pyo3::PyResult<Self> {
if asn1::BitString::new(data.as_bytes(py), padding_bits).is_none() {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"invalid BIT STRING: data: {data}, padding_bits: {padding_bits}"
)));
}

Ok(BitString { data, padding_bits })
}

#[pyo3(signature = ())]
pub fn as_bytes(&self, py: pyo3::Python<'_>) -> pyo3::Py<pyo3::types::PyBytes> {
self.data.clone_ref(py)
}

#[pyo3(signature = ())]
pub fn padding_bits(&self) -> u8 {
self.padding_bits
}

fn __eq__(&self, py: pyo3::Python<'_>, other: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<bool> {
Ok((**self.data.bind(py)).eq(other.data.bind(py))?
&& self.padding_bits == other.padding_bits)
}

pub fn __repr__(&self) -> pyo3::PyResult<String> {
Ok(format!(
"BitString(data: {}, padding_bits: {})",
self.data, self.padding_bits,
))
}
}

/// Utility function for converting builtin Python types
/// to their Rust `Type` equivalent.
#[pyo3::pyfunction]
Expand All @@ -270,6 +320,8 @@ pub fn non_root_python_to_rust<'p>(
Type::UtcTime().into_pyobject(py)
} else if class.is(GeneralizedTime::type_object(py)) {
Type::GeneralizedTime().into_pyobject(py)
} else if class.is(BitString::type_object(py)) {
Type::BitString().into_pyobject(py)
} else {
Err(pyo3::exceptions::PyTypeError::new_err(format!(
"cannot handle type: {class:?}"
Expand Down Expand Up @@ -326,6 +378,7 @@ pub(crate) fn type_to_tag(t: &Type, encoding: &Option<pyo3::Py<Encoding>>) -> as
Type::PrintableString() => asn1::PrintableString::TAG,
Type::UtcTime() => asn1::UtcTime::TAG,
Type::GeneralizedTime() => asn1::GeneralizedTime::TAG,
Type::BitString() => asn1::BitString::TAG,
};

match encoding {
Expand Down
4 changes: 2 additions & 2 deletions src/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ mod _rust {

#[pymodule_export]
use crate::declarative_asn1::types::{
non_root_python_to_rust, AnnotatedType, Annotation, Encoding, GeneralizedTime,
PrintableString, Size, Type, UtcTime,
non_root_python_to_rust, AnnotatedType, Annotation, BitString, Encoding,
GeneralizedTime, PrintableString, Size, Type, UtcTime,
};
}

Expand Down
29 changes: 29 additions & 0 deletions tests/hazmat/asn1/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,35 @@ def test_invalid_generalized_time(self) -> None:
# We don't allow naive datetime objects
asn1.GeneralizedTime(datetime.datetime(2000, 1, 1, 10, 10, 10))

def test_bitstring_getters(self) -> None:
data = b"\x01\x02\x30"
bt = asn1.BitString(data=data, padding_bits=2)

assert bt.as_bytes() == data
assert bt.padding_bits() == 2

def test_repr_bitstring(self) -> None:
data = b"\x01\x02\x30"
assert (
repr(asn1.BitString(data, 2))
== f"BitString(data: {data!r}, padding_bits: 2)"
)

def test_invalid_bitstring(self) -> None:
with pytest.raises(
ValueError,
match="invalid BIT STRING",
):
# Padding bits cannot be > 7
asn1.BitString(data=b"\x01\x02\x03", padding_bits=8)

with pytest.raises(
ValueError,
match="invalid BIT STRING",
):
# Padding bits have to be zero
asn1.BitString(data=b"\x01\x02\x03", padding_bits=2)


class TestSequenceAPI:
def test_fail_unsupported_field(self) -> None:
Expand Down
55 changes: 54 additions & 1 deletion tests/hazmat/asn1/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,51 @@ def test_generalized_time(self) -> None:
)


class TestBitString:
def test_ok_bitstring(self) -> None:
assert_roundtrips(
[
(
asn1.BitString(data=b"\x6e\x5d\xc0", padding_bits=6),
b"\x03\x04\x06\x6e\x5d\xc0",
),
(
asn1.BitString(data=b"", padding_bits=0),
b"\x03\x01\x00",
),
(
asn1.BitString(data=b"\x00", padding_bits=7),
b"\x03\x02\x07\x00",
),
(
asn1.BitString(data=b"\x80", padding_bits=7),
b"\x03\x02\x07\x80",
),
(
asn1.BitString(data=b"\x81\xf0", padding_bits=4),
b"\x03\x03\x04\x81\xf0",
),
]
)

def test_fail_bitstring(self) -> None:
with pytest.raises(ValueError, match="error parsing asn1 value"):
# Prefix with number of padding bits missing
asn1.decode_der(asn1.BitString, b"\x03\x00")

with pytest.raises(ValueError, match="error parsing asn1 value"):
# Non-zero padding bits
asn1.decode_der(asn1.BitString, b"\x03\x02\x07\x01")

with pytest.raises(ValueError, match="error parsing asn1 value"):
# Non-zero padding bits
asn1.decode_der(asn1.BitString, b"\x03\x02\x07\x40")

with pytest.raises(ValueError, match="error parsing asn1 value"):
# Padding bits > 7
asn1.decode_der(asn1.BitString, b"\x03\x02\x08\x00")


class TestSequence:
def test_ok_sequence_single_field(self) -> None:
@asn1.sequence
Expand Down Expand Up @@ -492,12 +537,20 @@ class Example:
e: typing.Union[asn1.UtcTime, None]
f: typing.Union[asn1.GeneralizedTime, None]
g: typing.Union[typing.List[int], None]
h: typing.Union[asn1.BitString, None]

assert_roundtrips(
[
(
Example(
a=None, b=None, c=None, d=None, e=None, f=None, g=None
a=None,
b=None,
c=None,
d=None,
e=None,
f=None,
g=None,
h=None,
),
b"\x30\x00",
)
Expand Down