Merge pull request #8431 from atombrella/ec_dsa_2163

Implements support for ECDSA keys. Fixes #2163.
This commit is contained in:
Adrien Ferrand 2020-11-04 23:43:46 +01:00 committed by GitHub
commit 198f5a99bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 709 additions and 51 deletions

View file

@ -154,6 +154,7 @@ Authors
* [Luca Olivetti](https://github.com/olivluca)
* [Luke Rogers](https://github.com/lukeroge)
* [Maarten](https://github.com/mrtndwrd)
* [Mads Jensen](https://github.com/atombrella)
* [Maikel Martens](https://github.com/krukas)
* [Malte Janduda](https://github.com/MalteJ)
* [Mantas Mikulėnas](https://github.com/grawity)
@ -213,6 +214,7 @@ Authors
* [Richard Barnes](https://github.com/r-barnes)
* [Richard Panek](https://github.com/kernelpanek)
* [Robert Buchholz](https://github.com/rbu)
* [Robert Dailey](https://github.com/pahrohfit)
* [Robert Habermann](https://github.com/frennkie)
* [Robert Xiao](https://github.com/nneonneo)
* [Roland Shoemaker](https://github.com/rolandshoemaker)

View file

@ -0,0 +1,14 @@
This directory contains your keys and certificates.
`privkey.pem` : the private key for your certificate.
`fullchain.pem`: the certificate file used in most server software.
`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
`cert.pem` : will break many server configurations, and should not be used
without reading further documentation (see link below).
WARNING: DO NOT MOVE OR RENAME THESE FILES!
Certbot expects these files to remain in this location in order
to function properly!
We recommend not moving these files. For more information, see the Certbot
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.

View file

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIIBvrEnbPRYu8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjEwNzQw
WhcNMjUxMDEyMjEwNzQwWjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs
ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARjMhuW0ENPPC33PjB5XsYU
CRw640kPQENIDatcTJaENZIZdqKd6rI6jc+lpbmXot7Zi52clJlSJS+V6oDAt2Lh
o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUj7Kd3ENqxlPf8B2bIGhsjydX
mPswHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww
GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCl
k0JXsa8y7fg41WWMDhw60bPW77O0FtOmTcnhdI5daYNemQVk+Q5EMaBLQ/oGjgXd
9QXFzXH1PL904YEnSLt+iTpXn++7rQSNzQsdYqw0neWk4f5pEBiN+WORpb6mwobV
ifMtBOkNEHvrJ2Pkci9U1lLwtKD/DSew6QtJU5DSkmH1XdGuMJiubygEIvELtvgq
cP9S368ZvPmPGmKaJQXBiuaR8MTjY/Bkr79aXQMjKbf+mpn7h0POCcePk1DY/rm6
Da+X16lf0hHyQhSUa7Vgyim6rK1/hlw+Z00i+sQCKD9Ih7kXuuGqfSDC33cfO8Tj
o/MXO8lcxkrem5zU5QWP
-----END CERTIFICATE-----

View file

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx
MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy
NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq
mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB
qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5
CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH
nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY
MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx
PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE
bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT
MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq
uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P
fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV
EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW
fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG
9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE
-----END CERTIFICATE-----

View file

@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIILlmGtZhUFEwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjA1MDM0
WhcNMjUxMDEyMjA1MDM0WjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs
ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARHEzR8JPWrEmpmgM+F2bk5
9mT0u6CjzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/
o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU1CsVL+bPnzaxxQ5jUENmQJIO
lKwwHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww
GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBn
2D8loC7pfk28JYpFLr5lmFKJWWmtLGlpsWDj61fVjtTfGKLziJz+MM6il4Y3hIz5
58qiFK0ue0M63dIBJ33N+XxSEXon4Q0gy/zRWfH9jtPJ3FwfjkU/RT9PAUClYi0G
ptNWnTmgQkNzousbcAtRNXuuShH3856vhUnwkX+xM+cbIDi1JVmFjcGrEEQJ0rUF
mv2ZTyfbWbUs3v4rReETi2NVzr1Ql6J+ByNcMvHODzFy3t0L6yelAw2ca1I+c9HU
+Z0tnp/ykR7eXNuVLivok8UBf5OC413lh8ZO5g+Bgzh/LdtkUuavg1MYtEX0H6mX
9U7y3nVI8WEbPGf+HDeu
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx
MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy
NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq
mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB
qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5
CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH
nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY
MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx
PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE
bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT
MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq
uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P
fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV
EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW
fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG
9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE
-----END CERTIFICATE-----

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNgefv2dad4U1VYEi
0WkdHuqywi5QXAe30OwNTTGjhbihRANCAARHEzR8JPWrEmpmgM+F2bk59mT0u6Cj
zmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/
-----END PRIVATE KEY-----

View file

@ -0,0 +1,14 @@
This directory contains your keys and certificates.
`privkey.pem` : the private key for your certificate.
`fullchain.pem`: the certificate file used in most server software.
`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
`cert.pem` : will break many server configurations, and should not be used
without reading further documentation (see link below).
WARNING: DO NOT MOVE OR RENAME THESE FILES!
Certbot expects these files to remain in this location in order
to function properly!
We recommend not moving these files. For more information, see the Certbot
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.

View file

@ -0,0 +1 @@
../../archive/c.encryption-example.com/cert.pem

View file

@ -0,0 +1 @@
../../archive/c.encryption-example.com/chain.pem

View file

@ -0,0 +1 @@
../../archive/c.encryption-example.com/fullchain.pem

View file

@ -0,0 +1 @@
../../archive/c.encryption-example.com/privkey.pem

View file

@ -0,0 +1,17 @@
# renew_before_expiry = 30 days
version = 1.10.0.dev0
archive_dir = sample-config/archive/c.encryption-example.com
cert = sample-config/live/c.encryption-example.com/cert.pem
privkey = sample-config/live/c.encryption-example.com/privkey.pem
chain = sample-config/live/c.encryption-example.com/chain.pem
fullchain = sample-config/live/c.encryption-example.com/fullchain.pem
# Options used in the renewal process
[renewalparams]
authenticator = apache
installer = apache
account = 48d6b9e8d767eccf7e4d877d6ffa81e3
key_type = ecdsa
config_dir = sample-config-ec
elliptic_curve = secp256r1
manual_public_ip_logging_ok = True

View file

@ -2,6 +2,10 @@
import io
import os
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
try:
import grp
POSIX_MODE = True
@ -16,6 +20,16 @@ SYSTEM_SID = 'S-1-5-18'
ADMINS_SID = 'S-1-5-32-544'
def assert_elliptic_key(key, curve):
with open(key, 'rb') as file:
privkey1 = file.read()
key = load_pem_private_key(data=privkey1, password=None, backend=default_backend())
assert isinstance(key, EllipticCurvePrivateKey)
assert isinstance(key.curve, curve)
def assert_hook_execution(probe_path, probe_content):
"""
Assert that a certbot hook has been executed

View file

@ -9,12 +9,17 @@ import shutil
import subprocess
import time
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1, SECP384R1
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import NameOID
import pytest
from certbot_integration_tests.certbot_tests import context as certbot_context
from certbot_integration_tests.certbot_tests.assertions import assert_cert_count_for_lineage
from certbot_integration_tests.certbot_tests.assertions import assert_elliptic_key
from certbot_integration_tests.certbot_tests.assertions import assert_equals_group_owner
from certbot_integration_tests.certbot_tests.assertions import assert_equals_group_permissions
from certbot_integration_tests.certbot_tests.assertions import assert_equals_world_read_permissions
@ -289,7 +294,7 @@ def test_renew_with_changed_private_key_complexity(context):
assert_cert_count_for_lineage(context.config_dir, certname, 1)
context.certbot(['renew'])
assert_cert_count_for_lineage(context.config_dir, certname, 2)
key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
assert os.stat(key2).st_size > 3000
@ -421,20 +426,80 @@ def test_reuse_key(context):
assert len({cert1, cert2, cert3}) == 3
def test_incorrect_key_type(context):
with pytest.raises(subprocess.CalledProcessError):
context.certbot(['--key-type="failwhale"'])
def test_ecdsa(context):
"""Test certificate issuance with ECDSA key."""
"""Test issuance for ECDSA CSR based request (legacy supported mode)."""
key_path = join(context.workspace, 'privkey-p384.pem')
csr_path = join(context.workspace, 'csr-p384.der')
cert_path = join(context.workspace, 'cert-p384.pem')
chain_path = join(context.workspace, 'chain-p384.pem')
misc.generate_csr([context.get_domain('ecdsa')], key_path, csr_path, key_type=misc.ECDSA_KEY_TYPE)
context.certbot(['auth', '--csr', csr_path, '--cert-path', cert_path, '--chain-path', chain_path])
misc.generate_csr(
[context.get_domain('ecdsa')],
key_path, csr_path,
key_type=misc.ECDSA_KEY_TYPE
)
context.certbot([
'auth', '--csr', csr_path, '--cert-path', cert_path,
'--chain-path', chain_path,
])
certificate = misc.read_certificate(cert_path)
assert 'ASN1 OID: secp384r1' in certificate
def test_default_key_type(context):
"""Test default key type is RSA"""
certname = context.get_domain('renew')
context.certbot([
'certonly',
'--cert-name', certname, '-d', certname
])
filename = join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname)
with open(filename, 'rb') as file:
privkey1 = file.read()
key = load_pem_private_key(data=privkey1, password=None, backend=default_backend())
assert isinstance(key, RSAPrivateKey)
def test_default_curve_type(context):
"""test that the curve used when not specifying any is secp256r1"""
certname = context.get_domain('renew')
context.certbot([
'--key-type', 'ecdsa', '--cert-name', certname, '-d', certname
])
key1 = join(context.config_dir, 'archive/{0}/privkey1.pem'.format(certname))
assert_elliptic_key(key1, SECP256R1)
def test_renew_with_ec_keys(context):
"""Test proper renew with updated private key complexity."""
certname = context.get_domain('renew')
context.certbot([
'certonly',
'--cert-name', certname,
'--key-type', 'ecdsa', '--elliptic-curve', 'secp256r1',
'--force-renewal', '-d', certname,
])
key1 = join(context.config_dir, "archive", certname, 'privkey1.pem')
assert 200 < os.stat(key1).st_size < 250 # ec keys of 256 bits are ~225 bytes
assert_elliptic_key(key1, SECP256R1)
assert_cert_count_for_lineage(context.config_dir, certname, 1)
context.certbot(['renew', '--elliptic-curve', 'secp384r1'])
assert_cert_count_for_lineage(context.config_dir, certname, 2)
key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
assert_elliptic_key(key2, SECP384R1)
assert 280 < os.stat(key2).st_size < 320 # ec keys of 384 bits are ~310 bytes
def test_ocsp_must_staple(context):
"""Test that OCSP Must-Staple is correctly set in the generated certificate."""
if context.acme_server == 'pebble':
@ -657,4 +722,4 @@ def test_preferred_chain(context):
with open(conf_path, 'r') as f:
assert 'preferred_chain = {}'.format(requested) in f.read(), \
'Expected preferred_chain to be set in renewal config'
'Expected preferred_chain to be set in renewal config'

View file

@ -280,7 +280,11 @@ def load_sample_data_path(workspace):
if os.name == 'nt':
# Fix the symlinks on Windows if GIT is not configured to create them upon checkout
for lineage in ['a.encryption-example.com', 'b.encryption-example.com']:
for lineage in [
'a.encryption-example.com',
'b.encryption-example.com',
'c.encryption-example.com',
]:
current_live = os.path.join(copied, 'live', lineage)
for name in os.listdir(current_live):
if name != 'README':

View file

@ -8,7 +8,12 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
* Added timeout to DNS query function calls for dns-rfc2136 plugin.
* Confirmation when deleting certificates
*
* CLI flag `--key-type` has been added to specify 'rsa' or 'ecdsa' (default 'rsa').
Only accepts a single value at this time.
* CLI flag `--elliptic-curve` has been added which takes an NIST/SECG elliptic curve. Either of
`secp256r1`, `secp284r1` and `secp521r1` are accepted values.
* The command `certbot certficates` lists the which type of the private key that was used
for the private key.
### Changed
@ -55,7 +60,7 @@ More details about these changes can be found on our GitHub repo.
### Added
* Added the ability to remove email and phone contact information from an account
* Added the ability to remove email and phone contact information from an account
using `update_account --register-unsafely-without-email`
### Changed
@ -67,7 +72,7 @@ More details about these changes can be found on our GitHub repo.
* The problem causing the Apache plugin in the Certbot snap on ARM systems to
fail to load the Augeas library it depends on has been fixed.
* The `acme` library can now tell the ACME server to clear contact information by passing an empty
`tuple` to the `contact` field of a `Registration` message.
`tuple` to the `contact` field of a `Registration` message.
* Fixed the `*** stack smashing detected ***` error in the Certbot snap on some systems.
More details about these changes can be found on our GitHub repo.

View file

@ -106,6 +106,8 @@ Current Features
* Can get domain-validated (DV) certificates.
* Can revoke certificates.
* Adjustable RSA key bit-length (2048 (default), 4096, ...).
* Adjustable [EC](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)
key (`secp256r1` (default), `secp384r1`, `secp521r1`).
* Can optionally install a http -> https redirect, so your site effectively
runs https only (Apache only)
* Fully automated.

View file

@ -65,6 +65,7 @@ def rename_lineage(config):
disp.notification("Successfully renamed {0} to {1}."
.format(certname, new_certname), pause=False)
def certificates(config):
"""Display information about certs configured with Certbot
@ -87,6 +88,7 @@ def certificates(config):
# Describe all the certs
_describe_certs(config, parsed_certs, parse_failures)
def delete(config):
"""Delete Certbot files associated with a certificate lineage."""
certnames = get_certnames(config, "delete", allow_multiple=True)
@ -123,11 +125,13 @@ def lineage_for_certname(cli_config, certname):
logger.debug("Traceback was:\n%s", traceback.format_exc())
return None
def domains_for_certname(config, certname):
"""Find the domains in the cert with name certname."""
lineage = lineage_for_certname(config, certname)
return lineage.names() if lineage else None
def find_duplicative_certs(config, domains):
"""Find existing certs that match the given domain names.
@ -172,6 +176,7 @@ def find_duplicative_certs(config, domains):
return _search_lineages(config, update_certs_for_domain_matches, (None, None))
def _archive_files(candidate_lineage, filetype):
""" In order to match things like:
/etc/letsencrypt/archive/example.com/chain1.pem.
@ -193,6 +198,7 @@ def _archive_files(candidate_lineage, filetype):
return pattern
return None
def _acceptable_matches():
""" Generates the list that's passed to match_and_check_overlaps. Is its own function to
make unit testing easier.
@ -203,6 +209,7 @@ def _acceptable_matches():
return [lambda x: x.fullchain_path, lambda x: x.cert_path,
lambda x: _archive_files(x, "cert"), lambda x: _archive_files(x, "fullchain")]
def cert_path_to_lineage(cli_config):
""" If config.cert_path is defined, try to find an appropriate value for config.certname.
@ -219,6 +226,7 @@ def cert_path_to_lineage(cli_config):
lambda x: cli_config.cert_path[0], lambda x: x.lineagename)
return match[0]
def match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func):
""" Searches through all lineages for a match, and checks for duplicates.
If a duplicate is found, an error is raised, as performing operations on lineages
@ -284,20 +292,23 @@ def human_readable_cert_info(config, cert, skip_filter_checks=False):
valid_string = "{0} ({1})".format(cert.target_expiry, status)
serial = format(crypto_util.get_serial_from_cert(cert.cert_path), 'x')
certinfo.append(" Certificate Name: {0}\n"
" Serial Number: {1}\n"
" Domains: {2}\n"
" Expiry Date: {3}\n"
" Certificate Path: {4}\n"
" Private Key Path: {5}".format(
certinfo.append(" Certificate Name: {}\n"
" Serial Number: {}\n"
" Key Type: {}\n"
" Domains: {}\n"
" Expiry Date: {}\n"
" Certificate Path: {}\n"
" Private Key Path: {}".format(
cert.lineagename,
serial,
cert.private_key_type,
" ".join(cert.names()),
valid_string,
cert.fullchain,
cert.privkey))
return "".join(certinfo)
def get_certnames(config, verb, allow_multiple=False, custom_prompt=None):
"""Get certname from flag, interactively, or error out.
"""
@ -337,10 +348,12 @@ def get_certnames(config, verb, allow_multiple=False, custom_prompt=None):
# Private Helpers
###################
def _report_lines(msgs):
"""Format a results report for a category of single-line renewal outcomes"""
return " " + "\n ".join(str(msg) for msg in msgs)
def _report_human_readable(config, parsed_certs):
"""Format a results report for a parsed cert"""
certinfo = []
@ -348,6 +361,7 @@ def _report_human_readable(config, parsed_certs):
certinfo.append(human_readable_cert_info(config, cert))
return "\n".join(certinfo)
def _describe_certs(config, parsed_certs, parse_failures):
"""Print information about the certs we know about"""
out = [] # type: List[str]
@ -369,6 +383,7 @@ def _describe_certs(config, parsed_certs, parse_failures):
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("\n".join(out), pause=False, wrap=False)
def _search_lineages(cli_config, func, initial_rv, *args):
"""Iterate func over unbroken lineages, allowing custom return conditions.

View file

@ -313,6 +313,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False):
helpful.add(
"security", "--rsa-key-size", type=int, metavar="N",
default=flag_default("rsa_key_size"), help=config_help("rsa_key_size"))
helpful.add(
"security", "--key-type", choices=['rsa', 'ecdsa'], type=str,
default=flag_default("key_type"), help=config_help("key_type"))
helpful.add(
"security", "--elliptic-curve", type=str, choices=[
'secp256r1',
'secp384r1',
'secp521r1',
], metavar="N",
default=flag_default("elliptic_curve"), help=config_help("elliptic_curve"))
helpful.add(
"security", "--must-staple", action="store_true",
dest="must_staple", default=flag_default("must_staple"),

View file

@ -230,6 +230,10 @@ class HelpfulArgumentParser(object):
raise errors.Error(
"Parameters --hsts and --auto-hsts cannot be used simultaneously.")
if isinstance(parsed_args.key_type, list) and len(parsed_args.key_type) > 1:
raise errors.Error(
"Only *one* --key-type type may be provided at this time.")
return parsed_args
def set_test_server(self, parsed_args):

View file

@ -312,7 +312,6 @@ class Client(object):
:rtype: tuple
"""
# We need to determine the key path, key PEM data, CSR path,
# and CSR PEM data. For a dry run, the paths are None because
# they aren't permanently saved to disk. For a lineage with
@ -335,16 +334,41 @@ class Client(object):
# The key is set to None here but will be created below.
key = None
key_size = self.config.rsa_key_size
elliptic_curve = None
# key-type defaults to a list, but we are only handling 1 currently
if isinstance(self.config.key_type, list):
self.config.key_type = self.config.key_type[0]
if self.config.elliptic_curve and self.config.key_type == 'ecdsa':
elliptic_curve = self.config.elliptic_curve
self.config.auth_chain_path = "./chain-ecdsa.pem"
self.config.auth_cert_path = "./cert-ecdsa.pem"
self.config.key_path = "./key-ecdsa.pem"
elif self.config.rsa_key_size and self.config.key_type.lower() == 'rsa':
key_size = self.config.rsa_key_size
# Create CSR from names
if self.config.dry_run:
key = key or util.Key(file=None,
pem=crypto_util.make_key(self.config.rsa_key_size))
key = key or util.Key(
file=None,
pem=crypto_util.make_key(
bits=key_size,
elliptic_curve=elliptic_curve,
key_type=self.config.key_type,
),
)
csr = util.CSR(file=None, form="pem",
data=acme_crypto_util.make_csr(
key.pem, domains, self.config.must_staple))
else:
key = key or crypto_util.init_save_key(self.config.rsa_key_size,
self.config.key_dir)
key = key or crypto_util.init_save_key(
key_size=key_size,
key_dir=self.config.key_dir,
key_type=self.config.key_type,
elliptic_curve=elliptic_curve,
)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names)

View file

@ -57,6 +57,8 @@ CLI_DEFAULTS = dict(
https_port=443,
break_my_certs=False,
rsa_key_size=2048,
elliptic_curve="secp256r1",
key_type="rsa",
must_staple=False,
redirect=None,
auto_hsts=False,

View file

@ -1009,6 +1009,7 @@ def delete(config, unused_plugins):
"""
cert_manager.delete(config)
def certificates(config, unused_plugins):
"""Display information about certs configured with Certbot
@ -1024,6 +1025,7 @@ def certificates(config, unused_plugins):
"""
cert_manager.certificates(config)
# TODO: coop with renewal config
def revoke(config, unused_plugins):
"""Revoke a previously obtained certificate.
@ -1156,6 +1158,7 @@ def _csr_get_and_save_cert(config, le_client):
os.path.normpath(config.chain_path), os.path.normpath(config.fullchain_path))
return cert_path, fullchain_path
def renew_cert(config, plugins, lineage):
"""Renew & save an existing cert. Do not install it.

View file

@ -10,7 +10,7 @@ import time
import traceback
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import OpenSSL
import six
@ -40,7 +40,7 @@ logger = logging.getLogger(__name__)
STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
"server", "account", "authenticator", "installer",
"renew_hook", "pre_hook", "post_hook", "http01_address",
"preferred_chain"]
"preferred_chain", "key_type", "elliptic_curve"]
INT_CONFIG_ITEMS = ["rsa_key_size", "http01_port"]
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key",
"autorenew"]
@ -506,6 +506,10 @@ def _update_renewal_params_from_key(key_path, config):
with open(key_path, 'rb') as file_h:
key = load_pem_private_key(file_h.read(), password=None, backend=default_backend())
if isinstance(key, rsa.RSAPrivateKey):
config.key_type = 'rsa'
config.rsa_key_size = key.key_size
elif isinstance(key, ec.EllipticCurvePrivateKey):
config.key_type = 'ecdsa'
config.elliptic_curve = key.curve.name
else:
raise errors.Error('Key at {0} is of an unsupported type: {1}.'.format(key_path, type(key)))

View file

@ -10,6 +10,9 @@ import configobj
import parsedatetime
import pytz
import six
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import certbot
from certbot import crypto_util
@ -46,6 +49,7 @@ def renewal_conf_files(config):
result.sort()
return result
def renewal_file_for_certname(config, certname):
"""Return /path/to/certname.conf in the renewal conf directory"""
path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname))
@ -1055,6 +1059,23 @@ class RenewableCert(interfaces.RenewableCert):
target, values)
return cls(new_config.filename, cli_config)
@property
def private_key_type(self):
"""
:returns: The type of algorithm for the private, RSA or ECDSA
:rtype: str
"""
with open(self.configuration["privkey"], "rb") as priv_key_file:
key = load_pem_private_key(
data=priv_key_file.read(),
password=None,
backend=default_backend()
)
if isinstance(key, RSAPrivateKey):
return "RSA"
else:
return "ECDSA"
def save_successor(self, prior_version, new_cert,
new_privkey, new_chain, cli_config):
"""Save new cert and chain as a successor of a prior version.

View file

@ -11,14 +11,16 @@ import warnings
import re
# See https://github.com/pyca/cryptography/issues/4275
from cryptography import x509 # type: ignore
from cryptography.exceptions import InvalidSignature
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat
from OpenSSL import crypto
from OpenSSL import SSL # type: ignore
import pyrfc3339
import six
import zope.component
@ -34,7 +36,9 @@ logger = logging.getLogger(__name__)
# High level functions
def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
def init_save_key(
key_size, key_dir, key_type="rsa", elliptic_curve="secp256r1", keyname="key-certbot.pem"
):
"""Initializes and saves a privkey.
Inits key and saves it in PEM format on the filesystem.
@ -42,8 +46,10 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
.. note:: keyname is the attempted filename, it may be different if a file
already exists at the path.
:param int key_size: RSA key size in bits
:param int key_size: key size in bits if key size is rsa.
:param str key_dir: Key save directory.
:param str key_type: Key Type [rsa, ecdsa]
:param str elliptic_curve: Name of the elliptic curve if key type is ecdsa.
:param str keyname: Filename of key
:returns: Key
@ -53,7 +59,9 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
"""
try:
key_pem = make_key(key_size)
key_pem = make_key(
bits=key_size, elliptic_curve=elliptic_curve or "secp256r1", key_type=key_type,
)
except ValueError as err:
logger.error("", exc_info=True)
raise err
@ -65,7 +73,10 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
os.path.join(key_dir, keyname), 0o600, "wb")
with key_f:
key_f.write(key_pem)
logger.debug("Generating key (%d bits): %s", key_size, key_path)
if key_type == 'rsa':
logger.debug("Generating RSA key (%d bits): %s", key_size, key_path)
else:
logger.debug("Generating ECDSA key (%d bits): %s", key_size, key_path)
return util.Key(key_path, key_pem)
@ -174,18 +185,45 @@ def import_csr_file(csrfile, data):
return PEM, util.CSR(file=csrfile, data=data_pem, form="pem"), domains
def make_key(bits):
"""Generate PEM encoded RSA key.
def make_key(bits=1024, key_type="rsa", elliptic_curve=None):
"""Generate PEM encoded RSA|EC key.
:param int bits: Number of bits, at least 1024.
:param int bits: Number of bits if key_type=rsa. At least 1024 for RSA.
:returns: new RSA key in PEM form with specified number of bits
:param str ec_curve: The elliptic curve to use.
:returns: new RSA or ECDSA key in PEM form with specified number of bits
or of type ec_curve when key_type ecdsa is used.
:rtype: str
"""
assert bits >= 1024 # XXX
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, bits)
if key_type == 'rsa':
if bits < 1024:
raise errors.Error("Unsupported RSA key length: {}".format(bits))
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, bits)
elif key_type == 'ecdsa':
try:
name = elliptic_curve.upper()
if name in ('SECP256R1', 'SECP384R1', 'SECP512R1'):
_key = ec.generate_private_key(
curve=getattr(ec, elliptic_curve.upper(), None)(),
backend=default_backend()
)
else:
raise errors.Error("Unsupported elliptic curve: {}".format(elliptic_curve))
except TypeError:
raise errors.Error("Unsupported elliptic curve: {}".format(elliptic_curve))
except UnsupportedAlgorithm as e:
raise six.raise_from(e, errors.Error(str(e)))
_key_pem = _key.private_bytes(
encoding=Encoding.PEM,
format=PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=NoEncryption()
)
key = crypto.load_privatekey(crypto.FILETYPE_PEM, _key_pem)
else:
raise errors.Error("Invalid key_type specified: {}. Use [rsa|ecdsa]".format(key_type))
return crypto.dump_privatekey(crypto.FILETYPE_PEM, key)

View file

@ -198,6 +198,13 @@ class IConfig(zope.interface.Interface):
"register multiple emails, ex: u1@example.com,u2@example.com. "
"(default: Ask).")
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
elliptic_curve = zope.interface.Attribute(
"The SECG elliptic curve name to use. Please see RFC 8446 "
"for supported values."
)
key_type = zope.interface.Attribute(
"Type of generated private key"
"(Only *ONE* per invocation can be provided at this time)")
must_staple = zope.interface.Attribute(
"Adds the OCSP Must Staple extension to the certificate. "
"Autoconfigures OCSP Stapling for supported setups "
@ -260,6 +267,7 @@ class IConfig(zope.interface.Interface):
"offered chain will be used."
)
class IInstaller(IPlugin):
"""Generic Certbot Installer Interface.

View file

@ -2,10 +2,16 @@ The following command has been used to generate test keys:
for x in 256 512 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
For the elliptic curve private keys, this command was used:
for k in "prime256v1" "secp384r1" "secp521r1" do
openssl genpkey -algorithm ${k} -out ec_${k}_key.pem
done
and for the CSR PEM (Certificate Signing Request):
openssl req -new -out csr-Xsans_X.pem -key rsa512_key.pem [-config csr-Xsans_X.conf | -subj '/CN=example.com'] [-outform DER > csr_X.der]
and for the certificate:
openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der]
openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der]

View file

@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDqQPQl69kuh+DrecC8SFPt21f0F/HHDP3T4/Lf0zIVFoAoGCCqGSM49
AwEHoUQDQgAEHou50Ee9u+8Vial6VbUHExlzsiCHtORlW0X0pKo5RspIKB0QyKwo
dUXvBbv95I9yCO5+MlGkKjwLHtIEze0Hww==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,9 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDAgvZGw5C7Mp26N0cXA+vIg5K/J5MJw+MVGYfGF4ZutuCLeYMrWT68R
A0h6hJvDtMSgBwYFK4EEACKhZANiAAR1uQYZeU5Kml5o53Q8/PCdwUbqdgCSkV0C
J5a6bhDRMp20fdp2T/mbkdxuVEl81lqfKPZhsd4CZsLaVIU3RUoGgIT1R3QKawpJ
SuXq37yWFX2hqlgt+lsBufZ8RD5QnZc=
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,10 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIACWWVKm1qAIejZ6qmqk9D69wQW5FAe3Er0IxWAMkonTEhu8EH5Q2i
2vT2bESm730zhGTe2Pn11b85H6UI9hxhCHygBwYFK4EEACOhgYkDgYYABAEQi1WF
m3suHjPyWACyOJYGUn1Kx6rfBo0PjC7X2TU9jr8umLkIpaaF5UsBuMBmdz1IHL0U
k0gQtoOQ0Qu8N74GuAGzGR0S3RYIv6gfYVz3dS1K4n4b307Lx62bnvtlNxcIvt3w
hmS5OdvQ1Kdxh6oqbSVhhbQmJcgab78Txx3R2QeCxw==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIIBvrEnbPRYu8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjEwNzQw
WhcNMjUxMDEyMjEwNzQwWjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs
ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARjMhuW0ENPPC33PjB5XsYU
CRw640kPQENIDatcTJaENZIZdqKd6rI6jc+lpbmXot7Zi52clJlSJS+V6oDAt2Lh
o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUj7Kd3ENqxlPf8B2bIGhsjydX
mPswHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww
GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCl
k0JXsa8y7fg41WWMDhw60bPW77O0FtOmTcnhdI5daYNemQVk+Q5EMaBLQ/oGjgXd
9QXFzXH1PL904YEnSLt+iTpXn++7rQSNzQsdYqw0neWk4f5pEBiN+WORpb6mwobV
ifMtBOkNEHvrJ2Pkci9U1lLwtKD/DSew6QtJU5DSkmH1XdGuMJiubygEIvELtvgq
cP9S368ZvPmPGmKaJQXBiuaR8MTjY/Bkr79aXQMjKbf+mpn7h0POCcePk1DY/rm6
Da+X16lf0hHyQhSUa7Vgyim6rK1/hlw+Z00i+sQCKD9Ih7kXuuGqfSDC33cfO8Tj
o/MXO8lcxkrem5zU5QWP
-----END CERTIFICATE-----

View file

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx
MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy
NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq
mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB
qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5
CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH
nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY
MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx
PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE
bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT
MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq
uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P
fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV
EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW
fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG
9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE
-----END CERTIFICATE-----

View file

@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIILlmGtZhUFEwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjA1MDM0
WhcNMjUxMDEyMjA1MDM0WjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs
ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARHEzR8JPWrEmpmgM+F2bk5
9mT0u6CjzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/
o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU1CsVL+bPnzaxxQ5jUENmQJIO
lKwwHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww
GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBn
2D8loC7pfk28JYpFLr5lmFKJWWmtLGlpsWDj61fVjtTfGKLziJz+MM6il4Y3hIz5
58qiFK0ue0M63dIBJ33N+XxSEXon4Q0gy/zRWfH9jtPJ3FwfjkU/RT9PAUClYi0G
ptNWnTmgQkNzousbcAtRNXuuShH3856vhUnwkX+xM+cbIDi1JVmFjcGrEEQJ0rUF
mv2ZTyfbWbUs3v4rReETi2NVzr1Ql6J+ByNcMvHODzFy3t0L6yelAw2ca1I+c9HU
+Z0tnp/ykR7eXNuVLivok8UBf5OC413lh8ZO5g+Bgzh/LdtkUuavg1MYtEX0H6mX
9U7y3nVI8WEbPGf+HDeu
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx
MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy
NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq
mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB
qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5
CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH
nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY
MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx
PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE
bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT
MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq
uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P
fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV
EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW
fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG
9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE
-----END CERTIFICATE-----

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNgefv2dad4U1VYEi
0WkdHuqywi5QXAe30OwNTTGjhbihRANCAARHEzR8JPWrEmpmgM+F2bk59mT0u6Cj
zmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/
-----END PRIVATE KEY-----

View file

@ -0,0 +1,79 @@
# add some stuff here
# assets/integration_tests
cert = MAGICDIR/live/sample-renewal-ec/cert.pem
privkey = MAGICDIR/live/sample-renewal-ec/privkey.pem
chain = MAGICDIR/live/sample-renewal-ec/chain.pem
fullchain = MAGICDIR/live/sample-renewal-ec/fullchain.pem
renew_before_expiry = 4 years
# Options and defaults used in the renewal process
[renewalparams]
no_self_upgrade = False
apache_enmod = a2enmod
no_verify_ssl = False
ifaces = None
apache_dismod = a2dismod
register_unsafely_without_email = False
apache_handle_modules = True
uir = None
installer = None
nginx_ctl = nginx
config_dir = MAGICDIR
text_mode = False
func = <function obtain_cert at 0x7f093a163c08>
staging = True
prepare = False
work_dir = /var/lib/letsencrypt
tos = False
init = False
http01_port = 80
duplicate = False
noninteractive_mode = True
key_path = None
nginx = False
nginx_server_root = /etc/nginx
fullchain_path = /home/ubuntu/letsencrypt/chain.pem
email = None
csr = None
agree_dev_preview = None
redirect = None
verb = certonly
verbose_count = -3
config_file = None
renew_by_default = False
hsts = False
apache_handle_sites = True
authenticator = standalone
domains = isnot.org,
key_type = ecdsa
elliptic_curve = secp256r1
apache_challenge_location = /etc/apache2
checkpoints = 1
manual_test_mode = False
apache = False
cert_path = /home/ubuntu/letsencrypt/cert.pem
webroot_path = None
reinstall = False
expand = False
strict_permissions = False
apache_server_root = /etc/apache2
account = None
dry_run = False
manual_public_ip_logging_ok = False
chain_path = /home/ubuntu/letsencrypt/chain.pem
break_my_certs = False
standalone = True
manual = False
server = https://acme-staging-v02.api.letsencrypt.org/directory
webroot = False
os_packages_only = False
apache_init_script = None
user_agent = None
apache_le_vhost_ext = -le-ssl.conf
debug = False
logs_dir = /var/log/letsencrypt
apache_vhost_root = /etc/apache2/sites-available
configurator = None
must_staple = True
[[webroot_map]]

View file

@ -44,6 +44,7 @@ apache_handle_sites = True
authenticator = standalone
domains = isnot.org,
rsa_key_size = 2048
elliptic_curve = secp256r1
apache_challenge_location = /etc/apache2
checkpoints = 1
manual_test_mode = False

View file

@ -93,7 +93,7 @@ def load_pyopenssl_private_key(*names):
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
def make_lineage(config_dir, testfile):
def make_lineage(config_dir, testfile, ec=False):
"""Creates a lineage defined by testfile.
This creates the archive, live, and renewal directories if
@ -119,7 +119,7 @@ def make_lineage(config_dir, testfile):
if not os.path.exists(directory):
filesystem.makedirs(directory)
sample_archive = vector_path('sample-archive')
sample_archive = vector_path('sample-archive{}'.format('-ec' if ec else ''))
for kind in os.listdir(sample_archive):
shutil.copyfile(os.path.join(sample_archive, kind),
os.path.join(archive_dir, kind))

View file

@ -80,8 +80,8 @@ its preferences in accordance with its own policy or its administrators'
preferences, and use different cryptographic mechanisms or parameters,
or a different priority order, than the defaults provided by Certbot.
If you don't use Certbot to configure your server directly, because the
client doesn't integrate with your server software or because you chose
If you don't use Certbot to configure your server directly, because the
client doesn't integrate with your server software or because you chose
not to use this integration, then the cryptographic defaults haven't been
modified, and the cryptography chosen by the server will still be whatever
the default for your software was. For example, if you obtain a
@ -254,7 +254,7 @@ https://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-secur
U.S. Government 18F
~~~~~~~~~~~~~~~~~~~
The 18F site (https://18f.gsa.gov/) is using
The 18F site (https://18f.gsa.gov/) is using
::

View file

@ -1,4 +1,4 @@
usage:
usage:
certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
@ -188,6 +188,12 @@ security:
Security parameters & server settings
--rsa-key-size N Size of the RSA key. (default: 2048)
--key-type type The type of algorithm to use for the the private key.
Either ``rsa`` or ``ecdsa``. (default: ``rsa``).
--elliptic-curve The elliptic curve to use when choosing ``ecdsa`` as the key
type. Accepted values are SECG curve names as defined by
the cryptography library. ``secp256r1``, ``secp384r1``,
``secp521r1``. (default: secp256r1).
--must-staple Adds the OCSP Must Staple extension to the
certificate. Autoconfigures OCSP Stapling for
supported setups (Apache version >= 2.3.3 ). (default:

View file

@ -319,6 +319,7 @@ This returns information in the following format::
Domains: example.com, www.example.com
Expiry Date: 2017-02-19 19:53:00+00:00 (VALID: 30 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
Key Type: RSA
Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
``Certificate Name`` shows the name of the certificate. Pass this name
@ -346,7 +347,6 @@ control Certbot's behavior when re-creating
a certificate with the same name as an existing certificate.
If you don't specify a requested behavior, Certbot may ask you what you intended.
``--force-renewal`` tells Certbot to request a new certificate
with the same domains as an existing certificate. Each domain
must be explicitly specified via ``-d``. If successful, this certificate
@ -380,7 +380,6 @@ If you prefer, you can specify the domains individually like this:
Consider using ``--cert-name`` instead of ``--expand``, as it gives more control
over which certificate is modified and it lets you remove domains as well as adding them.
``--allow-subset-of-names`` tells Certbot to continue with certificate generation if
only some of the specified domain authorizations can be obtained. This may
be useful if some domains specified in a certificate no longer point at this
@ -411,6 +410,19 @@ replace that set entirely::
certbot certonly --cert-name example.com -d example.org,www.example.org
Migrating to certificates based on ECDSA keys
---------------------------------------------
As of version 1.10, Certbot supports two types of private key algorithms:
``rsa`` and ``ecdsa``. You may freely upgrade an existing certificate with a
new private key. This requires issuing a new command, or changing the renewal
file for the certificates so it will happen on the next renewal. The two
options that you need for the renewal command are ``--key-type`` and
``--elliptic-curve <name>`` in case you either want to be explicit or want to
use something else than the default curve ``secp256r1``::
certbot renew --key-type ecdsa --cert-name example.com -d example.org,www.example.org
Revoking certificates
---------------------

View file

@ -7,6 +7,10 @@
# certificate on a system with several certificates should not be placed
# here.
# Use ECC for the private key
key-type = ecdsa
elliptic-curve = secp384r1
# Use a 4096 bit RSA key instead of 2048
rsa-key-size = 4096

View file

@ -359,6 +359,21 @@ class ParseTest(unittest.TestCase):
self.assertFalse(cli.option_was_set(
'authenticator', cli.flag_default('authenticator')))
def test_ecdsa_key_option(self):
elliptic_curve_option = 'elliptic_curve'
elliptic_curve_option_value = cli.flag_default(elliptic_curve_option)
self.parse('--elliptic-curve {0}'.format(elliptic_curve_option_value).split())
self.assertIs(cli.option_was_set(elliptic_curve_option, elliptic_curve_option_value), True)
def test_invalid_key_type(self):
key_type_option = 'key_type'
key_type_value = cli.flag_default(key_type_option)
self.parse('--key-type {0}'.format(key_type_value).split())
self.assertIs(cli.option_was_set(key_type_option, key_type_value), True)
with self.assertRaises(SystemExit):
self.parse("--key-type foo")
def test_encode_revocation_reason(self):
for reason, code in constants.REVOCATION_REASONS.items():
namespace = self.parse(['--reason', reason])

View file

@ -329,7 +329,11 @@ class ClientTest(ClientTestCommon):
self._test_obtain_certificate_common(mock.sentinel.key, csr)
mock_crypto_util.init_save_key.assert_called_once_with(
self.config.rsa_key_size, self.config.key_dir)
key_size=self.config.rsa_key_size,
key_dir=self.config.key_dir,
key_type=self.config.key_type,
elliptic_curve=None, # elliptic curve is not set
)
mock_crypto_util.init_save_csr.assert_called_once_with(
mock.sentinel.key, self.eg_domains, self.config.csr_dir)
mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with(
@ -365,7 +369,11 @@ class ClientTest(ClientTestCommon):
self.client.config.dry_run = True
self._test_obtain_certificate_common(key, csr)
mock_crypto.make_key.assert_called_once_with(self.config.rsa_key_size)
mock_crypto.make_key.assert_called_once_with(
bits=self.config.rsa_key_size,
elliptic_curve=None, # not making an elliptic private key
key_type=self.config.key_type,
)
mock_acme_crypto.make_csr.assert_called_once_with(
mock.sentinel.key_pem, self.eg_domains, self.config.must_staple)
mock_crypto.init_save_key.assert_not_called()

View file

@ -4,7 +4,7 @@ import unittest
try:
import mock
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
from unittest import mock
import OpenSSL
import zope.component
@ -32,6 +32,7 @@ CERT_LEAF = test_util.load_vector('cert_leaf.pem')
CERT_ISSUER = test_util.load_vector('cert_intermediate_1.pem')
CERT_ALT_ISSUER = test_util.load_vector('cert_intermediate_2.pem')
class InitSaveKeyTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.init_save_key."""
def setUp(self):
@ -174,11 +175,54 @@ class ImportCSRFileTest(unittest.TestCase):
class MakeKeyTest(unittest.TestCase):
"""Tests for certbot.crypto_util.make_key."""
def test_it(self): # pylint: disable=no-self-use
def test_rsa(self): # pylint: disable=no-self-use
# RSA Key Type Test
from certbot.crypto_util import make_key
# Do not test larger keys as it takes too long.
OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, make_key(1024))
def test_ec(self): # pylint: disable=no-self-use
# ECDSA Key Type Tests
from certbot.crypto_util import make_key
# Do not test larger keys as it takes too long.
# Try a good key size for ECDSA
OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, make_key(1024))
OpenSSL.crypto.FILETYPE_PEM, make_key(elliptic_curve="secp256r1", key_type='ecdsa'))
def test_bad_key_sizes(self):
from certbot.crypto_util import make_key
# Try a bad key size for RSA and ECDSA
with self.assertRaises(errors.Error) as e:
make_key(bits=512, key_type='rsa')
self.assertEqual(
"Unsupported RSA key length: 512",
str(e.exception),
"Unsupported RSA key length: 512"
)
def test_bad_elliptic_curve_name(self):
from certbot.crypto_util import make_key
with self.assertRaises(errors.Error) as e:
make_key(elliptic_curve="nothere", key_type='ecdsa')
self.assertEqual(
"Unsupported elliptic curve: nothere",
str(e.exception),
"Unsupported elliptic curve: nothere"
)
def test_bad_key_type(self):
from certbot.crypto_util import make_key
# Try a bad --key-type
with self.assertRaises(errors.Error) as e:
OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, make_key(1024, key_type='unf'))
self.assertEqual(
"Invalid key_type specified: unf. Use [rsa|ecdsa]",
str(e.exception),
"Invalid key_type specified: unf. Use [rsa|ecdsa]",
)
class VerifyCertSetup(unittest.TestCase):

View file

@ -74,6 +74,30 @@ class RenewalTest(test_util.ConfigTestCase):
assert self.config.rsa_key_size == 2048
def test_reuse_ec_key_renewal_params(self):
self.config.elliptic_curve = 'INVALID_CURVE'
self.config.reuse_key = True
self.config.dry_run = True
self.config.key_type = 'ecdsa'
config = configuration.NamespaceConfig(self.config)
rc_path = test_util.make_lineage(
self.config.config_dir,
'sample-renewal-ec.conf',
ec=True,
)
lineage = storage.RenewableCert(rc_path, config)
le_client = mock.MagicMock()
le_client.obtain_certificate.return_value = (None, None, None, None)
from certbot._internal import renewal
with mock.patch('certbot._internal.renewal.hooks.renew_hook'):
renewal.renew_cert(self.config, None, le_client, lineage)
assert self.config.elliptic_curve == 'secp256r1'
class RestoreRequiredConfigElementsTest(test_util.ConfigTestCase):
"""Tests for certbot._internal.renewal.restore_required_config_elements."""