Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.

Commit 9a66867

Browse files
committed
feat: expand and improve SerialNumber type
1 parent 07349a0 commit 9a66867

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

src/database/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub struct IdCsr {
6464
pub for_actor_uaid: Uuid,
6565
pub actor_public_key_id: i64,
6666
pub actor_signature: String,
67-
pub session_id: SerialNumber,
67+
pub session_id: String, // TODO make this serialnumba
6868
pub valid_not_before: NaiveDateTime,
6969
pub valid_not_after: NaiveDateTime,
7070
pub extensions: String,

src/database/serial_number.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ use rand::TryRngCore;
33
use sqlx::types::BigDecimal;
44
use sqlx::{Decode, Encode};
55

6-
#[derive(Debug, Encode, Decode)]
6+
use crate::StdResult;
7+
8+
// TODO: This could be in polyproto instead
9+
10+
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)]
711
/// A serial number for a [polyproto::certs::IdCert].
812
pub struct SerialNumber(BigDecimal);
913

@@ -20,21 +24,85 @@ impl SerialNumber {
2024
) -> Result<Self, crate::errors::StdError> {
2125
let mut buf = [0u8; 20];
2226
rng.try_fill_bytes(&mut buf)?;
27+
Self::normalize_first_byte(&mut buf);
2328
Ok(Self(BigDecimal::from_biguint(BigUint::from_bytes_be(&buf), 0)))
2429
}
2530

2631
/// Derive [Self] from 20 bytes. These bytes should be sourced from a CSPRNG or another information
2732
/// source with high entropy.
33+
///
34+
/// ## Important
35+
///
36+
/// Serial numbers with an MSB which is larger than 127 in decimal are likely not considered valid
37+
/// in all x509 implementations. This is because RFC 5280 is ambiguous about whether numbers
38+
/// which result in 21 octets of ASN.1 Uint are considered valid. Sonata does not consider
39+
/// serial numbers with an MSB of > 127 valid for encoding, but it does consider them valid for
40+
/// decoding. It follows the `x509-cert` crate in this regard.
2841
pub fn new_from_bytes(bytes: [u8; 20]) -> Self {
2942
Self(BigDecimal::from_biguint(BigUint::from_bytes_be(&bytes), 0))
3043
}
44+
45+
/// ASN.1 and its consequences have been a disaster for the human race.
46+
///
47+
/// ## Preamble
48+
///
49+
/// This is just my understanding of the underlying problem. Feel free to correct me, if I am
50+
/// wrong.
51+
///
52+
/// ## Situation
53+
///
54+
/// The x509-cert crate says:
55+
///
56+
/// ```txt
57+
/// The user might give us a 20 byte unsigned integer with a high MSB,
58+
/// which we'd then encode with 21 octets to preserve the sign bit.
59+
/// RFC 5280 is ambiguous about whether this is valid, so we limit
60+
/// `SerialNumber` *encodings* to 20 bytes or fewer while permitting
61+
/// `SerialNumber` *decodings* to have up to 21 bytes below.
62+
/// ```
63+
///
64+
/// This means that the first octet of a 20-octet array may not be larger than 128 in decimal,
65+
/// if the resulting [SerialNumber] is to be a valid x509 serial number regardless of how
66+
/// you may interpret the spec.
67+
///
68+
/// ## The (hacky) solution
69+
///
70+
/// To adjust for this, we simply take the modulo 128 of the first octet in the array. This way,
71+
/// we have a lossy projection from the CSPRNG generated first-maybe-valid-octet to a
72+
/// definitely-valid first octet.
73+
///
74+
/// ## Is this cryptographically safe?
75+
///
76+
/// I don't know. :3
77+
///
78+
/// But I suspect that it is sufficient for this purpose, because serial numbers are not meant
79+
/// to be cryptographically safe on their own; they should likely just be random *enough*.
80+
/// In my head, the worst case is that instead of 160 bits of entropy, there will still be 159
81+
/// bits of entropy left, and that is still a lot of entropy.
82+
fn normalize_first_byte(buf: &mut [u8; 20]) {
83+
buf[0] %= 128;
84+
}
85+
}
86+
87+
impl From<polyproto::types::x509_cert::SerialNumber> for SerialNumber {
88+
fn from(value: polyproto::types::x509_cert::SerialNumber) -> Self {
89+
Self(BigDecimal::from_biguint(BigUint::from_bytes_be(value.as_bytes()), 0))
90+
}
91+
}
92+
93+
impl From<SerialNumber> for polyproto::types::x509_cert::SerialNumber {
94+
fn from(value: SerialNumber) -> Self {
95+
Self::from_bytes_be(value.0.into_bigint_and_scale().0.to_bytes_be().1.as_slice()).unwrap()
96+
}
3197
}
3298

3399
#[cfg(test)]
34100
#[allow(clippy::unwrap_used)]
35101
mod test {
36102
use rand::rng;
37103

104+
use crate::database::serial_number;
105+
38106
#[test]
39107
fn generate_random_serials() {
40108
let mut rng = rng();
@@ -51,4 +119,20 @@ mod test {
51119
let bytes = [1u8; 20];
52120
dbg!(super::SerialNumber::new_from_bytes(bytes));
53121
}
122+
123+
#[test]
124+
fn as_bytes_polyproto_eq_from_be_bytes() {
125+
let serial_number = super::SerialNumber::new_from_bytes([0; 20]);
126+
let p2_serial_number =
127+
polyproto::types::x509_cert::SerialNumber::from(serial_number.clone());
128+
let converted_back = super::SerialNumber::from(p2_serial_number);
129+
assert_eq!(converted_back, serial_number);
130+
for _ in 0..5000 {
131+
let serial_number = super::SerialNumber::try_generate_random(&mut rng()).unwrap();
132+
let p2_serial_number =
133+
polyproto::types::x509_cert::SerialNumber::from(serial_number.clone());
134+
let converted_back = super::SerialNumber::from(p2_serial_number);
135+
assert_eq!(converted_back, serial_number)
136+
}
137+
}
54138
}

0 commit comments

Comments
 (0)