certapi 0.1.1__tar.gz → 0.2.0__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.
Files changed (25) hide show
  1. {certapi-0.1.1 → certapi-0.2.0}/PKG-INFO +1 -3
  2. {certapi-0.1.1 → certapi-0.2.0}/setup.py +1 -1
  3. {certapi-0.1.1 → certapi-0.2.0}/src/certapi/Acme.py +21 -9
  4. {certapi-0.1.1 → certapi-0.2.0}/src/certapi/__init__.py +1 -0
  5. {certapi-0.1.1 → certapi-0.2.0}/src/certapi/certauthority.py +63 -8
  6. {certapi-0.1.1 → certapi-0.2.0}/src/certapi/challenge.py +13 -31
  7. certapi-0.2.0/src/certapi/cloudflare_challenge_store.py +57 -0
  8. certapi-0.2.0/src/certapi/cloudflare_client.py +137 -0
  9. {certapi-0.1.1 → certapi-0.2.0}/src/certapi/crypto.py +20 -1
  10. certapi-0.2.0/src/certapi/crypto_classes.py +240 -0
  11. certapi-0.2.0/src/certapi/custom_certauthority.py +127 -0
  12. {certapi-0.1.1 → certapi-0.2.0}/src/certapi/db.py +1 -1
  13. {certapi-0.1.1 → certapi-0.2.0}/src/certapi.egg-info/PKG-INFO +1 -3
  14. {certapi-0.1.1 → certapi-0.2.0}/src/certapi.egg-info/SOURCES.txt +3 -2
  15. certapi-0.1.1/src/certapi/crypto_classes.py +0 -135
  16. certapi-0.1.1/src/certapi/custom_certauthority.py +0 -94
  17. certapi-0.1.1/tests/test_custom_certauthority.py +0 -43
  18. {certapi-0.1.1 → certapi-0.2.0}/MANIFEST.in +0 -0
  19. {certapi-0.1.1 → certapi-0.2.0}/README.md +0 -0
  20. {certapi-0.1.1 → certapi-0.2.0}/pyproject.toml +0 -0
  21. {certapi-0.1.1 → certapi-0.2.0}/setup.cfg +0 -0
  22. {certapi-0.1.1 → certapi-0.2.0}/src/certapi/util.py +0 -0
  23. {certapi-0.1.1 → certapi-0.2.0}/src/certapi.egg-info/dependency_links.txt +0 -0
  24. {certapi-0.1.1 → certapi-0.2.0}/src/certapi.egg-info/requires.txt +0 -0
  25. {certapi-0.1.1 → certapi-0.2.0}/src/certapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: certapi
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Python Package for managing keys, request SSL certificates from ACME.
5
5
  Home-page: https://github.com/mesudip/certapi
6
6
  Author: Sudip Bhattarai
@@ -10,8 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.6
12
12
  Description-Content-Type: text/markdown
13
- Requires-Dist: cryptography
14
- Requires-Dist: requests
15
13
 
16
14
  # CertApi
17
15
 
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="0.1.1",
5
+ version="0.2.0",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -8,11 +8,11 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
8
8
  from cryptography.x509 import CertificateSigningRequest, Certificate
9
9
  from . import crypto
10
10
  import requests
11
- from .crypto import sign, digest_sha256, csr_to_der, jwk, get_algorithm_name
11
+ from .crypto import sign, digest_sha256, csr_to_der, jwk, get_algorithm_name, sign_for_jws
12
12
  from .util import b64_encode, b64_string
13
13
 
14
- # acme_url = os.environ.get("LETSENCRYPT_API", "https://acme-staging-v02.api.letsencrypt.org/directory")
15
- acme_url = os.environ.get("LETSENCRYPT_API", None)
14
+ acme_url = os.environ.get("LETSENCRYPT_API", "https://acme-staging-v02.api.letsencrypt.org/directory")
15
+ # acme_url = os.environ.get("LETSENCRYPT_API", None)
16
16
 
17
17
 
18
18
  class AcmeError(Exception):
@@ -177,6 +177,7 @@ def request(method, step: str, url: str, json=None, headers=None, throw=True) ->
177
177
  step,
178
178
  )
179
179
  if 199 <= res.status_code > 299:
180
+
180
181
  [print(x, y) for (x, y) in res.headers.items()]
181
182
  print("Response:", res.text)
182
183
  json_data = None
@@ -211,8 +212,9 @@ class Acme:
211
212
  self.account_key = account_key
212
213
  # json web key format for public key
213
214
  self.jwk = jwk(self.account_key)
215
+ print(self.jwk)
214
216
  self.nonce = []
215
- self.acme_url = url if url else self.URL_PROD
217
+ self.acme_url = url if url else self.URL_STAGING
216
218
  self.key_id = None
217
219
  self.directory = None
218
220
  self._nonce_lock = threading.Lock() # Mutex for safe access to nonce
@@ -267,7 +269,7 @@ class Acme:
267
269
  payload = {
268
270
  "protected": protectedb64.decode("utf-8"),
269
271
  "payload": payload64.decode("utf-8"),
270
- "signature": b64_string(sign(self.account_key, b".".join([protectedb64, payload64]))),
272
+ "signature": b64_string(sign_for_jws(self.account_key, b".".join([protectedb64, payload64]))),
271
273
  }
272
274
  try:
273
275
 
@@ -357,6 +359,7 @@ class Challenge:
357
359
  challenge = self.get_challenge()
358
360
  self.token = challenge["token"]
359
361
  self.verified = challenge["status"] == "valid"
362
+ self.domain = data["identifier"]["value"] # Add domain attribute
360
363
 
361
364
  jwk_json = json.dumps(self._acme.jwk, sort_keys=True, separators=(",", ":"))
362
365
  thumbprint = b64_encode(digest_sha256(jwk_json.encode("utf8")))
@@ -364,9 +367,11 @@ class Challenge:
364
367
 
365
368
  self.url = "http://{0}/.well-known/acme-challenge/{1}".format(data["identifier"]["value"], self.token)
366
369
 
367
- def verify(self) -> bool:
370
+ def verify(self, dns=False) -> bool:
368
371
  if not self.verified:
369
- response = self._acme._signed_req(self.get_challenge()["url"], {}, step="Verify Challenge", throw=False)
372
+ response = self._acme._signed_req(
373
+ self.get_challenge(key="dns-01" if dns else "http-01")["url"], {}, step="Verify Challenge", throw=False
374
+ )
370
375
  if response.status_code == 200 and response.json()["status"] == "valid":
371
376
  self.verified = True
372
377
  return True
@@ -398,9 +403,16 @@ class Challenge:
398
403
  return False
399
404
 
400
405
  def get_challenge(self, key="http-01"):
401
- for method in self._data["challenges"]:
406
+ challenges = self._data["challenges"]
407
+ for method in challenges:
402
408
  if method["type"] == key:
