Skip to content

Commit

Permalink
Initial RFC 5280 test cases (#7)
Browse files Browse the repository at this point in the history
* assets: fix issuers

These were accidentally skipping a level.

Signed-off-by: William Woodruff <william@trailofbits.com>

* limbo: begin work on 5280 testcases

Signed-off-by: William Woodruff <william@trailofbits.com>

* reformat, linting

Signed-off-by: William Woodruff <william@trailofbits.com>

* cleanup

Signed-off-by: William Woodruff <william@trailofbits.com>

* limbo: add a stub webpki mod

Signed-off-by: William Woodruff <william@trailofbits.com>

* testcases/rfc5280: critical aki, unknown ext

* limbo: replace `v3_root_ca`, more rfc5280 tests

* limbo.json: regenerate

Signed-off-by: Andrew Pan <a@tny.town>

* limbo: AKI, SAN test cases

Signed-off-by: Andrew Pan <a@tny.town>

* limbo: conform to CA/B on partial wildcards

Signed-off-by: Andrew Pan <a@tny.town>

* Update limbo/testcases/webpki.py

Co-authored-by: William Woodruff <william@trailofbits.com>

* gocryptox509: implement skipping

Signed-off-by: Andrew Pan <a@tny.town>

* limbo: rework `Builder.intermediate_ca`

Signed-off-by: Andrew Pan <a@tny.town>

* webpki: `cryptography.io` chain happy path test

Signed-off-by: Andrew Pan <a@tny.town>

* gocryptox509: squash bug with ExpectedResult

Signed-off-by: Andrew Pan <a@tny.town>

* `make reformat`

Signed-off-by: Andrew Pan <a@tny.town>

* webpki: negative `cryptography.io` case

Signed-off-by: Andrew Pan <a@tny.town>

* assets: cryptography.io.cer -> cryptography.io.pem

Signed-off-by: Andrew Pan <a@tny.town>

* webpki: chain of pain case

Signed-off-by: Andrew Pan <a@tny.town>

* webpki: `chain_untrusted_root`

Signed-off-by: Andrew Pan <a@tny.town>

* `make reformat`

Signed-off-by: Andrew Pan <a@tny.town>

* limbo: use `importlib.resources` for `_assets`

Signed-off-by: Andrew Pan <a@tny.town>

* `make reformat`

Signed-off-by: Andrew Pan <a@tny.town>

---------

Signed-off-by: William Woodruff <william@trailofbits.com>
Signed-off-by: Andrew Pan <a@tny.town>
Co-authored-by: Andrew Pan <a@tny.town>
Co-authored-by: Andrew Pan <3821575+tnytown@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 11, 2023
1 parent e649e56 commit 6182708
Show file tree
Hide file tree
Showing 10 changed files with 1,326 additions and 166 deletions.
3 changes: 2 additions & 1 deletion harness/gocryptox509/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,12 @@ func evaluateTestcase(testcase Testcase) (testcaseResult, error) {
ts, err = time.Parse(time.RFC3339, *testcase.ValidationTime)

if err != nil {
fmt.Printf("%s\n", err)
return testcaseSkipped, errors.Wrap(err, "unable to parse testcase time as RFC3339")
}
}

expectSuccess := testcase.ExpectedResult == testcasePassed
expectSuccess := testcaseResult(testcase.ExpectedResult.(string)) == testcasePassed

// TODO: Support testcases that constrain signature algorthms.
if len(testcase.SignatureAlgorithms) != 0 {
Expand Down
421 changes: 399 additions & 22 deletions limbo.json

Large diffs are not rendered by default.

Empty file added limbo/_assets/__init__.py
Empty file.
91 changes: 91 additions & 0 deletions limbo/_assets/cryptography.io.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
-----BEGIN CERTIFICATE-----
MIIFJDCCBAygAwIBAgISBCjrgR1TEHICklNpQDzj1PqPMA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yMzA1MjkxODQ0MDBaFw0yMzA4MjcxODQzNTlaMBoxGDAWBgNVBAMT
D2NyeXB0b2dyYXBoeS5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AObo0GReSiFFL4eMlFHutcV+LpLDorPpzzFxxJsXhrm19GyWYHdr4ml7GIAEjqI7
QZp0aYw1lmtHwgNnaRySU+aWj6LMWI/rIP5rXZYIZLyXSfLbHP0xlfYEvcrcprOm
Au0YuQgy3TBO0qz6FKx5PtfbDc7p/LYD5tnG5NkbQ4o+7Ko361w787WSb8OV5NFd
nPqSeIjwxqSy62G6oOHL4wRFDTCOdNjHeYJnPC0L3P9qkGeC6zjqt2h8Q+GE9zNQ
enqaEOeBIZo46mti6Tvzzc7dqILw1ATqIXJdjwABzuT8Ob34/LsPorLQoRP1+YHF
++D2JyyvYKM/aFpQI+HHfGUCAwEAAaOCAkowggJGMA4GA1UdDwEB/wQEAwIFoDAd
BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNV
HQ4EFgQUOtGXHs6fLoMQEwjlwSu88r4qLf0wHwYDVR0jBBgwFoAUFC6zF7dYVsuu
UAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8v
cjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNyLm9y
Zy8wGgYDVR0RBBMwEYIPY3J5cHRvZ3JhcGh5LmlvMEwGA1UdIARFMEMwCAYGZ4EM
AQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0
c2VuY3J5cHQub3JnMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHcAtz77JN+cTbp1
8jnFulj0bF38Qs96nzXEnh0JgSXttJkAAAGIaQn30wAABAMASDBGAiEAglrQJj8G
a7/1upmZ2Is6AqPT9pQpSty0sH4PgnqyQxICIQDEpKnk6Rt6KzvEpIIIEtXgrYx+
crerlx4SQVQbnwfz0gB1AHoyjFTYty22IOo44FIe6YQWcDIThU070ivBOlejUutS
AAABiGkJ9+sAAAQDAEYwRAIgaLwFE4CfhV09wq5IR5zmo/90y5OQJ2MnW5gpRZZh
s4YCICEAGxUN/f95xFmxOCfqXv3SEozwkrMHA33abVjCQiaGMA0GCSqGSIb3DQEB
CwUAA4IBAQBSTN5U/3yp6cGMBXlS5WcrB/XOY6TtxPmeSvLM3vqNbpRGu1JOFFtn
31eweHOTj66GWowSy9+uAhp1V9Uf0hoJMa/b+CkCelyJN4QZCcMfhKrPAD4prbHa
GYFaLo5SQqkK1hYHo9LH+qhaOBx9hF5aLrGbEFWXQE9/W7KSeCzz6LBLw9xVrB2v
NTLlXXt5tUiczOIzge5KGaSQr5wgc1viddcRsYuZjtgWlqJ5E5QcZxD8xLTfBe5W
9vl/k1CB4CZ1IG8Sa9+n91Kxm3HTLL6TcrEOutChwMfZfrLH/piWoRQxezCpn82N
RaeeHd1Bv3oH3SeVJUHLxgzUv/dh6GSi
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
nLRbwHOoq7hHwg==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
163 changes: 41 additions & 122 deletions limbo/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,43 @@
import logging
from dataclasses import dataclass
from functools import cache, cached_property
from importlib import resources
from typing import Generic, TypeVar

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from cryptography.x509 import NameOID
from cryptography.x509 import ExtensionType, NameOID, SubjectAlternativeName

_EPOCH = datetime.datetime.fromtimestamp(0)
_ONE_THOUSAND_YEARS_OF_TORMENT = _EPOCH + datetime.timedelta(days=365 * 1000)
_ASSETS_PATH = resources.files("limbo._assets")
_ExtensionType = TypeVar("_ExtensionType", bound=ExtensionType)


logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class CertificatePair:
class Certificate:
"""
An X.509 certificate and its associated private key.
An X.509 certificate.
"""

cert: x509.Certificate

@cached_property
def cert_pem(self) -> str:
return self.cert.public_bytes(encoding=serialization.Encoding.PEM).decode()


@dataclass(frozen=True)
class CertificatePair(Certificate):
"""
An X.509 certificate and its associated private key.
"""

key: PrivateKeyTypes

@cached_property
Expand All @@ -39,135 +55,31 @@ def key_pem(self) -> str:
encryption_algorithm=serialization.NoEncryption(),
).decode()

@cached_property
def cert_pem(self) -> str:
return self.cert.public_bytes(encoding=serialization.Encoding.PEM).decode()


@cache
def v3_root_ca(*, aki: bool = True, ski: bool = True) -> CertificatePair:
@dataclass(frozen=True)
class _Extension(Generic[_ExtensionType]):
"""
A (configurable) X.509v3 root CA.
An X.509 extension and its criticality.
"""
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)

