| #!/usr/bin/env python |
| |
| # |
| # =================================================================== |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # =================================================================== |
| # |
| |
| # This script creates the private keys and certificates required for |
| # running the serf test suite. |
| # |
| # It should be run from the test/certs folder without arguments. |
| # Certificates will be created in the test/certs folder, private keys in the |
| # test/certs/private folder. |
| # |
| # You'll need to install 'cryptography' for this script to work. |
| |
| from datetime import datetime, timedelta, UTC |
| from ipaddress import IPv4Address, IPv6Address |
| |
| from cryptography.x509 import ( |
| AccessDescription, |
| Certificate, |
| CertificateBuilder, |
| RevokedCertificateBuilder, |
| CertificateRevocationListBuilder, |
| CRLReason, ReasonFlags, |
| Name, NameAttribute, NameOID, |
| SubjectKeyIdentifier, |
| AuthorityKeyIdentifier, |
| AuthorityInformationAccess, |
| DNSName, IPAddress, |
| UniformResourceIdentifier) |
| from cryptography.hazmat.primitives.hashes import SHA256 |
| from cryptography.hazmat.primitives.asymmetric import rsa |
| from cryptography.hazmat.primitives.serialization import ( |
| BestAvailableEncryption, |
| Encoding, |
| NoEncryption, |
| PrivateFormat, |
| pkcs12) |
| from cryptography.x509.extensions import ( |
| BasicConstraints, |
| ExtendedKeyUsage, |
| SubjectAlternativeName) |
| from cryptography.x509.oid import ( |
| ExtendedKeyUsageOID, |
| AuthorityInformationAccessOID) |
| |
| |
| # for serf, update this number every time the certs are updated. |
| SERIAL_NUMBER=20250711 |
| |
| KEY_ALGO = rsa |
| KEY_EXPONENT = 65537 |
| KEY_SIZE = 2048 |
| SIGN_ALGO = SHA256() |
| NOT_BEFORE = datetime(2025, 6, 18, 10, 39, 14, 0, UTC) |
| NOT_AFTER = datetime(2125, 5, 25, 10, 39, 14, 0, UTC) |
| |
| |
| def create_key(keyfile, passphrase=None): |
| key = KEY_ALGO.generate_private_key(KEY_EXPONENT, KEY_SIZE) |
| if type(passphrase) is type(b''): |
| encryption = BestAvailableEncryption(passphrase) |
| else: |
| encryption = NoEncryption() |
| |
| with open(keyfile, 'wb') as fd: |
| fd.write(key.private_bytes(encoding=Encoding.PEM, |
| format=PrivateFormat.PKCS8, |
| encryption_algorithm=encryption)) |
| return key |
| |
| |
| def create_pkcs12(name, key, cert, issuer, pkcs12file, passphrase=None): |
| if type(passphrase) is type(b''): |
| encryption = BestAvailableEncryption(passphrase) |
| else: |
| encryption = NoEncryption() |
| |
| with open(pkcs12file, 'wb') as fd: |
| fd.write(pkcs12.serialize_key_and_certificates( |
| name, key, cert, [issuer], encryption)) |
| |
| |
| def create_crl(revokedcert, cakey, cacert, crlfile): |
| revoked = (RevokedCertificateBuilder() |
| .serial_number(revokedcert.serial_number) |
| .revocation_date(datetime.now(UTC)) |
| .add_extension(CRLReason(ReasonFlags.unspecified), critical=False) |
| .build()) |
| |
| crl = (CertificateRevocationListBuilder() |
| .issuer_name(cacert.subject) |
| .last_update(NOT_BEFORE) |
| .next_update(NOT_AFTER) |
| .add_revoked_certificate(revoked) |
| .sign(cakey, SIGN_ALGO)) |
| |
| with open(crlfile, 'wb') as fd: |
| fd.write(crl.public_bytes(Encoding.PEM)) |
| |
| |
| # subjectAltName |
| def create_cert(subjectkey, certfile, issuer=None, issuerkey=None, country='', |
| state='', city='', org='', ou='', cn='', email='', ca=False, |
| not_before=NOT_BEFORE, not_after=NOT_AFTER, |
| subjectAltName=None, ocsp_responder_url=None, ocsp_signer=False): |
| ''' |
| Create a X509 signed certificate. |
| |
| subjectAltName |
| Array of fully qualified subject alternative names (use OpenSSL syntax): |
| For a DNS entry, use: ['DNS:localhost']. Other options are 'email', 'URI', 'IP'. |
| ''' |
| |
| subject = [] |
| if country: |
| subject.append(NameAttribute(NameOID.COUNTRY_NAME, country)) |
| if state: |
| subject.append(NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state)) |
| if city: |
| subject.append(NameAttribute(NameOID.LOCALITY_NAME, city)) |
| if org: |
| subject.append(NameAttribute(NameOID.ORGANIZATION_NAME, org)) |
| if ou: |
| subject.append(NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, ou)) |
| if cn: |
| subject.append(NameAttribute(NameOID.COMMON_NAME, cn)) |
| if email: |
| subject.append(NameAttribute(NameOID.EMAIL_ADDRESS, email)) |
| |
| pubkey = subjectkey.public_key() |
| builder = (CertificateBuilder() |
| .serial_number(SERIAL_NUMBER) |
| .subject_name(Name(subject)) |
| .public_key(pubkey) |
| .not_valid_before(not_before) |
| .not_valid_after(not_after)) |
| if issuer is None: |
| # self-signed certificate |
| builder = builder.issuer_name(Name(subject)) |
| issuerkey = subjectkey |
| else: |
| builder = builder.issuer_name(issuer.subject) |
| |
| if ca: |
| bc = BasicConstraints(ca=True, path_length=None) |
| ski = SubjectKeyIdentifier.from_public_key(pubkey) |
| aki = AuthorityKeyIdentifier.from_issuer_public_key(issuerkey.public_key()) |
| builder = (builder |
| .add_extension(bc, critical=False) |
| .add_extension(ski, critical=False) |
| .add_extension(aki, critical=False)) |
| |
| if subjectAltName: |
| alt = [] |
| for name in subjectAltName: |
| if name.startswith('DNS:'): |
| alt.append(DNSName(name[4:])) |
| if name.startswith('IP4:'): |
| alt.append(IPAddress(IPv4Address(name[4:]))) |
| if name.startswith('IP6:'): |
| alt.append(IPAddress(IPv6Address(name[4:]))) |
| if alt: |
| builder = builder.add_extension(SubjectAlternativeName(alt), |
| critical=True if not cn else False) |
| |
| if ocsp_responder_url: |
| ocsp = AccessDescription(AuthorityInformationAccessOID.OCSP, |
| UniformResourceIdentifier(ocsp_responder_url)) |
| builder = builder.add_extension( |
| AuthorityInformationAccess([ocsp]), |
| critical=False) |
| |
| if ocsp_signer: |
| builder = builder.add_extension( |
| ExtendedKeyUsage([ExtendedKeyUsageOID.OCSP_SIGNING]), |
| critical=True) |
| |
| cert = builder.sign(issuerkey, SIGN_ALGO) |
| |
| with open(certfile, 'wb') as fd: |
| fd.write(cert.public_bytes(Encoding.PEM)) |
| return cert |
| |
| |
| if __name__ == '__main__': |
| # root CA key pair and certificate. |
| # This key will be used to sign the intermediate CA certificate |
| rootcakey = create_key('private/serfrootcakey.pem', b'serftest') |
| |
| rootcacert = create_cert(subjectkey=rootcakey, |
| certfile='serfrootcacert.pem', |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Root CA', cn='Serf Root CA', |
| email='serfrootca@example.com', ca=True) |
| |
| # intermediate CA key pair and certificate |
| # This key will be used to sign all server certificates |
| cakey = create_key('private/serfcakey.pem', b'serftest') |
| |
| cacert = create_cert(subjectkey=cakey, certfile='serfcacert.pem', |
| issuer=rootcacert, issuerkey=rootcakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite CA', cn='Serf CA', |
| email='serfca@example.com', ca=True) |
| |
| # server key pair |
| # server certificate, no errors |
| serverkey = create_key('private/serfserverkey.pem', b'serftest') |
| |
| servercert = create_cert(subjectkey=serverkey, |
| certfile='serfservercert.pem', |
| issuer=cacert, issuerkey=cakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Server', cn='localhost', |
| email='serfserver@example.com') |
| |
| # server certificate that expired a year ago |
| expiredcert = create_cert(subjectkey=serverkey, |
| certfile='serfserver_expired_cert.pem', |
| issuer=cacert, issuerkey=cakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Server', cn='localhost', |
| email='serfserver@example.com', |
| not_before=NOT_BEFORE - timedelta(days=365), |
| not_after=NOT_BEFORE - timedelta(days=355)) |
| |
| # server certificate that will be valid in 100 years |
| notyetvalidcert = create_cert(subjectkey=serverkey, |
| certfile='serfserver_future_cert.pem', |
| issuer=cacert, issuerkey=cakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Server', cn='localhost', |
| email='serfserver@example.com', |
| not_before=NOT_AFTER, |
| not_after=NOT_AFTER + timedelta(days=10)) |
| |
| # server certificate with SubjectAltName and empty CN |
| san_nocncert = create_cert(subjectkey=serverkey, |
| certfile='serfserver_san_nocn_cert.pem', |
| issuer=cacert, issuerkey=cakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Server', |
| cn='', |
| email='serfserver@example.com', |
| subjectAltName=['DNS:localhost']) |
| |
| # server certificate with OCSP responder URL |
| ocspcert = create_cert(subjectkey=serverkey, |
| certfile='serfserver_san_ocsp_cert.pem', |
| issuer=cacert, issuerkey=cakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Server', |
| cn='localhost', |
| email='serfserver@example.com', |
| subjectAltName=['DNS:localhost'], |
| ocsp_responder_url='http://localhost:17080') |
| |
| # OCSP responder certifi |
| ocsprspcert = create_cert(subjectkey=serverkey, |
| certfile='serfocspresponder.pem', |
| issuer=cacert, issuerkey=cakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Server', |
| cn='localhost', |
| email='serfserver@example.com', |
| ocsp_signer=True) |
| |
| # client key pair and certificate |
| clientkey = create_key('private/serfclientkey.pem', b'serftest') |
| |
| clientcert = create_cert(subjectkey=clientkey, |
| certfile='serfclientcert.pem', |
| issuer=cacert, issuerkey=cakey, |
| country='BE', state='Antwerp', city='Mechelen', |
| org='In Serf we trust, Inc.', |
| ou='Test Suite Client', cn='Serf Client', |
| email='serfclient@example.com') |
| |
| clientpkcs12 = create_pkcs12(b'serfclient', clientkey, clientcert, cacert, |
| 'serfclientcert.p12', b'serftest') |
| |
| # crl |
| crl = create_crl(servercert, cakey, cacert, 'serfservercrl.pem') |