403
409
  return method
410
+ if len(challenges) == 1:
411
+ return challenges[0]
412
+
413
+ ch_types = [x["type"] for x in self._data["challenges"]]
404
414
  raise AcmeError(
405
- "'http-01' not found in challenges", {"response": self._data["challenges"]}, "Acme Challenge Verification"
415
+ f"'{key}' not found in challenges. available:{str(ch_types)}",
416
+ {"response": self._data["challenges"]},
417
+ "Acme Challenge Verification",
406
418
  )
@@ -1,5 +1,6 @@
1
1
  from .Acme import Acme, Order, AcmeNetworkError, AcmeHttpError, Challenge
2
2
  from .certauthority import CertAuthority
3
+ from .custom_certauthority import CertificateIssuer
3
4
  from .crypto import gen_key_ed25519, create_csr
4
5
  from .db import KeyStore, FilesystemKeyStore, SqliteKeyStore, PostgresKeyStore
5
6
  from .challenge import InMemoryChallengeStore, FileSystemChallengeStore
@@ -6,19 +6,35 @@ import requests
6
6
  from cryptography.x509 import Certificate
7
7
  from requests import Response
8
8
 
9
+ from typing import List, Union, Callable, Tuple, Dict
10
+ import json
11
+ import time
12
+
13
+ import requests
14
+ from cryptography.x509 import Certificate
15
+ from requests import Response
16
+
9
17
  from . import Acme, Challenge, Order
10
18
  from . import crypto
11
19
  from . import challenge
12
- from .crypto import cert_to_pem, key_to_pem
20
+ from .crypto import cert_to_pem, key_to_pem, digest_sha256
13
21
  from .crypto_classes import Key
14
22
  from .db import KeyStore
23
+ from .util import b64_string
15
24
 
16
25
 
17
26
  class CertAuthority:
18
- def __init__(self, challenge_store: challenge.ChallengeStore, key_store: KeyStore, acme_url=None):
27
+ def __init__(
28
+ self,
29
+ challenge_store: challenge.ChallengeStore,
30
+ key_store: KeyStore,
31
+ acme_url=None,
32
+ dns_stores: List[challenge.ChallengeStore] = None,
33
+ ):
19
34
  self.acme = Acme(key_store.account_key, url=acme_url)
20
35
  self.key_store = key_store
21
36
  self.challengesStore: challenge.ChallengeStore = challenge_store
37
+ self.dns_stores = dns_stores if dns_stores is not None else []
22
38
 
23
39
  def setup(self):
24
40
  self.acme.setup()
@@ -35,18 +51,51 @@ class CertAuthority:
35
51
  existing = {c[0]: c[1] for c in [(h, self.key_store.get_cert(h)) for h in host] if c[1] is not None}
36
52
  missing = [h for h in host if h not in existing]
37
53
  if len(missing) > 0:
54
+ has_wildcard = False
55
+ # Determine which challenge store to use
56
+ challenge_store_to_use = self.challengesStore
57
+ for h in missing:
58
+ if h.startswith("*."): # Wildcard domain
59
+ has_wildcard = True
60
+ found_dns_store = False
61
+
62
+ for dns_store in self.dns_stores:
63
+ if dns_store.has_domain(h.lstrip("*.")): # Check if the DNS store can handle the base domain
64
+ challenge_store_to_use = dns_store
65
+ found_dns_store = True
66
+ break
67
+ if not found_dns_store:
68
+ raise Exception(f"No DNS challenge store found for wildcard domain {h}")
69
+ break # Assuming all domains in a single request will use the same challenge type
70
+
38
71
  private_key = crypto.gen_key_secp256r1()
39
72
  order = self.acme.create_authorized_order(missing)
40
73
 
41
74
  challenges = order.remaining_challenges()
75
+
42
76
  for c in challenges:
43
77
  print("[ Challenge ]", c.token, "=", c.authorization_key)
44
- self.challengesStore[c.token] = c.authorization_key
78
+ # For DNS-01 challenges, the key should be _acme-challenge.<domain>
79
+ challenge_name = f"_acme-challenge.{c.domain}" if has_wildcard else c.token
80
+
81
+ # For DNS-01 challenges, the value is the SHA256 hash of the authorization_key, base64url encoded
82
+ challenge_value = (
83
+ b64_string(digest_sha256(c.authorization_key.encode("utf8")))
84
+ if has_wildcard
85
+ else c.authorization_key
86
+ )
87
+
88
+ challenge_store_to_use.save_challenge(challenge_name, challenge_value, c.domain)
89
+
90
+ # Add an initial sleep to allow DNS propagation
91
+ if has_wildcard:
92
+ print("Waiting for DNS propagation (10 seconds)...")
93
+ time.sleep(10)
94
+
45
95
  for c in challenges:
46
96
  # c.self_verify()
47
- c.verify()
48
-
49
- end = time.time() + 40 # max 12 seconds
97
+ c.verify(dns=has_wildcard)
98
+ end = time.time() + 60 # Increase overall timeout
50
99
  source: List[Challenge] = [x for x in challenges]
51
100
  sink = []
52
101
  counter = 1
@@ -61,8 +110,6 @@ class CertAuthority:
61
110
  if len(sink) > 0:
62
111
  time.sleep(3)
63
112
  source, sink, counter = sink, [], counter + 1
64
- else:
65
- print("Order is already Ready.")
66
113
  csr = crypto.create_csr(private_key, missing[0], missing[1:])
67
114
  order.finalize(csr)
68
115
 
@@ -75,9 +122,17 @@ class CertAuthority:
75
122
  key_id = self.key_store.save_key(private_key, missing[0])
76
123
  cert_id = self.key_store.save_cert(key_id, certificate, missing)
77
124
  issued_cert = IssuedCert(key_to_pem(private_key), certificate, missing)
125
+ # Clean up challenges after successful certificate issuance
126
+ for c in challenges:
127
+ challenge_name = f"_acme-challenge.{c.domain}" if has_wildcard else c.token
128
+ challenge_store_to_use.delete_challenge(challenge_name, c.domain)
78
129
  return createExistingResponse(existing, [issued_cert])
79
130
  elif order.status == "processing":
80
131
  if count == 0:
132
+ # Clean up challenges if timeout occurs
133
+ for c in challenges:
134
+ challenge_name = f"_acme-challenge.{c.domain}" if has_wildcard else c.token
135
+ challenge_store_to_use.delete_challenge(challenge_name, c.domain)
81
136
  return None
82
137
  return obtain_cert()
83
138
  return None
@@ -2,28 +2,19 @@ import os
2
2
  from collections.abc import MutableMapping
3
3
 
4
4
 
5
- class ChallengeStore(MutableMapping):
5
+ class ChallengeStore:
6
6
  """
7
7
  Abstract base class for a challenge store.
8
- Provides dictionary-like behavior by inheriting from MutableMapping.
9
8
  """
