Skip to content

Commit

Permalink
Support for more extensions for 'show'
Browse files Browse the repository at this point in the history
  • Loading branch information
markokr committed Aug 21, 2019
1 parent 9f22594 commit 72262d7
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ max-attributes=50
max-bool-expr=10

# Maximum number of branch for function / method body.
max-branches=50
max-branches=500

# Maximum number of locals for function / method body.
max-locals=50
Expand Down
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Version history
===============

dev
---

* [feature] Disallow non-standard key formats unless ``--unsafe``
switch is given.
* [feature] Read-write support for PolicyConstraints.
* [feature] Read-only support for Certificate Transparency extensions,
to allow ``show`` to work.

1.3
---

Expand Down
19 changes: 18 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,22 @@ Options useful only when apps support them:

Extension: NameConstraints_.

**--inhibit-any N**
Disallow special handling of ``any`` policy (2.5.29.32.0)
after N levels.

Extension: InhibitAny_.

**--require-explicit-policy N**
Require explicit certificate policy for whole path after N levels.

Extension: PolicyConstraints_.

**--inhibit-policy-mapping N**
Disallow policy mapping processing after N levels.

Extension: PolicyConstraints_.

sign
~~~~

Expand Down Expand Up @@ -506,4 +522,5 @@ actually used.
.. _CRLIssuingDistributionPoint: https://tools.ietf.org/html/rfc5280#section-5.2.5
.. _CRLReason: https://tools.ietf.org/html/rfc5280#section-5.3.1
.. _CRLInvalidityDate: https://tools.ietf.org/html/rfc5280#section-5.3.2

.. _InhibitAny: https://tools.ietf.org/html/rfc5280#section-4.2.1.14
.. _PolicyConstraints: https://tools.ietf.org/html/rfc5280#section-4.2.1.11
146 changes: 135 additions & 11 deletions sysca.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import re
import subprocess
import sys
import pprint
import binascii

from datetime import datetime, timedelta

Expand All @@ -19,11 +21,13 @@
from cryptography.hazmat.primitives.hashes import SHA256, SHA384, SHA512
from cryptography.hazmat.primitives.serialization import (
Encoding, PrivateFormat, PublicFormat,
BestAvailableEncryption, NoEncryption, load_pem_private_key)
BestAvailableEncryption, NoEncryption,
load_pem_private_key, load_der_private_key)

from cryptography import x509
from cryptography import __version__ as crypto_version
from cryptography.x509.oid import (
ObjectIdentifier,
NameOID, ExtendedKeyUsageOID, CRLEntryExtensionOID,
ExtensionOID, AuthorityInformationAccessOID, SignatureAlgorithmOID)

Expand Down Expand Up @@ -158,6 +162,7 @@ def get_curve_for_name(name):
"email": ExtendedKeyUsageOID.EMAIL_PROTECTION,
"time": ExtendedKeyUsageOID.TIME_STAMPING,
"ocsp": ExtendedKeyUsageOID.OCSP_SIGNING,
"precert-ca": ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3"),
}

# minimal KeyUsage defaults to add when ExtendedKeyUsage is given
Expand All @@ -169,6 +174,7 @@ def get_curve_for_name(name):
"email": ["digital_signature"], # content_commitment, key_agreement, key_encipherment
"time": ["digital_signature"], # content_commitment
"ocsp": ["digital_signature"], # content_commitment
"precert-ca": ["key_cert_sign"],

"encipher_only": ["key_agreement"],
"decipher_only": ["key_agreement"],
Expand Down Expand Up @@ -278,6 +284,21 @@ def show_list(desc, lst, writeln):
writeln("%s: %s" % (desc, val))


def show_pprint(desc, val, writeln):
"""Pretty-print compex dict/list.
"""
if not val:
return
tab = '\n '
txt = pprint.pformat(val, indent=2)
txt = tab + txt.replace('\n', tab)
writeln("%s:%s" % (desc, txt))


def to_hex(data):
return binascii.b2a_hex(data).decode('ascii')


