• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Generate test data
15
16Generate data needed for unit tests, i.e. certificates, keys, and CRLSet.
17"""
18
19import argparse
20import subprocess
21import sys
22from datetime import datetime, timedelta
23from typing import List, Tuple
24
25from cryptography import x509
26from cryptography.hazmat.primitives import hashes
27from cryptography.hazmat.primitives import serialization
28from cryptography.hazmat.primitives.asymmetric import rsa
29from cryptography.x509.oid import NameOID
30
31CERTS_AND_KEYS_HEADER = """// Copyright 2021 The Pigweed Authors
32//
33// Licensed under the Apache License, Version 2.0 (the "License"); you may not
34// use this file except in compliance with the License. You may obtain a copy
35// of the License at
36//
37//     https://www.apache.org/licenses/LICENSE-2.0
38//
39// Unless required by applicable law or agreed to in writing, software
40// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
41// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
42// License for the specific language governing permissions and limitations under
43// the License.
44
45#pragma once
46
47#include "pw_bytes/span.h"
48
49"""
50
51
52class Subject:
53    """A subject wraps a name, private key and extensions for issuers
54    to issue its certificate"""
55
56    def __init__(
57        self, name: str, extensions: List[Tuple[x509.ExtensionType, bool]]
58    ):
59        self._subject_name = x509.Name(
60            [
61                x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
62                x509.NameAttribute(
63                    NameOID.STATE_OR_PROVINCE_NAME, u"California"
64                ),
65                x509.NameAttribute(NameOID.LOCALITY_NAME, u"Mountain View"),
66                x509.NameAttribute(NameOID.ORGANIZATION_NAME, name),
67                x509.NameAttribute(NameOID.COMMON_NAME, u"Google-Pigweed"),
68            ]
69        )
70        self._private_key = rsa.generate_private_key(
71            public_exponent=65537, key_size=2048
72        )
73        self._extensions = extensions
74
75    def subject_name(self) -> x509.Name:
76        """Returns the subject name"""
77        return self._subject_name
78
79    def public_key(self) -> rsa.RSAPublicKey:
80        """Returns the public key of this subject"""
81        return self._private_key.public_key()
82
83    def private_key(self) -> rsa.RSAPrivateKey:
84        """Returns the private key of this subject"""
85        return self._private_key
86
87    def extensions(self) -> List[Tuple[x509.ExtensionType, bool]]:
88        """Returns the requested extensions for issuer"""
89        return self._extensions
90
91
92class CA(Subject):
93    """A CA/Sub-ca that issues certificates"""
94
95    def __init__(self, *args, **kwargs):
96        ext = [
97            (x509.BasicConstraints(True, None), True),
98            (
99                x509.KeyUsage(
100                    digital_signature=False,
101                    content_commitment=False,
102                    key_encipherment=False,
103                    data_encipherment=False,
104                    key_agreement=False,
105                    crl_sign=False,
106                    encipher_only=False,
107                    decipher_only=False,
108                    key_cert_sign=True,
109                ),
110                True,
111            ),
112        ]
113        super().__init__(*args, extensions=ext, **kwargs)
114
115    def sign(
116        self, subject: Subject, not_before: datetime, not_after: datetime
117    ) -> x509.Certificate:
118        """Issues a certificate for another CA/Sub-ca/Server"""
119        builder = x509.CertificateBuilder()
120
121        # Subject name is the target's subject name
122        builder = builder.subject_name(subject.subject_name())
123
124        # Issuer name is this CA/sub-ca's subject name
125        builder = builder.issuer_name(self._subject_name)
126
127        # Public key is the target's public key.
128        builder = builder.public_key(subject.public_key())
129
130        # Validity period.
131        builder = builder.not_valid_before(not_before).not_valid_after(
132            not_after
133        )
134
135        # Uses a random serial number
136        builder = builder.serial_number(x509.random_serial_number())
137
138        # Add extensions
139        for extension, critical in subject.extensions():
140            builder = builder.add_extension(extension, critical)
141
142        # Sign and returns the certificate.
143        return builder.sign(self._private_key, hashes.SHA256())
144
145    def self_sign(
146        self, not_before: datetime, not_after: datetime
147    ) -> x509.Certificate:
148        """Issues a self sign certificate"""
149        return self.sign(self, not_before, not_after)
150
151
152class Server(Subject):
153    """The end-entity server"""
154
155    def __init__(self, *args, **kwargs):
156        ext = [
157            (x509.BasicConstraints(False, None), True),
158            (
159                x509.KeyUsage(
160                    digital_signature=True,
161                    content_commitment=False,
162                    key_encipherment=False,
163                    data_encipherment=False,
164                    key_agreement=False,
165                    crl_sign=False,
166                    encipher_only=False,
167                    decipher_only=False,
168                    key_cert_sign=False,
169                ),
170                True,
171            ),
172            (
173                x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
174                True,
175            ),
176        ]
177        super().__init__(*args, extensions=ext, **kwargs)
178
179
180def c_escaped_string(data: bytes):
181    """Generates a C byte string representation for a byte array
182
183    For example, given a byte sequence of [0x12, 0x34, 0x56]. The function
184    generates the following byte string code:
185
186            {"\x12\x34\x56", 3}
187    """
188    body = ''.join([f'\\x{b:02x}' for b in data])
189    return f'{{\"{body}\", {len(data)}}}'
190
191
192def byte_array_declaration(data: bytes, name: str) -> str:
193    """Generates a ConstByteSpan declaration for a byte array"""
194    type_name = '[[maybe_unused]] const pw::ConstByteSpan'
195    array_body = f'pw::as_bytes(pw::span{c_escaped_string(data)})'
196    return f'{type_name} {name} = {array_body};'
197
198
199class Codegen:
200    """Base helper class for code generation"""
201
202    def generate_code(self) -> str:  # pylint: disable=no-self-use
203        """Generates C++ code for this object"""
204        return ''
205
206
207class PrivateKeyGen(Codegen):
208    """Codegen class for a private key"""
209
210    def __init__(self, key: rsa.RSAPrivateKey, name: str):
211        self._key = key
212        self._name = name
213
214    def generate_code(self) -> str:
215        """Code generation"""
216        return byte_array_declaration(
217            self._key.private_bytes(
218                serialization.Encoding.DER,
219                serialization.PrivateFormat.TraditionalOpenSSL,
220                serialization.NoEncryption(),
221            ),
222            self._name,
223        )
224
225
226class CertificateGen(Codegen):
227    """Codegen class for a single certificate"""
228
229    def __init__(self, cert: x509.Certificate, name: str):
230        self._cert = cert
231        self._name = name
232
233    def generate_code(self) -> str:
234        """Code generation"""
235        return byte_array_declaration(
236            self._cert.public_bytes(serialization.Encoding.DER), self._name
237        )
238
239
240def generate_test_data() -> str:
241    """Generates test data"""
242    subjects: List[Codegen] = []
243
244    # Working valid period.
245    # Start from yesterday, to make sure we are in the valid period.
246    not_before = datetime.utcnow() - timedelta(days=1)
247    # Valid for 1 year.
248    not_after = not_before + timedelta(days=365)
249
250    # Generate a root-A CA certificates
251    root_a = CA("root-A")
252    subjects.append(
253        CertificateGen(root_a.self_sign(not_before, not_after), "kRootACert")
254    )
255
256    # Generate a sub CA certificate signed by root-A.
257    sub = CA("sub")
258    subjects.append(
259        CertificateGen(root_a.sign(sub, not_before, not_after), "kSubCACert")
260    )
261
262    # Generate a valid server certificate signed by sub
263    server = Server("server")
264    subjects.append(
265        CertificateGen(sub.sign(server, not_before, not_after), "kServerCert")
266    )
267    subjects.append(PrivateKeyGen(server.private_key(), "kServerKey"))
268
269    root_b = CA("root-B")
270    subjects.append(
271        CertificateGen(root_b.self_sign(not_before, not_after), "kRootBCert")
272    )
273
274    code = 'namespace {\n\n'
275    for subject in subjects:
276        code += subject.generate_code() + '\n\n'
277    code += '}\n'
278
279    return code
280
281
282def clang_format(file):
283    subprocess.run(
284        [
285            "clang-format",
286            "-i",
287            file,
288        ],
289        check=True,
290    )
291
292
293def parse_args():
294    """Setup argparse."""
295    parser = argparse.ArgumentParser()
296    parser.add_argument(
297        "certs_and_keys_header",
298        help="output header file for test certificates and keys",
299    )
300    return parser.parse_args()
301
302
303def main() -> int:
304    """Main"""
305    args = parse_args()
306
307    certs_and_keys = generate_test_data()
308
309    with open(args.certs_and_keys_header, 'w') as header:
310        header.write(CERTS_AND_KEYS_HEADER)
311        header.write(certs_and_keys)
312
313    clang_format(args.certs_and_keys_header)
314    return 0
315
316
317if __name__ == "__main__":
318    sys.exit(main())
319