builder = x509.CertificateBuilder()
builder = builder.subject_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, "x509-limbo-root"),
]
)
)
builder = builder.issuer_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, "x509-limbo-root"),
]
)
)
builder = builder.not_valid_before(_EPOCH)
builder = builder.not_valid_after(_ONE_THOUSAND_YEARS_OF_TORMENT)
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(key.public_key())
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
critical=False,
)
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=False,
key_cert_sign=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=False,
)
certificate = builder.sign(
private_key=key,
algorithm=hashes.SHA256(),
)
ext: _ExtensionType
critical: bool

return CertificatePair(certificate, key)


@cache
def intermediate_ca_pathlen_n(parent: CertificatePair, pathlen: int) -> CertificatePair:
def ext(extension: _ExtensionType, *, critical: bool) -> _Extension[_ExtensionType]:
"""
An intermediate CA chained up to a root CA.
The intermediate CA has a `pathlen:N` constraint, where `N` varies.
These intermediates can be used to assert various behaviors, including:
* That `pathlen:N` constraints are properly honored;
* That certificates are correctly uniqued by both their key **and** their
subject (as each intermediate generated here shares the same key)
Constructs a new _Extension to pass into certificate builder helpers.
"""
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)

builder = x509.CertificateBuilder()
builder = builder.subject_name(
x509.Name(
[
x509.NameAttribute(
NameOID.COMMON_NAME, f"x509-limbo-intermediate-pathlen-{pathlen}"
),
]
)
)
builder = builder.issuer_name(parent.cert.subject)
builder = builder.not_valid_before(_EPOCH)
builder = builder.not_valid_after(_ONE_THOUSAND_YEARS_OF_TORMENT)
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(key.public_key())
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
critical=False,
)
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(
parent.key.public_key() # type: ignore[arg-type]
),
critical=False,
)
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=pathlen),
critical=True,
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=False,
key_cert_sign=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=False,
)
certificate = builder.sign(
private_key=parent.key, # type: ignore[arg-type]
algorithm=hashes.SHA256(),
)