10
9
 
11
- def __setitem__(self, key, value):
12
- self.save_challenge(key, value)
13
-
14
- def __getitem__(self, key):
15
- value = self.get_challenge(key)
16
- if value is None:
17
- raise KeyError(key)
18
- return value
10
+ def save_challenge(self, key: str, value: str, domain: str = None):
11
+ raise NotImplementedError("Must implement `save_challenge` method.")
19
12
 
20
- def __delitem__(self, key):
21
- if key not in self:
22
- raise KeyError(key)
23
- self.delete_challenge(key)
13
+ def get_challenge(self, key: str, domain: str = None) -> str:
14
+ raise NotImplementedError("Must implement `get_challenge` method.")
24
15
 
25
- def __contains__(self, key):
26
- return self.get_challenge(key) is not None
16
+ def delete_challenge(self, key: str, domain: str = None):
17
+ raise NotImplementedError("Must implement `delete_challenge` method.")
27
18
 
28
19
  def __iter__(self):
29
20
  raise NotImplementedError("Must implement `__iter__` method.")
@@ -31,15 +22,6 @@ class ChallengeStore(MutableMapping):
31
22
  def __len__(self):
32
23
  raise NotImplementedError("Must implement `__len__` method.")
33
24
 
34
- def save_challenge(self, key: str, value: str):
35
- raise NotImplementedError("Must implement `save_challenge` method.")
36
-
37
- def get_challenge(self, key: str) -> str:
38
- raise NotImplementedError("Must implement `get_challenge` method.")
39
-
40
- def delete_challenge(self, key: str):
41
- raise NotImplementedError("Must implement `delete_challenge` method.")
42
-
43
25
 
44
26
  class InMemoryChallengeStore(ChallengeStore):
