Refactor the signing backends
Prepare for new signing backend implementations which reuse the existing functionality. This abstracts most of the current signing function, so that the signature generation itself can be replaced. Change-Id: I99a28f4bcb08f010f397faf49e23276672977bc1
This commit is contained in:
parent
8dad1687a5
commit
d0fa9b519d
|
@ -50,20 +50,6 @@ SIGNING_ALGORITHMS = {
|
||||||
SIGNING_ALGORITHMS_INV = dict((v, k) for k, v in SIGNING_ALGORITHMS.items())
|
SIGNING_ALGORITHMS_INV = dict((v, k) for k, v in SIGNING_ALGORITHMS.items())
|
||||||
|
|
||||||
|
|
||||||
SIGNER_CONSTRUCTION = {
|
|
||||||
sha224WithRSAEncryption: (lambda key: key.signer(padding.PKCS1v15(),
|
|
||||||
hashes.SHA224())),
|
|
||||||
sha256WithRSAEncryption: (lambda key: key.signer(padding.PKCS1v15(),
|
|
||||||
hashes.SHA256())),
|
|
||||||
sha384WithRSAEncryption: (lambda key: key.signer(padding.PKCS1v15(),
|
|
||||||
hashes.SHA384())),
|
|
||||||
sha512WithRSAEncryption: (lambda key: key.signer(padding.PKCS1v15(),
|
|
||||||
hashes.SHA512())),
|
|
||||||
id_dsa_with_sha224: (lambda key: key.signer(hashes.SHA224())),
|
|
||||||
id_dsa_with_sha256: (lambda key: key.signer(hashes.SHA256())),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
VERIFIER_CONSTRUCTION = {
|
VERIFIER_CONSTRUCTION = {
|
||||||
sha224WithRSAEncryption: (lambda key, signature: key.verifier(
|
sha224WithRSAEncryption: (lambda key, signature: key.verifier(
|
||||||
signature, padding.PKCS1v15(), hashes.SHA224())),
|
signature, padding.PKCS1v15(), hashes.SHA224())),
|
||||||
|
@ -96,17 +82,10 @@ class SignatureMixin(object):
|
||||||
Both operations rely on the functions provided by the certificate and
|
Both operations rely on the functions provided by the certificate and
|
||||||
csr classes.
|
csr classes.
|
||||||
"""
|
"""
|
||||||
def sign(self, key, md="sha256"):
|
def sign(self, encryption, md, signer):
|
||||||
"""Sign the current object."""
|
"""Sign the current object."""
|
||||||
md = md.upper()
|
md = md.upper()
|
||||||
|
|
||||||
if isinstance(key, rsa.RSAPrivateKey):
|
|
||||||
encryption = 'RSA'
|
|
||||||
elif isinstance(key, dsa.DSAPrivateKey):
|
|
||||||
encryption = 'DSA'
|
|
||||||
else:
|
|
||||||
raise errors.X509Error("Unknown key type: %s" % (key.__class__,))
|
|
||||||
|
|
||||||
signature_type = SIGNING_ALGORITHMS.get((encryption, md))
|
signature_type = SIGNING_ALGORITHMS.get((encryption, md))
|
||||||
if signature_type is None:
|
if signature_type is None:
|
||||||
raise errors.X509Error(
|
raise errors.X509Error(
|
||||||
|
@ -120,9 +99,7 @@ class SignatureMixin(object):
|
||||||
|
|
||||||
self._embed_signature_algorithm(algo_id)
|
self._embed_signature_algorithm(algo_id)
|
||||||
to_sign = self._get_bytes_to_sign()
|
to_sign = self._get_bytes_to_sign()
|
||||||
signer = SIGNER_CONSTRUCTION[signature_type](key)
|
signature = signer(to_sign)
|
||||||
signer.update(to_sign)
|
|
||||||
signature = signer.finalize()
|
|
||||||
|
|
||||||
self._embed_signature(algo_id, signature)
|
self._embed_signature(algo_id, signature)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ from __future__ import absolute_import
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import stat
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import paste
|
import paste
|
||||||
|
@ -23,57 +22,40 @@ from paste import translogger # noqa
|
||||||
import pecan
|
import pecan
|
||||||
|
|
||||||
from anchor import audit
|
from anchor import audit
|
||||||
|
from anchor import errors
|
||||||
from anchor import jsonloader
|
from anchor import jsonloader
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ConfigValidationException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def config_check_domains(validator_set):
|
def config_check_domains(validator_set):
|
||||||
for name, step in validator_set.items():
|
for name, step in validator_set.items():
|
||||||
if 'allowed_domains' in step:
|
if 'allowed_domains' in step:
|
||||||
for domain in step['allowed_domains']:
|
for domain in step['allowed_domains']:
|
||||||
if not domain.startswith('.'):
|
if not domain.startswith('.'):
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Domain that does not start with "
|
"Domain that does not start with "
|
||||||
"a '.' <{}>".format(domain))
|
"a '.' <{}>".format(domain))
|
||||||
|
|
||||||
|
|
||||||
def _check_file_permissions(path):
|
|
||||||
# checks that file is owner readable only
|
|
||||||
expected_permissions = (stat.S_IRUSR | stat.S_IFREG) # 0o100400
|
|
||||||
st = os.stat(path)
|
|
||||||
if st.st_mode != expected_permissions:
|
|
||||||
raise ConfigValidationException("CA file: %s has incorrect "
|
|
||||||
"permissions set, expected "
|
|
||||||
"owner readable only" % path)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_file_exists(path):
|
|
||||||
if not (os.path.isfile(path) and
|
|
||||||
os.access(path, os.R_OK)):
|
|
||||||
raise ConfigValidationException("could not read file: %s" %
|
|
||||||
path)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config(conf):
|
def validate_config(conf):
|
||||||
for old_name in ['auth', 'ca', 'validators']:
|
for old_name in ['auth', 'ca', 'validators']:
|
||||||
if old_name in conf.config:
|
if old_name in conf.config:
|
||||||
raise ConfigValidationException("The config seems to be for an "
|
raise errors.ConfigValidationException(
|
||||||
"old version of Anchor. Please "
|
"The config seems to be for an old version of Anchor. Please "
|
||||||
"check documentation.")
|
"check documentation.")
|
||||||
|
|
||||||
if not conf.config.get('registration_authority'):
|
if not conf.config.get('registration_authority'):
|
||||||
raise ConfigValidationException("No registration authorities present")
|
raise errors.ConfigValidationException(
|
||||||
|
"No registration authorities present")
|
||||||
|
|
||||||
if not conf.config.get('signing_ca'):
|
if not conf.config.get('signing_ca'):
|
||||||
raise ConfigValidationException("No signing CA configurations present")
|
raise errors.ConfigValidationException(
|
||||||
|
"No signing CA configurations present")
|
||||||
|
|
||||||
if not conf.config.get('authentication'):
|
if not conf.config.get('authentication'):
|
||||||
raise ConfigValidationException("No authentication methods present")
|
raise errors.ConfigValidationException(
|
||||||
|
"No authentication methods present")
|
||||||
|
|
||||||
for name in conf.registration_authority.keys():
|
for name in conf.registration_authority.keys():
|
||||||
logger.info("Checking config for registration authority: %s", name)
|
logger.info("Checking config for registration authority: %s", name)
|
||||||
|
@ -99,13 +81,13 @@ def validate_audit_config(conf):
|
||||||
|
|
||||||
audit_conf = conf.audit
|
audit_conf = conf.audit
|
||||||
if audit_conf.get('target', 'log') not in valid_targets:
|
if audit_conf.get('target', 'log') not in valid_targets:
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Audit target not known (expected one of %s)" % (
|
"Audit target not known (expected one of %s)" % (
|
||||||
", ".join(valid_targets),))
|
", ".join(valid_targets),))
|
||||||
|
|
||||||
if audit_conf.get('target') == 'messaging':
|
if audit_conf.get('target') == 'messaging':
|
||||||
if audit_conf.get('url') is None:
|
if audit_conf.get('url') is None:
|
||||||
raise ConfigValidationException("Audit url required")
|
raise errors.ConfigValidationException("Audit url required")
|
||||||
|
|
||||||
|
|
||||||
def validate_authentication_config(name, conf):
|
def validate_authentication_config(name, conf):
|
||||||
|
@ -115,11 +97,11 @@ def validate_authentication_config(name, conf):
|
||||||
default_secret = "simplepassword"
|
default_secret = "simplepassword"
|
||||||
|
|
||||||
if not auth_conf.get('backend'):
|
if not auth_conf.get('backend'):
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Authentication method %s doesn't define backend" % name)
|
"Authentication method %s doesn't define backend" % name)
|
||||||
|
|
||||||
if auth_conf['backend'] not in ('static', 'keystone', 'ldap'):
|
if auth_conf['backend'] not in ('static', 'keystone', 'ldap'):
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Authentication backend % unknown" % (auth_conf['backend'],))
|
"Authentication backend % unknown" % (auth_conf['backend'],))
|
||||||
|
|
||||||
# Check for anchor being run with default user/secret
|
# Check for anchor being run with default user/secret
|
||||||
|
@ -132,51 +114,45 @@ def validate_authentication_config(name, conf):
|
||||||
|
|
||||||
def validate_signing_ca_config(name, conf):
|
def validate_signing_ca_config(name, conf):
|
||||||
ca_conf = conf.signing_ca[name]
|
ca_conf = conf.signing_ca[name]
|
||||||
|
backend_name = ca_conf.get('backend')
|
||||||
|
if not backend_name:
|
||||||
|
raise errors.ConfigValidationException(
|
||||||
|
"Backend type not defined for RA '%s'" % name)
|
||||||
|
sign_func = jsonloader.conf.get_signing_backend(backend_name)
|
||||||
|
if not sign_func:
|
||||||
|
raise errors.ConfigValidationException(
|
||||||
|
"Backend '%s' could not be found" % backend_name)
|
||||||
|
|
||||||
# mandatory CA settings
|
if hasattr(sign_func, "_config_validator"):
|
||||||
ca_config_requirements = ["cert_path", "key_path", "output_path",
|
sign_func._config_validator(name, ca_conf)
|
||||||
"signing_hash", "valid_hours"]
|
|
||||||
|
|
||||||
for requirement in ca_config_requirements:
|
|
||||||
if requirement not in ca_conf.keys():
|
|
||||||
raise ConfigValidationException(
|
|
||||||
"CA config missing: %s (for signing CA %s)" % (requirement,
|
|
||||||
name))
|
|
||||||
|
|
||||||
# all are specified, check the CA certificate and key are readable with
|
|
||||||
# sane permissions
|
|
||||||
_check_file_exists(ca_conf['cert_path'])
|
|
||||||
_check_file_exists(ca_conf['key_path'])
|
|
||||||
|
|
||||||
_check_file_permissions(ca_conf['key_path'])
|
|
||||||
|
|
||||||
|
|
||||||
def validate_registration_authority_config(ra_name, conf):
|
def validate_registration_authority_config(ra_name, conf):
|
||||||
ra_conf = conf.registration_authority[ra_name]
|
ra_conf = conf.registration_authority[ra_name]
|
||||||
auth_name = ra_conf.get('authentication')
|
auth_name = ra_conf.get('authentication')
|
||||||
if not auth_name:
|
if not auth_name:
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"No authentication configured for registration authority: %s" %
|
"No authentication configured for registration authority: %s" %
|
||||||
ra_name)
|
ra_name)
|
||||||
|
|
||||||
if not conf.authentication.get(auth_name):
|
if not conf.authentication.get(auth_name):
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Authentication method %s configured for registration authority "
|
"Authentication method %s configured for registration authority "
|
||||||
"%s doesn't exist" % (auth_name, ra_name))
|
"%s doesn't exist" % (auth_name, ra_name))
|
||||||
|
|
||||||
ca_name = ra_conf.get('signing_ca')
|
ca_name = ra_conf.get('signing_ca')
|
||||||
if not ca_name:
|
if not ca_name:
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"No signing CA configuration present for registration authority: "
|
"No signing CA configuration present for registration authority: "
|
||||||
"%s" % ra_name)
|
"%s" % ra_name)
|
||||||
|
|
||||||
if not conf.signing_ca.get(ca_name):
|
if not conf.signing_ca.get(ca_name):
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Signing CA %s configured for registration authority %s doesn't "
|
"Signing CA %s configured for registration authority %s doesn't "
|
||||||
"exist" % (ca_name, ra_name))
|
"exist" % (ca_name, ra_name))
|
||||||
|
|
||||||
if not ra_conf.get("validators"):
|
if not ra_conf.get("validators"):
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"No validators configured for registration authority: %s" %
|
"No validators configured for registration authority: %s" %
|
||||||
ra_name)
|
ra_name)
|
||||||
|
|
||||||
|
@ -186,7 +162,7 @@ def validate_registration_authority_config(ra_name, conf):
|
||||||
try:
|
try:
|
||||||
jsonloader.conf.get_validator(step)
|
jsonloader.conf.get_validator(step)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Unknown validator <{}> found (for registration "
|
"Unknown validator <{}> found (for registration "
|
||||||
"authority {})".format(step, ra_name))
|
"authority {})".format(step, ra_name))
|
||||||
|
|
||||||
|
@ -199,7 +175,7 @@ def validate_registration_authority_config(ra_name, conf):
|
||||||
try:
|
try:
|
||||||
jsonloader.conf.get_fixup(step)
|
jsonloader.conf.get_fixup(step)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConfigValidationException(
|
raise errors.ConfigValidationException(
|
||||||
"Unknown fixup <{}> found (for registration "
|
"Unknown fixup <{}> found (for registration "
|
||||||
"authority {})".format(step, ra_name))
|
"authority {})".format(step, ra_name))
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,6 @@ from __future__ import absolute_import
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
from webob import exc as http_status
|
from webob import exc as http_status
|
||||||
|
@ -26,9 +24,7 @@ from anchor import jsonloader
|
||||||
from anchor import util
|
from anchor import util
|
||||||
from anchor import validation
|
from anchor import validation
|
||||||
from anchor.X509 import certificate
|
from anchor.X509 import certificate
|
||||||
from anchor.X509 import extension
|
|
||||||
from anchor.X509 import signing_request
|
from anchor.X509 import signing_request
|
||||||
from anchor.X509 import utils as x509_utils
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -39,10 +35,6 @@ logger = logging.getLogger(__name__)
|
||||||
VALID_ENCODINGS = ['pem']
|
VALID_ENCODINGS = ['pem']
|
||||||
|
|
||||||
|
|
||||||
class SigningError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def parse_csr(data, encoding):
|
def parse_csr(data, encoding):
|
||||||
"""Loads the user provided CSR into the backend X509 library.
|
"""Loads the user provided CSR into the backend X509 library.
|
||||||
|
|
||||||
|
@ -204,79 +196,3 @@ def fixup_csr(ra_name, csr, request):
|
||||||
pecan.abort(500, "Could not finish all required modifications")
|
pecan.abort(500, "Could not finish all required modifications")
|
||||||
|
|
||||||
return args['csr']
|
return args['csr']
|
||||||
|
|
||||||
|
|
||||||
def sign(csr, ca_conf):
|
|
||||||
"""Generate an X.509 certificate and sign it.
|
|
||||||
|
|
||||||
:param csr: X509 certificate signing request
|
|
||||||
:param ca_conf: signing CA configuration
|
|
||||||
:return: signed certificate in PEM format
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
ca = certificate.X509Certificate.from_file(
|
|
||||||
ca_conf['cert_path'])
|
|
||||||
except Exception as e:
|
|
||||||
raise SigningError("Cannot load the signing CA: %s" % (e,))
|
|
||||||
|
|
||||||
try:
|
|
||||||
key = x509_utils.get_private_key_from_file(ca_conf['key_path'])
|
|
||||||
except Exception as e:
|
|
||||||
raise SigningError("Cannot load the signing CA key: %s" % (e,))
|
|
||||||
|
|
||||||
new_cert = certificate.X509Certificate()
|
|
||||||
new_cert.set_version(2)
|
|
||||||
|
|
||||||
start_time = int(time.time())
|
|
||||||
end_time = start_time + (ca_conf['valid_hours'] * 60 * 60)
|
|
||||||
new_cert.set_not_before(start_time)
|
|
||||||
new_cert.set_not_after(end_time)
|
|
||||||
|
|
||||||
new_cert.set_pubkey(pkey=csr.get_pubkey())
|
|
||||||
new_cert.set_subject(csr.get_subject())
|
|
||||||
new_cert.set_issuer(ca.get_subject())
|
|
||||||
|
|
||||||
serial = int(uuid.uuid4().hex, 16)
|
|
||||||
new_cert.set_serial_number(serial)
|
|
||||||
|
|
||||||
exts = csr.get_extensions()
|
|
||||||
|
|
||||||
ext_i = 0
|
|
||||||
for ext in exts:
|
|
||||||
# this check is separate from standards validator - the signing backend
|
|
||||||
# may know about more/fewer extensions than we do
|
|
||||||
if ext.get_oid() not in extension.EXTENSION_CLASSES.keys():
|
|
||||||
if ext.get_critical():
|
|
||||||
logger.warning("CSR submitted with unknown extension oid %s, "
|
|
||||||
"refusing to sign", ext.get_oid())
|
|
||||||
raise SigningError("Unknown critical extension %s" % (
|
|
||||||
ext.get_oid(),))
|
|
||||||
else:
|
|
||||||
logger.info("CSR submitted with non-critical unknown oid %s, "
|
|
||||||
"not including extension", (ext.get_oid(),))
|
|
||||||
else:
|
|
||||||
logger.info("Adding certificate extension: %i %s", ext_i, str(ext))
|
|
||||||
# authority id will be replaced with current signer
|
|
||||||
# this cannot be a fixup, because they don't get access to the CA
|
|
||||||
if isinstance(ext, extension.X509ExtensionAuthorityKeyId):
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_cert.add_extension(ext, ext_i)
|
|
||||||
ext_i += 1
|
|
||||||
|
|
||||||
ca_exts = ca.get_extensions(extension.X509ExtensionSubjectKeyId)
|
|
||||||
auth_key_id = extension.X509ExtensionAuthorityKeyId()
|
|
||||||
if ca_exts:
|
|
||||||
auth_key_id.set_key_id(ca_exts[0].get_key_id())
|
|
||||||
else:
|
|
||||||
auth_key_id.set_key_id(ca.get_key_id())
|
|
||||||
new_cert.add_extension(auth_key_id, ext_i)
|
|
||||||
|
|
||||||
logger.info("Signing certificate for <%s> with serial <%s>",
|
|
||||||
csr.get_subject(), serial)
|
|
||||||
|
|
||||||
new_cert.sign(key, ca_conf['signing_hash'])
|
|
||||||
|
|
||||||
cert_pem = new_cert.as_pem()
|
|
||||||
|
|
||||||
return cert_pem
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
class ConfigValidationException(Exception):
|
||||||
|
pass
|
|
@ -0,0 +1,91 @@
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from anchor.X509 import certificate
|
||||||
|
from anchor.X509 import extension
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def config_validator(val):
|
||||||
|
def patcher(f):
|
||||||
|
setattr(f, "_config_validator", val)
|
||||||
|
return f
|
||||||
|
return patcher
|
||||||
|
|
||||||
|
|
||||||
|
class SigningError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def sign_generic(csr, ca_conf, encryption, signer):
|
||||||
|
"""Generate an X.509 certificate and sign it.
|
||||||
|
|
||||||
|
:param csr: X509 certificate signing request
|
||||||
|
:param ca_conf: signing CA configuration
|
||||||
|
:return: signed certificate in PEM format
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ca = certificate.X509Certificate.from_file(
|
||||||
|
ca_conf['cert_path'])
|
||||||
|
except Exception as e:
|
||||||
|
raise SigningError("Cannot load the signing CA: %s" % (e,))
|
||||||
|
|
||||||
|
new_cert = certificate.X509Certificate()
|
||||||
|
new_cert.set_version(2)
|
||||||
|
|
||||||
|
start_time = int(time.time())
|
||||||
|
end_time = start_time + (ca_conf['valid_hours'] * 60 * 60)
|
||||||
|
new_cert.set_not_before(start_time)
|
||||||
|
new_cert.set_not_after(end_time)
|
||||||
|
|
||||||
|
new_cert.set_pubkey(pkey=csr.get_pubkey())
|
||||||
|
new_cert.set_subject(csr.get_subject())
|
||||||
|
new_cert.set_issuer(ca.get_subject())
|
||||||
|
|
||||||
|
serial = int(uuid.uuid4().hex, 16)
|
||||||
|
new_cert.set_serial_number(serial)
|
||||||
|
|
||||||
|
exts = csr.get_extensions()
|
||||||
|
|
||||||
|
ext_i = 0
|
||||||
|
for ext in exts:
|
||||||
|
# this check is separate from standards validator - the signing backend
|
||||||
|
# may know about more/fewer extensions than we do
|
||||||
|
if ext.get_oid() not in extension.EXTENSION_CLASSES.keys():
|
||||||
|
if ext.get_critical():
|
||||||
|
logger.warning("CSR submitted with unknown extension oid %s, "
|
||||||
|
"refusing to sign", ext.get_oid())
|
||||||
|
raise SigningError("Unknown critical extension %s" % (
|
||||||
|
ext.get_oid(),))
|
||||||
|
else:
|
||||||
|
logger.info("CSR submitted with non-critical unknown oid %s, "
|
||||||
|
"not including extension", (ext.get_oid(),))
|
||||||
|
else:
|
||||||
|
logger.info("Adding certificate extension: %i %s", ext_i, str(ext))
|
||||||
|
# authority id will be replaced with current signer
|
||||||
|
# this cannot be a fixup, because they don't get access to the CA
|
||||||
|
if isinstance(ext, extension.X509ExtensionAuthorityKeyId):
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_cert.add_extension(ext, ext_i)
|
||||||
|
ext_i += 1
|
||||||
|
|
||||||
|
ca_exts = ca.get_extensions(extension.X509ExtensionSubjectKeyId)
|
||||||
|
auth_key_id = extension.X509ExtensionAuthorityKeyId()
|
||||||
|
if ca_exts:
|
||||||
|
auth_key_id.set_key_id(ca_exts[0].get_key_id())
|
||||||
|
else:
|
||||||
|
auth_key_id.set_key_id(ca.get_key_id())
|
||||||
|
new_cert.add_extension(auth_key_id, ext_i)
|
||||||
|
|
||||||
|
logger.info("Signing certificate for <%s> with serial <%s>",
|
||||||
|
csr.get_subject(), serial)
|
||||||
|
|
||||||
|
new_cert.sign(encryption, ca_conf['signing_hash'], signer)
|
||||||
|
|
||||||
|
cert_pem = new_cert.as_pem()
|
||||||
|
|
||||||
|
return cert_pem
|
|
@ -0,0 +1,74 @@
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import dsa
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
|
||||||
|
from anchor import errors
|
||||||
|
from anchor import signers
|
||||||
|
from anchor import util
|
||||||
|
from anchor.X509 import utils as x509_utils
|
||||||
|
|
||||||
|
|
||||||
|
SIGNER_CONSTRUCTION = {
|
||||||
|
('RSA', 'SHA224'): (lambda key: key.signer(padding.PKCS1v15(),
|
||||||
|
hashes.SHA224())),
|
||||||
|
('RSA', 'SHA256'): (lambda key: key.signer(padding.PKCS1v15(),
|
||||||
|
hashes.SHA256())),
|
||||||
|
('RSA', 'SHA384'): (lambda key: key.signer(padding.PKCS1v15(),
|
||||||
|
hashes.SHA384())),
|
||||||
|
('RSA', 'SHA512'): (lambda key: key.signer(padding.PKCS1v15(),
|
||||||
|
hashes.SHA512())),
|
||||||
|
('DSA', 'SHA224'): (lambda key: key.signer(hashes.SHA224())),
|
||||||
|
('DSA', 'SHA256'): (lambda key: key.signer(hashes.SHA256())),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def conf_validator(name, ca_conf):
|
||||||
|
# mandatory CA settings
|
||||||
|
ca_config_requirements = ["cert_path", "key_path", "output_path",
|
||||||
|
"signing_hash", "valid_hours"]
|
||||||
|
|
||||||
|
for requirement in ca_config_requirements:
|
||||||
|
if requirement not in ca_conf.keys():
|
||||||
|
raise errors.ConfigValidationException(
|
||||||
|
"CA config missing: %s (for signing CA %s)" % (requirement,
|
||||||
|
name))
|
||||||
|
|
||||||
|
# all are specified, check the CA certificate and key are readable with
|
||||||
|
# sane permissions
|
||||||
|
util.check_file_exists(ca_conf['cert_path'])
|
||||||
|
util.check_file_exists(ca_conf['key_path'])
|
||||||
|
|
||||||
|
util.check_file_permissions(ca_conf['key_path'])
|
||||||
|
|
||||||
|
|
||||||
|
def make_signer(key, encryption, md):
|
||||||
|
signer = SIGNER_CONSTRUCTION.get((encryption, md.upper()))
|
||||||
|
if signer is None:
|
||||||
|
raise signers.SigningError(
|
||||||
|
"Unknown hash/encryption combination (%s/%s)" % (md, encryption))
|
||||||
|
signer = signer(key)
|
||||||
|
|
||||||
|
def cryptography_io_signer(to_sign):
|
||||||
|
signer.update(to_sign)
|
||||||
|
return signer.finalize()
|
||||||
|
|
||||||
|
return cryptography_io_signer
|
||||||
|
|
||||||
|
|
||||||
|
@signers.config_validator(conf_validator)
|
||||||
|
def sign(csr, ca_conf):
|
||||||
|
try:
|
||||||
|
key = x509_utils.get_private_key_from_file(ca_conf['key_path'])
|
||||||
|
except Exception as e:
|
||||||
|
raise signers.SigningError("Cannot load the signing CA key: %s" % (e,))
|
||||||
|
|
||||||
|
if isinstance(key, rsa.RSAPrivateKey):
|
||||||
|
encryption = 'RSA'
|
||||||
|
elif isinstance(key, dsa.DSAPrivateKey):
|
||||||
|
encryption = 'DSA'
|
||||||
|
else:
|
||||||
|
raise signers.SigningError("Unknown key type: %s" % (key.__class__,))
|
||||||
|
|
||||||
|
signer = make_signer(key, encryption, ca_conf['signing_hash'])
|
||||||
|
return signers.sign_generic(csr, ca_conf, encryption, signer)
|
|
@ -15,7 +15,11 @@ from __future__ import absolute_import
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hmac
|
import hmac
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import stat
|
||||||
|
|
||||||
|
from anchor import errors
|
||||||
|
|
||||||
|
|
||||||
def constant_time_compare(val1, val2):
|
def constant_time_compare(val1, val2):
|
||||||
|
@ -100,3 +104,20 @@ def extract_pem(data, use_markers=True):
|
||||||
return None
|
return None
|
||||||
decoder = getattr(base64, 'decodebytes', base64.decodestring)
|
decoder = getattr(base64, 'decodebytes', base64.decodestring)
|
||||||
return decoder(b64_content)
|
return decoder(b64_content)
|
||||||
|
|
||||||
|
|
||||||
|
def check_file_permissions(path):
|
||||||
|
# checks that file is owner readable only
|
||||||
|
expected_permissions = (stat.S_IRUSR | stat.S_IFREG) # 0o100400
|
||||||
|
st = os.stat(path)
|
||||||
|
if st.st_mode != expected_permissions:
|
||||||
|
raise errors.ConfigValidationException("CA file: %s has incorrect "
|
||||||
|
"permissions set, expected "
|
||||||
|
"owner readable only" % path)
|
||||||
|
|
||||||
|
|
||||||
|
def check_file_exists(path):
|
||||||
|
if not (os.path.isfile(path) and
|
||||||
|
os.access(path, os.R_OK)):
|
||||||
|
raise errors.ConfigValidationException("could not read file: %s" %
|
||||||
|
path)
|
||||||
|
|
|
@ -53,9 +53,11 @@ Backends are simple functions which need to take 2 parameters: the CSR in PEM
|
||||||
format and the configuration block contents. Configuration can contain any keys
|
format and the configuration block contents. Configuration can contain any keys
|
||||||
required by the backend.
|
required by the backend.
|
||||||
|
|
||||||
The return value must be a signed certificate in PEM format. The backend may
|
The return value must be a signed certificate in PEM format, however in most
|
||||||
either throw a specific ``WebOb`` HTTP exception, or any other exception which
|
cases it's enough to implement the actual hash signing part and rely on
|
||||||
will result in a generic 500 response.
|
``anchor.signer.sign_generic`` framework. The backend may either throw a
|
||||||
|
specific ``WebOb`` HTTP exception, or SigningError exception which will result
|
||||||
|
in a 500 response.
|
||||||
|
|
||||||
For security, http exceptions from the signing backend should not expose any
|
For security, http exceptions from the signing backend should not expose any
|
||||||
specific information about the reason for failure. Internal exceptions are
|
specific information about the reason for failure. Internal exceptions are
|
||||||
|
@ -66,3 +68,6 @@ are applied to the submitted CSR in Anchor, they will invalidate the signature.
|
||||||
Unless the backend is intended to work only with validators, and not any fixup
|
Unless the backend is intended to work only with validators, and not any fixup
|
||||||
operations in the future, the signature field should be ignored and the request
|
operations in the future, the signature field should be ignored and the request
|
||||||
treated as already correct/verified.
|
treated as already correct/verified.
|
||||||
|
|
||||||
|
Configuration is verified using the function provided using the
|
||||||
|
``@signers.config_validator(f)`` decorator.
|
||||||
|
|
|
@ -28,7 +28,7 @@ source-dir = doc/source
|
||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
anchor.signing_backends =
|
anchor.signing_backends =
|
||||||
anchor = anchor.certificate_ops:sign
|
anchor = anchor.signers.cryptography_io:sign
|
||||||
|
|
||||||
anchor.validators =
|
anchor.validators =
|
||||||
check_domains = anchor.validators.custom:check_domains
|
check_domains = anchor.validators.custom:check_domains
|
||||||
|
|
|
@ -25,7 +25,6 @@ from anchor.X509 import certificate
|
||||||
from anchor.X509 import errors as x509_errors
|
from anchor.X509 import errors as x509_errors
|
||||||
from anchor.X509 import extension
|
from anchor.X509 import extension
|
||||||
from anchor.X509 import name as x509_name
|
from anchor.X509 import name as x509_name
|
||||||
from anchor.X509 import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestX509Cert(unittest.TestCase):
|
class TestX509Cert(unittest.TestCase):
|
||||||
|
@ -238,17 +237,6 @@ class TestX509Cert(unittest.TestCase):
|
||||||
with self.assertRaises(x509_errors.X509Error):
|
with self.assertRaises(x509_errors.X509Error):
|
||||||
self.cert.get_fingerprint('no_such_hash')
|
self.cert.get_fingerprint('no_such_hash')
|
||||||
|
|
||||||
def test_sign_bad_md(self):
|
|
||||||
key = utils.get_private_key_from_pem(self.key_rsa_data)
|
|
||||||
self.assertRaises(x509_errors.X509Error,
|
|
||||||
self.cert.sign,
|
|
||||||
key, "BAD")
|
|
||||||
|
|
||||||
def test_sign_bad_key(self):
|
|
||||||
self.assertRaises(x509_errors.X509Error,
|
|
||||||
self.cert.sign,
|
|
||||||
"BAD")
|
|
||||||
|
|
||||||
def test_get_version(self):
|
def test_get_version(self):
|
||||||
v = self.cert.get_version()
|
v = self.cert.get_version()
|
||||||
self.assertEqual(v, 2)
|
self.assertEqual(v, 2)
|
||||||
|
@ -290,16 +278,6 @@ class TestX509Cert(unittest.TestCase):
|
||||||
with self.assertRaises(x509_errors.X509Error):
|
with self.assertRaises(x509_errors.X509Error):
|
||||||
self.cert.add_extension("abcdef", 2)
|
self.cert.add_extension("abcdef", 2)
|
||||||
|
|
||||||
def test_sign_unknown_key(self):
|
|
||||||
key = object()
|
|
||||||
with self.assertRaises(x509_errors.X509Error):
|
|
||||||
self.cert.sign(key, 'sha1')
|
|
||||||
|
|
||||||
def test_sign_unknown_hash(self):
|
|
||||||
key = utils.get_private_key_from_pem(self.key_rsa_data)
|
|
||||||
with self.assertRaises(x509_errors.X509Error):
|
|
||||||
self.cert.sign(key, 'no_such_hash')
|
|
||||||
|
|
||||||
def test_verify_unknown_key(self):
|
def test_verify_unknown_key(self):
|
||||||
with self.assertRaises(x509_errors.X509Error):
|
with self.assertRaises(x509_errors.X509Error):
|
||||||
self.cert.verify("abc")
|
self.cert.verify("abc")
|
||||||
|
|
|
@ -21,6 +21,7 @@ import unittest
|
||||||
import mock
|
import mock
|
||||||
from pyasn1_modules import rfc2459
|
from pyasn1_modules import rfc2459
|
||||||
|
|
||||||
|
from anchor.signers import cryptography_io
|
||||||
from anchor.X509 import errors as x509_errors
|
from anchor.X509 import errors as x509_errors
|
||||||
from anchor.X509 import extension
|
from anchor.X509 import extension
|
||||||
from anchor.X509 import name as x509_name
|
from anchor.X509 import name as x509_name
|
||||||
|
@ -158,7 +159,8 @@ class TestX509Csr(tests.DefaultRequestMixin, unittest.TestCase):
|
||||||
|
|
||||||
def test_sign(self):
|
def test_sign(self):
|
||||||
key = utils.get_private_key_from_pem(self.key_rsa_data)
|
key = utils.get_private_key_from_pem(self.key_rsa_data)
|
||||||
self.csr.sign(key)
|
signer = cryptography_io.make_signer(key, 'RSA', 'SHA256')
|
||||||
|
self.csr.sign('RSA', 'SHA256', signer)
|
||||||
# 10 bytes is definitely enough for non malicious case, right?
|
# 10 bytes is definitely enough for non malicious case, right?
|
||||||
self.assertEqual(b'\x16\xbd!\x9b\xfb\xfd\x10\xa1\xaf\x92',
|
self.assertEqual(b'\x16\xbd!\x9b\xfb\xfd\x10\xa1\xaf\x92',
|
||||||
self.csr._get_signature()[:10])
|
self.csr._get_signature()[:10])
|
||||||
|
|
|
@ -22,7 +22,9 @@ import unittest
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from anchor import app
|
from anchor import app
|
||||||
|
from anchor import errors
|
||||||
from anchor import jsonloader
|
from anchor import jsonloader
|
||||||
|
from anchor import util
|
||||||
import tests
|
import tests
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,8 +41,8 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
def test_self_test(self):
|
def test_self_test(self):
|
||||||
self.assertTrue(True)
|
self.assertTrue(True)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_exists')
|
@mock.patch('anchor.util.check_file_exists')
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_config_check_domains_good(self, a, b):
|
def test_config_check_domains_good(self, a, b):
|
||||||
self.sample_conf_ra['default_ra']['validators'] = {
|
self.sample_conf_ra['default_ra']['validators'] = {
|
||||||
"common_name": {
|
"common_name": {
|
||||||
|
@ -54,8 +56,8 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
with mock.patch("os.stat", **config):
|
with mock.patch("os.stat", **config):
|
||||||
self.assertEqual(app.validate_config(jsonloader.conf), None)
|
self.assertEqual(app.validate_config(jsonloader.conf), None)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_exists')
|
@mock.patch('anchor.util.check_file_exists')
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_config_check_domains_bad(self, a, b):
|
def test_config_check_domains_bad(self, a, b):
|
||||||
self.sample_conf_ra['default_ra']['validators'] = {
|
self.sample_conf_ra['default_ra']['validators'] = {
|
||||||
"common_name": {
|
"common_name": {
|
||||||
|
@ -68,7 +70,7 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
config = {'return_value.st_mode': (stat.S_IRUSR | stat.S_IFREG)}
|
config = {'return_value.st_mode': (stat.S_IRUSR | stat.S_IFREG)}
|
||||||
with mock.patch("os.stat", **config):
|
with mock.patch("os.stat", **config):
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
app.ConfigValidationException,
|
errors.ConfigValidationException,
|
||||||
app.validate_config,
|
app.validate_config,
|
||||||
jsonloader.conf
|
jsonloader.conf
|
||||||
)
|
)
|
||||||
|
@ -76,13 +78,13 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
def test_check_file_permissions_good(self):
|
def test_check_file_permissions_good(self):
|
||||||
config = {'return_value.st_mode': (stat.S_IRUSR | stat.S_IFREG)}
|
config = {'return_value.st_mode': (stat.S_IRUSR | stat.S_IFREG)}
|
||||||
with mock.patch("os.stat", **config):
|
with mock.patch("os.stat", **config):
|
||||||
app._check_file_permissions("/mock/path")
|
util.check_file_permissions("/mock/path")
|
||||||
|
|
||||||
def test_check_file_permissions_bad(self):
|
def test_check_file_permissions_bad(self):
|
||||||
config = {'return_value.st_mode': (stat.S_IWOTH | stat.S_IFREG)}
|
config = {'return_value.st_mode': (stat.S_IWOTH | stat.S_IFREG)}
|
||||||
with mock.patch("os.stat", **config):
|
with mock.patch("os.stat", **config):
|
||||||
self.assertRaises(app.ConfigValidationException,
|
self.assertRaises(errors.ConfigValidationException,
|
||||||
app._check_file_permissions, "/mock/path")
|
util.check_file_permissions, "/mock/path")
|
||||||
|
|
||||||
def test_validate_old_config(self):
|
def test_validate_old_config(self):
|
||||||
config = json.dumps({
|
config = json.dumps({
|
||||||
|
@ -91,45 +93,45 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
"validators": {},
|
"validators": {},
|
||||||
})
|
})
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"old version of Anchor",
|
"old version of Anchor",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_validate_config_no_registration_authorities(self,
|
def test_validate_config_no_registration_authorities(self,
|
||||||
mock_check_perm):
|
mock_check_perm):
|
||||||
del self.sample_conf['registration_authority']
|
del self.sample_conf['registration_authority']
|
||||||
config = json.dumps(self.sample_conf)
|
config = json.dumps(self.sample_conf)
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"No registration authorities present",
|
"No registration authorities present",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_validate_config_no_auth(self, mock_check_perm):
|
def test_validate_config_no_auth(self, mock_check_perm):
|
||||||
del self.sample_conf['authentication']
|
del self.sample_conf['authentication']
|
||||||
config = json.dumps(self.sample_conf)
|
config = json.dumps(self.sample_conf)
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"No authentication methods present",
|
"No authentication methods present",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_validate_config_no_auth_backend(self, mock_check_perm):
|
def test_validate_config_no_auth_backend(self, mock_check_perm):
|
||||||
del self.sample_conf_auth['default_auth']['backend']
|
del self.sample_conf_auth['default_auth']['backend']
|
||||||
config = json.dumps(self.sample_conf)
|
config = json.dumps(self.sample_conf)
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"Authentication method .* doesn't define "
|
"Authentication method .* doesn't define "
|
||||||
"backend",
|
"backend",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_validate_config_no_ra_auth(self, mock_check_perm):
|
def test_validate_config_no_ra_auth(self, mock_check_perm):
|
||||||
del self.sample_conf_ra['default_ra']['authentication']
|
del self.sample_conf_ra['default_ra']['authentication']
|
||||||
config = json.dumps(self.sample_conf)
|
config = json.dumps(self.sample_conf)
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"No authentication .* for .* default_ra",
|
"No authentication .* for .* default_ra",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
|
@ -137,20 +139,20 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
del self.sample_conf['signing_ca']
|
del self.sample_conf['signing_ca']
|
||||||
config = json.dumps(self.sample_conf)
|
config = json.dumps(self.sample_conf)
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"No signing CA configurations present",
|
"No signing CA configurations present",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_validate_config_no_ra_ca(self, mock_check_perm):
|
def test_validate_config_no_ra_ca(self, mock_check_perm):
|
||||||
del self.sample_conf_ra['default_ra']['signing_ca']
|
del self.sample_conf_ra['default_ra']['signing_ca']
|
||||||
config = json.dumps(self.sample_conf)
|
config = json.dumps(self.sample_conf)
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"No signing CA .* for .* default_ra",
|
"No signing CA .* for .* default_ra",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
def test_validate_config_ca_config_reqs(self, mock_check_perm):
|
def test_validate_config_ca_config_reqs(self, mock_check_perm):
|
||||||
ca_config_requirements = ["cert_path", "key_path", "output_path",
|
ca_config_requirements = ["cert_path", "key_path", "output_path",
|
||||||
"signing_hash", "valid_hours"]
|
"signing_hash", "valid_hours"]
|
||||||
|
@ -162,7 +164,7 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
# with 'missing_req', perform validation. Each should raise in turn
|
# with 'missing_req', perform validation. Each should raise in turn
|
||||||
for req in ca_config_requirements:
|
for req in ca_config_requirements:
|
||||||
jsonloader.conf.load_str_data(config.replace(req, "missing_req"))
|
jsonloader.conf.load_str_data(config.replace(req, "missing_req"))
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"CA config missing: %s" % req,
|
"CA config missing: %s" % req,
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
|
@ -171,11 +173,11 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
config = json.dumps(self.sample_conf)
|
config = json.dumps(self.sample_conf)
|
||||||
jsonloader.conf.load_str_data(config)
|
jsonloader.conf.load_str_data(config)
|
||||||
isfile.return_value = False
|
isfile.return_value = False
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"could not read file: tests/CA/root-ca.crt",
|
"could not read file: tests/CA/root-ca.crt",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
@mock.patch('os.path.isfile')
|
@mock.patch('os.path.isfile')
|
||||||
@mock.patch('os.access')
|
@mock.patch('os.access')
|
||||||
@mock.patch('os.stat')
|
@mock.patch('os.stat')
|
||||||
|
@ -187,11 +189,11 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
isfile.return_value = True
|
isfile.return_value = True
|
||||||
access.return_value = True
|
access.return_value = True
|
||||||
stat.return_value.st_mode = self.expected_key_permissions
|
stat.return_value.st_mode = self.expected_key_permissions
|
||||||
self.assertRaisesRegexp(app.ConfigValidationException,
|
self.assertRaisesRegexp(errors.ConfigValidationException,
|
||||||
"No validators configured",
|
"No validators configured",
|
||||||
app.validate_config, jsonloader.conf)
|
app.validate_config, jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
@mock.patch('os.path.isfile')
|
@mock.patch('os.path.isfile')
|
||||||
@mock.patch('os.access')
|
@mock.patch('os.access')
|
||||||
@mock.patch('os.stat')
|
@mock.patch('os.stat')
|
||||||
|
@ -203,13 +205,13 @@ class TestApp(tests.DefaultConfigMixin, unittest.TestCase):
|
||||||
isfile.return_value = True
|
isfile.return_value = True
|
||||||
access.return_value = True
|
access.return_value = True
|
||||||
stat.return_value.st_mode = self.expected_key_permissions
|
stat.return_value.st_mode = self.expected_key_permissions
|
||||||
with self.assertRaises(app.ConfigValidationException,
|
with self.assertRaises(errors.ConfigValidationException,
|
||||||
msg="Unknown validator <unknown_validator> "
|
msg="Unknown validator <unknown_validator> "
|
||||||
"found (for registration authority "
|
"found (for registration authority "
|
||||||
"default)"):
|
"default)"):
|
||||||
app.validate_config(jsonloader.conf)
|
app.validate_config(jsonloader.conf)
|
||||||
|
|
||||||
@mock.patch('anchor.app._check_file_permissions')
|
@mock.patch('anchor.util.check_file_permissions')
|
||||||
@mock.patch('os.path.isfile')
|
@mock.patch('os.path.isfile')
|
||||||
@mock.patch('os.access')
|
@mock.patch('os.access')
|
||||||
@mock.patch('os.stat')
|
@mock.patch('os.stat')
|
||||||
|
|
|
@ -14,14 +14,17 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from pyasn1.type import univ as asn1_univ
|
from pyasn1.type import univ as asn1_univ
|
||||||
|
|
||||||
from anchor import certificate_ops
|
from anchor import signers
|
||||||
|
from anchor.signers import cryptography_io
|
||||||
from anchor.X509 import certificate
|
from anchor.X509 import certificate
|
||||||
from anchor.X509 import extension
|
from anchor.X509 import extension
|
||||||
from anchor.X509 import signing_request
|
from anchor.X509 import signing_request
|
||||||
|
from anchor.X509 import utils
|
||||||
import tests
|
import tests
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +41,8 @@ class SigningBackendExtensions(tests.DefaultConfigMixin,
|
||||||
ext.add_dns_id("example.com")
|
ext.add_dns_id("example.com")
|
||||||
csr.add_extension(ext)
|
csr.add_extension(ext)
|
||||||
|
|
||||||
pem = certificate_ops.sign(csr, self.sample_conf_ca['default_ca'])
|
pem = signers.sign_generic(csr, self.sample_conf_ca['default_ca'],
|
||||||
|
'RSA', lambda x: b"")
|
||||||
cert = certificate.X509Certificate.from_buffer(pem)
|
cert = certificate.X509Certificate.from_buffer(pem)
|
||||||
self.assertEqual(1, len(cert.get_extensions(
|
self.assertEqual(1, len(cert.get_extensions(
|
||||||
extension.X509ExtensionSubjectAltName)))
|
extension.X509ExtensionSubjectAltName)))
|
||||||
|
@ -48,7 +52,8 @@ class SigningBackendExtensions(tests.DefaultConfigMixin,
|
||||||
ext = UnknownExtension()
|
ext = UnknownExtension()
|
||||||
csr.add_extension(ext)
|
csr.add_extension(ext)
|
||||||
|
|
||||||
pem = certificate_ops.sign(csr, self.sample_conf_ca['default_ca'])
|
pem = signers.sign_generic(csr, self.sample_conf_ca['default_ca'],
|
||||||
|
'RSA', lambda x: b"")
|
||||||
cert = certificate.X509Certificate.from_buffer(pem)
|
cert = certificate.X509Certificate.from_buffer(pem)
|
||||||
self.assertEqual(0, len(cert.get_extensions(UnknownExtension)))
|
self.assertEqual(0, len(cert.get_extensions(UnknownExtension)))
|
||||||
|
|
||||||
|
@ -58,5 +63,35 @@ class SigningBackendExtensions(tests.DefaultConfigMixin,
|
||||||
ext.set_critical(True)
|
ext.set_critical(True)
|
||||||
csr.add_extension(ext)
|
csr.add_extension(ext)
|
||||||
|
|
||||||
with self.assertRaises(certificate_ops.SigningError):
|
with self.assertRaises(signers.SigningError):
|
||||||
certificate_ops.sign(csr, self.sample_conf_ca['default_ca'])
|
signers.sign_generic(csr, self.sample_conf_ca['default_ca'],
|
||||||
|
'RSA', lambda x: b"")
|
||||||
|
|
||||||
|
|
||||||
|
class TestCryptographyBackend(tests.DefaultConfigMixin,
|
||||||
|
tests.DefaultRequestMixin, unittest.TestCase):
|
||||||
|
key_rsa_data = textwrap.dedent("""
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIICXAIBAAKBgQCeeqg1Qeccv8hqj1BP9KEJX5QsFCxR62M8plPb5t4sLo8UYfZd
|
||||||
|
6kFLcOP8xzwwvx/eFY6Sux52enQ197o8aMwyP77hMhZqtd8NCgLJMVlUbRhwLti0
|
||||||
|
SkHFPic0wAg+esfXa6yhd5TxC+bti7MgV/ljA80XQxHH8xOjdOoGN0DHfQIDAQAB
|
||||||
|
AoGBAJ2ozJpe+7qgGJPaCz3f0izvBwtq7kR49fqqRZbo8HHnx7OxWVVI7LhOkKEy
|
||||||
|
2/Bq0xsvOu1CdiXL4LynvIDIiQqLaeINzG48Rbk+0HadbXblt3nDkIWdYII6zHKI
|
||||||
|
W9ewX4KpHEPbrlEO9BjAlAcYsDIvFIMYpQhtQ+0R/gmZ99WJAkEAz5C2a6FIcMbE
|
||||||
|
o3aTc9ECq99zY7lxh+6aLpUdIeeHyb/QzfGDBdlbpBAkA6EcxSqp0aqH4xIQnYHa
|
||||||
|
3P5ZCShqSwJBAMN1sb76xq94xkg2cxShPFPAE6xKRFyKqLgsBYVtulOdfOtOnjh9
|
||||||
|
1SK2XQQfBRIRdG4Q/gDoCP8XQHpJcWMk+FcCQDnuJqulaOVo5GrG5mJ1nCxCAh98
|
||||||
|
G06X7lo/7dCPoRtSuMExvaK9RlFk29hTeAcjYCAPWzupyA9dtarmJg1jRT8CQCKf
|
||||||
|
gYnb8D/6+9yk0IPR/9ayCooVacCeyz48hgnZowzWs98WwQ4utAd/GED3obVOpDov
|
||||||
|
Bl9wus889i3zPoOac+cCQCZHredQcJGd4dlthbVtP2NhuPXz33JuETGR9pXtsDUZ
|
||||||
|
uX/nSq1oo9kUh/dPOz6aP5Ues1YVe3LExmExPBQfwIE=
|
||||||
|
-----END RSA PRIVATE KEY-----""").encode('ascii')
|
||||||
|
|
||||||
|
def test_sign_bad_md(self):
|
||||||
|
key = utils.get_private_key_from_pem(self.key_rsa_data)
|
||||||
|
with self.assertRaises(signers.SigningError):
|
||||||
|
cryptography_io.make_signer(key, "BAD", "RSA")
|
||||||
|
|
||||||
|
def test_sign_bad_key(self):
|
||||||
|
with self.assertRaises(signers.SigningError):
|
||||||
|
cryptography_io.make_signer("BAD", "sha256", "RSA")
|
||||||
|
|
Loading…
Reference in New Issue