Repository: pyca/cryptography

Issue:
Non-RFC conformant X509 certificate serial number is generated
https://tools.ietf.org/html/rfc5280  4.1.2.2.  Serial Numbers

`Conforming CAs MUST NOT  use serialNumber values longer than 20 octets.`

Well, let's look to source code:

cryptography/hazmat/backends/openssl/backend.py :

``` python
def _int_to_bn(self, num, bn=None):
    ...
    binary = num.to_bytes(int(num.bit_length() / 8.0 + 1), "big")
    ...
```

cryptography/x509/base.py:

``` python
class CertificateBuilder:
    def serial_number(self, number):
        if utils.bit_length(number) > 160
```

Since serial number should be positive, for my example below it should add additional zero.

Sample program:

``` python
import datetime
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.x509 import NameOID
key = rsa.generate_private_key(public_exponent=65537, key_size=512, backend=default_backend())
serial = int.from_bytes(b'\xff' * 20, 'big')
subj = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'qwe')])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subj)
builder = builder.issuer_name(subj)
builder = builder.not_valid_before(datetime.datetime.today())
builder = builder.not_valid_after(datetime.datetime.today())
builder = builder.serial_number(serial)
builder = builder.public_key(key.public_key())
cert = builder.sign(private_key=key, algorithm=SHA256(), backend=default_backend())
print(cert.public_bytes(encoding=serialization.Encoding.PEM).decode('ascii'))
```

`./qwe.py | openssl asn1parse -inform pem  -i` gives:

```
...
13:d=2  hl=2 l=  21 prim:   INTEGER           :FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
...
```

Note 21 byte (!)



