@@ -3,7 +3,11 @@ use rand::TryRngCore;
33use sqlx:: types:: BigDecimal ;
44use 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].
812pub 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) ]
35101mod 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