summaryrefslogtreecommitdiff
path: root/tools/ca
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2023-01-18 08:33:46 +0100
committerGregor Kleen <gkleen@yggdrasil.li>2023-01-18 08:33:46 +0100
commit6b0fd8d0197eccd18a6e079e5d9c58d40002ba44 (patch)
treef30b82828b2ca7bd79fd517ac70391440741b141 /tools/ca
parentbd30654df189b3b4693ea1bb81b23d1338846ad3 (diff)
downloadnixos-6b0fd8d0197eccd18a6e079e5d9c58d40002ba44.tar
nixos-6b0fd8d0197eccd18a6e079e5d9c58d40002ba44.tar.gz
nixos-6b0fd8d0197eccd18a6e079e5d9c58d40002ba44.tar.bz2
nixos-6b0fd8d0197eccd18a6e079e5d9c58d40002ba44.tar.xz
nixos-6b0fd8d0197eccd18a6e079e5d9c58d40002ba44.zip
...
Diffstat (limited to 'tools/ca')
-rw-r--r--tools/ca/ca/__main__.py207
1 files changed, 125 insertions, 82 deletions
diff --git a/tools/ca/ca/__main__.py b/tools/ca/ca/__main__.py
index 6615da55..bfaee63a 100644
--- a/tools/ca/ca/__main__.py
+++ b/tools/ca/ca/__main__.py
@@ -32,6 +32,7 @@ import subprocess
32import json 32import json
33from leapseconddata import LeapSecondData 33from leapseconddata import LeapSecondData
34from collections.abc import Iterable 34from collections.abc import Iterable
35import ipaddress
35 36
36 37
37class KeyType(Enum): 38class KeyType(Enum):
@@ -208,8 +209,12 @@ def mv_bak(path):
208 break 209 break
209 bak_path = path.parent / f'{path.name}.bak{n}' 210 bak_path = path.parent / f'{path.name}.bak{n}'
210 211
211 logger.warn('Renaming ‘%s’ to ‘%s’...', path, bak_path) 212 try:
212 path.rename(bak_path) 213 path.rename(bak_path)
214 except FileNotFoundError:
215 pass
216 else:
217 logger.warn('Renamed ‘%s’ to ‘%s’...', path, bak_path)
213 218
214def tai64nint(dt): 219def tai64nint(dt):
215 global leapsecond_data 220 global leapsecond_data
@@ -262,6 +267,44 @@ def write_genkey(key_type, sops, keyfile):
262 267
263 return key 268 return key
264 269
270def to_dn(alternative_names):
271 def go(alternative_name):
272 dn = None
273 try:
274 dn = x509.Name.from_rfc4514_string(alternative_name)
275 except ValueError:
276 pass
277
278 if dn:
279 logger.info('‘%s’ interpreted as directory name: %s', alternative_name, dn)
280 return x509.DirectoryName(dn)
281
282 addr = None
283 try:
284 addr = ipaddress.IPv4Network(alternative_name)
285 except (ipaddress.AddressValueError, ipaddress.NetmaskValueError, ValueError):
286 pass
287 try:
288 addr = ipaddress.IPv4Address(alternative_name)
289 except ipaddress.AddressValueError:
290 pass
291 try:
292 addr = ipaddress.IPv6Network(alternative_name)
293 except (ipaddress.AddressValueError, ipaddress.NetmaskValueError, ValueError):
294 pass
295 try:
296 addr = ipaddress.IPv6Address(alternative_name)
297 except ipaddress.AddressValueError:
298 pass
299
300 if addr:
301 logger.info('‘%s’ interpreted as ip address/subnet: %s', alternative_name, addr)
302 return x509.IPAddress(addr)
303
304 return x509.DNSName(alternative_name)
305
306 return map(go, alternative_names)
307
265def initca(ca_cert, ca_key, key_type, subject, clock_skew, validity, sops): 308def initca(ca_cert, ca_key, key_type, subject, clock_skew, validity, sops):
266 global logger 309 global logger
267 310
@@ -273,14 +316,8 @@ def initca(ca_cert, ca_key, key_type, subject, clock_skew, validity, sops):
273 if not key_type.aligned(key): 316 if not key_type.aligned(key):
274 logger.warn('Private key ‘%s’ does not align with requested type %s', ca_key, key_type) 317 logger.warn('Private key ‘%s’ does not align with requested type %s', ca_key, key_type)
275 318
276 try: 319 mv_bak(ca_key)
277 mv_bak(ca_key) 320 mv_bak(ca_cert)
278 except FileNotFoundError:
279 pass
280 try:
281 mv_bak(ca_cert)
282 except FileNotFoundError:
283 pass
284 321
285 raise FileNotFoundError(f'Key does not align with requested type: {ca_key}') 322 raise FileNotFoundError(f'Key does not align with requested type: {ca_key}')
286 except FileNotFoundError: 323 except FileNotFoundError:
@@ -295,9 +332,14 @@ def initca(ca_cert, ca_key, key_type, subject, clock_skew, validity, sops):
295 logger.debug('Generating new certificate...') 332 logger.debug('Generating new certificate...')
296 333
297 now = datetime.utcnow() 334 now = datetime.utcnow()
298 name = x509.Name([ 335 name = None
299 x509.NameAttribute(NameOID.COMMON_NAME, subject.relative) 336 try:
300 ]) 337 name = x509.Name.from_rfc4514_string(subject)
338 logger.info('‘%s’ interpreted as directory name: %s', subject, name)
339 except ValueError:
340 name = x509.Name([
341 x509.NameAttribute(NameOID.COMMON_NAME, subject)
342 ])
301 343
302 cert = x509.CertificateBuilder().subject_name( 344 cert = x509.CertificateBuilder().subject_name(
303 name 345 name
@@ -342,24 +384,28 @@ def signcsr(ca_cert, ca_key, clock_skew, validity, subject, alternative_name, ke
342 csr_bytes = csr 384 csr_bytes = csr
343 385
344 csr = x509.load_pem_x509_csr(csr_bytes) 386 csr = x509.load_pem_x509_csr(csr_bytes)
387 name = None
345 if not subject: 388 if not subject:
346 common_name_attrs = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) 389 name = csr.subject
347 if len(common_name_attrs) != 1: 390 else:
348 raise InvalidParamsError('Invalid name structure in CSR') 391 try:
349 subject = common_name_attrs[0].value.lower() 392 name = x509.Name.from_rfc4514_string(subject)
350 logger.warn('Using subject common name from csr: %s', subject) 393 logger.info('‘%s’ interpreted as directory name: %s', subject, name)
351 name = x509.Name([ 394 except ValueError:
352 x509.NameAttribute(NameOID.COMMON_NAME, subject) 395 name = x509.Name([
353 ]) 396 x509.NameAttribute(NameOID.COMMON_NAME, subject)
397 ])
354 398
355 if not ignore_alternative_names: 399 if not ignore_alternative_names:
356 try: 400 try:
357 ext = csr.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME) 401 ext = csr.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
358 csr_alt_names = ext.value.get_values_for_type(x509.DNSName) 402 csr_alt_names = set(ext.value)
359 logger.warn('Using alternative names from csr: %s', csr_alt_names) 403 logger.warn('Using alternative names from csr: %s', csr_alt_names)
360 alternative_name = list(set(alternative_name) | set(csr_alt_names)) 404 alternative_name = set(to_dn(alternative_name)) | csr_alt_names
361 except ExtensionNotFound: 405 except ExtensionNotFound:
362 pass 406 pass
407 else:
408 alternative_name = to_dn(alternative_name)
363 409
364 ca_key = load_key(ca_key) 410 ca_key = load_key(ca_key)
365 with open(ca_cert, 'rb') as fh: 411 with open(ca_cert, 'rb') as fh:
@@ -397,9 +443,7 @@ def signcsr(ca_cert, ca_key, clock_skew, validity, subject, alternative_name, ke
397 443
398 if alternative_name: 444 if alternative_name:
399 cert = cert.add_extension( 445 cert = cert.add_extension(
400 x509.SubjectAlternativeName( 446 x509.SubjectAlternativeName(alternative_name),
401 list(map(x509.DNSName, alternative_name))
402 ),
403 False 447 False
404 ) 448 )
405 449
@@ -407,10 +451,7 @@ def signcsr(ca_cert, ca_key, clock_skew, validity, subject, alternative_name, ke
407 451
408 output = output.with_suffix('.crt') 452 output = output.with_suffix('.crt')
409 453
410 try: 454 mv_bak(output)
411 mv_bak(output)
412 except FileNotFoundError:
413 pass
414 with umask(0o0133), atomic_write(output, overwrite=False, mode='wb') as cf: 455 with umask(0o0133), atomic_write(output, overwrite=False, mode='wb') as cf:
415 logger.info('Writing new certificate to ‘%s’...', output) 456 logger.info('Writing new certificate to ‘%s’...', output)
416 cf.write(cert.public_bytes(serialization.Encoding.PEM)) 457 cf.write(cert.public_bytes(serialization.Encoding.PEM))
@@ -429,26 +470,28 @@ def new_client(ca_cert, ca_key, key_type, clock_skew, validity, subject, alterna
429 if not key_type.aligned(key): 470 if not key_type.aligned(key):
430 logger.warn('Private key ‘%s’ does not align with requested type %s', key_file, key_type) 471 logger.warn('Private key ‘%s’ does not align with requested type %s', key_file, key_type)
431 472
432 try: 473 mv_bak(key_file)
433 mv_bak(key_file) 474 mv_bak(cert_file)
434 except FileNotFoundError:
435 pass
436 try:
437 mv_bak(cert_file)
438 except FileNotFoundError:
439 pass
440 475
441 raise FileNotFoundError(f'Key does not align with requested type: {key_file}') 476 raise FileNotFoundError(f'Key does not align with requested type: {key_file}')
442 except FileNotFoundError: 477 except FileNotFoundError:
443 key = write_genkey(key_type, sops, key_file) 478 key = write_genkey(key_type, sops, key_file)
444 479
445 csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ 480 name = None
446 x509.NameAttribute(NameOID.COMMON_NAME, subject) 481 try:
447 ])) 482 name = x509.Name.from_rfc4514_string(subject)
483 logger.info('‘%s’ interpreted as directory name: %s', subject, name)
484 except ValueError:
485 name = x509.Name([
486 x509.NameAttribute(NameOID.COMMON_NAME, subject)
487 ])
488
489 csr = x509.CertificateSigningRequestBuilder().subject_name(name)
490
448 if alternative_name: 491 if alternative_name:
449 csr = csr.add_extension( 492 csr = csr.add_extension(
450 x509.SubjectAlternativeName( 493 x509.SubjectAlternativeName(
451 list(map(x509.DNSName, alternative_name)) 494 to_dn(alternative_name)
452 ), 495 ),
453 False 496 False
454 ) 497 )
@@ -553,54 +596,54 @@ def main():
553 596
554 597
555 parser = argparse.ArgumentParser(prog='ca', formatter_class=argparse.ArgumentDefaultsHelpFormatter) 598 parser = argparse.ArgumentParser(prog='ca', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
556 parser.add_argument('--verbosity', dest='log_level', action='append', type=int) 599 parser.add_argument('--verbosity', dest='log_level', action='append', type=int, help='Numeric verbosity')
557 parser.add_argument('--verbose', '-v', dest='log_level', action='append_const', const=1) 600 parser.add_argument('--verbose', '-v', dest='log_level', action='append_const', const=1, help='Increase verbosity')
558 parser.add_argument('--quiet', '-q', dest='log_level', action='append_const', const=-1) 601 parser.add_argument('--quiet', '-q', dest='log_level', action='append_const', const=-1, help='Decrease verbosity')
559 subparsers = parser.add_subparsers(help='Subcommands', required=True) 602 subparsers = parser.add_subparsers(help='Subcommands', required=True)
560 603
561 subparser = subparsers.add_parser('init', aliases=['initca', 'init-ca', 'ca'], formatter_class=argparse.ArgumentDefaultsHelpFormatter) 604 subparser = subparsers.add_parser('init', aliases=['initca', 'init-ca', 'ca'], formatter_class=argparse.ArgumentDefaultsHelpFormatter, description="Generate a new selfsigned CA certificate and associated private key\n\nPrivate key is only generated if it does not yet exist")
562 subparser.add_argument('--ca-cert', type=Path, default=Path('ca.crt')) 605 subparser.add_argument('--ca-cert', type=Path, default=Path('ca.crt'), help='Path to file containing CA certificate')
563 subparser.add_argument('--ca-key', type=Path, default=Path('ca.key')) 606 subparser.add_argument('--ca-key', type=Path, default=Path('ca.key'), help='Path to file containing CA private key')
564 subparser.add_argument('--key-type', type=KeyType.from_string, choices=list(KeyType), default=KeyType.ED448.value) 607 subparser.add_argument('--key-type', type=KeyType.from_string, choices=list(KeyType), default=KeyType.ED448.value, help='Type of private key to generate')
565 subparser.add_argument('--clock-skew', metavar='DURATION', type=duration, default=timedelta(minutes=5)) 608 subparser.add_argument('--clock-skew', metavar='DURATION', type=duration, default=timedelta(minutes=5), help='How far to shift begin of validity into the past')
566 subparser.add_argument('--validity', metavar='DURATION', type=duration, default=timedelta(days=ceil(365.2425*10))) 609 subparser.add_argument('--validity', metavar='DURATION', type=duration, default=timedelta(days=ceil(365.2425*10)), help='How far to shift end of validity into the future')
567 subparser.add_argument('--sops', '--no-sops', action=BooleanAction, default=True) 610 subparser.add_argument('--sops', '--no-sops', action=BooleanAction, default=True, help='Encrypt private key using SOPS')
568 subparser.add_argument('--subject', metavar='FQDN', type=ValidFQDN, required=True) 611 subparser.add_argument('--subject', metavar='DN', type=str, required=True, help='Subject name')
569 subparser.set_defaults(cmd=initca) 612 subparser.set_defaults(cmd=initca)
570 613
571 subparser = subparsers.add_parser('sign', aliases=['signcsr', 'sign-csr'], formatter_class=argparse.ArgumentDefaultsHelpFormatter) 614 subparser = subparsers.add_parser('sign', aliases=['signcsr', 'sign-csr'], formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sign an existing CSR')
572 subparser.add_argument('--ca-cert', type=Path, default=Path('ca.crt')) 615 subparser.add_argument('--ca-cert', type=Path, default=Path('ca.crt'), help='Path to file containing CA certificate')
573 subparser.add_argument('--ca-key', type=Path, default=Path('ca.key')) 616 subparser.add_argument('--ca-key', type=Path, default=Path('ca.key'), help='Path to file containing CA private key')
574 subparser.add_argument('--clock-skew', metavar='DURATION', type=duration, default=timedelta(minutes=5)) 617 subparser.add_argument('--clock-skew', metavar='DURATION', type=duration, default=timedelta(minutes=5), help='How far to shift begin of validity into the past')
575 subparser.add_argument('--validity', metavar='DURATION', type=duration, default=timedelta(days=ceil(365.2425*10))) 618 subparser.add_argument('--validity', metavar='DURATION', type=duration, default=timedelta(days=ceil(365.2425*10)), help='How far to shift end of validity into the future')
576 subparser.add_argument('--subject', metavar='CN', type=str, required=False) 619 subparser.add_argument('--subject', metavar='DN', type=str, required=False, help='Override subject name')
577 subparser.add_argument('--ignore-alternative-names', '--no-ignore-alternative-names', action=BooleanAction, default=True) 620 subparser.add_argument('--ignore-alternative-names', '--no-ignore-alternative-names', action=BooleanAction, default=True, help='Ignore subject alternative names provided in CSR')
578 subparser.add_argument('--key-usage', metavar='KEY_USAGE', type=SupportedKeyUsage, action=ExtendAction, default=[SupportedKeyUsage.CLIENT_AUTH]) 621 subparser.add_argument('--key-usage', metavar='KEY_USAGE', type=SupportedKeyUsage, action=ExtendAction, default=[SupportedKeyUsage.CLIENT_AUTH], help='Allowed key usages')
579 subparser.add_argument('--alternative-name', metavar='CN', type=str, action='append') 622 subparser.add_argument('--alternative-name', metavar='CN', type=str, action='append', help='Subject alternative names')
580 subparser.add_argument('--output', type=Path, required=True) 623 subparser.add_argument('--output', type=Path, required=True, help='Output path')
581 subparser.add_argument('csr', metavar='FILE', type=argparse.FileType(mode='rb')) 624 subparser.add_argument('csr', metavar='FILE', type=argparse.FileType(mode='rb'), help='Path to file containing CSR')
582 subparser.set_defaults(cmd=signcsr) 625 subparser.set_defaults(cmd=signcsr)
583 626
584 subparser = subparsers.add_parser('new-client', aliases=['new', 'new-client', 'client'], formatter_class=argparse.ArgumentDefaultsHelpFormatter) 627 subparser = subparsers.add_parser('new-client', aliases=['new', 'new-client', 'client'], formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Generate a new CSR and sign it immediately')
585 subparser.add_argument('--ca-cert', type=Path, default=Path('ca.crt')) 628 subparser.add_argument('--ca-cert', type=Path, default=Path('ca.crt'), help='Path to file containing CA certificate')
586 subparser.add_argument('--ca-key', type=Path, default=Path('ca.key')) 629 subparser.add_argument('--ca-key', type=Path, default=Path('ca.key'), help='Path to file containing CA private key')
587 subparser.add_argument('--key-type', type=KeyType.from_string, choices=list(KeyType), default=KeyType.ED25519.value) 630 subparser.add_argument('--key-type', type=KeyType.from_string, choices=list(KeyType), default=KeyType.ED25519.value, help='Type of private key to generate')
588 subparser.add_argument('--clock-skew', metavar='DURATION', type=duration, default=timedelta(minutes=5)) 631 subparser.add_argument('--clock-skew', metavar='DURATION', type=duration, default=timedelta(minutes=5), help='How far to shift begin of validity into the past')
589 subparser.add_argument('--validity', metavar='DURATION', type=duration, default=timedelta(days=ceil(365.2425*10))) 632 subparser.add_argument('--validity', metavar='DURATION', type=duration, default=timedelta(days=ceil(365.2425*10)), help='How far to shift end of validity into the future')
590 subparser.add_argument('--sops', '--no-sops', action=BooleanAction, default=True) 633 subparser.add_argument('--sops', '--no-sops', action=BooleanAction, default=True, help='Encrypt private key using SOPS')
591 subparser.add_argument('--subject', metavar='CN', type=str, required=True) 634 subparser.add_argument('--subject', metavar='DN', type=str, required=True, help='Subject name')
592 subparser.add_argument('--key-usage', metavar='KEY_USAGE', type=SupportedKeyUsage, action=ExtendAction, default=[SupportedKeyUsage.CLIENT_AUTH]) 635 subparser.add_argument('--key-usage', metavar='KEY_USAGE', type=SupportedKeyUsage, action=ExtendAction, default=[SupportedKeyUsage.CLIENT_AUTH], help='Allowed key usages')
593 subparser.add_argument('--alternative-name', metavar='CN', type=str, action='append') 636 subparser.add_argument('--alternative-name', metavar='CN', type=str, action='append', help='Subject alternative names')
594 subparser.add_argument('--output', type=Path, required=True) 637 subparser.add_argument('--output', type=Path, required=True, help='Output path')
595 subparser.set_defaults(cmd=new_client) 638 subparser.set_defaults(cmd=new_client)
596 639
597 subparser = subparsers.add_parser('pkcs12', aliases=['p12', 'pfx'], formatter_class=argparse.ArgumentDefaultsHelpFormatter) 640 subparser = subparsers.add_parser('pkcs12', aliases=['p12', 'pfx'], formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Convert existing certificate and private key to PKCS#12 format')
598 subparser.add_argument('--random-password', '--no-random-password', action=BooleanAction, default=True) 641 subparser.add_argument('--random-password', '--no-random-password', action=BooleanAction, default=True, help='Encrypt PKCS#12 file with random passphrase -- otherwise prompt for one')
599 subparser.add_argument('--random-password-length', type=int, default=12) 642 subparser.add_argument('--random-password-length', type=int, default=12, help='Number of words in random passphrase')
600 subparser.add_argument('--weak-encryption', '--no-weak-encryption', action=BooleanAction, default=False) 643 subparser.add_argument('--weak-encryption', '--no-weak-encryption', action=BooleanAction, default=False, help='Use weak, but more compatible, encryption')
601 subparser.add_argument('--temporary-output', '--no-temporary-output', action=BooleanAction, default=True) 644 subparser.add_argument('--temporary-output', '--no-temporary-output', action=BooleanAction, default=True, help='If output path is not given, generate output file in temporary directory')
602 subparser.add_argument('--output', type=Path) 645 subparser.add_argument('--output', type=Path, help='Output path')
603 subparser.add_argument('filename', metavar='BASENAME', type=Path) 646 subparser.add_argument('filename', metavar='BASENAME', type=Path, help='Input path')
604 subparser.set_defaults(cmd=to_pkcs12) 647 subparser.set_defaults(cmd=to_pkcs12)
605 648
606 args = parser.parse_args() 649 args = parser.parse_args()