45
27
  """
@@ -49,13 +31,13 @@ class InMemoryChallengeStore(ChallengeStore):
49
31
  def __init__(self):
50
32
  self.challenges = {}
51
33
 
52
- def save_challenge(self, key: str, value: str):
34
+ def save_challenge(self, key: str, value: str, domain: str = None):
53
35
  self.challenges[key] = value
54
36
 
55
- def get_challenge(self, key: str) -> str:
37
+ def get_challenge(self, key: str, domain: str = None) -> str:
56
38
  return self.challenges.get(key, "")
57
39
 
58
- def delete_challenge(self, key: str):
40
+ def delete_challenge(self, key: str, domain: str = None):
59
41
  if key in self.challenges:
60
42
  del self.challenges[key]
61
43
 
@@ -75,19 +57,19 @@ class FileSystemChallengeStore(ChallengeStore):
75
57
  self.directory = directory
76
58
  os.makedirs(self.directory, exist_ok=True)
77
59
 
78
- def save_challenge(self, key: str, value: str):
60
+ def save_challenge(self, key: str, value: str, domain: str = None):
79
61
  file_path = os.path.join(self.directory, key)
80
62
  with open(file_path, "w") as file:
81
63
  file.write(value)
82
64
 
83
- def get_challenge(self, key: str) -> str:
65
+ def get_challenge(self, key: str, domain: str = None) -> str:
84
66
  file_path = os.path.join(self.directory, key)
85
67
  if not os.path.exists(file_path):
86
68
  return None
87
69
  with open(file_path, "r") as file:
88
70
  return file.read()
89
71
 
90
- def delete_challenge(self, key: str):
72
+ def delete_challenge(self, key: str, domain: str = None):
91
73
  file_path = os.path.join(self.directory, key)
92
74
  if os.path.exists(file_path):
93
75
  os.remove(file_path)
@@ -0,0 +1,57 @@
1
+ import os
2
+ from collections.abc import MutableMapping
3
+ from certapi.challenge import ChallengeStore
4
+ from certapi.cloudflare_client import Cloudflare
5
+
6
+
7
+ class CloudflareChallengeStore(ChallengeStore):
8
+ def __init__(self):
9
+ self.cloudflare = Cloudflare()
10
+ self.challenges_map = {} # Stores key: record_id (still needed for deletion)
11
+
12
+ def has_domain(self, domain: str) -> bool:
13
+ """
14
+ Checks if the Cloudflare account has access to the given domain (or its base domain)
15
+ as a registered zone.
16
+ """
17
+ try:
18
+ self.cloudflare.determine_registered_domain(domain)
19
+ return True
20
+ except Exception:
21
+ return False
22
+
23
+ def save_challenge(self, key: str, value: str, domain=None):
24
+ # key example: _acme-challenge.sub.example.com
25
+ # value example: ACME_CHALLENGE_TOKEN
26
+ base_domain = self.cloudflare.determine_registered_domain(domain)
27
+
28
+ record_id = self.cloudflare.create_record(name=key, data=value, domain=base_domain)
29
+ self.challenges_map[key] = record_id
30
+ print(f"CloudflareChallengeStore: Saved challenge for {key} with record ID {record_id}")
31
+
32
+ def get_challenge(self, key: str, domain: str) -> str:
33
+ base_domain = self.cloudflare.determine_registered_domain(domain)
34
+ records = self.cloudflare.list_txt_records(base_domain, name_filter=key)
35
+ for record in records:
36
+ if record["name"] == key:
37
+ return record["content"]
38
+ return None # Return None if not found, as per ChallengeStore's __getitem__ behavior
39
+
40
+ def delete_challenge(self, key: str, domain: str):
41
+ if key not in self.challenges_map:
42
+ raise KeyError(f"Challenge {key} not found in store (no record_id stored).")
43
+
44
+ record_id = self.challenges_map[key]
45
+ base_domain = self.cloudflare.determine_registered_domain(domain)
46
+ self.cloudflare.delete_record(record=record_id, domain=base_domain)
47
+ del self.challenges_map[key]
48
+ print(f"CloudflareChallengeStore: Deleted challenge for {key} with record ID {record_id}")
49
+
50
+ def __iter__(self):
51
+ # This is tricky as we can't easily iterate all challenges across all domains
52
+ # If the user wants a full API-driven iteration, they need to clarify how to get all domains.
53
+ return iter(self.challenges_map)
54
+
55
+ def __len__(self):
56
+ # Similar to __iter__, this will count challenges managed by this store instance.
57
+ return len(self.challenges_map)
@@ -0,0 +1,137 @@
1
+ import json
2
+ import time
3
+ from os import getenv
4
+ from urllib.request import urlopen, Request
5
+
6
+
7
+ class Cloudflare(object):
8
+ name = "cloudflare"
9
+
10
+ def __init__(self):
11
+ self.token = getenv("CLOUDFLARE_API_TOKEN")
12
+ self.account_id = getenv("CLOUDFLARE_ACCOUNT_ID")
13
+ self.api = "https://api.cloudflare.com/client/v4"
14
+ if not self.token:
15
+ raise Exception("CLOUDFLARE_API_TOKEN not found in environment")
16
+
17
+ self._zones_cache = None
18
+ self._zones_cache_time = 0 # Unix timestamp of last cache update
19
+
20
+ def _cloudflare_headers(self):
21
+ return {"Content-Type": "application/json", "Authorization": "Bearer " + self.token}
22
+
23
+ def _get_zones(self):
24
+ """Fetch and cache Cloudflare zones"""
25
+ # Cache for 1 day (86400 seconds)
26
+ if self._zones_cache and (time.time() - self._zones_cache_time) < 86400:
27
+ return self._zones_cache
28
+
29
+ request_headers = self._cloudflare_headers()
30
+ api_url = "{0}/zones?per_page=50".format(self.api)
31
+ response = urlopen(Request(api_url, headers=request_headers))
32
+ if response.getcode() != 200:
33
+ raise Exception(json.loads(response.read().decode("utf8")))
34
+
35
+ zones = json.loads(response.read().decode("utf8"))["result"]
36
+ self._zones_cache = zones
37
+ self._zones_cache_time = time.time()
38
+ return zones
39
+
40
+ def _get_zone_id(self, domain):
41
+ """Determine Cloudflare Zone ID for a given domain"""
42
+ zones = self._get_zones()
43
+ for zone in zones:
44
+ if zone["name"] == domain:
45
+ return zone["id"]
46
+ raise Exception("No Cloudflare zone found for domain: {0}".format(domain))
47
+
48
+ def determine_registered_domain(self, domain: str) -> str:
49
+ """
50
+ Determine the registered domain in Cloudflare for a given (sub)domain.
51
+ This method iterates through parts of the domain to find a matching Cloudflare zone.
52
+ """
53
+ parts = domain.split(".")
54
+ err = None
55
+ for i in range(len(parts)):
56
+ potential_domain = ".".join(parts[i:])
57
+ try:
58
+ self._get_zone_id(potential_domain)
59
+ return potential_domain
60
+ except Exception as e:
61
+ err = e
62
+ continue
63
+ if err:
64
+ raise err
65
+ else:
66
+ raise Exception("Could not determine Cloudflare registered domain for: {0}".format(domain))
67
+
68
+ def list_txt_records(self, domain: str, name_filter: str = None) -> list:
69
+ """
70
+ Lists TXT records for a given domain, optionally filtered by name.
71
+ Returns a list of dictionaries, each representing a TXT record.
72
+ """
73
+ registered_domain = self.determine_registered_domain(domain)
74
+ zone_id = self._get_zone_id(registered_domain)
75
+ api_url = f"{self.api}/zones/{zone_id}/dns_records?type=TXT"
76
+ if name_filter:
77
+ api_url += f"&name={name_filter}"
78
+
79
+ request_headers = self._cloudflare_headers()
80
+ response = urlopen(Request(api_url, headers=request_headers))
81
+
82
+ if response.getcode() != 200:
83
+ raise Exception(json.loads(response.read().decode("utf8")))
84
+
85
+ result = json.loads(response.read().decode("utf8"))
86
+ if not result.get("success"):
87
+ raise Exception(result.get("errors", "Unknown error listing TXT records"))
88
+
89
+ return result["result"]
90
+
91
+ def create_record(self, name, data, domain):
92
+ """
93
+ Create DNS record
94
+ Params:
95
+ name, string, record name (e.g., _acme-challenge.example.com)
96
+ data, string, record data (e.g., ACME challenge token)
97
+ domain, string, dns domain (e.g., example.com) - This will be used to determine the registered zone.
98
+ Return:
99
+ record_id, string, created record id
100
+ """
101
+ registered_domain = self.determine_registered_domain(domain)
102
+ zone_id = self._get_zone_id(registered_domain)
103
+ api_url = "{0}/zones/{1}/dns_records".format(self.api, zone_id)
104
+ request_headers = self._cloudflare_headers()
105
+ request_data = {
106
+ "type": "TXT",
107
+ "name": name,
108
+ "content": data,
109
+ "ttl": 120, # Cloudflare minimum TTL for TXT is 120 seconds
110
+ "proxied": False,
111
+ }
112
+ response = urlopen(Request(api_url, data=json.dumps(request_data).encode("utf8"), headers=request_headers))
113
+
114
+ if response.getcode() != 200:
115
+ raise Exception(json.loads(response.read().decode("utf8")))
116
+ result = response.read().decode("utf8")
117
+ print("Cloudflare create record", name, result)
118
+ return json.loads(result)["result"]["id"]
119
+
120
+ def delete_record(self, record, domain):
121
+ """
122
+ Delete DNS record
123
+ Params:
124
+ record, string, record id number
125
+ domain, string, dns domain - This will be used to determine the registered zone.
126
+ """
127
+ registered_domain = self.determine_registered_domain(domain)
128
+ zone_id = self._get_zone_id(registered_domain)
129
+ api_url = "{0}/zones/{1}/dns_records/{2}".format(self.api, zone_id, record)
130
+ request_headers = self._cloudflare_headers()
131
+ request = Request(api_url, headers=request_headers)
132
+ request.get_method = lambda: "DELETE"
133
+ response = urlopen(request)
134
+ result = response.read().decode("utf8")
135
+ print(f"Delete dns record [{response.getcode()}]", result)
136
+ if response.getcode() != 200:
137
+ raise Exception(json.loads(response.read().decode("utf8")))
@@ -2,6 +2,7 @@ from typing import List, Union
2
2
 
3
3
  from cryptography.hazmat.backends import default_backend
4
4
  from cryptography.hazmat.primitives import serialization
5
+ from cryptography.hazmat.primitives.asymmetric import utils
5
6
  from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding, ed25519
6
7
  from cryptography import x509
7
8
  from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
@@ -31,6 +32,10 @@ def gen_key_ed25519():
31
32
  return ed25519.Ed25519PrivateKey.generate()
32
33
 
33
34
 
35
+ def gen_key_ecdsa():
36
+ return ec.generate_private_key(ec.SECP256R1())
37
+
38
+
34
39
  def get_algorithm_name(key):
35
40
  if isinstance(key, RSAPrivateKey):
36
41
  return "RS256"
@@ -44,6 +49,8 @@ def get_algorithm_name(key):
44
49
  return "ES384"
45
50
  elif curve_name == "secp521r1":
46
51
  return "ES512"
52
+ else:
53
+ raise ValueError(f"Unsupported EC curve: {curve_name}")
47
54
  else:
48
55
  raise ValueError("Unsupported key type")
49
56
 
@@ -132,11 +139,23 @@ def sign(key: Union[RSAPrivateKey, Ed25519PrivateKey, EllipticCurvePrivateKey],
132
139
  return key.sign(message, ec.ECDSA(hasher))
133
140
 
134
141
 
142
+ def sign_for_jws(key, message, hasher=hashes.SHA256()):
143
+ if isinstance(key, EllipticCurvePrivateKey):
144
+ der_sig = key.sign(message, ec.ECDSA(hasher))
145
+ r, s = utils.decode_dss_signature(der_sig)
146
+ num_bytes = (key.curve.key_size + 7) // 8
147
+ r_bytes = r.to_bytes(num_bytes, "big")
148
+ s_bytes = s.to_bytes(num_bytes, "big")
149
+ return r_bytes + s_bytes
150
+ else:
151
+ return sign(key, message, hasher)
152
+
153
+
135
154
  def sign_jws(key: RSAPrivateKey, data: object):
136
155
  pass
137
156
 
138
157
 
139
- def key_to_der(key: [RSAPrivateKey, Ed25519PrivateKey, EllipticCurvePrivateKey]) -> bytes:
158
+ def key_to_der(key: RSAPrivateKey | Ed25519PrivateKey | EllipticCurvePrivateKey) -> bytes:
140
159
  return key.private_bytes(
141
160
  encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=__no_enc
142
161
  )
@@ -0,0 +1,240 @@
1
+ from abc import ABC, abstractmethod
2
+ from cryptography.hazmat.primitives.asymmetric import rsa, ed25519, ec
3
+ from cryptography.hazmat.primitives import serialization, hashes, padding
4
+ from typing import Literal, Optional, List, Union
5
+ from cryptography import x509
6
+ from cryptography.x509.oid import NameOID
7
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
8
+
9
+ from .crypto import key_to_der, key_to_pem
10
+ from .util import b64_string
11
+
12
+
13
+ class Key(ABC):
14
+ key: rsa.RSAPrivateKey | ed25519.Ed25519PrivateKey | ec.EllipticCurvePrivateKey
15
+
16
+ @abstractmethod
17
+ def jwk(self):
18
+ pass
19
+
20
+ @abstractmethod
21
+ def sign(self, message):
22
+ pass
23
+
24
+ @abstractmethod
25
+ def sign_csr(self, csr):
26
+ pass
27
+
28
+ @staticmethod
29
+ def generate(key_type: Literal["rsa", "ecdsa", "ed25519"]) -> "Key":
30
+ if key_type == "rsa":
31
+ return RSAKey.generate()
32
+ elif key_type == "ecdsa":
33
+ return ECDSAKey.generate()
34
+ elif key_type == "ed25519":
35
+ return Ed25519Key.generate()
36
+ else:
37
+ raise ValueError("Unsupported key type. Use 'rsa' or 'ecdsa'")
38
+
39
+ @staticmethod
40
+ def from_der(der_bytes):
41
+ key = serialization.load_der_private_key(der_bytes, password=None)
42
+ if isinstance(key, rsa.RSAPrivateKey):
43
+ return RSAKey(key)
44
+ elif isinstance(key, ec.EllipticCurvePrivateKey):
45
+ return ECDSAKey(key)
46
+ elif isinstance(key, Ed25519PrivateKey):
47
+ return Ed25519Key(key)
48
+ else:
49
+ raise ValueError("Unsupported key type")
50
+
51
+ @staticmethod
52
+ def from_pem(der_bytes):
53
+ key = serialization.load_pem_private_key(der_bytes, password=None)
54
+ if isinstance(key, rsa.RSAPrivateKey):
55
+ return RSAKey(key)
56
+ elif isinstance(key, ec.EllipticCurvePrivateKey):
57
+ return ECDSAKey(key)
58
+ elif isinstance(key, Ed25519PrivateKey):
59
+ return Ed25519Key(key)
60
+ else:
61
+ raise ValueError("Unsupported key type")
62
+
63
+ def to_der(self) -> bytes:
64
+ return key_to_der(self.key)
65
+
66
+ def to_pem(self) -> bytes:
67
+ return key_to_pem(self.key)
68
+
69
+ def _build_name(self, fields: dict, include_user_id=False, domain=None) -> x509.Name:
70
+ name_attrs = []
71
+ field_map = {
72
+ "country": NameOID.COUNTRY_NAME,
73
+ "state": NameOID.STATE_OR_PROVINCE_NAME,
74
+ "locality": NameOID.LOCALITY_NAME,
75
+ "organization": NameOID.ORGANIZATION_NAME,
76
+ "common_name": NameOID.COMMON_NAME,
77
+ }
78
+
79
+ for key, oid in field_map.items():
80
+ value = fields.get(key)
81
+ if value:
82
+ name_attrs.append(x509.NameAttribute(oid, value))
83
+
84
+ if include_user_id:
85
+ user_id = fields.get("user_id") or domain
86
+ if user_id:
87
+ name_attrs.append(x509.NameAttribute(NameOID.USER_ID, user_id))
88
+
89
+ return x509.Name(name_attrs)
90
+
91
+ def create_csr(
92
+ self,
93
+ domain: str,
94
+ alt_names: List[str] = (),
95
+ country: Optional[str] = None,
96
+ state: Optional[str] = None,
97
+ locality: Optional[str] = None,
98
+ organization: Optional[str] = None,
99
+ user_id: Optional[str] = None,
100
+ ) -> x509.CertificateSigningRequest:
101
+ """
102
+ Create a Certificate Signing Request (CSR) with the specified parameters.
103
+
104
+ Args:
105
+ domain: The common name (CN) for the CSR.
106
+ alt_names: List of Subject Alternative Names (SAN) for the CSR.
107
+ country: Country name for the subject.
108
+ state: State or province name for the subject.
109
+ locality: Locality name for the subject.
110
+ organization: Organization name for the subject.
111
+ user_id: Optional user ID to include in the subject.
112
+
113
+ Returns:
114
+ x509.CertificateSigningRequest: The generated CSR.
115
+ """
116
+ # Build subject fields
117
+ subject_fields = {
118
+ "country": country,
119
+ "state": state,
120
+ "locality": locality,
121
+ "organization": organization,
122
+ "common_name": domain,
123
+ "user_id": user_id or domain,
124
+ }
125
+ subject = self._build_name(subject_fields, include_user_id=True, domain=domain)
126
+
127
+ # Build CSR with optional SAN extension
128
+ csr_builder = x509.CertificateSigningRequestBuilder().subject_name(subject)
129
+ if alt_names:
130
+ csr_builder = csr_builder.add_extension(
131
+ x509.SubjectAlternativeName([x509.DNSName(name) for name in alt_names]),
132
+ critical=False,
133
+ )
134
+
135
+ # Sign the CSR using the subclass-specific signing method
136
+ return self.sign_csr(csr_builder)
137
+
138
+
139
+ class RSAKey(Key):
140
+ def __init__(self, key: rsa.RSAPrivateKey, hasher=hashes.SHA256()):
141
+ self.key = key
142
+ self.hasher = hasher
143
+
144
+ @staticmethod
145
+ def generate():
146
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
147
+ return RSAKey(key)
148
+
149
+ def jwk(self):
150
+ public = self.key.public_key().public_numbers()
151
+ return {
152
+ "e": b64_string((public.e).to_bytes((public.e.bit_length() + 7) // 8, "big")),
153
+ "kty": "RSA",
154
+ "n": b64_string((public.n).to_bytes((public.n.bit_length() + 7) // 8, "big")),
155
+ }
156
+
157
+ def sign(self, message):
158
+ return self.key.sign(message, padding.PKCS1v15(), self.hasher)
159
+
160
+ def sign_csr(self, csr):
161
+ return csr.sign(self.key, self.hasher)
162
+
163
+ def algorithm_name(self):
164
+ return "RS" + str(self.hasher.digest_size * 8)
165
+
166
+
167
+ class Ed25519Key(Key):
168
+ def __init__(self, key: ed25519.Ed25519PrivateKey):
169
+ self.key = key
170
+ self.keyid = "e"
171
+ public = self.key.public_key().public_bytes(
172
+ encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
173
+ )
174
+ self.jwk = {
175
+ "crv": "Ed25519",
176
+ "kty": "OKP",
177
+ "x": b64_string(public),
178
+ }
179
+
180
+ @staticmethod
181
+ def generate():
182
+ key = ed25519.Ed25519PrivateKey.generate()
183
+ return Ed25519Key(key)
184
+
185
+ def jwk(self):
186
+ return self.jwk
187
+
188
+ def sign(self, message):
189
+ return self.key.sign(message)
190
+
191
+ def sign_csr(self, csr):
192
+ return csr.sign(self.key, None)
193
+
194
+
195
+ class ECDSAKey(Key):
196
+ def __init__(self, key: ec.EllipticCurvePrivateKey):
197
+ self.key = key
198
+ public_key = self.key.public_key()
199
+ public_numbers = public_key.public_numbers()
200
+ self.jwk = {
201
+ "kty": "EC",
202
+ "crv": self.key.curve.name,
203
+ "x": b64_string(public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, "big")),
204
+ "y": b64_string(public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, "big")),
205
+ }
206
+
207
+ @staticmethod
208
+ def generate():
209
+ key = ec.generate_private_key(ec.SECP256R1())
210
+ return ECDSAKey(key)
211
+
212
+ def jwk(self):
213
+ return self.jwk
214
+
215
+ def algorithm_name(self):
216
+ return self.key.curve.name
217
+
218
+ def sign(self, message):
219
+ key_size = self.key.curve.key_size
220
+ if key_size == 256:
221
+ algorithm = hashes.SHA256()
222
+ elif key_size == 384:
223
+ algorithm = hashes.SHA384()
224
+ elif key_size == 521:
225
+ algorithm = hashes.SHA512()
226
+ else:
227
+ raise ValueError(f"Unsupported curve with key size {key_size}")
228
+ return self.key.sign(message, ec.ECDSA(algorithm))
229
+
230
+ def sign_csr(self, csr):
231
+ key_size = self.key.curve.key_size
232
+ if key_size == 256:
233
+ algorithm = hashes.SHA256()
234
+ elif key_size == 384:
235
+ algorithm = hashes.SHA384()
236
+ elif key_size == 521:
237
+ algorithm = hashes.SHA512()
238
+ else:
239
+ raise ValueError(f"Unsupported curve with key size {key_size}")
240
+ return csr.sign(self.key, algorithm)
@@ -0,0 +1,127 @@
1
+ from typing import Optional, List, Union
2
+ from datetime import datetime, timedelta, timezone
3
+ from cryptography import x509
4
+ from cryptography.x509.oid import NameOID
5
+ from certapi.crypto_classes import Key, RSAKey, ECDSAKey, Ed25519Key # Assuming Key classes are in crypto_keys module
6
+
7
+
8
+ class CertificateIssuer:
9
+ def __init__(
10
+ self,
11
+ key: Key,
12
+ *,
13
+ country: Optional[str] = "NP",
14
+ state: Optional[str] = "Kathmandu",
15
+ locality: Optional[str] = "Buddhanagar",
16
+ organization: Optional[str] = "Sireto Technology",
17
+ common_name: Optional[str] = "sireto.io",
18
+ ):
19
+ """Initialize the CertificateIssuer with a Key object."""
20
+ self.root_key: Key = key
21
+ self.issuer_fields = {
22
+ "country": country,
23
+ "state": state,
24
+ "locality": locality,
25
+ "organization": organization,
26
+ "common_name": common_name,
27
+ }
28
+ self.issuer = self._build_name(self.issuer_fields)
29
+
30
+ def _build_name(self, fields: dict, include_user_id=False, domain=None) -> x509.Name:
31
+ """Build an X509 Name object from field dictionary."""
32
+ name_attrs = []
33
+ field_map = {
34
+ "country": NameOID.COUNTRY_NAME,
35
+ "state": NameOID.STATE_OR_PROVINCE_NAME,
36
+ "locality": NameOID.LOCALITY_NAME,
37
+ "organization": NameOID.ORGANIZATION_NAME,
38
+ "common_name": NameOID.COMMON_NAME,
39
+ }
40
+
41
+ for key, oid in field_map.items():
42
+ value = fields.get(key)
43
+ if value:
44
+ name_attrs.append(x509.NameAttribute(oid, value))
45
+
46
+ if include_user_id:
47
+ user_id = fields.get("user_id") or domain
48
+ if user_id:
49
+ name_attrs.append(x509.NameAttribute(NameOID.USER_ID, user_id))
50
+
51
+ return x509.Name(name_attrs)
52
+
53
+ def get_ca_cert(self) -> x509.Certificate:
54
+ """Generate a self-signed CA certificate."""
55
+ now = datetime.now(timezone.utc)
56
+ builder = (
57
+ x509.CertificateBuilder()
58
+ .subject_name(self.issuer)
59
+ .issuer_name(self.issuer)
60
+ .public_key(self.root_key.key.public_key())
61
+ .serial_number(x509.random_serial_number())
62
+ .not_valid_before(now)
63
+ .not_valid_after(now + timedelta(days=365))
64
+ .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
65
+ )
66
+ return self.root_key.sign_csr(builder)
67
+
68
+ def sign_csr(
69
+ self,
70
+ csr: x509.CertificateSigningRequest,
71
+ expiry_days: int = 90,
72
+ ) -> x509.Certificate:
73
+ """Sign a CSR and return a certificate signed by this CA."""
74
+ now = datetime.now(timezone.utc)
75
+ builder = (
76
+ x509.CertificateBuilder()
77
+ .subject_name(csr.subject)
78
+ .issuer_name(self.issuer)
79
+ .public_key(csr.public_key())
80
+ .serial_number(x509.random_serial_number())
81
+ .not_valid_before(now)
82
+ .not_valid_after(now + timedelta(days=expiry_days))
83
+ )
84
+
85
+ for ext in csr.extensions:
86
+ builder = builder.add_extension(ext.value, ext.critical)
87
+
88
+ return self.root_key.sign_csr(builder)
89
+
90
+ def create_key_and_cert(
91
+ self,
92
+ domain: str,
93
+ alt_names: List[str] = (),
94
+ key_type: str = "rsa",
95
+ expiry_days: int = 90,
96
+ country: Optional[str] = None,
97
+ state: Optional[str] = None,
98
+ locality: Optional[str] = None,
99
+ organization: Optional[str] = None,
100
+ user_id: Optional[str] = None,
101
+ ) -> tuple:
102
+ """Create a new certificate with a generated key."""
103
+ # Generate new key based on key_type
104
+ if key_type == "rsa":
105
+ new_key = RSAKey.generate()
106
+ elif key_type == "ecdsa":
107
+ new_key = ECDSAKey.generate()
108
+ elif key_type == "ed25519":
109
+ new_key = Ed25519Key.generate()
110
+ else:
111
+ raise ValueError("Unsupported key type. Use 'rsa' or 'ecdsa'")
112
+
113
+ # Create CSR using the new key
114
+ csr = new_key.create_csr(
115
+ domain=domain,
116
+ alt_names=alt_names,
117
+ country=country or self.issuer_fields.get("country"),
118
+ state=state or self.issuer_fields.get("state"),
119
+ locality=locality or self.issuer_fields.get("locality"),
120
+ organization=organization or self.issuer_fields.get("organization"),
121
+ user_id=user_id or domain,
122
+ )
123
+
124
+ # Sign the CSR to get the certificate
125
+ cert = self.sign_csr(csr, expiry_days=expiry_days)
126
+
127
+ return new_key, cert
@@ -162,7 +162,7 @@ class FilesystemKeyStore(KeyStore):
162
162
  return name # Dummy ID since filesystem does not use numeric IDs
163
163
 
164
164
  def gen_key(self, name: str = None, size: int = 4096) -> RSAPrivateKey:
165
- key = gen_key_rsa(size)
165
+ key = gen_key_secp256r1()
166
166
  self.save_key(key, name)
167
167
  return key
168
168
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: certapi
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Python Package for managing keys, request SSL certificates from ACME.
5
5
  Home-page: https://github.com/mesudip/certapi
6
6
  Author: Sudip Bhattarai
@@ -10,8 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.6
12
12
  Description-Content-Type: text/markdown
13
- Requires-Dist: cryptography
14
- Requires-Dist: requests
15
13
 
16
14
  # CertApi
17
15
 
@@ -6,6 +6,8 @@ src/certapi/Acme.py
6
6
  src/certapi/__init__.py
7
7
  src/certapi/certauthority.py
8
8
  src/certapi/challenge.py
9
+ src/certapi/cloudflare_challenge_store.py
10
+ src/certapi/cloudflare_client.py
9
11
  src/certapi/crypto.py
10
12
  src/certapi/crypto_classes.py
11
13
  src/certapi/custom_certauthority.py
@@ -15,5 +17,4 @@ src/certapi.egg-info/PKG-INFO
15
17
  src/certapi.egg-info/SOURCES.txt
16
18
  src/certapi.egg-info/dependency_links.txt
17
19
  src/certapi.egg-info/requires.txt
18
- src/certapi.egg-info/top_level.txt
19
- tests/test_custom_certauthority.py
20
+ src/certapi.egg-info/top_level.txt
@@ -1,135 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from cryptography.hazmat.primitives.asymmetric import rsa, ed25519, ec
3
- from cryptography.hazmat.primitives import serialization, hashes, hmac, padding
4
- from typing import Union
5
-
6
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
-
8
- from .crypto import key_to_der, key_to_pem
9
- from .util import b64_string
10
-
11
-
12
- class Key(ABC):
13
- @abstractmethod
14
- def jwk(self):
15
- pass
16
-
17
- @abstractmethod
18
- def sign(self, message):
19
- pass
20
-
21
- @abstractmethod
22
- def sign_csr(self, csr):
23
- pass
24
-
25
- @staticmethod
26
- def from_der(der_bytes):
27
- key = serialization.load_der_private_key(der_bytes, password=None)
28
- if isinstance(key, rsa.RSAPrivateKey):
29
- return RSAKey(key)
30
- elif isinstance(key, ec.EllipticCurvePrivateKey):
31
- return ECDSAKey(key)
32
- elif isinstance(key, Ed25519PrivateKey):
33
- return Ed25519Key(key)
34
- else:
35
- raise ValueError("Unsupported key type")
36
-
37
- @staticmethod
38
- def from_pem(der_bytes):
39
- key = serialization.load_pem_private_key(der_bytes, password=None)
40
- if isinstance(key, rsa.RSAPrivateKey):
41
- return RSAKey(key)
42
- elif isinstance(key, ec.EllipticCurvePrivateKey):
43
- return ECDSAKey(key)
44
- elif isinstance(key, Ed25519PrivateKey):
45
- return Ed25519Key(key)
46
- else:
47
- raise ValueError("Unsupported key type")
48
-
49
- def to_der(self) -> bytes:
50
- return key_to_der(self.key)
51
-
52
- def to_pem(self) -> bytes:
53
- return key_to_pem(self.key)
54
-
55
-
56
- class RSAKey(Key):
57
- def __init__(self, key: rsa.RSAPrivateKey, hasher=hashes.SHA256()):
58
- self.key = key
59
- self.hasher = hasher
60
-
61
- def jwk(self):
62
- public = self.key.public_key().public_numbers()
63
- return {
64
- "e": b64_string((public.e).to_bytes((public.e.bit_length() + 7) // 8, "big")),
65
- "kty": "RSA",
66
- "n": b64_string((public.n).to_bytes((public.n.bit_length() + 7) // 8, "big")),
67
- }
68
-
69
- def sign(self, message):
70
- return self.key.sign(message, padding.PKCS1v15(), self.hasher)
71
-
72
- def sign_csr(self, csr):
73
- return csr.sign(self.key, self.hasher)
74
-
75
- def algorithm_name(self):
76
- return "RS" + str(self.hasher.digest_size * 8)
77
-
78
-
79
- class Ed25519Key(Key):
80
-
81
- def __init__(self, key: ed25519.Ed25519PrivateKey):
82
- self.key = key
83
- self.keyid = "e"
84
- public = self.key.public_key().public_bytes(
85
- encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
86
- )
87
- self.jwk = {
88
- "crv": "Ed25519",
89
- "kty": "OKP",
90
- "x": b64_string(public),
91
- }
92
-
93
- def jwk(self):
94
- return self.jwk
95
-
96
- def sign(self, message):
97
- return self.key.sign(message)
98
-
99
- def sign_csr(self, csr):
100
- return csr.sign(self.key, hashes.SHA256())
101
-
102
-
103
- class ECDSAKey(Key):
104
-
105
- def __init__(self, key: ec.EllipticCurvePrivateKey):
106
- self.key = key
107
- public_key = self.key.public_key()
108
- public_numbers = public_key.public_numbers()
109
- self.jwk = {
110
- "kty": "EC",
111
- "crv": self.key.curve.name,
112
- "x": b64_string(public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, "big")),
113
- "y": b64_string(public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, "big")),
114
- }
115
-
116
- def jwk(self):
117
- return self.jwk
118
-
119
- def algorithm_name(self):
120
- return self.key.curve.name
121
-
122
- def sign(self, message):
123
- key_size = self.key.curve.key_size
124
- if key_size == 256:
125
- algorithm = hashes.SHA256()
126
- elif key_size == 384:
127
- algorithm = hashes.SHA384()
128
- elif key_size == 521:
129
- algorithm = hashes.SHA512()
130
- else:
131
- raise ValueError(f"Unsupported curve with key size {key_size}")
132
- return self.key.sign(message, ec.ECDSA(algorithm))
133
-
134
- def sign_csr(self, csr):
135
- return csr.sign(self.key, hashes.SHA256())
@@ -1,94 +0,0 @@
1
- import json
2
- import time
3
- from datetime import datetime, timedelta
4
- from typing import Union, Callable, List
5
-
6
- import requests
7
- from cryptography import x509
8
- from cryptography.hazmat._oid import NameOID
9
- from cryptography.hazmat.primitives import hashes, serialization
10
- from cryptography.hazmat.primitives.asymmetric.dsa import DSAPrivateKey
11
- from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
12
- from cryptography.x509 import Certificate
13
- from requests import Response
14
-
15
- from . import Acme
16
- from . import db
17
- from . import crypto
18
- from . import challenge
19
- from .crypto import csr_to_pem, create_csr, gen_key_secp256r1
20
- from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding, ed25519, dsa
21
-
22
-
23
- # pathlib.Path.mkdir("/var/www/html/.acme/well-known")
24
-
25
-
26
- class CustomCertAuthority:
27
- def __init__(self, key: Union[RSAPrivateKey, DSAPrivateKey, ec.EllipticCurvePrivateKey]):
28
- self.root_key = key
29
- self.issuer = x509.Name(
30
- [
31
- x509.NameAttribute(NameOID.COUNTRY_NAME, "NP"),
32
- x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Kathmandu"),
33
- x509.NameAttribute(NameOID.LOCALITY_NAME, "Buddhanagar"),
34
- x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Sireto Technology"),
35
- x509.NameAttribute(NameOID.COMMON_NAME, "sireto.io"),
36
- ]
37
- )
38
-
39
- def get_ca_cert(self):
40
-
41
- # Assuming self.root_key is the CA private key
42
- cert = (
43
- x509.CertificateBuilder()
44
- .subject_name(self.issuer)
45
- .issuer_name(self.issuer)
46
- .public_key(self.root_key.public_key())
47
- .serial_number(x509.random_serial_number())
48
- .not_valid_before(datetime.utcnow())
49
- .not_valid_after(datetime.utcnow() + timedelta(days=365))
50
- .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
51
- .sign(self.root_key, hashes.SHA256())
52
- )
53
-
54
- return cert
55
-
56
- # Assuming CustomCertAuthority is your class
57
-
58
- def create_cert(self, domain: str, alt_names: List[str] = (), key_type: str = "rsa", expiry_days=90):
59
- if key_type == "rsa":
60
- key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
61
- elif key_type == "dsa":
62
- key = dsa.generate_private_key(key_size=2048)
63
- elif key_type == "ecdsa":
64
- key = gen_key_secp256r1()
65
- else:
66
- raise ValueError("Unsupported key type")
67
-
68
- subject = x509.Name(
69
- [
70
- x509.NameAttribute(NameOID.COUNTRY_NAME, "NP"),
71
- x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Kathmandu"),
72
- x509.NameAttribute(NameOID.LOCALITY_NAME, "Buddhanagar"),
73
- x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Sireto Technology"),
74
- x509.NameAttribute(NameOID.USER_ID, domain),
75
- ]
76
- )
77
-
78
- now = datetime.utcnow()
79
- cert_builder = (
80
- x509.CertificateBuilder()
81
- .subject_name(subject)
82
- .issuer_name(self.issuer)
83
- .public_key(key.public_key())
84
- .serial_number(x509.random_serial_number())
85
- .not_valid_before(now)
86
- .not_valid_after(now + timedelta(days=expiry_days))
87
- )
88
-
89
- for alt_name in alt_names:
90
- cert_builder.add_extension(x509.SubjectAlternativeName([x509.DNSName(alt_name)]), critical=False)
91
-
92
- cert = cert_builder.sign(self.root_key, hashes.SHA256())
93
-
94
- return (key, cert)
@@ -1,43 +0,0 @@
1
- import pytest
2
- from cryptography.hazmat.primitives import serialization
3
- from certapi.crypto import gen_key_rsa, key_to_pem
4
- from certapi.custom_certauthority import CustomCertAuthority
5
- import os
6
- from contextlib import contextmanager
7
-
8
-
9
- @pytest.fixture
10
- def rsa_key():
11
- return gen_key_rsa()
12
-
13
-
14
- test_new_file_dir = "./build/tests/intemediate-files"
15
- os.makedirs(test_new_file_dir, exist_ok=True)
16
-
17
-
18
- @contextmanager
19
- def tmp_test_file(filename):
20
- with open(os.path.join(test_new_file_dir, filename), "wb") as f:
21
- yield f
22
-
23
-
24
- def write_tmp_file(filename, content):
25
- with tmp_test_file(filename) as f:
26
- f.write(content)
27
-
28
-
29
- def test_self_sign_certificat(rsa_key):
30
- certauthority = CustomCertAuthority(rsa_key)
31
- (key, cert) = certauthority.create_cert("sudip.sireto.io", key_type="rsa")
32
- write_tmp_file("test_self_sign_private.key", key_to_pem(key))
33
- write_tmp_file("test_self_sign_certificate.crt", cert.public_bytes(serialization.Encoding.PEM))
34
-
35
- with tmp_test_file("test_self_sign_certificate.p12") as f:
36
- f.write(cert.public_bytes(serialization.Encoding.PEM))
37
- f.write(key_to_pem(key))
38
-
39
-
40
- def test_get_ca(rsa_key):
41
- certauthority = CustomCertAuthority(rsa_key)
42
- cert = certauthority.get_ca_cert()
43
- write_tmp_file("test_self_sign_ca.crt", cert.public_bytes(serialization.Encoding.PEM))
File without changes
File without changes
File without changes
File without changes
File without changes