xref: /openbmc/bmcweb/scripts/generate_auth_certificates.py (revision 34b7ca4b6fc7f6d98842ca86e67c6c06a524f8fa)
1#!/usr/bin/env python3
2"""
3Script to generate certificates for a CA, server, and client allowing for
4client authentication using mTLS certificates. This can then be used to test
5mTLS client authentication for Redfish and WebUI.
6"""
7
8import argparse
9import datetime
10import errno
11import ipaddress
12import os
13import socket
14import time
15from typing import Optional
16
17import asn1
18import httpx
19from cryptography import x509
20from cryptography.hazmat.primitives import hashes, serialization
21from cryptography.hazmat.primitives.asymmetric import ec
22from cryptography.hazmat.primitives.serialization import (
23    load_pem_private_key,
24    pkcs12,
25)
26from cryptography.x509.oid import NameOID
27
28replaceCertPath = "/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate"
29# https://oidref.com/1.3.6.1.4.1.311.20.2.3
30upnObjectIdentifier = "1.3.6.1.4.1.311.20.2.3"
31
32
33class RedfishSessionContext:
34    def __init__(self, client, username="root", password="0penBmc"):
35        self.client = client
36        self.session_uri = None
37        self.x_auth_token = None
38        self.username = username
39        self.password = password
40
41    def __enter__(self):
42        r = self.client.post(
43            "/redfish/v1/SessionService/Sessions",
44            json={
45                "UserName": self.username,
46                "Password": self.password,
47                "Context": f"pythonscript::{os.path.basename(__file__)}",
48            },
49            headers={"content-type": "application/json"},
50        )
51        r.raise_for_status()
52        self.x_auth_token = r.headers["x-auth-token"]
53        self.session_uri = r.headers["location"]
54        return self
55
56    def __exit__(self, type, value, traceback):
57        if not self.session_uri:
58            return
59        r = self.client.delete(self.session_uri)
60        r.raise_for_status()
61
62
63def get_hostname(redfish_session, username, password, url):
64    service_root = redfish_session.get("/redfish/v1/")
65    service_root.raise_for_status()
66
67    manager_uri = service_root.json()["Links"]["ManagerProvidingService"][
68        "@odata.id"
69    ]
70
71    manager_response = redfish_session.get(manager_uri)
72    manager_response.raise_for_status()
73
74    network_protocol_uri = manager_response.json()["NetworkProtocol"][
75        "@odata.id"
76    ]
77
78    network_protocol_response = redfish_session.get(network_protocol_uri)
79    network_protocol_response.raise_for_status()
80
81    hostname = network_protocol_response.json()["HostName"]
82
83    return hostname
84
85
86def generateCA():
87    private_key = ec.generate_private_key(ec.SECP256R1())
88    public_key = private_key.public_key()
89    builder = x509.CertificateBuilder()
90
91    name = x509.Name(
92        [
93            x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
94            x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
95            x509.NameAttribute(NameOID.COMMON_NAME, "Test CA"),
96        ]
97    )
98    builder = builder.subject_name(name)
99    builder = builder.issuer_name(name)
100
101    builder = builder.not_valid_before(
102        datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
103    )
104    builder = builder.not_valid_after(
105        datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
106    )
107    builder = builder.serial_number(x509.random_serial_number())
108    builder = builder.public_key(public_key)
109
110    basic_constraints = x509.BasicConstraints(ca=True, path_length=None)
111    builder = builder.add_extension(basic_constraints, critical=True)
112
113    usage = x509.KeyUsage(
114        content_commitment=False,
115        crl_sign=True,
116        data_encipherment=False,
117        decipher_only=False,
118        digital_signature=False,
119        encipher_only=False,
120        key_agreement=False,
121        key_cert_sign=True,
122        key_encipherment=False,
123    )
124    builder = builder.add_extension(usage, critical=False)
125
126    auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
127
128    builder = builder.add_extension(auth_key, critical=False)
129
130    root_cert = builder.sign(
131        private_key=private_key, algorithm=hashes.SHA256()
132    )
133
134    return private_key, root_cert
135
136
137def signCsr(csr, ca_key):
138    csr.sign(ca_key, algorithm=hashes.SHA256())
139    return
140
141
142def generate_client_key_and_cert(
143    ca_cert,
144    ca_key,
145    common_name: Optional[str] = None,
146    upn: Optional[str] = None,
147):
148    private_key = ec.generate_private_key(ec.SECP256R1())
149    public_key = private_key.public_key()
150    builder = x509.CertificateBuilder()
151
152    cert_names = [
153        x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
154        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
155        x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
156        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
157        x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
158    ]
159    if common_name is not None:
160        cert_names.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name))
161
162    builder = builder.subject_name(x509.Name(cert_names))
163
164    builder = builder.issuer_name(ca_cert.subject)
165    builder = builder.public_key(public_key)
166    builder = builder.serial_number(x509.random_serial_number())
167    builder = builder.not_valid_before(
168        datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
169    )
170    builder = builder.not_valid_after(
171        datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
172    )
173
174    usage = x509.KeyUsage(
175        content_commitment=False,
176        crl_sign=False,
177        data_encipherment=False,
178        decipher_only=False,
179        digital_signature=True,
180        encipher_only=False,
181        key_agreement=True,
182        key_cert_sign=False,
183        key_encipherment=False,
184    )
185    builder = builder.add_extension(usage, critical=False)
186
187    exusage = x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH])
188    builder = builder.add_extension(exusage, critical=True)
189
190    auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
191    builder = builder.add_extension(auth_key, critical=False)
192
193    if upn is not None:
194        encoder = asn1.Encoder()
195        encoder.start()
196        encoder.write(upn, asn1.Numbers.UTF8String)
197
198        builder = builder.add_extension(
199            x509.SubjectAlternativeName(
200                [
201                    x509.OtherName(
202                        x509.ObjectIdentifier(upnObjectIdentifier),
203                        encoder.output(),
204                    )
205                ]
206            ),
207            critical=False,
208        )
209
210    signed = builder.sign(private_key=ca_key, algorithm=hashes.SHA256())
211
212    return private_key, signed
213
214
215def generateServerCert(url, ca_key, ca_cert, csr):
216    builder = x509.CertificateBuilder()
217
218    builder = builder.subject_name(csr.subject)
219    builder = builder.issuer_name(ca_cert.subject)
220    builder = builder.public_key(csr.public_key())
221    builder = builder.serial_number(x509.random_serial_number())
222    builder = builder.not_valid_before(
223        datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
224    )
225    builder = builder.not_valid_after(
226        datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
227    )
228
229    usage = x509.KeyUsage(
230        content_commitment=False,
231        crl_sign=False,
232        data_encipherment=False,
233        decipher_only=False,
234        digital_signature=True,
235        encipher_only=False,
236        key_agreement=False,
237        key_cert_sign=True,
238        key_encipherment=True,
239    )
240    builder = builder.add_extension(usage, critical=True)
241
242    exusage = x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH])
243    builder = builder.add_extension(exusage, critical=True)
244
245    san_list = [x509.DNSName("localhost")]
246    try:
247        value = ipaddress.ip_address(url)
248        san_list.append(x509.IPAddress(value))
249    except ValueError:
250        san_list.append(x509.DNSName(url))
251
252    altname = x509.SubjectAlternativeName(san_list)
253    builder = builder.add_extension(altname, critical=True)
254    basic_constraints = x509.BasicConstraints(ca=False, path_length=None)
255    builder = builder.add_extension(basic_constraints, critical=True)
256
257    builder = builder.add_extension(
258        x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()),
259        critical=False,
260    )
261    authkeyident = x509.AuthorityKeyIdentifier.from_issuer_public_key(
262        ca_key.public_key()
263    )
264    builder = builder.add_extension(authkeyident, critical=False)
265
266    signed = builder.sign(private_key=ca_key, algorithm=hashes.SHA256())
267
268    return signed
269
270
271def generateCsr(
272    redfish_session,
273    commonName,
274    manager_uri,
275):
276    try:
277        socket.inet_aton(commonName)
278        commonName = "IP: " + commonName
279    except socket.error:
280        commonName = "DNS: " + commonName
281
282    CSRRequest = {
283        "CommonName": commonName,
284        "City": "San Fransisco",
285        "Country": "US",
286        "Organization": "",
287        "OrganizationalUnit": "",
288        "State": "CA",
289        "CertificateCollection": {
290            "@odata.id": f"{manager_uri}/NetworkProtocol/HTTPS/Certificates",
291        },
292        "AlternativeNames": [
293            commonName,
294            "DNS: localhost",
295            "IP: 127.0.0.1",
296        ],
297    }
298
299    response = redfish_session.post(
300        "/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR",
301        json=CSRRequest,
302    )
303    response.raise_for_status()
304
305    csrString = response.json()["CSRString"]
306    csr = x509.load_pem_x509_csr(csrString.encode())
307    if not csr.is_signature_valid:
308        raise Exception("CSR was not valid")
309    return csr
310
311
312def install_ca_cert(redfish_session, ca_cert_dump, manager_uri):
313    ca_certJSON = {
314        "CertificateString": ca_cert_dump.decode(),
315        "CertificateType": "PEM",
316    }
317    ca_certPath = f"{manager_uri}/Truststore/Certificates"
318    print("Attempting to install CA certificate to BMC.")
319
320    response = redfish_session.post(ca_certPath, json=ca_certJSON)
321    if response.status_code == 500:
322        print(
323            "An existing CA certificate is likely already installed."
324            " Replacing..."
325        )
326        ca_certJSON["CertificateUri"] = {
327            "@odata.id": ca_certPath + "/1",
328        }
329
330        response = redfish_session.post(replaceCertPath, json=ca_certJSON)
331        if response.status_code == 200:
332            print("Successfully replaced existing CA certificate.")
333        else:
334            raise Exception(
335                "Could not install or replace CA certificate."
336                "Please check if a certificate is already installed. If a"
337                "certificate is already installed, try performing a factory"
338                "restore to clear such settings."
339            )
340    response.raise_for_status()
341    print("Successfully installed CA certificate.")
342
343
344def install_server_cert(redfish_session, manager_uri, server_cert_dump):
345
346    server_cert_json = {
347        "CertificateString": server_cert_dump.decode(),
348        "CertificateUri": {
349            "@odata.id": f"{manager_uri}/NetworkProtocol/HTTPS/Certificates/1",
350        },
351        "CertificateType": "PEM",
352    }
353
354    print("Replacing server certificate...")
355    response = redfish_session.post(replaceCertPath, json=server_cert_json)
356    if response.status_code == 200:
357        print("Successfully replaced server certificate.")
358    else:
359        raise Exception(f"Could not replace certificate: {response.json()}")
360
361    tls_patch_json = {"Oem": {"OpenBMC": {"AuthMethods": {"TLS": True}}}}
362    print("Ensuring TLS authentication is enabled.")
363    response = redfish_session.patch(
364        "/redfish/v1/AccountService", json=tls_patch_json
365    )
366    if response.status_code == 200:
367        print("Successfully enabled TLS authentication.")
368    else:
369        raise Exception("Could not enable TLS auth: " + response.read)
370
371
372def generate_pk12(certs_dir, key, client_cert, username):
373    print("Generating p12 cert file for browser authentication.")
374    p12 = pkcs12.serialize_key_and_certificates(
375        username.encode(),
376        key,
377        client_cert,
378        None,
379        serialization.NoEncryption(),
380    )
381    with open(os.path.join(certs_dir, "client.p12"), "wb") as f:
382        f.write(p12)
383
384
385def test_mtls_auth(url, certs_dir):
386    with httpx.Client(
387        base_url=f"https://{url}",
388        verify=os.path.join(certs_dir, "CA-cert.cer"),
389        cert=(
390            os.path.join(certs_dir, "client-cert.pem"),
391            os.path.join(certs_dir, "client-key.pem"),
392        ),
393    ) as client:
394        print("Testing mTLS auth with CommonName")
395        response = client.get(
396            "/redfish/v1/SessionService/Sessions",
397        )
398        response.raise_for_status()
399
400        print("Changing CertificateMappingAttribute to use UPN")
401        patch_json = {
402            "MultiFactorAuth": {
403                "ClientCertificate": {
404                    "CertificateMappingAttribute": "UserPrincipalName"
405                }
406            }
407        }
408        response = client.patch(
409            "/redfish/v1/AccountService",
410            json=patch_json,
411        )
412        response.raise_for_status()
413
414    with httpx.Client(
415        base_url=f"https://{url}",
416        verify=os.path.join(certs_dir, "CA-cert.cer"),
417        cert=(
418            os.path.join(certs_dir, "upn-client-cert.pem"),
419            os.path.join(certs_dir, "upn-client-key.pem"),
420        ),
421    ) as client:
422        print("Retesting mTLS auth with UPN")
423        response = client.get(
424            "/redfish/v1/SessionService/Sessions",
425        )
426        response.raise_for_status()
427
428        print("Changing CertificateMappingAttribute to use CommonName")
429        patch_json = {
430            "MultiFactorAuth": {
431                "ClientCertificate": {
432                    "CertificateMappingAttribute": "CommonName"
433                }
434            }
435        }
436        response = client.patch(
437            "/redfish/v1/AccountService",
438            json=patch_json,
439        )
440        response.raise_for_status()
441
442
443def setup_server_cert(
444    redfish_session,
445    ca_cert_dump,
446    certs_dir,
447    client_key,
448    client_cert,
449    username,
450    url,
451    server_intermediate_key,
452    server_intermediate_cert,
453):
454    service_root = redfish_session.get("/redfish/v1/")
455    service_root.raise_for_status()
456
457    manager_uri = service_root.json()["Links"]["ManagerProvidingService"][
458        "@odata.id"
459    ]
460
461    install_ca_cert(redfish_session, ca_cert_dump, manager_uri)
462    generate_pk12(certs_dir, client_key, client_cert, username)
463
464    csr = generateCsr(
465        redfish_session,
466        url,
467        manager_uri,
468    )
469    serverCert = generateServerCert(
470        url,
471        server_intermediate_key,
472        server_intermediate_cert,
473        csr,
474    )
475    server_cert_dump = serverCert.public_bytes(
476        encoding=serialization.Encoding.PEM
477    )
478
479    # If using intermediate certificate, save server cert without intermediate for debugging
480    if (
481        isinstance(server_intermediate_cert, x509.Certificate)
482        and server_intermediate_cert.subject != server_intermediate_cert.issuer
483    ):
484        with open(os.path.join(certs_dir, "server-cert-only.pem"), "wb") as f:
485            f.write(server_cert_dump)
486            print("Server cert (without intermediate) saved for debugging.")
487        intermediate_cert_dump = server_intermediate_cert.public_bytes(
488            encoding=serialization.Encoding.PEM
489        )
490        server_cert_dump = server_cert_dump + intermediate_cert_dump
491
492    with open(os.path.join(certs_dir, "server-cert.pem"), "wb") as f:
493        f.write(server_cert_dump)
494        print("Server cert generated.")
495
496    install_server_cert(redfish_session, manager_uri, server_cert_dump)
497
498    print("Make sure setting CertificateMappingAttribute to CommonName")
499    patch_json = {
500        "MultiFactorAuth": {
501            "ClientCertificate": {"CertificateMappingAttribute": "CommonName"}
502        }
503    }
504    response = redfish_session.patch(
505        "/redfish/v1/AccountService", json=patch_json
506    )
507    response.raise_for_status()
508
509
510def generateIntermediateCA(ca_key, ca_cert, name_prefix):
511    private_key = ec.generate_private_key(ec.SECP256R1())
512    public_key = private_key.public_key()
513    builder = x509.CertificateBuilder()
514
515    name = x509.Name(
516        [
517            x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
518            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
519            x509.NameAttribute(NameOID.LOCALITY_NAME, "Santa Clara"),
520            x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
521            x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
522            x509.NameAttribute(NameOID.COMMON_NAME, "Test Intermediate"),
523        ]
524    )
525    builder = builder.subject_name(name)
526    builder = builder.issuer_name(ca_cert.subject)
527
528    builder = builder.not_valid_before(
529        datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
530    )
531    builder = builder.not_valid_after(
532        datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
533    )
534
535    builder = builder.serial_number(x509.random_serial_number())
536    builder = builder.public_key(public_key)
537
538    basic_constraints = x509.BasicConstraints(ca=True, path_length=0)
539    builder = builder.add_extension(basic_constraints, critical=True)
540
541    usage = x509.KeyUsage(
542        content_commitment=False,
543        crl_sign=True,
544        data_encipherment=False,
545        decipher_only=False,
546        digital_signature=False,
547        encipher_only=False,
548        key_agreement=False,
549        key_cert_sign=True,
550        key_encipherment=False,
551    )
552    builder = builder.add_extension(usage, critical=True)
553
554    auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
555    builder = builder.add_extension(auth_key, critical=False)
556
557    intermediate_cert = builder.sign(
558        private_key=ca_key, algorithm=hashes.SHA256()
559    )
560
561    return private_key, intermediate_cert
562
563
564def generate_and_load_certs(url, username, password, use_intermediate=False):
565    certs_dir = os.path.expanduser("~/certs")
566    print(f"Writing certs to {certs_dir}")
567    try:
568        print("Making certs directory.")
569        os.mkdir(certs_dir)
570    except OSError as error:
571        if error.errno != errno.EEXIST:
572            raise
573
574    ca_cert_filename = os.path.join(certs_dir, "CA-cert.cer")
575    ca_key_filename = os.path.join(certs_dir, "CA-key.pem")
576    if not os.path.exists(ca_cert_filename):
577        ca_key, ca_cert = generateCA()
578
579        ca_key_dump = ca_key.private_bytes(
580            encoding=serialization.Encoding.PEM,
581            format=serialization.PrivateFormat.TraditionalOpenSSL,
582            encryption_algorithm=serialization.NoEncryption(),
583        )
584        ca_cert_dump = ca_cert.public_bytes(
585            encoding=serialization.Encoding.PEM
586        )
587
588        with open(ca_cert_filename, "wb") as f:
589            f.write(ca_cert_dump)
590            print("CA cert generated.")
591        with open(ca_key_filename, "wb") as f:
592            f.write(ca_key_dump)
593            print("CA key generated.")
594
595    with open(ca_cert_filename, "rb") as ca_cert_file:
596        ca_cert_dump = ca_cert_file.read()
597    ca_cert = x509.load_pem_x509_certificate(ca_cert_dump)
598
599    with open(ca_key_filename, "rb") as ca_key_file:
600        ca_key_dump = ca_key_file.read()
601    ca_key = load_pem_private_key(ca_key_dump, None)
602
603    # Generate intermediate certificates if requested
604    if use_intermediate:
605        # Generate client intermediate
606        client_intermediate_key, client_intermediate_cert = (
607            generateIntermediateCA(ca_key, ca_cert, "client")
608        )
609        # ... save client intermediate certs ...
610
611        # Generate server intermediate
612        server_intermediate_key, server_intermediate_cert = (
613            generateIntermediateCA(ca_key, ca_cert, "server")
614        )
615    else:
616        client_intermediate_key = ca_key
617        client_intermediate_cert = ca_cert
618        server_intermediate_key = ca_key
619        server_intermediate_cert = ca_cert
620
621    # Update client cert generation to use intermediate
622    client_key, client_cert = generate_client_key_and_cert(
623        ca_cert=client_intermediate_cert,
624        ca_key=client_intermediate_key,
625        common_name=username,
626    )
627    client_key_dump = client_key.private_bytes(
628        encoding=serialization.Encoding.PEM,
629        format=serialization.PrivateFormat.TraditionalOpenSSL,
630        encryption_algorithm=serialization.NoEncryption(),
631    )
632
633    with open(os.path.join(certs_dir, "client-key.pem"), "wb") as f:
634        f.write(client_key_dump)
635        print("Client key generated.")
636    client_cert_dump = client_cert.public_bytes(
637        encoding=serialization.Encoding.PEM
638    )
639
640    # Save client certificate without intermediate for debugging
641    if use_intermediate:
642        with open(os.path.join(certs_dir, "client-cert-only.pem"), "wb") as f:
643            f.write(client_cert_dump)
644            print("Client cert (without intermediate) saved for debugging.")
645        client_cert_dump = (
646            client_cert_dump
647            + client_intermediate_cert.public_bytes(
648                encoding=serialization.Encoding.PEM
649            )
650        )  # Append intermediate to create chain
651
652    with open(os.path.join(certs_dir, "client-cert.pem"), "wb") as f:
653        f.write(client_cert_dump)
654        print("Client cert generated.")
655
656    print(f"Connecting to {url}")
657    with httpx.Client(
658        base_url=f"https://{url}", verify=False, follow_redirects=False
659    ) as redfish_session:
660        with RedfishSessionContext(
661            redfish_session, username, password
662        ) as rf_session:
663            redfish_session.headers["X-Auth-Token"] = rf_session.x_auth_token
664
665            hostname = get_hostname(redfish_session, username, password, url)
666            print(f"Hostname: {hostname}")
667
668            upn_client_key, upn_client_cert = generate_client_key_and_cert(
669                ca_cert,
670                ca_key,
671                upn=f"{username}@{hostname}",
672            )
673            upn_client_key_dump = upn_client_key.private_bytes(
674                encoding=serialization.Encoding.PEM,
675                format=serialization.PrivateFormat.TraditionalOpenSSL,
676                encryption_algorithm=serialization.NoEncryption(),
677            )
678            with open(
679                os.path.join(certs_dir, "upn-client-key.pem"), "wb"
680            ) as f:
681                f.write(upn_client_key_dump)
682                print("UPN client key generated.")
683
684            upn_client_cert_dump = upn_client_cert.public_bytes(
685                encoding=serialization.Encoding.PEM
686            )
687            with open(
688                os.path.join(certs_dir, "upn-client-cert.pem"), "wb"
689            ) as f:
690                f.write(upn_client_cert_dump)
691                print("UPN client cert generated.")
692
693            setup_server_cert(
694                redfish_session,
695                ca_cert_dump,
696                certs_dir,
697                client_key,
698                client_cert,
699                username,
700                url,
701                server_intermediate_key,
702                server_intermediate_cert,
703            )
704
705    print("Testing redfish TLS authentication with generated certs.")
706    time.sleep(2)
707    test_mtls_auth(url, certs_dir)
708    print("Redfish TLS authentication success!")
709
710
711def main():
712    parser = argparse.ArgumentParser()
713    parser.add_argument(
714        "--username",
715        help="Username to connect with",
716        default="root",
717    )
718    parser.add_argument(
719        "--password",
720        help="Password for user in order to install certs over Redfish.",
721        default="0penBmc",
722    )
723    parser.add_argument(
724        "--use-intermediate",
725        action="store_true",
726        default=False,
727        help="Generate and use an intermediate certificate",
728    )
729    parser.add_argument("host", help="Host to connect to")
730
731    args = parser.parse_args()
732    generate_and_load_certs(
733        args.host, args.username, args.password, args.use_intermediate
734    )
735
736
737if __name__ == "__main__":
738    main()
739