Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion scapy/layers/tls/cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -1090,13 +1090,16 @@ def import_from_asn1pkt(self, cert):

self.pubkey = PubKey(bytes(tbsCert.subjectPublicKeyInfo))
self.subjectAltName = None
self.cA = False
self.pathLenConstraint = None

if tbsCert.extensions:
for extn in tbsCert.extensions:
if extn.extnID.oidname == "basicConstraints":
self.cA = False
if extn.extnValue.cA:
self.cA = not (extn.extnValue.cA.val == 0)
if extn.extnValue.pathLenConstraint is not None:
self.pathLenConstraint = extn.extnValue.pathLenConstraint.val
elif extn.extnID.oidname == "keyUsage":
self.keyUsage = extn.extnValue.get_keyUsage()
elif extn.extnID.oidname == "extKeyUsage":
Expand Down Expand Up @@ -1876,6 +1879,26 @@ def verify(self, cert, hostname=None, now=None, allow_expired=False):
c.subject_str, c.notBefore_str, c.notAfter_str
)
)
# Every certificate that issued another one in the chain must be a
# CA, and may bound the number of CAs below it (RFC 5280 sect 6.1.4
# (k) to (m)). The chain goes from the certificate right below the
# root down to cert, and ends with the root, which is trusted as is
# so that a non-CA certificate can still be pinned.
intermediates = chain[:-2]
for i, c in enumerate(intermediates):
if not c.cA:
raise ValueError(
"Certificate %s is not a CA but issued %s !" % (
c.subject_str, chain[i + 1].subject_str
)
)
if c.pathLenConstraint is not None and \
len(intermediates) - i - 1 > c.pathLenConstraint:
raise ValueError(
"Certificate %s allows at most %d CA below it !" % (
c.subject_str, c.pathLenConstraint
)
)
if hostname is not None and not cert.matchesHostname(hostname):
raise ValueError(
"Certificate %s was not issued to %s !" % (
Expand Down
70 changes: 70 additions & 0 deletions test/scapy/layers/tls/cert.uts
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,76 @@ try:
except ValueError:
pass

= CertTree class : verify rejects an issuer that is not a CA

# srv_cert.pem is CA:FALSE. A certificate for another name signed with its
# key must not chain up to the CA through it.
pki_ca = Cert(scapy_path("/test/scapy/layers/tls/pki/ca_cert.pem"))
pki_ca_key = PrivKey(scapy_path("/test/scapy/layers/tls/pki/ca_key.pem"))
pki_srv = Cert(scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem"))
pki_srv_key = PrivKey(scapy_path("/test/scapy/layers/tls/pki/srv_key.pem"))
# The test PKI expired in September 2026
in2025 = (2025, 1, 1, 0, 0, 0, 2, 1, 0)

assert not pki_srv.cA
CertTree([pki_srv], pki_ca).verify(pki_srv, now=in2025)

forged = Cert(scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem"))
forged.x509Cert.tbsCertificate.issuer = pki_srv.x509Cert.tbsCertificate.subject
forged.x509Cert.tbsCertificate.subject[-1].rdn[0].value = ASN1_PRINTABLE_STRING(b"victim.example")
forged.setSubjectPublicKeyFromPrivateKey(PrivKeyECDSA())
forged = Cert(pki_srv_key.resignCert(forged))
assert forged.isIssuer(pki_srv)

try:
CertTree([forged, pki_srv], pki_ca).verify(forged, hostname="victim.example", now=in2025)
assert False
except ValueError:
pass

= CertTree class : verify checks pathLenConstraint

# Make a sub CA out of srv_cert.pem, with pathLenConstraint 0
subca_key = PrivKeyECDSA()
subca = Cert(scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem"))
subca.x509Cert.tbsCertificate.subject[-1].rdn[0].value = ASN1_PRINTABLE_STRING(b"Scapy Test Sub CA")
subca_bc = next(x for x in subca.x509Cert.tbsCertificate.extensions if x.extnID.oidname == "basicConstraints")
subca_bc.extnValue.cA = ASN1_BOOLEAN(0xff)
subca_bc.extnValue.pathLenConstraint = ASN1_INTEGER(0)
subca.setSubjectPublicKeyFromPrivateKey(subca_key)
subca = Cert(pki_ca_key.resignCert(subca))
assert subca.cA and subca.pathLenConstraint == 0

# A CA below it, which issues a certificate
subsubca_key = PrivKeyECDSA()
subsubca = Cert(subca.der)
subsubca.x509Cert.tbsCertificate.issuer = subca.x509Cert.tbsCertificate.subject
subsubca.x509Cert.tbsCertificate.subject[-1].rdn[0].value = ASN1_PRINTABLE_STRING(b"Scapy Test Sub Sub CA")
subsubca.setSubjectPublicKeyFromPrivateKey(subsubca_key)
subsubca = Cert(subca_key.resignCert(subsubca))

leaf = Cert(scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem"))
leaf.x509Cert.tbsCertificate.issuer = subsubca.x509Cert.tbsCertificate.subject
leaf.setSubjectPublicKeyFromPrivateKey(PrivKeyECDSA())
leaf = Cert(subsubca_key.resignCert(leaf))

# The sub sub CA is the only CA under the sub CA: fine
CertTree([subca], pki_ca).verify(subsubca, now=in2025)

# The leaf has two CAs between it and the root: too many for the sub CA
try:
CertTree([subca, subsubca], pki_ca).verify(leaf, now=in2025)
assert False
except ValueError:
pass

# With pathLenConstraint 1 the same chain is accepted
subca_bc = next(x for x in subca.x509Cert.tbsCertificate.extensions if x.extnID.oidname == "basicConstraints")
subca_bc.extnValue.pathLenConstraint = ASN1_INTEGER(1)
subca = Cert(pki_ca_key.resignCert(subca))
assert subca.pathLenConstraint == 1
CertTree([subca, subsubca], pki_ca).verify(leaf, now=in2025)

= Test GeneralizedTime

data = b"MHAwXAIBADANBgkqhkiG9w0BAQ0FADAAMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMAAwHDANBgkqhkiG9w0BAQEFAAMLADAIAgEAAgMBAAGjAjAAMA0GCSqGSIb3DQEBDQUAAwEA"
Expand Down