return CertificatePair(certificate, key)
return _Extension(extension, critical)


@cache
def ee_cert(parent: CertificatePair) -> CertificatePair:
def ee_cert(
parent: CertificatePair,
subject_alternative_name: _Extension[SubjectAlternativeName] | None = None,
*,
extra_extension: _Extension | None = None,
) -> CertificatePair:
"""
Produces an end-entity (EE) certificate, signed by the given `parent`'s
key.
Expand Down Expand Up @@ -200,7 +112,7 @@ def ee_cert(parent: CertificatePair) -> CertificatePair:
)
builder = builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
critical=False,
)
builder = builder.add_extension(
x509.KeyUsage(
Expand All @@ -216,6 +128,13 @@ def ee_cert(parent: CertificatePair) -> CertificatePair:
),
critical=False,
)
if subject_alternative_name is not None:
builder = builder.add_extension(
subject_alternative_name.ext, subject_alternative_name.critical
)
if extra_extension is not None:
builder = builder.add_extension(extra_extension.ext, extra_extension.critical)

certificate = builder.sign(
private_key=parent.key, # type: ignore[arg-type]
algorithm=hashes.SHA256(),
Expand Down
2 changes: 2 additions & 0 deletions limbo/testcases/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from limbo.testcases._core import registry

from .pathlen import * # noqa: F403
from .rfc5280 import * # noqa: F403
from .webpki import * # noqa: F403

__all__ = [
"registry",
Expand Down
Loading

0 comments on commit 6182708

Please sign in to comment.