diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/ca/ca/__main__.py | 207 |
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 | |||
| 32 | import json | 32 | import json |
| 33 | from leapseconddata import LeapSecondData | 33 | from leapseconddata import LeapSecondData |
| 34 | from collections.abc import Iterable | 34 | from collections.abc import Iterable |
| 35 | import ipaddress | ||
| 35 | 36 | ||
| 36 | 37 | ||
| 37 | class KeyType(Enum): | 38 | class 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 | ||
| 214 | def tai64nint(dt): | 219 | def 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 | ||
| 270 | def 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 | |||
| 265 | def initca(ca_cert, ca_key, key_type, subject, clock_skew, validity, sops): | 308 | def 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() |
