certapi 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {certapi-0.4.1 → certapi-0.4.2}/PKG-INFO +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/setup.py +1 -1
- certapi-0.4.2/src/certapi/__init__.py +23 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/acme/Acme.py +3 -2
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/acme/Challenge.py +12 -15
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/acme/Order.py +3 -2
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/acme/__init__.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/acme/http.py +1 -2
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/ChallengeStore.py +3 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/__init__.py +2 -3
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/__init__.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/cloudflare/cloudflare_challenge_store.py +4 -4
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/cloudflare/cloudflare_client.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/digitalocean/digitalocean_challenge_store.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/digitalocean/digitalocean_client.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/client/cert_manager_client.py +29 -11
- certapi-0.4.2/src/certapi/crypto/__init__.py +2 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/crypto/crypto.py +4 -3
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/crypto/crypto_classes.py +5 -5
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/http/types.py +13 -6
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/issuers/AcmeCertIssuer.py +28 -17
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/issuers/SelfCertIssuer.py +3 -2
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/issuers/__init__.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/issuers/abstract_certissuer.py +22 -14
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/keystore/FileSystemKeystore.py +7 -7
- certapi-0.4.2/src/certapi/keystore/KeyStore.py +71 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/keystore/PostgresqlKeyStore.py +12 -5
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/keystore/RemoteKeyStore.py +7 -3
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/keystore/SqliteKeyStore.py +7 -4
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/keystore/__init__.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/manager/acme_cert_manager.py +35 -29
- certapi-0.4.2/src/certapi/server/api.py +103 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/server/cert_api.py +48 -36
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/server/key_api.py +25 -28
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/utils.py +0 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi.egg-info/PKG-INFO +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi.egg-info/SOURCES.txt +0 -1
- certapi-0.4.1/src/certapi/__init__.py +0 -9
- certapi-0.4.1/src/certapi/challenge_store/FilesystemChallengeStore.py +0 -38
- certapi-0.4.1/src/certapi/crypto/__init__.py +0 -2
- certapi-0.4.1/src/certapi/keystore/KeyStore.py +0 -66
- certapi-0.4.1/src/certapi/server/api.py +0 -89
- {certapi-0.4.1 → certapi-0.4.2}/MANIFEST.in +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/README.md +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/pyproject.toml +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/setup.cfg +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/acme/AcmeError.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/FileSystemChallengeStore.py +1 -1
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/InmemoryChallengeStore.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/cloudflare/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/digitalocean/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/client/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/http/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/http/client.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/manager/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/server/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi/util.py +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-0.4.1 → certapi-0.4.2}/src/certapi.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .acme.Acme import Acme, Order, AcmeNetworkError, AcmeHttpError, Challenge
|
|
2
|
+
from .manager.acme_cert_manager import AcmeCertManager
|
|
3
|
+
from .http.types import CertificateResponse, IssuedCert
|
|
4
|
+
|
|
5
|
+
from .crypto import (
|
|
6
|
+
Certificate,
|
|
7
|
+
CertificateSigningRequest,
|
|
8
|
+
CertificateSigningRequestBuilder,
|
|
9
|
+
Key,
|
|
10
|
+
Ed25519Key,
|
|
11
|
+
ECDSAKey,
|
|
12
|
+
Ed25519PrivateKey,
|
|
13
|
+
EllipticCurvePrivateKey,
|
|
14
|
+
)
|
|
15
|
+
from .keystore import FileSystemKeystore, SqliteKeyStore, PostgresKeyStore, KeyStore
|
|
16
|
+
from .challenge_store import (
|
|
17
|
+
ChallengeStore,
|
|
18
|
+
InMemoryChallengeStore,
|
|
19
|
+
FileSystemChallengeStore,
|
|
20
|
+
CloudflareChallengeStore,
|
|
21
|
+
DigitalOceanChallengeStore,
|
|
22
|
+
)
|
|
23
|
+
from .issuers import SelfCertIssuer
|
|
@@ -9,13 +9,14 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
|
9
9
|
from cryptography.x509 import CertificateSigningRequest, Certificate
|
|
10
10
|
from certapi.crypto import crypto
|
|
11
11
|
import requests
|
|
12
|
-
from certapi.crypto import sign, digest_sha256, csr_to_der, jwk, get_algorithm_name, sign_for_jws,Key
|
|
12
|
+
from certapi.crypto import sign, digest_sha256, csr_to_der, jwk, get_algorithm_name, sign_for_jws, Key
|
|
13
13
|
from certapi.util import b64_encode, b64_string
|
|
14
14
|
from .AcmeError import *
|
|
15
15
|
from .http import *
|
|
16
16
|
from .Challenge import Challenge
|
|
17
17
|
from .Order import Order
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
class Acme:
|
|
20
21
|
|
|
21
22
|
URL_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
@@ -23,7 +24,7 @@ class Acme:
|
|
|
23
24
|
|
|
24
25
|
def __init__(self, account_key: Union[RSAPrivateKey, EllipticCurvePrivateKey | Key], url=URL_STAGING):
|
|
25
26
|
self.account_key = account_key.key if isinstance(account_key, Key) else account_key
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
# json web key format for public key
|
|
28
29
|
self.jwk = jwk(self.account_key)
|
|
29
30
|
print(self.jwk)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import json
|
|
3
2
|
from typing import Literal, Union
|
|
4
3
|
|
|
@@ -7,7 +6,8 @@ import requests
|
|
|
7
6
|
from certapi.util import b64_encode, b64_string
|
|
8
7
|
from certapi.crypto import digest_sha256
|
|
9
8
|
from .AcmeError import *
|
|
10
|
-
from .http import post,get
|
|
9
|
+
from .http import post, get
|
|
10
|
+
|
|
11
11
|
|
|
12
12
|
class Challenge:
|
|
13
13
|
def __init__(self, auth_url, data, acme):
|
|
@@ -15,7 +15,7 @@ class Challenge:
|
|
|
15
15
|
self._acme = acme
|
|
16
16
|
self._data = data
|
|
17
17
|
challenge = self.get_challenge()
|
|
18
|
-
self.token:str = challenge["token"]
|
|
18
|
+
self.token: str = challenge["token"]
|
|
19
19
|
self.verified = challenge["status"] == "valid"
|
|
20
20
|
self.domain = data["identifier"]["value"] # Add domain attribute
|
|
21
21
|
|
|
@@ -25,24 +25,21 @@ class Challenge:
|
|
|
25
25
|
|
|
26
26
|
self.url = "http://{0}/.well-known/acme-challenge/{1}".format(data["identifier"]["value"], self.token)
|
|
27
27
|
|
|
28
|
-
def as_key_value(self,type:Literal["http-01", "dns-01", "tls-alpn-01"]=None):
|
|
29
|
-
challenge=self.get_challenge(type)
|
|
28
|
+
def as_key_value(self, type: Literal["http-01", "dns-01", "tls-alpn-01"] = None):
|
|
29
|
+
challenge = self.get_challenge(type)
|
|
30
30
|
|
|
31
31
|
if challenge["type"] == "dns-01":
|
|
32
32
|
key = f"_acme-challenge.{self.domain}"
|
|
33
33
|
value = b64_string(digest_sha256(self.authorization_key.encode("utf8")))
|
|
34
34
|
else:
|
|
35
|
-
key =
|
|
36
|
-
value =
|
|
37
|
-
|
|
38
|
-
return (key,value)
|
|
35
|
+
key = self.token
|
|
36
|
+
value = self.authorization_key
|
|
39
37
|
|
|
38
|
+
return (key, value)
|
|
40
39
|
|
|
41
|
-
def verify(self, type:Literal["http-01", "dns-01", "tls-alpn-01"]=None) -> bool:
|
|
40
|
+
def verify(self, type: Literal["http-01", "dns-01", "tls-alpn-01"] = None) -> bool:
|
|
42
41
|
if not self.verified:
|
|
43
|
-
response = self._acme._signed_req(
|
|
44
|
-
self.get_challenge(type)["url"], {}, step="Verify Challenge", throw=False
|
|
45
|
-
)
|
|
42
|
+
response = self._acme._signed_req(self.get_challenge(type)["url"], {}, step="Verify Challenge", throw=False)
|
|
46
43
|
if response.status_code == 200 and response.json()["status"] == "valid":
|
|
47
44
|
self.verified = True
|
|
48
45
|
return True
|
|
@@ -73,9 +70,9 @@ class Challenge:
|
|
|
73
70
|
else:
|
|
74
71
|
return False
|
|
75
72
|
|
|
76
|
-
def get_challenge(self, type:Literal["http-01", "dns-01", "tls-alpn-01"]=None):
|
|
73
|
+
def get_challenge(self, type: Literal["http-01", "dns-01", "tls-alpn-01"] = None):
|
|
77
74
|
challenges = self._data["challenges"]
|
|
78
|
-
def_key= type if type is not None else "http-01"
|
|
75
|
+
def_key = type if type is not None else "http-01"
|
|
79
76
|
for method in challenges:
|
|
80
77
|
if method["type"] == def_key:
|
|
81
78
|
return method
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from typing import List, Tuple
|
|
2
|
-
from certapi.crypto import crypto,csr_to_der
|
|
2
|
+
from certapi.crypto import crypto, csr_to_der
|
|
3
3
|
from certapi.util import b64_string
|
|
4
|
-
from cryptography.x509 import CertificateSigningRequest,Certificate
|
|
4
|
+
from cryptography.x509 import CertificateSigningRequest, Certificate
|
|
5
5
|
from .Challenge import *
|
|
6
6
|
from .http import get
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class Order:
|
|
9
10
|
def __init__(self, url, data, challenges, acme):
|
|
10
11
|
self.url = url
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
1
|
import requests
|
|
3
2
|
from .AcmeError import *
|
|
4
3
|
|
|
4
|
+
|
|
5
5
|
def request(method, step: str, url: str, json=None, headers=None, throw=True) -> requests.Response:
|
|
6
6
|
res = None
|
|
7
7
|
try:
|
|
@@ -53,4 +53,3 @@ def post(step: str, url: str, json=None, headers=None, throw=True) -> requests.R
|
|
|
53
53
|
|
|
54
54
|
def get(step: str, url) -> requests.Response:
|
|
55
55
|
return request("GET", step, url)
|
|
56
|
-
|
|
@@ -7,6 +7,7 @@ class ChallengeStore(ABC):
|
|
|
7
7
|
"""
|
|
8
8
|
Abstract base class for a challenge store.
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
@abstractmethod
|
|
11
12
|
def supports_domain(self, domain: str) -> bool:
|
|
12
13
|
pass
|
|
@@ -22,6 +23,7 @@ class ChallengeStore(ABC):
|
|
|
22
23
|
@abstractmethod
|
|
23
24
|
def delete_challenge(self, key: str, domain: str = None):
|
|
24
25
|
pass
|
|
26
|
+
|
|
25
27
|
def store_details():
|
|
26
28
|
return {
|
|
27
29
|
"name": "Abstract Challenge Store",
|
|
@@ -31,4 +33,4 @@ class ChallengeStore(ABC):
|
|
|
31
33
|
raise NotImplementedError("Must implement `__iter__` method.")
|
|
32
34
|
|
|
33
35
|
def __len__(self):
|
|
34
|
-
raise NotImplementedError("Must implement `__len__` method.")
|
|
36
|
+
raise NotImplementedError("Must implement `__len__` method.")
|
|
@@ -2,7 +2,8 @@ from .ChallengeStore import ChallengeStore
|
|
|
2
2
|
from .InmemoryChallengeStore import InMemoryChallengeStore
|
|
3
3
|
import os
|
|
4
4
|
from .FileSystemChallengeStore import FileSystemChallengeStore
|
|
5
|
-
from .dns import CloudflareChallengeStore,DigitalOceanChallengeStore
|
|
5
|
+
from .dns import CloudflareChallengeStore, DigitalOceanChallengeStore
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
def get_challenge_store():
|
|
8
9
|
"""
|
|
@@ -21,5 +22,3 @@ def get_challenge_store():
|
|
|
21
22
|
return FileSystemChallengeStore(directory)
|
|
22
23
|
else:
|
|
23
24
|
raise ValueError(f"Unknown CHALLENGE_STORE_TYPE: {store_type}")
|
|
24
|
-
|
|
25
|
-
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from .digitalocean.digitalocean_challenge_store import DigitalOceanChallengeStore
|
|
2
|
-
from .cloudflare.cloudflare_challenge_store import CloudflareChallengeStore
|
|
2
|
+
from .cloudflare.cloudflare_challenge_store import CloudflareChallengeStore
|
|
@@ -6,10 +6,10 @@ from .cloudflare_client import Cloudflare
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class CloudflareChallengeStore(ChallengeStore):
|
|
9
|
-
def __init__(self,api_key:str=None):
|
|
9
|
+
def __init__(self, api_key: str = None):
|
|
10
10
|
self.cloudflare = Cloudflare(api_key)
|
|
11
|
-
self.challenges_map = {}
|
|
12
|
-
|
|
11
|
+
self.challenges_map = {}
|
|
12
|
+
|
|
13
13
|
def supports_domain(self, domain: str) -> bool:
|
|
14
14
|
"""
|
|
15
15
|
Checks if the Cloudflare account has access to the given domain (or its base domain)
|
|
@@ -29,7 +29,7 @@ class CloudflareChallengeStore(ChallengeStore):
|
|
|
29
29
|
record_id = self.cloudflare.create_record(name=key, data=value, domain=base_domain)
|
|
30
30
|
self.challenges_map[key] = record_id
|
|
31
31
|
print(f"CloudflareChallengeStore: Saved challenge for {key} with record ID {record_id}")
|
|
32
|
-
time.sleep(10)
|
|
32
|
+
time.sleep(10) # wait for dns propagation. it may not be immediate
|
|
33
33
|
|
|
34
34
|
def get_challenge(self, key: str, domain: str) -> str:
|
|
35
35
|
base_domain = self.cloudflare.determine_registered_domain(domain)
|
{certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/cloudflare/cloudflare_client.py
RENAMED
|
@@ -7,7 +7,7 @@ from urllib.request import urlopen, Request
|
|
|
7
7
|
class Cloudflare(object):
|
|
8
8
|
name = "cloudflare"
|
|
9
9
|
|
|
10
|
-
def __init__(self,api_key:str):
|
|
10
|
+
def __init__(self, api_key: str):
|
|
11
11
|
self.token = api_key
|
|
12
12
|
self.api = "https://api.cloudflare.com/client/v4"
|
|
13
13
|
if not self.token:
|
|
@@ -5,7 +5,7 @@ from .digitalocean_client import DigitalOcean
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class DigitalOceanChallengeStore(ChallengeStore):
|
|
8
|
-
def __init__(self,api_key:str=None):
|
|
8
|
+
def __init__(self, api_key: str = None):
|
|
9
9
|
self.digitalocean = DigitalOcean(api_key)
|
|
10
10
|
self.challenges_map = {} # Stores key: record_id (needed for deletion)
|
|
11
11
|
|
{certapi-0.4.1 → certapi-0.4.2}/src/certapi/challenge_store/dns/digitalocean/digitalocean_client.py
RENAMED
|
@@ -5,7 +5,7 @@ from urllib.request import urlopen, Request # Python 3
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class DigitalOcean(object):
|
|
8
|
-
def __init__(self, api_key:str=None):
|
|
8
|
+
def __init__(self, api_key: str = None):
|
|
9
9
|
self.token = api_key
|
|
10
10
|
self.api = "https://api.digitalocean.com/v2/domains"
|
|
11
11
|
if not self.token:
|
|
@@ -2,6 +2,7 @@ import requests
|
|
|
2
2
|
from typing import List, Union, Dict, Optional, Any
|
|
3
3
|
from certapi.http.types import CertificateResponse, IssuedCert
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
class CertManagerClient:
|
|
6
7
|
def __init__(self, base_url: str):
|
|
7
8
|
self.base_url = base_url
|
|
@@ -11,8 +12,15 @@ class CertManagerClient:
|
|
|
11
12
|
response.raise_for_status()
|
|
12
13
|
return response.json()
|
|
13
14
|
|
|
14
|
-
def _post(
|
|
15
|
-
|
|
15
|
+
def _post(
|
|
16
|
+
self, path: str, data: Optional[Union[Dict[str, Any], str]] = None, headers: Optional[Dict[str, str]] = None
|
|
17
|
+
) -> Any:
|
|
18
|
+
response = requests.post(
|
|
19
|
+
f"{self.base_url}{path}",
|
|
20
|
+
json=data if isinstance(data, dict) else None,
|
|
21
|
+
data=data if isinstance(data, str) else None,
|
|
22
|
+
headers=headers,
|
|
23
|
+
)
|
|
16
24
|
response.raise_for_status()
|
|
17
25
|
return response.json()
|
|
18
26
|
|
|
@@ -32,17 +40,24 @@ class CertManagerClient:
|
|
|
32
40
|
"key_type": key_type,
|
|
33
41
|
"expiry_days": expiry_days,
|
|
34
42
|
}
|
|
35
|
-
if country:
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
if
|
|
43
|
+
if country:
|
|
44
|
+
params["country"] = country
|
|
45
|
+
if state:
|
|
46
|
+
params["state"] = state
|
|
47
|
+
if locality:
|
|
48
|
+
params["locality"] = locality
|
|
49
|
+
if organization:
|
|
50
|
+
params["organization"] = organization
|
|
51
|
+
if user_id:
|
|
52
|
+
params["user_id"] = user_id
|
|
40
53
|
|
|
41
54
|
data = self._get("/obtain", params=params)
|
|
42
55
|
return CertificateResponse.from_json(data)
|
|
43
56
|
|
|
44
57
|
def sign_csr(self, csr_pem: str) -> str:
|
|
45
|
-
response = requests.post(
|
|
58
|
+
response = requests.post(
|
|
59
|
+
f"{self.base_url}/sign_csr", data=csr_pem, headers={"Content-Type": "application/x-pem-file"}
|
|
60
|
+
)
|
|
46
61
|
response.raise_for_status()
|
|
47
62
|
return response.text
|
|
48
63
|
|
|
@@ -88,11 +103,14 @@ class CertManagerClient:
|
|
|
88
103
|
return None
|
|
89
104
|
raise
|
|
90
105
|
|
|
91
|
-
def save_cert(
|
|
106
|
+
def save_cert(
|
|
107
|
+
self, private_key_id: Union[int, str], cert_pem: str, domains: List[str], name: Optional[str] = None
|
|
108
|
+
) -> Dict[str, Any]:
|
|
92
109
|
data = {
|
|
93
110
|
"private_key_id": private_key_id,
|
|
94
111
|
"cert": cert_pem,
|
|
95
112
|
"domains": domains,
|
|
96
113
|
}
|
|
97
|
-
if name:
|
|
98
|
-
|
|
114
|
+
if name:
|
|
115
|
+
data["name"] = name
|
|
116
|
+
return self._post("/certs", data=data)
|
|
@@ -10,11 +10,11 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
10
10
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
11
11
|
from cryptography.x509 import Certificate, CertificateSigningRequestBuilder, CertificateSigningRequest
|
|
12
12
|
|
|
13
|
-
from cryptography.x509.oid import NameOID,ExtensionOID
|
|
13
|
+
from cryptography.x509.oid import NameOID, ExtensionOID
|
|
14
14
|
from cryptography.hazmat.primitives import hashes
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
from certapi.util import
|
|
17
|
+
from certapi.util import b64_string
|
|
18
18
|
|
|
19
19
|
__no_enc = serialization.NoEncryption()
|
|
20
20
|
|
|
@@ -177,6 +177,7 @@ def digest_sha256(data: bytes) -> bytes:
|
|
|
177
177
|
h.update(data)
|
|
178
178
|
return h.finalize()
|
|
179
179
|
|
|
180
|
+
|
|
180
181
|
def get_csr_hostnames(csr: x509.CertificateSigningRequest):
|
|
181
182
|
|
|
182
183
|
domains = []
|
|
@@ -203,4 +204,4 @@ def get_csr_hostnames(csr: x509.CertificateSigningRequest):
|
|
|
203
204
|
seen.add(d)
|
|
204
205
|
unique_domains.append(d)
|
|
205
206
|
|
|
206
|
-
return unique_domains
|
|
207
|
+
return unique_domains
|
|
@@ -18,11 +18,11 @@ class Key(ABC):
|
|
|
18
18
|
pass
|
|
19
19
|
|
|
20
20
|
@abstractmethod
|
|
21
|
-
def sign(self, message:bytes):
|
|
21
|
+
def sign(self, message: bytes):
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
@abstractmethod
|
|
25
|
-
def sign_csr(self, csr:x509.CertificateSigningRequestBuilder|x509.CertificateBuilder):
|
|
25
|
+
def sign_csr(self, csr: x509.CertificateSigningRequestBuilder | x509.CertificateBuilder):
|
|
26
26
|
pass
|
|
27
27
|
|
|
28
28
|
@staticmethod
|
|
@@ -37,7 +37,7 @@ class Key(ABC):
|
|
|
37
37
|
raise ValueError("Unsupported key type. Use 'rsa' or 'ecdsa'")
|
|
38
38
|
|
|
39
39
|
@staticmethod
|
|
40
|
-
def from_der(der_bytes:bytes,password:str=None):
|
|
40
|
+
def from_der(der_bytes: bytes, password: str = None):
|
|
41
41
|
key = serialization.load_der_private_key(der_bytes, password)
|
|
42
42
|
if isinstance(key, rsa.RSAPrivateKey):
|
|
43
43
|
return RSAKey(key)
|
|
@@ -49,7 +49,7 @@ class Key(ABC):
|
|
|
49
49
|
raise ValueError("Unsupported key type")
|
|
50
50
|
|
|
51
51
|
@staticmethod
|
|
52
|
-
def from_pem(pem_str:str,password:str=None):
|
|
52
|
+
def from_pem(pem_str: str, password: str = None):
|
|
53
53
|
key = serialization.load_pem_private_key(pem_str, password)
|
|
54
54
|
if isinstance(key, rsa.RSAPrivateKey):
|
|
55
55
|
return RSAKey(key)
|
|
@@ -127,7 +127,7 @@ class Key(ABC):
|
|
|
127
127
|
csr_builder = x509.CertificateSigningRequestBuilder()
|
|
128
128
|
if domain:
|
|
129
129
|
subject = self._build_name(subject_fields, include_user_id=True, domain=domain)
|
|
130
|
-
csr_builder=csr_builder.subject_name(subject)
|
|
130
|
+
csr_builder = csr_builder.subject_name(subject)
|
|
131
131
|
|
|
132
132
|
# Always include the domain in SAN, and then add alt_names
|
|
133
133
|
all_alt_names = [domain] + list(alt_names)
|
|
@@ -4,8 +4,15 @@ from cryptography.x509 import Certificate
|
|
|
4
4
|
from ..crypto.crypto import cert_to_pem, certs_to_pem
|
|
5
5
|
from ..crypto.crypto_classes import Key
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class IssuedCert:
|
|
8
|
-
def __init__(
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
*,
|
|
12
|
+
key: Union[str, Key] = None,
|
|
13
|
+
cert: Union[str, Certificate, List[Certificate]] = None,
|
|
14
|
+
domains: List[str] = None,
|
|
15
|
+
):
|
|
9
16
|
if isinstance(key, Key):
|
|
10
17
|
key = key.to_pem().decode("utf-8")
|
|
11
18
|
elif isinstance(key, bytes):
|
|
@@ -25,9 +32,9 @@ class IssuedCert:
|
|
|
25
32
|
@staticmethod
|
|
26
33
|
def from_json(data: Dict[str, Any]) -> "IssuedCert":
|
|
27
34
|
return IssuedCert(
|
|
28
|
-
key=data.get(
|
|
29
|
-
cert=data.get(
|
|
30
|
-
domains=data.get(
|
|
35
|
+
key=data.get("privateKey"),
|
|
36
|
+
cert=data.get("certificate"),
|
|
37
|
+
domains=data.get("domains", []),
|
|
31
38
|
)
|
|
32
39
|
|
|
33
40
|
def __repr__(self):
|
|
@@ -48,8 +55,8 @@ class CertificateResponse:
|
|
|
48
55
|
@staticmethod
|
|
49
56
|
def from_json(data: Dict[str, Any]) -> "CertificateResponse":
|
|
50
57
|
return CertificateResponse(
|
|
51
|
-
existing=[IssuedCert.from_json(cert_data) for cert_data in data.get(
|
|
52
|
-
issued=[IssuedCert.from_json(cert_data) for cert_data in data.get(
|
|
58
|
+
existing=[IssuedCert.from_json(cert_data) for cert_data in data.get("existing", [])],
|
|
59
|
+
issued=[IssuedCert.from_json(cert_data) for cert_data in data.get("issued", [])],
|
|
53
60
|
)
|
|
54
61
|
|
|
55
62
|
def __repr__(self):
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from certapi.crypto import ECDSAKey, Ed25519Key, RSAKey,Key
|
|
1
|
+
from certapi.crypto import ECDSAKey, Ed25519Key, RSAKey, Key
|
|
2
2
|
from .abstract_certissuer import CertIssuer
|
|
3
3
|
from cryptography import x509
|
|
4
4
|
from cryptography.x509 import Certificate
|
|
5
5
|
from typing import List, Literal, Union, Callable, Tuple, Dict, Optional
|
|
6
6
|
import time
|
|
7
7
|
from requests import Response
|
|
8
|
-
from certapi.acme import Acme,Challenge, Order
|
|
8
|
+
from certapi.acme import Acme, Challenge, Order
|
|
9
9
|
from certapi.challenge_store import ChallengeStore
|
|
10
10
|
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ class AcmeCertIssuer(CertIssuer):
|
|
|
15
15
|
account_key: Key,
|
|
16
16
|
challenge_store: ChallengeStore,
|
|
17
17
|
acme_url=None,
|
|
18
|
-
self_verify_challenge=False,
|
|
18
|
+
self_verify_challenge=False, # This never needs to be set to True
|
|
19
19
|
):
|
|
20
20
|
self.acme = Acme(account_key, url=acme_url)
|
|
21
21
|
self.challenge_store = challenge_store
|
|
@@ -29,7 +29,9 @@ class AcmeCertIssuer(CertIssuer):
|
|
|
29
29
|
elif res.status_code != 200:
|
|
30
30
|
raise Exception("Acme registration didn't return 200 or 201 ", res.json())
|
|
31
31
|
|
|
32
|
-
def sign_csr(
|
|
32
|
+
def sign_csr(
|
|
33
|
+
self, csr: x509.CertificateSigningRequest, challenge_store: ChallengeStore = None, expiry_days: int = 90
|
|
34
|
+
) -> str:
|
|
33
35
|
challenge_store = challenge_store if challenge_store is not None else self.challenge_store
|
|
34
36
|
hosts = self.get_csr_hostnames(csr)
|
|
35
37
|
order: Order = self.acme.create_authorized_order(hosts)
|
|
@@ -41,7 +43,7 @@ class AcmeCertIssuer(CertIssuer):
|
|
|
41
43
|
for c in challenges:
|
|
42
44
|
if self.self_verify_challenge:
|
|
43
45
|
c.self_verify()
|
|
44
|
-
end = time.time() + max(len(challenges) * 10,300)
|
|
46
|
+
end = time.time() + max(len(challenges) * 10, 300)
|
|
45
47
|
remaining_now: List[Challenge] = [x for x in challenges]
|
|
46
48
|
next_remaining = []
|
|
47
49
|
counter = 1
|
|
@@ -75,40 +77,49 @@ class AcmeCertIssuer(CertIssuer):
|
|
|
75
77
|
return None
|
|
76
78
|
return obtain_cert()
|
|
77
79
|
return None
|
|
80
|
+
|
|
78
81
|
return obtain_cert()
|
|
79
82
|
|
|
80
|
-
def generate_key_and_cert_for_domains(
|
|
81
|
-
|
|
83
|
+
def generate_key_and_cert_for_domains(
|
|
84
|
+
self,
|
|
85
|
+
hosts: Union[str, List[str]],
|
|
86
|
+
key_type: Literal["rsa", "ecdsa", "ed25519"] = "rsa",
|
|
82
87
|
expiry_days: int = 90,
|
|
83
88
|
country: Optional[str] = None,
|
|
84
89
|
state: Optional[str] = None,
|
|
85
90
|
locality: Optional[str] = None,
|
|
86
91
|
organization: Optional[str] = None,
|
|
87
92
|
user_id: Optional[str] = None,
|
|
88
|
-
challenge_store: Optional[ChallengeStore] = None
|
|
89
|
-
|
|
93
|
+
challenge_store: Optional[ChallengeStore] = None,
|
|
94
|
+
):
|
|
95
|
+
if len(hosts) == 0:
|
|
90
96
|
raise ValueError("CertIssuer.generate_key_and_cert_for_domains: empty hosts array provided")
|
|
91
|
-
return self.generate_key_and_cert(
|
|
97
|
+
return self.generate_key_and_cert(
|
|
98
|
+
hosts[0], hosts[0:], key_type, expiry_days, country, state, locality, organization, user_id, challenge_store
|
|
99
|
+
)
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
def generate_key_and_cert_for_domain(
|
|
102
|
+
self,
|
|
103
|
+
host: str,
|
|
104
|
+
key_type: Literal["rsa", "ecdsa", "ed25519"] = "rsa",
|
|
96
105
|
expiry_days: int = 90,
|
|
97
106
|
country: Optional[str] = None,
|
|
98
107
|
state: Optional[str] = None,
|
|
99
108
|
locality: Optional[str] = None,
|
|
100
109
|
organization: Optional[str] = None,
|
|
101
110
|
user_id: Optional[str] = None,
|
|
102
|
-
challenge_store: Optional[ChallengeStore] = None
|
|
111
|
+
challenge_store: Optional[ChallengeStore] = None,
|
|
112
|
+
):
|
|
103
113
|
|
|
104
|
-
return self.generate_key_and_cert(
|
|
105
|
-
|
|
114
|
+
return self.generate_key_and_cert(
|
|
115
|
+
host, [], key_type, expiry_days, country, state, locality, organization, user_id, challenge_store
|
|
116
|
+
)
|
|
106
117
|
|
|
107
118
|
def generate_key_and_cert(
|
|
108
119
|
self,
|
|
109
120
|
domain: str,
|
|
110
121
|
alt_names: List[str] = (),
|
|
111
|
-
key_type: Literal["rsa","ecdsa","ed25519"] = "ecdsa",
|
|
122
|
+
key_type: Literal["rsa", "ecdsa", "ed25519"] = "ecdsa",
|
|
112
123
|
expiry_days: int = 90,
|
|
113
124
|
country: Optional[str] = None,
|
|
114
125
|
state: Optional[str] = None,
|
|
@@ -5,6 +5,7 @@ from cryptography.x509.oid import NameOID
|
|
|
5
5
|
from certapi.crypto import Key, RSAKey, ECDSAKey, Ed25519Key
|
|
6
6
|
from .abstract_certissuer import CertIssuer
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class SelfCertIssuer(CertIssuer):
|
|
9
10
|
def __init__(
|
|
10
11
|
self,
|
|
@@ -43,7 +44,7 @@ class SelfCertIssuer(CertIssuer):
|
|
|
43
44
|
name_attrs.append(x509.NameAttribute(oid, value))
|
|
44
45
|
|
|
45
46
|
if include_user_id:
|
|
46
|
-
user_id = fields.get("user_id",domain)
|
|
47
|
+
user_id = fields.get("user_id", domain)
|
|
47
48
|
if user_id:
|
|
48
49
|
name_attrs.append(x509.NameAttribute(NameOID.USER_ID, user_id))
|
|
49
50
|
|
|
@@ -84,4 +85,4 @@ class SelfCertIssuer(CertIssuer):
|
|
|
84
85
|
for ext in csr.extensions:
|
|
85
86
|
builder = builder.add_extension(ext.value, ext.critical)
|
|
86
87
|
|
|
87
|
-
return self.root_key.sign_csr(builder)
|
|
88
|
+
return self.root_key.sign_csr(builder)
|