def _unescape_char(m):
"""Unescape helper
"""
Expand Down Expand Up @@ -702,6 +723,7 @@ def __init__(self, subject=None, alt_names=None, ca=False, path_length=None,
usage=None, ocsp_urls=None, crl_urls=None, issuer_urls=None,
ocsp_nocheck=False, ocsp_must_staple=False, ocsp_must_staple_v2=False,
permit_subtrees=None, exclude_subtrees=None, inhibit_any=None,
require_explicit_policy=None, inhibit_policy_mapping=None,
load=None):
"""Initialize info object.
Expand Down Expand Up @@ -768,6 +790,17 @@ def __init__(self, subject=None, alt_names=None, ca=False, path_length=None,
self.version = None
self.serial_number = None
self.inhibit_any = inhibit_any
self.require_explicit_policy = require_explicit_policy
self.inhibit_policy_mapping = inhibit_policy_mapping
self.certificate_policies = None
self.precert_poison = False
self.precert_signed_timestamps = None
self.subject_key_identifier = None
self.authority_key_identifier = None
self.authority_cert_serial_number = None
self.authority_cert_issuer = None
self.not_valid_before = None
self.not_valid_after = None

if self.path_length is not None and self.path_length < 0:
self.path_length = None
Expand All @@ -788,10 +821,14 @@ def load_from_existing(self, obj):
else:
raise InvalidCertificate("Unsupported certificate version")
self.issuer_name = extract_name(obj.issuer)
self.not_valid_before = obj.not_valid_before
self.not_valid_after = obj.not_valid_after
elif isinstance(obj, x509.CertificateSigningRequest):
self.serial_number = None
self.version = None
self.issuer_name = None
self.not_valid_before = None
self.not_valid_after = None
else:
raise InvalidCertificate("Invalid obj type: %s" % type(obj))
self.public_key_info = get_key_name(obj.public_key())
Expand Down Expand Up @@ -848,9 +885,11 @@ def load_from_existing(self, obj):
self.permit_subtrees = extract_gnames(extobj.permitted_subtrees)
self.exclude_subtrees = extract_gnames(extobj.excluded_subtrees)
elif ext.oid == ExtensionOID.SUBJECT_KEY_IDENTIFIER:
pass
self.subject_key_identifier = to_hex(extobj.digest)
elif ext.oid == ExtensionOID.AUTHORITY_KEY_IDENTIFIER:
pass
self.authority_key_identifier = to_hex(extobj.key_identifier)
self.authority_cert_serial_number = extobj.authority_cert_serial_number
self.authority_cert_issuer = extract_gnames(self.authority_cert_issuer)
elif ext.oid == ExtensionOID.OCSP_NO_CHECK:
self.ocsp_nocheck = True
elif ext.oid == ExtensionOID.TLS_FEATURE:
Expand All @@ -863,8 +902,49 @@ def load_from_existing(self, obj):
raise InvalidCertificate("Unsupported TLSFeature: %r" % (tls_feature_code,))
elif ext.oid == ExtensionOID.INHIBIT_ANY_POLICY:
self.inhibit_any = extobj.skip_certs
elif ext.oid == ExtensionOID.POLICY_CONSTRAINTS:
self.require_explicit_policy = extobj.require_explicit_policy
self.inhibit_policy_mapping = extobj.inhibit_policy_mapping
elif ext.oid == ExtensionOID.CERTIFICATE_POLICIES:
if self.certificate_policies is None:
self.certificate_policies = []
for pol in extobj:
rpol = {
'policy_identifier': pol.policy_identifier.dotted_string,
}
self.certificate_policies.append(rpol)
if not pol.policy_qualifiers:
continue
rpol['policy_qualifiers'] = []
for q in pol.policy_qualifiers:
if isinstance(q, str):
rpol['policy_qualifiers'].append(q)
continue
unot = {
'notice_reference': None,
'explicit_text': q.explicit_text,
}
if q.notice_reference is not None:
unot['notice_reference'] = {
'organization': q.notice_reference.organization,
'notice_numbers': q.notice_reference.notice_numbers,
}
rpol['policy_qualifiers'].append(unot)
elif ext.oid == ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS:
if self.precert_signed_timestamps is None:
self.precert_signed_timestamps = []
for scts in extobj:
stamp = {}
self.precert_signed_timestamps.append(stamp)

stamp['version'] = str(scts.version).split('.')[1]
stamp['log_id'] = to_hex(scts.log_id)
stamp['timestamp'] = scts.timestamp.isoformat(' ')
stamp['entry_type'] = str(scts.entry_type).split('.')[1]
elif ext.oid == ExtensionOID.PRECERT_POISON:
self.precert_poison = True
else:
raise InvalidCertificate("Unsupported extension in CSR: %s" % (ext,))
raise InvalidCertificate("Unsupported extension in CRT: %s" % (ext,))

def extract_xkey_usage(self, ext):
"""Walk oid list, return keywords.
Expand Down Expand Up @@ -999,6 +1079,25 @@ def install_extensions(self, builder):
ext = x509.InhibitAnyPolicy(self.inhibit_any)
builder = builder.add_extension(ext, critical=True)

# PolicyConstraints
if self.require_explicit_policy is not None or self.inhibit_policy_mapping is not None:
if not self.ca:
raise InvalidCertificate("PolicyConstraints applies only to CA certificates")
ext = x509.PolicyConstraints(self.require_explicit_policy, self.inhibit_policy_mapping)
builder = builder.add_extension(ext, critical=True)

# CertificatePolicies
if self.certificate_policies:
raise InvalidCertificate("Writing CertificatePolicies not supported")

# Precert Poison
if self.precert_poison:
raise InvalidCertificate("Writing PrecertPoison not supported")

# PrecertificateSignedCertificateTimestamps
if self.precert_signed_timestamps:
raise InvalidCertificate("Writing PrecertificateSignedCertificateTimestamps not supported")

# configured builder
return builder

