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