sep2cert 0.1.0__py3-none-any.whl
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.
- sep2cert/__init__.py +9 -0
- sep2cert/__main__.py +3 -0
- sep2cert/cert_create.py +459 -0
- sep2cert/cert_id.py +102 -0
- sep2cert/cert_validate.py +142 -0
- sep2cert/cli.py +114 -0
- sep2cert/version.py +1 -0
- sep2cert-0.1.0.dist-info/METADATA +63 -0
- sep2cert-0.1.0.dist-info/RECORD +12 -0
- sep2cert-0.1.0.dist-info/WHEEL +4 -0
- sep2cert-0.1.0.dist-info/entry_points.txt +3 -0
- sep2cert-0.1.0.dist-info/licenses/LICENSE +21 -0
sep2cert/__init__.py
ADDED
sep2cert/__main__.py
ADDED
sep2cert/cert_create.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import asn1
|
|
7
|
+
from cryptography import x509
|
|
8
|
+
from cryptography.hazmat.backends import default_backend
|
|
9
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
11
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
12
|
+
from cryptography.x509.oid import ExtensionOID, NameOID, ObjectIdentifier
|
|
13
|
+
from dateutil import tz
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
DEFAULT_OUTPUT = Path("certs")
|
|
18
|
+
|
|
19
|
+
INDEF_EXPIRY = datetime(9999, 12, 31, 23, 59, 59, 0) # As per standard
|
|
20
|
+
|
|
21
|
+
ANY_POLICY_OID = ObjectIdentifier("2.5.29.32.0") # OID for "X509v3 Any Policy"
|
|
22
|
+
|
|
23
|
+
# IEEE 2030.5 device type assignments (Section 6.11.7.2)
|
|
24
|
+
SEP2_DEV_GENERIC = ObjectIdentifier("1.3.6.1.4.1.40732.1.1")
|
|
25
|
+
SEP2_DEV_MOBILE = ObjectIdentifier("1.3.6.1.4.1.40732.1.2")
|
|
26
|
+
SEP2_DEV_POSTMANUF = ObjectIdentifier("1.3.6.1.4.1.40732.1.3")
|
|
27
|
+
|
|
28
|
+
# IEEE 2030.5 policy assignments (Section 6.11.7.3)
|
|
29
|
+
SEP2_TEST_CERT = ObjectIdentifier("1.3.6.1.4.1.40732.2.1")
|
|
30
|
+
SEP2_SELFSIGNED = ObjectIdentifier("1.3.6.1.4.1.40732.2.2")
|
|
31
|
+
SEP2_SERVPROV = ObjectIdentifier("1.3.6.1.4.1.40732.2.3")
|
|
32
|
+
SEP2_BULK_CERT = ObjectIdentifier("1.3.6.1.4.1.40732.2.4")
|
|
33
|
+
|
|
34
|
+
# HardwareModuleName (Section 6.11.7.4)
|
|
35
|
+
SEP2_HARDWARE_MODULE_NAME = ObjectIdentifier("1.3.6.1.5.5.7.8.4")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
DEFAULT_MICA_POLICIES = [
|
|
39
|
+
SEP2_DEV_GENERIC,
|
|
40
|
+
SEP2_TEST_CERT,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
DEFAULT_DEV_POLICIES = [
|
|
45
|
+
SEP2_DEV_GENERIC,
|
|
46
|
+
SEP2_TEST_CERT,
|
|
47
|
+
SEP2_SELFSIGNED,
|
|
48
|
+
SEP2_BULK_CERT,
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def random_id() -> str:
|
|
53
|
+
return str(uuid.uuid4()).replace("-", "")[:10].upper()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def generate_csr_file(key_file: Path, hostnames: list[str] | None = None) -> Path:
|
|
57
|
+
# Load the private key
|
|
58
|
+
with open(key_file, "rb") as fh:
|
|
59
|
+
pem_data = fh.read()
|
|
60
|
+
key = serialization.load_pem_private_key(
|
|
61
|
+
pem_data, password=None, backend=default_backend()
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# CSR output file path
|
|
65
|
+
csr_file = key_file.with_suffix(".csr")
|
|
66
|
+
|
|
67
|
+
# Subject Alternative Names (SANs)
|
|
68
|
+
san_list = []
|
|
69
|
+
if hostnames:
|
|
70
|
+
msg = "Note that SEP2 Certificates do not require a hostname"
|
|
71
|
+
log.warning(msg)
|
|
72
|
+
for name in hostnames:
|
|
73
|
+
san = x509.DNSName(name)
|
|
74
|
+
san_list.append(san)
|
|
75
|
+
|
|
76
|
+
cb = x509.CertificateSigningRequestBuilder()
|
|
77
|
+
subject_name = ""
|
|
78
|
+
cb = cb.subject_name(x509.Name(subject_name))
|
|
79
|
+
if hostnames:
|
|
80
|
+
cb = cb.add_extension(
|
|
81
|
+
x509.SubjectAlternativeName(san_list),
|
|
82
|
+
critical=False,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
csr = cb.sign(key, SHA256())
|
|
86
|
+
|
|
87
|
+
csr_pem = csr.public_bytes(serialization.Encoding.PEM)
|
|
88
|
+
with open(csr_file, "wb") as fh:
|
|
89
|
+
fh.write(csr_pem)
|
|
90
|
+
log.info("Created CSR at %s", csr_file)
|
|
91
|
+
|
|
92
|
+
return csr_file
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def generate_key(
|
|
96
|
+
key_file: Path | None = None,
|
|
97
|
+
generate_csr: bool = True,
|
|
98
|
+
hostnames: list[str] | None = None,
|
|
99
|
+
) -> tuple[Path, Path | None]:
|
|
100
|
+
"""Generate a Private Key and Certificate Signing Request (CSR)"""
|
|
101
|
+
|
|
102
|
+
if not key_file:
|
|
103
|
+
output_dir = DEFAULT_OUTPUT
|
|
104
|
+
output_dir.mkdir(exist_ok=True)
|
|
105
|
+
name = random_id()
|
|
106
|
+
key_file = output_dir / f"{name}.key"
|
|
107
|
+
|
|
108
|
+
key = ec.generate_private_key(ec.SECP256R1())
|
|
109
|
+
key_pem = key.private_bytes(
|
|
110
|
+
encoding=serialization.Encoding.PEM,
|
|
111
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
112
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
113
|
+
)
|
|
114
|
+
with open(key_file, "wb") as fh:
|
|
115
|
+
fh.write(key_pem)
|
|
116
|
+
log.info("Created Key at %s", key_file)
|
|
117
|
+
|
|
118
|
+
csr_file = None
|
|
119
|
+
if generate_csr:
|
|
120
|
+
csr_file = generate_csr_file(key_file, hostnames=hostnames)
|
|
121
|
+
|
|
122
|
+
return key_file, csr_file
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def convert_pem_to_der(pem_file: Path, der_file: Path | None = None) -> Path:
|
|
126
|
+
"""Convert a PEM file to DER Certificate"""
|
|
127
|
+
with open(pem_file, "rb") as fh:
|
|
128
|
+
cert_data = fh.read()
|
|
129
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
|
130
|
+
der_data = cert.public_bytes(encoding=serialization.Encoding.DER)
|
|
131
|
+
|
|
132
|
+
if not der_file:
|
|
133
|
+
der_file = pem_file.with_suffix(".cer")
|
|
134
|
+
with open(der_file, "wb") as fh:
|
|
135
|
+
fh.write(der_data)
|
|
136
|
+
log.info("Created %s from %s", der_file, pem_file)
|
|
137
|
+
return der_file
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def generate_serca(
|
|
141
|
+
key_file: Path,
|
|
142
|
+
cert_file: Path | None = None,
|
|
143
|
+
org_name: str = "Smart Energy",
|
|
144
|
+
country_name: str = "AU",
|
|
145
|
+
) -> tuple[Path, Path]:
|
|
146
|
+
"""Use a CSR and MICA key pair to generate a SEP2 Certificate"""
|
|
147
|
+
|
|
148
|
+
if not cert_file:
|
|
149
|
+
output_dir = key_file.parent
|
|
150
|
+
output_dir.mkdir(exist_ok=True)
|
|
151
|
+
if key_file.suffix == "pem":
|
|
152
|
+
cert_name = f"{key_file.stem}-serca.pem"
|
|
153
|
+
else:
|
|
154
|
+
cert_name = f"{key_file.stem}.pem"
|
|
155
|
+
cert_file = output_dir / cert_name
|
|
156
|
+
|
|
157
|
+
with open(key_file, "rb") as fh:
|
|
158
|
+
pem_data = fh.read()
|
|
159
|
+
key = serialization.load_pem_private_key(pem_data, password=None)
|
|
160
|
+
|
|
161
|
+
valid_from = datetime.now(tz=tz.UTC)
|
|
162
|
+
valid_to = INDEF_EXPIRY # as per standard
|
|
163
|
+
|
|
164
|
+
policies = [x509.PolicyInformation(ANY_POLICY_OID, None)]
|
|
165
|
+
|
|
166
|
+
# Define the Subject Name
|
|
167
|
+
subject = x509.Name(
|
|
168
|
+
[
|
|
169
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, country_name),
|
|
170
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org_name),
|
|
171
|
+
x509.NameAttribute(NameOID.COMMON_NAME, "IEEE 2030.5 Root"),
|
|
172
|
+
x509.NameAttribute(NameOID.SERIAL_NUMBER, "1"),
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Generate a Subject Key Identifier (SKI)
|
|
177
|
+
ski = key.public_key().public_bytes(
|
|
178
|
+
encoding=serialization.Encoding.DER,
|
|
179
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
ski_digest = hashes.Hash(hashes.SHA1(), backend=default_backend())
|
|
183
|
+
ski_digest.update(ski)
|
|
184
|
+
ski_value = ski_digest.finalize()
|
|
185
|
+
|
|
186
|
+
cb = x509.CertificateBuilder()
|
|
187
|
+
cb = cb.subject_name(subject)
|
|
188
|
+
cb = cb.issuer_name(subject)
|
|
189
|
+
cb = cb.public_key(key.public_key())
|
|
190
|
+
cb = cb.serial_number(x509.random_serial_number())
|
|
191
|
+
cb = cb.not_valid_before(valid_from)
|
|
192
|
+
cb = cb.not_valid_after(valid_to)
|
|
193
|
+
cb = cb.add_extension(
|
|
194
|
+
x509.BasicConstraints(ca=True, path_length=None),
|
|
195
|
+
critical=True,
|
|
196
|
+
)
|
|
197
|
+
cb = cb.add_extension(
|
|
198
|
+
x509.KeyUsage(
|
|
199
|
+
key_agreement=False,
|
|
200
|
+
key_cert_sign=True,
|
|
201
|
+
crl_sign=True,
|
|
202
|
+
digital_signature=True,
|
|
203
|
+
content_commitment=False,
|
|
204
|
+
key_encipherment=False,
|
|
205
|
+
data_encipherment=False,
|
|
206
|
+
encipher_only=False,
|
|
207
|
+
decipher_only=False,
|
|
208
|
+
),
|
|
209
|
+
critical=True,
|
|
210
|
+
)
|
|
211
|
+
cb = cb.add_extension(
|
|
212
|
+
x509.SubjectKeyIdentifier(ski_value),
|
|
213
|
+
critical=False,
|
|
214
|
+
)
|
|
215
|
+
cb = cb.add_extension(
|
|
216
|
+
x509.CertificatePolicies(policies=policies),
|
|
217
|
+
critical=True,
|
|
218
|
+
)
|
|
219
|
+
cert = cb.sign(key, SHA256())
|
|
220
|
+
|
|
221
|
+
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
|
222
|
+
with open(cert_file, "wb") as fh:
|
|
223
|
+
fh.write(cert_pem)
|
|
224
|
+
log.info("Created SERCA %s", cert_file)
|
|
225
|
+
|
|
226
|
+
der_file = convert_pem_to_der(cert_file)
|
|
227
|
+
|
|
228
|
+
return cert_file, der_file
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def generate_mica(
|
|
232
|
+
csr_path: Path,
|
|
233
|
+
ca_cert_path: Path,
|
|
234
|
+
ca_key_path: Path,
|
|
235
|
+
cert_file: Path | None = None,
|
|
236
|
+
org_name: str = "Example Org Name",
|
|
237
|
+
country_name: str = "AU",
|
|
238
|
+
common_name: str = "IEEE 2030.5 MICA",
|
|
239
|
+
policy_oids: list[ObjectIdentifier] = DEFAULT_MICA_POLICIES,
|
|
240
|
+
) -> tuple[Path, Path]:
|
|
241
|
+
"""Use a CSR and MICA key pair to generate a SEP2 Certificate"""
|
|
242
|
+
|
|
243
|
+
with open(csr_path, "rb") as fh:
|
|
244
|
+
csr_data = fh.read()
|
|
245
|
+
csr = x509.load_pem_x509_csr(csr_data)
|
|
246
|
+
|
|
247
|
+
if not cert_file:
|
|
248
|
+
output_dir = csr_path.parent
|
|
249
|
+
output_dir.mkdir(exist_ok=True)
|
|
250
|
+
if csr_path.suffix == "pem":
|
|
251
|
+
cert_name = f"{csr_path.stem}-mica.pem"
|
|
252
|
+
else:
|
|
253
|
+
cert_name = f"{csr_path.stem}.pem"
|
|
254
|
+
cert_file = output_dir / cert_name
|
|
255
|
+
|
|
256
|
+
# Load certificate authority
|
|
257
|
+
with open(ca_cert_path, "rb") as fh:
|
|
258
|
+
ca_pem_data = fh.read()
|
|
259
|
+
ca_cert = x509.load_pem_x509_certificate(ca_pem_data)
|
|
260
|
+
with open(ca_key_path, "rb") as fh:
|
|
261
|
+
pem_data = fh.read()
|
|
262
|
+
ca_key = serialization.load_pem_private_key(pem_data, password=None)
|
|
263
|
+
|
|
264
|
+
valid_from = datetime.now(tz=tz.UTC)
|
|
265
|
+
valid_to = INDEF_EXPIRY # as per standard
|
|
266
|
+
|
|
267
|
+
policies = [x509.PolicyInformation(oid, None) for oid in policy_oids]
|
|
268
|
+
|
|
269
|
+
# Define the Subject Name
|
|
270
|
+
subject = x509.Name(
|
|
271
|
+
[
|
|
272
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, country_name),
|
|
273
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org_name),
|
|
274
|
+
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
|
275
|
+
x509.NameAttribute(NameOID.SERIAL_NUMBER, "1"),
|
|
276
|
+
]
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Generate a Subject Key Identifier (SKI)
|
|
280
|
+
ski = csr.public_key().public_bytes(
|
|
281
|
+
encoding=serialization.Encoding.DER,
|
|
282
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
ski_digest = hashes.Hash(hashes.SHA1(), backend=default_backend())
|
|
286
|
+
ski_digest.update(ski)
|
|
287
|
+
ski_value = ski_digest.finalize()
|
|
288
|
+
|
|
289
|
+
issuer_name = ca_cert.subject
|
|
290
|
+
iname = x509.Name(issuer_name)
|
|
291
|
+
|
|
292
|
+
cb = x509.CertificateBuilder()
|
|
293
|
+
|
|
294
|
+
cb = cb.subject_name(subject)
|
|
295
|
+
cb = cb.issuer_name(iname)
|
|
296
|
+
cb = cb.public_key(csr.public_key())
|
|
297
|
+
cb = cb.serial_number(x509.random_serial_number())
|
|
298
|
+
cb = cb.not_valid_before(valid_from)
|
|
299
|
+
cb = cb.not_valid_after(valid_to)
|
|
300
|
+
cb = cb.add_extension(
|
|
301
|
+
x509.BasicConstraints(ca=True, path_length=0),
|
|
302
|
+
critical=True,
|
|
303
|
+
)
|
|
304
|
+
cb = cb.add_extension(
|
|
305
|
+
x509.KeyUsage(
|
|
306
|
+
key_agreement=False,
|
|
307
|
+
key_cert_sign=True,
|
|
308
|
+
crl_sign=False,
|
|
309
|
+
digital_signature=True,
|
|
310
|
+
content_commitment=False,
|
|
311
|
+
key_encipherment=False,
|
|
312
|
+
data_encipherment=False,
|
|
313
|
+
encipher_only=False,
|
|
314
|
+
decipher_only=False,
|
|
315
|
+
),
|
|
316
|
+
critical=True,
|
|
317
|
+
)
|
|
318
|
+
cb = cb.add_extension(
|
|
319
|
+
x509.SubjectKeyIdentifier(ski_value),
|
|
320
|
+
critical=False,
|
|
321
|
+
)
|
|
322
|
+
cb = cb.add_extension(
|
|
323
|
+
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
|
|
324
|
+
ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
|
|
325
|
+
),
|
|
326
|
+
critical=False,
|
|
327
|
+
)
|
|
328
|
+
cb = cb.add_extension(
|
|
329
|
+
x509.CertificatePolicies(policies=policies),
|
|
330
|
+
critical=True,
|
|
331
|
+
)
|
|
332
|
+
cert = cb.sign(ca_key, SHA256())
|
|
333
|
+
|
|
334
|
+
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
|
335
|
+
with open(cert_file, "wb") as fh:
|
|
336
|
+
fh.write(cert_pem)
|
|
337
|
+
fh.write(ca_pem_data) # Append the signing certificate
|
|
338
|
+
log.info("Created MICA %s", cert_file)
|
|
339
|
+
|
|
340
|
+
der_file = convert_pem_to_der(cert_file)
|
|
341
|
+
|
|
342
|
+
return cert_file, der_file
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def generate_device_certificate(
|
|
346
|
+
csr_path: Path,
|
|
347
|
+
ca_cert_path: Path,
|
|
348
|
+
ca_key_path: Path,
|
|
349
|
+
pen: int,
|
|
350
|
+
hardware_serial_number: str,
|
|
351
|
+
cert_file: Path | None = None,
|
|
352
|
+
policy_oids: list[ObjectIdentifier] = DEFAULT_DEV_POLICIES,
|
|
353
|
+
valid_to: datetime | None = None,
|
|
354
|
+
) -> Path:
|
|
355
|
+
"""Use a CSR and Signing Certificate key pair to generate a SEP2 Certificate"""
|
|
356
|
+
|
|
357
|
+
if not cert_file:
|
|
358
|
+
output_dir = csr_path.parent
|
|
359
|
+
cert_name = f"{csr_path.stem}-cert.pem"
|
|
360
|
+
cert_file = output_dir / cert_name
|
|
361
|
+
|
|
362
|
+
with open(csr_path, "rb") as fh:
|
|
363
|
+
csr_data = fh.read()
|
|
364
|
+
csr = x509.load_pem_x509_csr(csr_data)
|
|
365
|
+
with open(ca_cert_path, "rb") as fh:
|
|
366
|
+
ca_pem_data = fh.read()
|
|
367
|
+
ca_cert = x509.load_pem_x509_certificate(ca_pem_data)
|
|
368
|
+
with open(ca_key_path, "rb") as fh:
|
|
369
|
+
pem_data = fh.read()
|
|
370
|
+
ca_key = serialization.load_pem_private_key(pem_data, password=None)
|
|
371
|
+
valid_from = datetime.now(tz=tz.UTC)
|
|
372
|
+
|
|
373
|
+
if valid_to and valid_to.year != 9999:
|
|
374
|
+
log.warning("Using a valid to year other than 9999 is a deviation from SEP2")
|
|
375
|
+
else:
|
|
376
|
+
valid_to = INDEF_EXPIRY # as per standard
|
|
377
|
+
|
|
378
|
+
policies = [x509.PolicyInformation(oid, None) for oid in policy_oids]
|
|
379
|
+
|
|
380
|
+
hardware_type_oid = f"1.3.6.1.4.1.40732.{pen}.1"
|
|
381
|
+
|
|
382
|
+
# SAN OtherName encoder
|
|
383
|
+
encoder = asn1.Encoder()
|
|
384
|
+
encoder.start()
|
|
385
|
+
encoder.enter(asn1.Numbers.Sequence)
|
|
386
|
+
encoder.write(str(hardware_type_oid), asn1.Numbers.ObjectIdentifier)
|
|
387
|
+
encoder.write(str(hardware_serial_number), asn1.Numbers.OctetString)
|
|
388
|
+
encoder.leave()
|
|
389
|
+
hw_module_name = encoder.output()
|
|
390
|
+
hw_other_name = x509.OtherName(SEP2_HARDWARE_MODULE_NAME, hw_module_name)
|
|
391
|
+
|
|
392
|
+
# Load Subject Alt Names from CSR if they exist
|
|
393
|
+
sans = [hw_other_name]
|
|
394
|
+
try:
|
|
395
|
+
san_oid = ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
|
396
|
+
san_extension = csr.extensions.get_extension_for_oid(san_oid)
|
|
397
|
+
san = san_extension.value
|
|
398
|
+
log.warning("Specifying a SubjectAlternativeName is a deviation from SEP2")
|
|
399
|
+
sans.extend(san)
|
|
400
|
+
except x509.ExtensionNotFound:
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
# Define the Subject Name
|
|
404
|
+
subject = x509.Name("")
|
|
405
|
+
|
|
406
|
+
issuer_name = ca_cert.subject
|
|
407
|
+
iname = x509.Name(issuer_name)
|
|
408
|
+
|
|
409
|
+
cb = x509.CertificateBuilder()
|
|
410
|
+
cb = cb.subject_name(subject)
|
|
411
|
+
cb = cb.issuer_name(iname)
|
|
412
|
+
cb = cb.public_key(csr.public_key())
|
|
413
|
+
cb = cb.serial_number(x509.random_serial_number())
|
|
414
|
+
cb = cb.not_valid_before(valid_from)
|
|
415
|
+
cb = cb.not_valid_after(valid_to)
|
|
416
|
+
cb = cb.add_extension(
|
|
417
|
+
x509.BasicConstraints(ca=False, path_length=None),
|
|
418
|
+
critical=True,
|
|
419
|
+
)
|
|
420
|
+
cb = cb.add_extension(
|
|
421
|
+
x509.KeyUsage(
|
|
422
|
+
key_agreement=True,
|
|
423
|
+
key_cert_sign=False,
|
|
424
|
+
crl_sign=False,
|
|
425
|
+
digital_signature=True,
|
|
426
|
+
content_commitment=False,
|
|
427
|
+
key_encipherment=False,
|
|
428
|
+
data_encipherment=False,
|
|
429
|
+
encipher_only=False,
|
|
430
|
+
decipher_only=False,
|
|
431
|
+
),
|
|
432
|
+
critical=True,
|
|
433
|
+
)
|
|
434
|
+
cb = cb.add_extension(
|
|
435
|
+
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
|
|
436
|
+
ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
|
|
437
|
+
),
|
|
438
|
+
critical=False,
|
|
439
|
+
)
|
|
440
|
+
cb = cb.add_extension(
|
|
441
|
+
x509.CertificatePolicies(policies=policies),
|
|
442
|
+
critical=True,
|
|
443
|
+
)
|
|
444
|
+
cb = cb.add_extension(
|
|
445
|
+
x509.SubjectAlternativeName(general_names=sans),
|
|
446
|
+
critical=True,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
cert = cb.sign(ca_key, SHA256())
|
|
450
|
+
|
|
451
|
+
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
|
452
|
+
with open(cert_file, "wb") as fh:
|
|
453
|
+
fh.write(cert_pem)
|
|
454
|
+
# Append the intermediate certificate(s)
|
|
455
|
+
fh.write(ca_pem_data)
|
|
456
|
+
log.info("Created Cert %s", cert_file)
|
|
457
|
+
|
|
458
|
+
convert_pem_to_der(cert_file)
|
|
459
|
+
return cert_file
|
sep2cert/cert_id.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
from itertools import zip_longest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cryptography import x509
|
|
7
|
+
from cryptography.hazmat.primitives import serialization
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def group_hex(item: str, group_size: int = 4) -> str:
|
|
13
|
+
"""Group into groups of four"""
|
|
14
|
+
|
|
15
|
+
def grouper(iterable, n, fillvalue=None):
|
|
16
|
+
args = [iter(iterable)] * n
|
|
17
|
+
return zip_longest(*args, fillvalue=fillvalue)
|
|
18
|
+
|
|
19
|
+
return "-".join("".join(x) for x in grouper(item, n=group_size))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_lfdi(fingerprint: str, group: bool = True) -> str:
|
|
23
|
+
"""Long-form device identifier (LFDI)
|
|
24
|
+
The LFDI SHALL be the certificate fingerprint left-truncated to 160 bits.
|
|
25
|
+
For display purposes, this SHALL be expressed as 40 hexadecimal digits in
|
|
26
|
+
groups of four.
|
|
27
|
+
"""
|
|
28
|
+
lfdi = fingerprint.replace("-", "")[0:40].upper()
|
|
29
|
+
if not group:
|
|
30
|
+
return lfdi
|
|
31
|
+
return group_hex(lfdi)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_sfdi(fingerprint, group: bool = True) -> str:
|
|
35
|
+
"""Short-form device identifier (SFDI)
|
|
36
|
+
The SFDI SHALL be the certificate fingerprint left-truncated to 36 bits.
|
|
37
|
+
For display purposes, this SHALL be expressed as 11 decimal (base 10) digits,
|
|
38
|
+
with an additional sum-of-digits checksum digit right concatenated.
|
|
39
|
+
"""
|
|
40
|
+
trunc = fingerprint.replace("-", "")[0:9]
|
|
41
|
+
sfdi = int(trunc, base=16) # Convert from Base 16 to Base 10
|
|
42
|
+
digits = map(int, str(sfdi))
|
|
43
|
+
checksum = 10 - sum(digits) % 10
|
|
44
|
+
if checksum == 10:
|
|
45
|
+
checksum = 0
|
|
46
|
+
|
|
47
|
+
full_sfdi = str(sfdi) + str(checksum)
|
|
48
|
+
if not group:
|
|
49
|
+
return full_sfdi
|
|
50
|
+
return group_hex(full_sfdi, 3)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_fingerprint(cert_path: Path) -> str:
|
|
54
|
+
"""Certificate fingerprint
|
|
55
|
+
The certificate fingerprint is the result of performing a SHA256 operation
|
|
56
|
+
over the whole DER-encoded certificate and is used to derive the SFDI and LFDI.
|
|
57
|
+
"""
|
|
58
|
+
with open(cert_path, "rb") as f:
|
|
59
|
+
fbytes = f.read() # read entire file as bytes
|
|
60
|
+
readable_hash = hashlib.sha256(fbytes).hexdigest().upper()
|
|
61
|
+
return readable_hash
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_pem_certificate(cert_path: Path) -> bool:
|
|
65
|
+
with open(cert_path, "rb") as f:
|
|
66
|
+
first_line = f.readline()
|
|
67
|
+
return b"-----BEGIN CERTIFICATE-----" in first_line
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_der_certificate_lfdi(cert_path: Path, group: bool = True):
|
|
71
|
+
"""Load X.509 DER Certificate and return LFDI"""
|
|
72
|
+
|
|
73
|
+
if not cert_path.exists():
|
|
74
|
+
raise ValueError(f"Cert not found at {cert_path}")
|
|
75
|
+
|
|
76
|
+
fingerprint = get_fingerprint(cert_path)
|
|
77
|
+
return get_lfdi(fingerprint, group=group)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_pem_certificate_lfdi(cert_path: Path, group: bool = True):
|
|
81
|
+
"""Load X.509 DER Certificate in PEM format and return LFDI"""
|
|
82
|
+
|
|
83
|
+
if not cert_path.exists():
|
|
84
|
+
raise ValueError(f"Cert not found at {cert_path}")
|
|
85
|
+
|
|
86
|
+
with open(cert_path, "rb") as pem_file:
|
|
87
|
+
cert_data = pem_file.read()
|
|
88
|
+
|
|
89
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
|
90
|
+
der_bytes = cert.public_bytes(serialization.Encoding.DER)
|
|
91
|
+
fingerprint = hashlib.sha256(der_bytes).hexdigest().upper()
|
|
92
|
+
lfdi = get_lfdi(fingerprint, group=group)
|
|
93
|
+
return lfdi
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_certificate_lfdi(cert_path: Path, group: bool = True):
|
|
97
|
+
"""Load X.509 DER Certificate in PEM or DER format and return LFDI"""
|
|
98
|
+
if is_pem_certificate(cert_path):
|
|
99
|
+
lfdi = get_pem_certificate_lfdi(cert_path, group=group)
|
|
100
|
+
else:
|
|
101
|
+
lfdi = get_der_certificate_lfdi(cert_path, group=group)
|
|
102
|
+
return lfdi
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from cryptography import x509
|
|
6
|
+
from cryptography.x509 import Certificate
|
|
7
|
+
|
|
8
|
+
from .cert_id import is_pem_certificate
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
INDEF_EXPIRY = datetime(9999, 12, 31, 23, 59, 59, 0, UTC) # As per standard
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_certificate(cert_path: Path) -> Certificate:
|
|
15
|
+
if not cert_path.exists():
|
|
16
|
+
raise ValueError(f"Cert not found at {cert_path}")
|
|
17
|
+
with open(cert_path, "rb") as pem_file:
|
|
18
|
+
cert_data = pem_file.read()
|
|
19
|
+
if is_pem_certificate(cert_path):
|
|
20
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
|
21
|
+
else:
|
|
22
|
+
cert = x509.load_der_x509_certificate(cert_data)
|
|
23
|
+
return cert
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_certificate_sans(cert_path: Path) -> list[str]:
|
|
27
|
+
"""Load X.509 DER Certificate in PEM format and return Policy OIDs"""
|
|
28
|
+
cert = load_certificate(cert_path)
|
|
29
|
+
sans = cert.extensions.get_extension_for_oid(
|
|
30
|
+
x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
|
31
|
+
).value
|
|
32
|
+
output = []
|
|
33
|
+
for entry in sans:
|
|
34
|
+
if isinstance(entry, x509.DNSName):
|
|
35
|
+
output.append(("DNS", entry.value))
|
|
36
|
+
elif isinstance(entry, x509.OtherName):
|
|
37
|
+
oid = entry.type_id.dotted_string
|
|
38
|
+
output.append((oid, entry.value))
|
|
39
|
+
return output
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_certificate_policy_oids(cert_path: Path) -> list[str]:
|
|
43
|
+
"""Load X.509 DER Certificate in PEM format and return Policy OIDs"""
|
|
44
|
+
|
|
45
|
+
cert = load_certificate(cert_path)
|
|
46
|
+
|
|
47
|
+
oids = []
|
|
48
|
+
cert_policies = cert.extensions.get_extension_for_oid(
|
|
49
|
+
x509.ExtensionOID.CERTIFICATE_POLICIES
|
|
50
|
+
).value
|
|
51
|
+
for policy in cert_policies:
|
|
52
|
+
oid = policy.policy_identifier.dotted_string
|
|
53
|
+
oids.append(oid)
|
|
54
|
+
return oids
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_cert_san_other_names_policy_oids(cert_path: Path) -> list[str]:
|
|
58
|
+
"""Load X.509 DER Certificate in PEM format and
|
|
59
|
+
return Subject Alternative Name Other Names Policy OIDs"""
|
|
60
|
+
|
|
61
|
+
cert = load_certificate(cert_path)
|
|
62
|
+
sans = cert.extensions.get_extension_for_oid(
|
|
63
|
+
x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
|
64
|
+
).value
|
|
65
|
+
other_names = []
|
|
66
|
+
for entry in sans:
|
|
67
|
+
if isinstance(entry, x509.OtherName):
|
|
68
|
+
oid = entry.type_id.dotted_string
|
|
69
|
+
other_names.append(oid)
|
|
70
|
+
return other_names
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_certificate(cert_path: Path) -> bool:
|
|
74
|
+
"""Load X.509 DER Certificate in PEM format and validate"""
|
|
75
|
+
|
|
76
|
+
cert = load_certificate(cert_path)
|
|
77
|
+
valid = True
|
|
78
|
+
|
|
79
|
+
# Check the validity period
|
|
80
|
+
current_time_utc = datetime.now(UTC)
|
|
81
|
+
if not cert.not_valid_before_utc <= current_time_utc:
|
|
82
|
+
msg = "Certificate is not valid yet. "
|
|
83
|
+
msg += "Not valid before {cert.not_valid_before_utc}"
|
|
84
|
+
log.error(msg)
|
|
85
|
+
valid = False
|
|
86
|
+
if not current_time_utc <= cert.not_valid_after_utc:
|
|
87
|
+
msg = "Certificate is no longer valid. "
|
|
88
|
+
msg += "Not valid after {cert.not_valid_after_utc}"
|
|
89
|
+
log.error(msg)
|
|
90
|
+
valid = False
|
|
91
|
+
if cert.not_valid_after_utc != INDEF_EXPIRY:
|
|
92
|
+
msg = f"Certificate expiry not {INDEF_EXPIRY} as per standard. "
|
|
93
|
+
msg += f"Expires {cert.not_valid_after_utc}"
|
|
94
|
+
log.warning(msg)
|
|
95
|
+
valid = False
|
|
96
|
+
|
|
97
|
+
# Verify the OIDs
|
|
98
|
+
oid_intro = "1.3.6.1.4.1"
|
|
99
|
+
oid_pen = "40732"
|
|
100
|
+
|
|
101
|
+
oid_into = f"{oid_intro}.{oid_pen}"
|
|
102
|
+
oid_device_start = f"{oid_intro}.{oid_pen}.1."
|
|
103
|
+
sep2_dev_type = False
|
|
104
|
+
|
|
105
|
+
oid_test_end = f".{oid_pen}.2.1"
|
|
106
|
+
test_oid = False
|
|
107
|
+
|
|
108
|
+
oids = get_certificate_policy_oids(cert_path)
|
|
109
|
+
for oid in oids:
|
|
110
|
+
if oid_test_end in oid:
|
|
111
|
+
test_oid = oid
|
|
112
|
+
elif oid_device_start in oid:
|
|
113
|
+
sep2_dev_type = True
|
|
114
|
+
elif oid_into in oid:
|
|
115
|
+
pass
|
|
116
|
+
else:
|
|
117
|
+
log.warning("Certificate has a non SEP2 policy specified: %s", oid)
|
|
118
|
+
|
|
119
|
+
if test_oid:
|
|
120
|
+
msg = f"Certificate is a test certificate as {test_oid} set."
|
|
121
|
+
log.warning(msg)
|
|
122
|
+
|
|
123
|
+
if not sep2_dev_type:
|
|
124
|
+
msg = "At least one SEP2 device type assignment policy must be specified. "
|
|
125
|
+
msg += f"That is an OID like {oid_device_start}X"
|
|
126
|
+
log.error(msg)
|
|
127
|
+
valid = False
|
|
128
|
+
|
|
129
|
+
# Verify the Other Name OID in the SAN
|
|
130
|
+
sep2_hw_mod_name = False
|
|
131
|
+
oid_hw_mod_name = "1.3.6.1.5.5.7.8.4"
|
|
132
|
+
san_oids = get_cert_san_other_names_policy_oids(cert_path)
|
|
133
|
+
for oid in san_oids:
|
|
134
|
+
if oid == oid_hw_mod_name:
|
|
135
|
+
sep2_hw_mod_name = True
|
|
136
|
+
|
|
137
|
+
if not sep2_hw_mod_name:
|
|
138
|
+
msg = f"SEP2 Hardware Module Name OID {oid_hw_mod_name} "
|
|
139
|
+
msg += "not found in SAN OtherName."
|
|
140
|
+
log.error(msg)
|
|
141
|
+
|
|
142
|
+
return valid
|
sep2cert/cli.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from .cert_create import (
|
|
7
|
+
generate_device_certificate,
|
|
8
|
+
generate_key,
|
|
9
|
+
generate_mica,
|
|
10
|
+
generate_serca,
|
|
11
|
+
)
|
|
12
|
+
from .cert_id import get_certificate_lfdi
|
|
13
|
+
from .cert_validate import validate_certificate
|
|
14
|
+
from .version import __version__
|
|
15
|
+
|
|
16
|
+
LOG_FORMAT = "%(asctime)s %(levelname)-8s %(message)s"
|
|
17
|
+
DEFAULT_DIR = Path("certs")
|
|
18
|
+
app = typer.Typer(no_args_is_help=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def version_callback(value: bool):
|
|
22
|
+
if value:
|
|
23
|
+
typer.echo(f"sep2cert version: {__version__}")
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.callback()
|
|
28
|
+
def callback(
|
|
29
|
+
version: bool = typer.Option(False, "--version", callback=version_callback),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""sep2cert
|
|
32
|
+
|
|
33
|
+
Functions for creating and validating IEEE 2030.5 (SEP2) certificates.
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def cert_lfdi(filepath: Path, verbose: bool = False) -> None:
|
|
40
|
+
log_level = "DEBUG" if verbose else "INFO"
|
|
41
|
+
logging.basicConfig(level=log_level, format=LOG_FORMAT)
|
|
42
|
+
|
|
43
|
+
if not filepath.exists():
|
|
44
|
+
msg = f"No such file {filepath}"
|
|
45
|
+
raise FileNotFoundError(msg)
|
|
46
|
+
|
|
47
|
+
lfdi = get_certificate_lfdi(filepath)
|
|
48
|
+
typer.echo(f"The LFDI is: {lfdi}")
|
|
49
|
+
|
|
50
|
+
validate_certificate(filepath)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def create_key(
|
|
55
|
+
key_file: Path | None = None,
|
|
56
|
+
hostname: str = "",
|
|
57
|
+
verbose: bool = False,
|
|
58
|
+
) -> None:
|
|
59
|
+
log_level = "DEBUG" if verbose else "INFO"
|
|
60
|
+
logging.basicConfig(level=log_level, format=LOG_FORMAT)
|
|
61
|
+
|
|
62
|
+
hostnames = None if not hostname else [hostname]
|
|
63
|
+
_key, _csr = generate_key(key_file, generate_csr=True, hostnames=hostnames)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def create_serca(
|
|
68
|
+
org_name: str = "Smart Energy",
|
|
69
|
+
verbose: bool = False,
|
|
70
|
+
output_dir: Path = DEFAULT_DIR,
|
|
71
|
+
) -> None:
|
|
72
|
+
log_level = "DEBUG" if verbose else "INFO"
|
|
73
|
+
logging.basicConfig(level=log_level, format=LOG_FORMAT)
|
|
74
|
+
|
|
75
|
+
output_dir.mkdir(exist_ok=True)
|
|
76
|
+
key_file = output_dir / "serca.key"
|
|
77
|
+
key, _csr = generate_key(key_file, generate_csr=False)
|
|
78
|
+
generate_serca(key, org_name=org_name)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command()
|
|
82
|
+
def create_mica(
|
|
83
|
+
ca_cert: Path,
|
|
84
|
+
ca_key: Path,
|
|
85
|
+
org_name: str = "Example Org",
|
|
86
|
+
verbose: bool = False,
|
|
87
|
+
output_dir: Path = DEFAULT_DIR,
|
|
88
|
+
) -> None:
|
|
89
|
+
log_level = "DEBUG" if verbose else "INFO"
|
|
90
|
+
logging.basicConfig(level=log_level, format=LOG_FORMAT)
|
|
91
|
+
|
|
92
|
+
output_dir.mkdir(exist_ok=True)
|
|
93
|
+
key_file = output_dir / "mica.key"
|
|
94
|
+
_key, csr = generate_key(key_file, generate_csr=True)
|
|
95
|
+
generate_mica(csr, ca_cert, ca_key, org_name=org_name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command()
|
|
99
|
+
def create_cert(
|
|
100
|
+
csr_path: Path,
|
|
101
|
+
ca_cert: Path,
|
|
102
|
+
ca_key: Path,
|
|
103
|
+
pen: int = 12345,
|
|
104
|
+
serno: str = "1",
|
|
105
|
+
verbose: bool = False,
|
|
106
|
+
) -> None:
|
|
107
|
+
log_level = "DEBUG" if verbose else "INFO"
|
|
108
|
+
logging.basicConfig(level=log_level, format=LOG_FORMAT)
|
|
109
|
+
|
|
110
|
+
cert_fp = generate_device_certificate(
|
|
111
|
+
csr_path, ca_cert, ca_key, pen=pen, hardware_serial_number=serno
|
|
112
|
+
)
|
|
113
|
+
lfdi = get_certificate_lfdi(cert_fp)
|
|
114
|
+
typer.echo(f"The LFDI is: {lfdi}")
|
sep2cert/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sep2cert
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Useful functions for working with IEEE 2030.5 (SEP2)
|
|
5
|
+
Author-email: Alex Guinman <alex@guinman.id.au>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: asn1==2.8.0
|
|
16
|
+
Requires-Dist: cryptography>=44.0.1
|
|
17
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
18
|
+
Requires-Dist: typer>=0.15.1
|
|
19
|
+
Requires-Dist: pytest>=2.7.3 ; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-cov>=6.0.0 ; extra == "test"
|
|
21
|
+
Project-URL: Source, https://github.com/aguinane/SEP2-Certs
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
|
|
24
|
+
# SEP2-Certs
|
|
25
|
+
|
|
26
|
+
Create and verify IEEE 2030.5 (SEP2) PKI certificates
|
|
27
|
+
|
|
28
|
+
## Certificate Inspection
|
|
29
|
+
|
|
30
|
+
Get the LFDI for a certificate. It will also do some validation checks.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
python -m sep2cert cert-lfdi certs/dev-ABC-cert.pem
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Certificate Creation
|
|
37
|
+
|
|
38
|
+
Note the below CLI commands are only intended for testing purposes.
|
|
39
|
+
For production certificates, additional functions and policies may be required.
|
|
40
|
+
|
|
41
|
+
### Signing Certificates
|
|
42
|
+
|
|
43
|
+
Create a SERCA, and a MICA.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
python -m sep2cert create-serca
|
|
47
|
+
python -m sep2cert create-mica certs/serca.pem certs/serca.key
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Device Certificates
|
|
51
|
+
|
|
52
|
+
To create a device certificate, first create a Key and Certificate Signing Request (CSR).
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
python -m sep2cert create-key --key-file certs/dev-ABC.key
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Once you have the CSR, build the cert by signing with the MICA.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
python -m sep2cert create-cert certs/dev-ABC.csr certs/mica.pem certs/mica.key --pen 12345 --serno ABC
|
|
62
|
+
```
|
|
63
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
sep2cert/__init__.py,sha256=KV6Ax6AqSvNkitZjVcc7IZRoMpCRHnMBICaMsFlfGN0,197
|
|
2
|
+
sep2cert/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
3
|
+
sep2cert/cert_create.py,sha256=ei4Plg5kXP5zr8q7_HorWe8eml0d2sul_5PB7RJh8ro,14363
|
|
4
|
+
sep2cert/cert_id.py,sha256=b0EttAN7U_hox51DfnMkbBI4d84pnoLGholNPb2WxGg,3408
|
|
5
|
+
sep2cert/cert_validate.py,sha256=r7nt8FOOBBxVY5EpIWFXPqdmJwhVaWgNgHFxQlNHUKo,4525
|
|
6
|
+
sep2cert/cli.py,sha256=2bmR5FqnvAdQU5tgbyAkCRpdwakSN8bkGzSdRjRE83w,2964
|
|
7
|
+
sep2cert/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
8
|
+
sep2cert-0.1.0.dist-info/entry_points.txt,sha256=iX-btyincvnZ1Z34LMZ2_DWH137e8opD79oVS1SP0_k,45
|
|
9
|
+
sep2cert-0.1.0.dist-info/licenses/LICENSE,sha256=S_wPB_kx6_LJeJwqjagIQWYn16cilgx2KNPODekCKeE,1069
|
|
10
|
+
sep2cert-0.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
11
|
+
sep2cert-0.1.0.dist-info/METADATA,sha256=5VfBMXv5AKW91RWLbhUYYWQCIB4Aizy_NBSLL-rpyBY,1789
|
|
12
|
+
sep2cert-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Guinman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|