Repository: pyca/cryptography (Python)

Issue:
Possible bug: empty sequence in NameConstraints
Hi,

Now I've been going through various RFCs again and again, and I'm still not quite sure if this is a subtle bug in cryptography, or if this is actually valid x509.  Regarding `permittedSubtrees` and `excludedSubtrees`: Is an empty sequence valid at all (and if yes, what are the semantics?).

cryptography does not treat an empty list any different then any other list, but treats `None` special. That means, you can even create an extension with an empty _list_ of permitted/excluded subtrees, while of course passing None raises an exception:

```python
>>> x509.NameConstraints(permitted_subtrees=[], excluded_subtrees=[])
<NameConstraints(permitted_subtrees=[], excluded_subtrees=[])>
>>> x509.NameConstraints(permitted_subtrees=None, excluded_subtrees=None)
...
    ValueError: At least one of permitted_subtrees and excluded_subtrees must not be None
```

The public bytes generated for an empty list also differ to a `None` value:

```
>>> x509.NameConstraints(permitted_subtrees=[x509.DNSName('example.com')], excluded_subtrees=None).public_bytes()
b'0\x11\xa0\x0f0\r\x82\x0bexample.com'
>>> x509.NameConstraints(permitted_subtrees=[x509.DNSName('example.com')], excluded_subtrees=[]).public_bytes()
b'0\x13\xa0\x0f0\r\x82\x0bexample.com\xa1\x00'
```

So is an empty list something we need to support? Or is it actually a bug?

Slightly related side-note: can we make both values default to `None` in the constructor, that way you can also pass just one of the values when creating the extension. If you agree, I will submit an MR.



Discussion:
Yes, this is a bug, we should be rejecting non-none values that are less than 0 because:

```
GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
```
GREAT! (I was hoping for that). I will submit a PR.
Great, thank you!
> Slightly related side-note: can we make both values default to None in the constructor, that way you can also pass just one of the values when creating the extension. If you agree, I will submit an MR.

And what do you think? :-)
We generally avoid having defaults in APIs -- to ensure that consumers aren't accidentally missing details that are relevant. We know it's a bit more verbose, but we think it's the right trade-off.



Patch:
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
index 725bd1334cbf..56427b3d5958 100644
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -1254,6 +1254,10 @@ def __init__(
     ) -> None:
         if permitted_subtrees is not None:
             permitted_subtrees = list(permitted_subtrees)
+            if not permitted_subtrees:
+                raise ValueError(
+                    "permitted_subtrees must be a non-empty list or None"
+                )
             if not all(isinstance(x, GeneralName) for x in permitted_subtrees):
                 raise TypeError(
                     "permitted_subtrees must be a list of GeneralName objects "
@@ -1264,6 +1268,10 @@ def __init__(
 
         if excluded_subtrees is not None:
             excluded_subtrees = list(excluded_subtrees)
+            if not excluded_subtrees:
+                raise ValueError(
+                    "excluded_subtrees must be a non-empty list or None"
+                )
             if not all(isinstance(x, GeneralName) for x in excluded_subtrees):
                 raise TypeError(
                     "excluded_subtrees must be a list of GeneralName objects "


Test patch:
diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py
index f9d117ffefa6..cfd85de22a11 100644
--- a/tests/x509/test_x509_ext.py
+++ b/tests/x509/test_x509_ext.py
@@ -3689,6 +3689,12 @@ def test_iter_input(self):
         assert nc.excluded_subtrees is not None
         assert list(nc.excluded_subtrees) == subtrees
 
+    def test_empty_lists(self):
+        with pytest.raises(ValueError):
+            x509.NameConstraints(permitted_subtrees=None, excluded_subtrees=[])
+        with pytest.raises(ValueError):
+            x509.NameConstraints(permitted_subtrees=[], excluded_subtrees=None)
+
     def test_repr(self):
         permitted = [x509.DNSName("name.local"), x509.DNSName("name2.local")]
         nc = x509.NameConstraints(
@@ -3911,7 +3917,7 @@ def test_certbuilder(self, backend):
         builder = builder.add_extension(
             NameConstraints(
                 permitted_subtrees=list(map(DNSName, permitted)),
-                excluded_subtrees=[],
+                excluded_subtrees=None,
             ),
             True,
         )