Skip to content

Commit

Permalink
Remove all oscrypto calls
Browse files Browse the repository at this point in the history
pyca/cryptography is now a required dependency and the only
CryptoBackend. oscrypto is also no longer used for loading keys
(at the time of writing it breaks badly on systems with openssl3)
  • Loading branch information
MatthiasValvekens committed Mar 15, 2024
1 parent 92e4232 commit d283063
Show file tree
Hide file tree
Showing 13 changed files with 122 additions and 266 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Requires Python 3.7 or later.
Certomancer is [available on PyPI](https://pypi.org/project/certomancer/). See `example.yml` for an example config file.

```bash
$ pip install 'certomancer[web-api,pkcs12]'
$ pip install 'certomancer[web-api]'
$ certomancer --config example.yml animate
```

Expand Down
13 changes: 0 additions & 13 deletions certomancer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from ._asn1_types import register_extensions
from .config_utils import ConfigurationError
from .crypto_utils import pyca_cryptography_present
from .registry import (
ArchLabel,
AttributeCertificateSpec,
Expand Down Expand Up @@ -189,12 +188,6 @@ def mass_summon(
):
cfg: CertomancerConfig = next(ctx.obj['config'])
pki_arch = cfg.get_pki_arch(ArchLabel(architecture))
if not no_pfx and not pyca_cryptography_present():
no_pfx = True
logger.warning(
"pyca/cryptography not installed, no PFX files will be created"
)

if pfx_pass is not None:
pfx_pass_bytes = pfx_pass.encode('utf8')
else:
Expand Down Expand Up @@ -258,12 +251,6 @@ def summon(
):
cfg: CertomancerConfig = next(ctx.obj['config'])
pki_arch = cfg.get_pki_arch(ArchLabel(architecture))
if as_pfx and not pyca_cryptography_present():
as_pfx = False
logger.warning(
"pyca/cryptography not installed, no PFX files will be created"
)

output_is_binary = as_pfx or no_pem

if (
Expand Down
127 changes: 19 additions & 108 deletions certomancer/crypto_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import hashlib
import logging
from typing import Optional, Tuple

from asn1crypto import algos, keys, pem, x509
from asn1crypto.keys import PublicKeyInfo
from cryptography.hazmat.primitives import serialization

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -31,97 +31,32 @@ def optimal_pss_params(
raise NotImplementedError


class OscryptoBackend(CryptoBackend):
def load_private_key(
self, key_bytes: bytes, password: Optional[bytes]
) -> Tuple[keys.PrivateKeyInfo, keys.PublicKeyInfo]:
from oscrypto import asymmetric
from oscrypto import keys as oskeys

private = oskeys.parse_private(key_bytes, password=password)
if private.algorithm == 'rsassa_pss':
loaded, public = _oscrypto_hacky_load_pss_exclusive_key(private)
else:
loaded = asymmetric.load_private_key(private)
public = loaded.public_key.asn1
return private, public

def load_public_key(self, key_bytes: bytes) -> keys.PublicKeyInfo:
from oscrypto import keys as oskeys

return oskeys.parse_public(key_bytes)

def generic_sign(
self,
private_key: keys.PrivateKeyInfo,
tbs_bytes: bytes,
sd_algo: algos.SignedDigestAlgorithm,
) -> bytes:
from oscrypto import asymmetric

pk_algo = private_key.algorithm
loaded_key = None
if pk_algo == 'rsa':
if sd_algo.signature_algo == 'rsassa_pss':
sign_fun = asymmetric.rsa_pss_sign
else:
sign_fun = asymmetric.rsa_pkcs1v15_sign
elif pk_algo == 'rsassa_pss':
loaded_key = _oscrypto_hacky_load_pss_exclusive_key(private_key)[0]
sign_fun = asymmetric.rsa_pss_sign
elif pk_algo == 'ec':
sign_fun = asymmetric.ecdsa_sign
elif pk_algo == 'dsa':
sign_fun = asymmetric.dsa_sign
else:
raise NotImplementedError(
f"The signing mechanism '{pk_algo}' is not supported."
)
if loaded_key is None:
loaded_key = asymmetric.load_private_key(private_key)
return sign_fun(loaded_key, tbs_bytes, sd_algo.hash_algo)

def optimal_pss_params(self, key: PublicKeyInfo, digest_algo: str):
key_algo = key.algorithm
if key_algo == 'rsassa_pss':
logger.warning(
"You seem to be using an RSA key that has been marked as "
"RSASSA-PSS exclusive. If it has non-null parameters, these "
"WILL be disregarded by the signer, since oscrypto doesn't "
"currently support RSASSA-PSS with arbitrary parameters."
)
# replicate default oscrypto PSS settings
salt_len = len(getattr(hashlib, digest_algo)().digest())
return algos.RSASSAPSSParams(
{
'hash_algorithm': algos.DigestAlgorithm(
{'algorithm': digest_algo}
),
'mask_gen_algorithm': algos.MaskGenAlgorithm(
{
'algorithm': 'mgf1',
'parameters': algos.DigestAlgorithm(
{'algorithm': digest_algo}
),
}
),
'salt_length': salt_len,
}
def _load_private_key_from_pemder_data(
key_bytes: bytes, passphrase: Optional[bytes]
) -> keys.PrivateKeyInfo:
load_fun = (
serialization.load_pem_private_key
if pem.detect(key_bytes)
else serialization.load_der_private_key
)

private_key = load_fun(key_bytes, password=passphrase)
return keys.PrivateKeyInfo.load(
private_key.private_bytes(
serialization.Encoding.DER,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
)


class PycaCryptographyBackend(CryptoBackend):
def load_private_key(
self, key_bytes: bytes, password: Optional[bytes]
) -> Tuple[keys.PrivateKeyInfo, keys.PublicKeyInfo]:
from cryptography.hazmat.primitives import serialization
from oscrypto import keys as oskeys

# use oscrypto parser here to parse the key to a PrivateKeyInfo object
# (It handles unarmoring/decryption/... without worrying about the
# key type, while load_der/pem_private_key would fail to process
# PSS-exclusive keys)
priv_key_info = oskeys.parse_private(key_bytes, password)
priv_key_info = _load_private_key_from_pemder_data(key_bytes, password)
assert isinstance(priv_key_info, keys.PrivateKeyInfo)
if priv_key_info.algorithm == 'rsassa_pss':
# these keys can't be loaded directly in pyca/cryptography,
Expand Down Expand Up @@ -281,31 +216,7 @@ def pyca_cryptography_present() -> bool:
return False


def _oscrypto_hacky_load_pss_exclusive_key(private: keys.PrivateKeyInfo):
from oscrypto import asymmetric

# HACK to load PSS-exclusive RSA keys in oscrypto
# Don't ever do this in production code!
algo_copy = private['private_key_algorithm'].native
private_copy = keys.PrivateKeyInfo.load(private.dump())
# set the algorithm to "generic RSA"
private_copy['private_key_algorithm'] = {'algorithm': 'rsa'}
loaded_key = asymmetric.load_private_key(private_copy)
public = loaded_key.public_key.asn1
public['algorithm'] = algo_copy
public._algorithm = None
return loaded_key, public


def _select_default_crypto_backend() -> CryptoBackend:
# pyca/cryptography required for EdDSA certs
if pyca_cryptography_present():
return PycaCryptographyBackend()
else:
return OscryptoBackend()


CRYPTO_BACKEND: CryptoBackend = _select_default_crypto_backend()
CRYPTO_BACKEND: CryptoBackend = PycaCryptographyBackend()


def generic_sign(
Expand Down
13 changes: 3 additions & 10 deletions certomancer/integrations/animator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from werkzeug.wrappers import Request, Response

from certomancer.config_utils import ConfigurationError
from certomancer.crypto_utils import pyca_cryptography_present
from certomancer.registry import (
ArchLabel,
AttributeCertificateSpec,
Expand All @@ -36,8 +35,6 @@

logger = logging.getLogger(__name__)

pfx_possible = pyca_cryptography_present()


def _now():
return datetime.now(tz=tzlocal.get_localzone())
Expand Down Expand Up @@ -70,7 +67,7 @@ class AnimatorCertInfo:
@staticmethod
def gather_cert_info(pki_arch: PKIArchitecture):
def _for_cert(spec: CertificateSpec):
pfx = pfx_possible and pki_arch.is_subject_key_available(spec.label)
pfx = pki_arch.is_subject_key_available(spec.label)
return AnimatorCertInfo(
spec=spec,
pfx_available=pfx,
Expand Down Expand Up @@ -111,7 +108,7 @@ class ArchServicesDescription:
cert_repo: list
attr_cert_repo: list
certs_by_issuer: Dict[EntityLabel, List[AnimatorCertInfo]]
attr_certs_by_issuer: Dict[EntityLabel, List[AnimatorCertInfo]]
attr_certs_by_issuer: Dict[EntityLabel, List[AnimatorAttrCertInfo]]

@classmethod
def compile(cls, pki_arch: PKIArchitecture):
Expand Down Expand Up @@ -252,7 +249,6 @@ def gen_index(architectures):
pki_archs=[
ArchServicesDescription.compile(arch) for arch in architectures
],
pfx_possible=pfx_possible,
web_ui_prefix=WEB_UI_URL_PREFIX,
)

Expand Down Expand Up @@ -543,10 +539,7 @@ def serve_pfx(self, request: Request, *, arch):
raise BadRequest()

cert_label = CertLabel(cert)
if not (
pyca_cryptography_present()
and pki_arch.is_subject_key_available(cert_label)
):
if not pki_arch.is_subject_key_available(cert_label):
raise NotFound()

pass_bytes = request.form.get('passphrase', '').encode('utf8')
Expand Down
94 changes: 45 additions & 49 deletions certomancer/integrations/animator_templates/arch_summary.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,55 +41,51 @@ <h3>Attribute certificates by issuer</h3>
{% endfor %}
</section>
<section>
{% if pfx_possible %}
<h3>Download PKCS&nbsp;#12 (.pfx) bundles</h3>
<p>
Choose a certificate label that you want to download
together with its issuance chain and private key.
You can optionally set a passphrase.
</p>
<form method="post" action="{{ web_ui_prefix }}/pfx-download/{{ arch_services.arch }}">
<table>
<thead>
<tr>
<th>
<label for="certs">Certificate</label>
</th>
<th>
<label for="passphrase">Passphrase</label>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select name="cert" id="certs">
{% for iss in arch_services.certs_by_issuer %}
<optgroup label="{{ iss }}">
{% for cert_info in arch_services.certs_by_issuer[iss] %}
<option value="{{ cert_info.spec.label }}">{{ cert_info.spec.label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</td>
<td>
<input type="text" name="passphrase" id="passphrase" width="20ch">
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<input type="submit" value="Download">
</td>
</tr>
</tfoot>
</table>
</form>
{% else %}
<p><i>[PKCS&nbsp;#12 support is unavailable.]</i></p>
{% endif %}
<h3>Download PKCS&nbsp;#12 (.pfx) bundles</h3>
<p>
Choose a certificate label that you want to download
together with its issuance chain and private key.
You can optionally set a passphrase.
</p>
<form method="post" action="{{ web_ui_prefix }}/pfx-download/{{ arch_services.arch }}">
<table>
<thead>
<tr>
<th>
<label for="certs">Certificate</label>
</th>
<th>
<label for="passphrase">Passphrase</label>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select name="cert" id="certs">
{% for iss in arch_services.certs_by_issuer %}
<optgroup label="{{ iss }}">
{% for cert_info in arch_services.certs_by_issuer[iss] %}
<option value="{{ cert_info.spec.label }}">{{ cert_info.spec.label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</td>
<td>
<input type="text" name="passphrase" id="passphrase" width="20ch">
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<input type="submit" value="Download">
</td>
</tr>
</tfoot>
</table>
</form>
</section>
<section>
<h3>Time stamping endpoints (RFC 3161 protocol)</h3>
Expand Down
3 changes: 1 addition & 2 deletions certomancer/registry/pki_arch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
check_config_keys,
key_dashes_to_underscores,
)
from ..crypto_utils import load_cert_from_pemder, pyca_cryptography_present
from ..crypto_utils import load_cert_from_pemder
from ..services import (
CertomancerServiceError,
CRLBuilder,
Expand Down Expand Up @@ -680,7 +680,6 @@ def is_subject_key_available(self, cert: CertLabel):
def _dump_certs(
self, use_pem=True, flat=False, include_pkcs12=False, pkcs12_pass=None
):
include_pkcs12 &= pyca_cryptography_present()
# start writing only after we know that all certs have been built
ext = '.cert.pem' if use_pem else '.crt'
for iss_label, iss_certs in self._cert_labels_by_issuer.items():
Expand Down
2 changes: 1 addition & 1 deletion docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ uWSGI is extremely easy if you've dealt with WSGI deployment before.
1. Follow the [uWSGI quickstart](https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html)
guide to learn more about deploying WSGI applications.
2. Install Certomancer using `pip install certomancer[web-api]` (either system-wide or in a
virtualenv). If you need PKCS#12 support, run `pip install certomancer[web-api,pkcs12]` or
virtualenv).
install the [pyca/cryptography](https://github.com/pyca/cryptography) library manually.
3. Generate or copy over some testing keys (RSA, DSA, ECDSA and EdDSA are supported).
4. Set `module = certomancer.integrations.animator:app` in your `uwsgi.ini` file.
Expand Down
Loading

0 comments on commit d283063

Please sign in to comment.