Discussion:
![bug](https://cloud.githubusercontent.com/assets/1157412/17203553/a883ecb4-54b9-11e6-87fd-5baf6ab5612c.png)

Note leading zero in serial number

The problem in that `bit_length()` mean length in bits of number in UNSIGNED representation, while ASN.1 requires integer to be signed.

```
6.5.3.15. Unsigned Integer

          An integer that is limited in range to non-negative values 
          including zero. For the purposes of this Standard, the sign
          bit is considered part of the Unsigned Integer value. This 
          type cannot be expressed as an ASN.1 INTEGER type, because
          an INTEGER always uses the sign bit.
```

Also, 
1. `int(num.bit_length() / 8.0 + 1` should be replaced with `(num.bit_length() + 7) // 8`. Using floating point to calculate byte lengths is overkill.
2. zero serial number should be just disallowed, or workaround should be done (`if num == 0: return 1`)
3. negative serial numbers should be disallowed at all.

Thanks for the report, this is indeed a bug. Your third point should be covered by:

``` python
        if number < 0:
            raise ValueError('The serial number should be non-negative.')
```

Which is the check prior to the bit length check, but we should alter it to encompass 0 and update the text to say "Serial numbers must be positive integers" or something to that effect.

Are you interested in providing a PR?


Resolving patch:
diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py
--- a/src/cryptography/x509/base.py
+++ b/src/cryptography/x509/base.py
@@ -425,10 +425,13 @@ def serial_number(self, number):
             raise TypeError('Serial number must be of integral type.')
         if self._serial_number is not None:
             raise ValueError('The serial number may only be set once.')
-        if number < 0:
-            raise ValueError('The serial number should be non-negative.')
-        if utils.bit_length(number) > 160:  # As defined in RFC 5280
-            raise ValueError('The serial number should not be more than 160 '
+        if number <= 0:
+            raise ValueError('The serial number should be positive.')
+
+        # ASN.1 integers are always signed, so most significant bit must be
+        # zero.
+        if utils.bit_length(number) >= 160:  # As defined in RFC 5280
+            raise ValueError('The serial number should not be more than 159 '
                              'bits.')
         return CertificateBuilder(
             self._issuer_name, self._subject_name,
@@ -635,10 +638,13 @@ def serial_number(self, number):
             raise TypeError('Serial number must be of integral type.')
         if self._serial_number is not None:
             raise ValueError('The serial number may only be set once.')
-        if number < 0:
-            raise ValueError('The serial number should be non-negative.')
-        if utils.bit_length(number) > 160:  # As defined in RFC 5280
-            raise ValueError('The serial number should not be more than 160 '
+        if number <= 0:
+            raise ValueError('The serial number should be positive')
+
+        # ASN.1 integers are always signed, so most significant bit must be
+        # zero.
+        if utils.bit_length(number) >= 160:  # As defined in RFC 5280
+            raise ValueError('The serial number should not be more than 159 '
                              'bits.')
         return RevokedCertificateBuilder(
             number, self._revocation_date, self._extensions


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -1689,12 +1689,55 @@ def test_serial_number_must_be_an_integer_type(self):
 
     def test_serial_number_must_be_non_negative(self):
         with pytest.raises(ValueError):
-            x509.CertificateBuilder().serial_number(-10)
+            x509.CertificateBuilder().serial_number(-1)
+
+    def test_serial_number_must_be_positive(self):
+        with pytest.raises(ValueError):
+            x509.CertificateBuilder().serial_number(0)
+
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_minimal_serial_number(self, backend):
+        subject_private_key = RSA_KEY_2048.private_key(backend)
+        builder = x509.CertificateBuilder().serial_number(
+            1
+        ).subject_name(x509.Name([
+            x509.NameAttribute(NameOID.COUNTRY_NAME, u'RU'),
+        ])).issuer_name(x509.Name([
+            x509.NameAttribute(NameOID.COUNTRY_NAME, u'RU'),
+        ])).public_key(
+            subject_private_key.public_key()
+        ).not_valid_before(
+            datetime.datetime(2002, 1, 1, 12, 1)
+        ).not_valid_after(
+            datetime.datetime(2030, 12, 31, 8, 30)
+        )
+        cert = builder.sign(subject_private_key, hashes.SHA256(), backend)
+        assert cert.serial_number == 1
+
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_biggest_serial_number(self, backend):
+        subject_private_key = RSA_KEY_2048.private_key(backend)
+        builder = x509.CertificateBuilder().serial_number(
+            (1 << 159) - 1
+        ).subject_name(x509.Name([
+            x509.NameAttribute(NameOID.COUNTRY_NAME, u'RU'),
+        ])).issuer_name(x509.Name([
+            x509.NameAttribute(NameOID.COUNTRY_NAME, u'RU'),
+        ])).public_key(
+            subject_private_key.public_key()
+        ).not_valid_before(
+            datetime.datetime(2002, 1, 1, 12, 1)
+        ).not_valid_after(
+            datetime.datetime(2030, 12, 31, 8, 30)
+        )
+        cert = builder.sign(subject_private_key, hashes.SHA256(), backend)
+        assert cert.serial_number == (1 << 159) - 1
 
     def test_serial_number_must_be_less_than_160_bits_long(self):
         with pytest.raises(ValueError):
-            # 2 raised to the 160th power is actually 161 bits
-            x509.CertificateBuilder().serial_number(2 ** 160)
+            x509.CertificateBuilder().serial_number(1 << 159)
 
     def test_serial_number_may_only_be_set_once(self):
         builder = x509.CertificateBuilder().serial_number(10)
diff --git a/tests/test_x509_revokedcertbuilder.py b/tests/test_x509_revokedcertbuilder.py
--- a/tests/test_x509_revokedcertbuilder.py
+++ b/tests/test_x509_revokedcertbuilder.py
@@ -21,10 +21,37 @@ def test_serial_number_must_be_non_negative(self):
         with pytest.raises(ValueError):
             x509.RevokedCertificateBuilder().serial_number(-1)
 
+    def test_serial_number_must_be_positive(self):
+        with pytest.raises(ValueError):
+            x509.RevokedCertificateBuilder().serial_number(0)
+
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_minimal_serial_number(self, backend):
+        revocation_date = datetime.datetime(2002, 1, 1, 12, 1)
+        builder = x509.RevokedCertificateBuilder().serial_number(
+            1
+        ).revocation_date(
+            revocation_date
+        )
+
+        revoked_certificate = builder.build(backend)
+        assert revoked_certificate.serial_number == 1
+
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_biggest_serial_number(self, backend):
+        revocation_date = datetime.datetime(2002, 1, 1, 12, 1)
+        builder = x509.RevokedCertificateBuilder().serial_number(
+            (1 << 159) - 1
+        ).revocation_date(
+            revocation_date
+        )
+
+        revoked_certificate = builder.build(backend)
+        assert revoked_certificate.serial_number == (1 << 159) - 1
+
     def test_serial_number_must_be_less_than_160_bits_long(self):
         with pytest.raises(ValueError):
-            # 2 raised to the 160th power is actually 161 bits
-            x509.RevokedCertificateBuilder().serial_number(2 ** 160)
+            x509.RevokedCertificateBuilder().serial_number(1 << 159)
 
     def test_set_serial_number_twice(self):
         builder = x509.RevokedCertificateBuilder().serial_number(3)