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:
Stanisław Pitucha 2016-02-09 20:40:46 +11:00
parent 8dad1687a5
commit d0fa9b519d
13 changed files with 304 additions and 225 deletions

View File

@ -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)

View File

@ -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))

View File

@ -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

2
anchor/errors.py Normal file
View File

@ -0,0 +1,2 @@
class ConfigValidationException(Exception):
pass

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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")

View File

@ -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])

View File

@ -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')

View File

@ -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")