Expand Down Expand Up @@ -1058,24 +1157,35 @@ def show(self, writeln):
"""
if self.version is not None:
writeln("Version: %s" % self.version)
if self.serial_number is not None:
writeln("Serial: %s" % serial_str(self.serial_number))
if self.public_key_info:
writeln("Public key: %s" % self.public_key_info)
if self.not_valid_before:
writeln("Not Valid Before: %s" % self.not_valid_before.isoformat(' '))
if self.not_valid_after:
writeln("Not Valid After: %s" % self.not_valid_after.isoformat(' '))
if self.serial_number is not None:
writeln("Serial: %s" % serial_str(self.serial_number))
if self.subject:
writeln("Subject: %s" % render_name(self.subject))
show_list("SAN", self.san, writeln)
show_list("Usage", self.usage, writeln)
show_list("OCSP URLs", self.ocsp_urls, writeln)
if self.subject_key_identifier:
writeln("Subject Key Identifier: %s" % self.subject_key_identifier)
if self.authority_key_identifier:
writeln("Authority Key Identifier: %s" % self.authority_key_identifier)
if self.authority_cert_serial_number:
writeln("Authority Cert Serial Number: %s" % serial_str(self.authority_cert_serial_number))
show_list("Authority Cert Issuer", self.authority_cert_issuer, writeln)
if self.issuer_name:
writeln("Issuer Name: %s" % render_name(self.issuer_name))
show_list("Issuer SAN", self.issuer_san, writeln)
show_list("Issuer URLs", self.issuer_urls, writeln)
show_list("OCSP URLs", self.ocsp_urls, writeln)
show_list("CRL URLs", self.crl_urls, writeln)
show_list("Permit", self.permit_subtrees, writeln)
show_list("Exclude", self.exclude_subtrees, writeln)
if self.ocsp_nocheck:
show_list("OCSP NoCheck", ["True"], writeln)
writeln("OCSP NoCheck: True")

tls_features = []
if self.ocsp_must_staple:
Expand All @@ -1085,6 +1195,14 @@ def show(self, writeln):
show_list("TLS Features", tls_features, writeln)
if self.inhibit_any is not None:
writeln("Inhibit ANY policy: skip_certs=%r" % self.inhibit_any)
if self.require_explicit_policy is not None:
writeln("Policy Constraint - Require Explicit Policy: %d" % self.require_explicit_policy)
if self.inhibit_policy_mapping is not None:
writeln("Policy Constraint - Inhibit Policy Mapping: %d" % self.inhibit_policy_mapping)
show_pprint("Certificate Policies", self.certificate_policies, writeln)
show_pprint("Precert Signed Timestamps", self.precert_signed_timestamps, writeln)
if self.precert_poison:
writeln("Precert Poison: True")


class RevCertInfo:
Expand Down Expand Up @@ -1626,6 +1744,8 @@ def info_from_args(args):
exclude_subtrees=parse_list(args.exclude_subtrees),
ca=args.CA,
inhibit_any=args.inhibit_any,
require_explicit_policy=args.require_explicit_policy,
inhibit_policy_mapping=args.inhibit_policy_mapping,
path_length=args.path_length)


Expand Down Expand Up @@ -1850,13 +1970,13 @@ def show_command_sysca(args):
ext = os.path.splitext(fn)[1].lower()
if ext == ".csr":
req = CertInfo(load=load_req(fn))
req.show(msg_show)
req.show(msg)
elif ext == ".crt":
crt = CertInfo(load=load_cert(fn))
crt.show(msg_show)
crt.show(msg)
elif ext == ".crl":
crl = CRLInfo(load=load_crl(fn))
crl.show(msg_show)
crl.show(msg)
else:
die("Unsupported file: %s", fn)

Expand Down Expand Up @@ -1965,6 +2085,10 @@ def setup_args():
help="Disallowed NameConstraints.")
g2.add_argument("--inhibit-any", metavar="N", type=int,
help="Number of levels after which 'any' policy is ignored.")
g2.add_argument("--require-explicit-policy", metavar="N", type=int,
help="Number of levels after which certificate policy is required.")
g2.add_argument("--inhibit-policy-mapping", metavar="N", type=int,
help="Number of levels after which policy mapping is disallowed.")

g3 = p.add_argument_group("Command 'sign'",
"Create certificate for key in certificate request. "
Expand Down
2 changes: 2 additions & 0 deletions tests/cli/test_selfsign.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ sysca selfsign \
--ocsp-must-staple \
--ocsp-must-staple-v2 \
--inhibit-any 5 \
--require-explicit-policy=3 \
--inhibit-policy-mapping=2 \
--CA \
--days 900 \
--out tmp/${pfx}_ca.crt
Expand Down
2 changes: 2 additions & 0 deletions tests/test_sysca.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ def test_passthrough():
'any',
],
inhibit_any=6,
require_explicit_policy=2,
inhibit_policy_mapping=3,
ocsp_must_staple=True,
ocsp_must_staple_v2=True,
ocsp_nocheck=True,
Expand Down

0 comments on commit 72262d7

Please sign in to comment.