Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | # This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 4 | |
michael@0 | 5 | # This file requires openssl 1.0.0 at least |
michael@0 | 6 | |
michael@0 | 7 | import os |
michael@0 | 8 | import random |
michael@0 | 9 | import pexpect |
michael@0 | 10 | import time |
michael@0 | 11 | import sys |
michael@0 | 12 | |
michael@0 | 13 | def init_dsa(db_dir): |
michael@0 | 14 | """ |
michael@0 | 15 | Initialize dsa parameters |
michael@0 | 16 | |
michael@0 | 17 | Sets up a set of params to be reused for DSA key generation |
michael@0 | 18 | |
michael@0 | 19 | Arguments: |
michael@0 | 20 | db_dir -- location of the temporary params for the certificate |
michael@0 | 21 | """ |
michael@0 | 22 | dsa_key_params = db_dir + "/dsa_param.pem" |
michael@0 | 23 | os.system("openssl dsaparam -out " + dsa_key_params + " 2048") |
michael@0 | 24 | |
michael@0 | 25 | |
michael@0 | 26 | def generate_cert_generic(db_dir, dest_dir, serial_num, key_type, name, |
michael@0 | 27 | ext_text, signer_key_filename = "", |
michael@0 | 28 | signer_cert_filename = "", |
michael@0 | 29 | subject_string = ""): |
michael@0 | 30 | """ |
michael@0 | 31 | Generate an x509 certificate with a sha256 signature |
michael@0 | 32 | |
michael@0 | 33 | Preconditions: |
michael@0 | 34 | if dsa keys are to be generated init_dsa must have been called before. |
michael@0 | 35 | |
michael@0 | 36 | |
michael@0 | 37 | Arguments: |
michael@0 | 38 | db_dir -- location of the temporary params for the certificate |
michael@0 | 39 | dest_dir -- location of the x509 cert |
michael@0 | 40 | serial_num -- serial number for the cert (must be unique for each signer |
michael@0 | 41 | key) |
michael@0 | 42 | key_type -- the type of key generated: potential values: 'rsa', 'dsa', |
michael@0 | 43 | or any of the curves found by 'openssl ecparam -list_curves' |
michael@0 | 44 | name -- the common name for the cert, will match the prefix of the |
michael@0 | 45 | output cert |
michael@0 | 46 | ext_text -- the text for the x509 extensions to be added to the |
michael@0 | 47 | certificate |
michael@0 | 48 | signer_key_filename -- the filename of the key from which the cert will |
michael@0 | 49 | be signed if null the cert will be self signed (think CA |
michael@0 | 50 | roots). |
michael@0 | 51 | signer_cert_filename -- the certificate that will sign the certificate |
michael@0 | 52 | (used to extract signer info) it must be in DER format. |
michael@0 | 53 | |
michael@0 | 54 | output: |
michael@0 | 55 | key_name -- the filename of the key file (PEM format) |
michael@0 | 56 | cert_name -- the filename of the output certificate (DER format) |
michael@0 | 57 | """ |
michael@0 | 58 | key_name = db_dir + "/"+ name + ".key" |
michael@0 | 59 | if key_type == 'rsa': |
michael@0 | 60 | os.system ("openssl genpkey -algorithm RSA -out " + key_name + |
michael@0 | 61 | " -pkeyopt rsa_keygen_bits:2048") |
michael@0 | 62 | elif key_type == 'dsa': |
michael@0 | 63 | dsa_key_params = db_dir + "/dsa_param.pem" |
michael@0 | 64 | os.system("openssl gendsa -out " + key_name + " " + dsa_key_params) |
michael@0 | 65 | else: |
michael@0 | 66 | #assume is ec |
michael@0 | 67 | os.system("openssl ecparam -out " + key_name + " -name "+ key_type + |
michael@0 | 68 | " -genkey"); |
michael@0 | 69 | csr_name = db_dir + "/"+ name + ".csr" |
michael@0 | 70 | if not subject_string: |
michael@0 | 71 | subject_string = '/CN=' + name |
michael@0 | 72 | os.system ("openssl req -new -key " + key_name + " -days 3650" + |
michael@0 | 73 | " -extensions v3_ca -batch -out " + csr_name + |
michael@0 | 74 | " -utf8 -subj '" + subject_string + "'") |
michael@0 | 75 | |
michael@0 | 76 | extensions_filename = db_dir + "/openssl-exts" |
michael@0 | 77 | f = open(extensions_filename,'w') |
michael@0 | 78 | f.write(ext_text) |
michael@0 | 79 | f.close() |
michael@0 | 80 | |
michael@0 | 81 | cert_name = dest_dir + "/"+ name + ".der" |
michael@0 | 82 | if not signer_key_filename: |
michael@0 | 83 | signer_key_filename = key_name; |
michael@0 | 84 | os.system ("openssl x509 -req -sha256 -days 3650 -in " + csr_name + |
michael@0 | 85 | " -signkey " + signer_key_filename + |
michael@0 | 86 | " -set_serial " + str(serial_num) + |
michael@0 | 87 | " -extfile " + extensions_filename + |
michael@0 | 88 | " -outform DER -out "+ cert_name) |
michael@0 | 89 | else: |
michael@0 | 90 | os.system ("openssl x509 -req -sha256 -days 3650 -in " + csr_name + |
michael@0 | 91 | " -CAkey " + signer_key_filename + |
michael@0 | 92 | " -CA " + signer_cert_filename + " -CAform DER " + |
michael@0 | 93 | " -set_serial " + str(serial_num) + " -out " + cert_name + |
michael@0 | 94 | " -outform DER -extfile " + extensions_filename) |
michael@0 | 95 | return key_name, cert_name |
michael@0 | 96 | |
michael@0 | 97 | |
michael@0 | 98 | |
michael@0 | 99 | def generate_int_and_ee(db_dir, dest_dir, ca_key, ca_cert, name, int_ext_text, |
michael@0 | 100 | ee_ext_text, key_type = 'rsa'): |
michael@0 | 101 | """ |
michael@0 | 102 | Generate an intermediate and ee signed by the generated intermediate. The |
michael@0 | 103 | name of the intermediate files will be the name '.der' or '.key'. The name |
michael@0 | 104 | of the end entity files with be "ee-"+ name plus the appropiate prefixes. |
michael@0 | 105 | The serial number will be generated radomly so it is potentially possible |
michael@0 | 106 | to have problem (but very unlikely). |
michael@0 | 107 | |
michael@0 | 108 | Arguments: |
michael@0 | 109 | db_dir -- location of the temporary params for the certificate |
michael@0 | 110 | dest_dir -- location of the x509 cert |
michael@0 | 111 | ca_key -- The filename of the key that will be used to sign the |
michael@0 | 112 | intermediate (PEM FORMAT) |
michael@0 | 113 | ca_cert -- The filename of the cert that will be used to sign the |
michael@0 | 114 | intermediate, it MUST be the private key for the ca_key. |
michael@0 | 115 | The file must be in DER format. |
michael@0 | 116 | name -- the common name for the intermediate, will match the prefix |
michael@0 | 117 | of the output intermediate. The ee will have the name |
michael@0 | 118 | prefixed with "ee-" |
michael@0 | 119 | int_ext_text -- the text for the x509 extensions to be added to the |
michael@0 | 120 | intermediate certificate |
michael@0 | 121 | ee_ext_text -- the text for the x509 extensions to be added to the |
michael@0 | 122 | end entity certificate |
michael@0 | 123 | key_type -- the type of key generated: potential values: 'rsa', 'dsa', |
michael@0 | 124 | or any of the curves found by 'openssl ecparam -list_curves' |
michael@0 | 125 | |
michael@0 | 126 | output: |
michael@0 | 127 | int_key -- the filename of the intermeidate key file (PEM format) |
michael@0 | 128 | int_cert -- the filename of the intermediate certificate (DER format) |
michael@0 | 129 | ee_key -- the filename of the end entity key file (PEM format) |
michael@0 | 130 | ee_cert -- the filename of the end entity certficate (DER format) |
michael@0 | 131 | |
michael@0 | 132 | """ |
michael@0 | 133 | [int_key, int_cert] = generate_cert_generic(db_dir, dest_dir, |
michael@0 | 134 | random.randint(100,40000000), |
michael@0 | 135 | key_type, "int-" + name, |
michael@0 | 136 | int_ext_text, |
michael@0 | 137 | ca_key, ca_cert) |
michael@0 | 138 | [ee_key, ee_cert] = generate_cert_generic(db_dir, dest_dir, |
michael@0 | 139 | random.randint(100,40000000), |
michael@0 | 140 | key_type, name, |
michael@0 | 141 | ee_ext_text, int_key, int_cert) |
michael@0 | 142 | |
michael@0 | 143 | return int_key, int_cert, ee_key, ee_cert |
michael@0 | 144 | |
michael@0 | 145 | def generate_pkcs12(db_dir, dest_dir, der_cert_filename, key_pem_filename, |
michael@0 | 146 | prefix): |
michael@0 | 147 | """ |
michael@0 | 148 | Generate a pkcs12 file for a given certificate name (in der format) and |
michael@0 | 149 | a key filename (key in pem format). The output file will have an empty |
michael@0 | 150 | password. |
michael@0 | 151 | |
michael@0 | 152 | Arguments: |
michael@0 | 153 | input: |
michael@0 | 154 | db_dir -- location of the temporary params for the certificate |
michael@0 | 155 | dest_dir -- location of the x509 cert |
michael@0 | 156 | der_cert_filename -- the filename of the certificate to be included in the |
michael@0 | 157 | output file (DER format) |
michael@0 | 158 | key_pem_filename -- the filename of the private key of the certificate to |
michael@0 | 159 | (PEM format) |
michael@0 | 160 | prefix -- the name to be prepended to the output pkcs12 file. |
michael@0 | 161 | output: |
michael@0 | 162 | pk12_filename -- the filename of the outgoing pkcs12 output file |
michael@0 | 163 | """ |
michael@0 | 164 | #make pem cert file from der filename |
michael@0 | 165 | pem_cert_filename = db_dir + "/" + prefix + ".pem" |
michael@0 | 166 | pk12_filename = dest_dir + "/" + prefix + ".p12" |
michael@0 | 167 | os.system("openssl x509 -inform der -in " + der_cert_filename + " -out " + |
michael@0 | 168 | pem_cert_filename ) |
michael@0 | 169 | #now make pkcs12 file |
michael@0 | 170 | child = pexpect.spawn("openssl pkcs12 -export -in " + pem_cert_filename + |
michael@0 | 171 | " -inkey " + key_pem_filename + " -out " + |
michael@0 | 172 | pk12_filename) |
michael@0 | 173 | child.expect('Enter Export Password:') |
michael@0 | 174 | child.sendline('') |
michael@0 | 175 | child.expect('Verifying - Enter Export Password:') |
michael@0 | 176 | child.sendline('') |
michael@0 | 177 | child.expect(pexpect.EOF) |
michael@0 | 178 | return pk12_filename |
michael@0 | 179 | |
michael@0 | 180 | def init_nss_db(db_dir): |
michael@0 | 181 | """ |
michael@0 | 182 | Remove the current nss database in the specified directory and create a new |
michael@0 | 183 | nss database with the cert8 format. |
michael@0 | 184 | Arguments |
michael@0 | 185 | db_dir -- the desired location of the new database |
michael@0 | 186 | output |
michael@0 | 187 | noise_file -- the path to a noise file suitable to generate TEST |
michael@0 | 188 | certificates. This does not have enough entropy for a real |
michael@0 | 189 | secret |
michael@0 | 190 | pwd_file -- the patch to the secret file used for the database. |
michael@0 | 191 | this file should be empty. |
michael@0 | 192 | """ |
michael@0 | 193 | nss_db_files = ["cert8.db", "key3.db", "secmod.db", "noise", "pwdfile"] |
michael@0 | 194 | for file in nss_db_files: |
michael@0 | 195 | if os.path.isfile(file): |
michael@0 | 196 | os.remove(file) |
michael@0 | 197 | # create noise file |
michael@0 | 198 | noise_file = db_dir + "/noise" |
michael@0 | 199 | nf = open(noise_file, 'w') |
michael@0 | 200 | nf.write(str(time.time())) |
michael@0 | 201 | nf.close() |
michael@0 | 202 | # create pwd file |
michael@0 | 203 | pwd_file = db_dir + "/pwfile" |
michael@0 | 204 | pf = open(pwd_file, 'w') |
michael@0 | 205 | pf.write("\n") |
michael@0 | 206 | pf.close() |
michael@0 | 207 | # create nss db |
michael@0 | 208 | os.system("certutil -d " + db_dir + " -N -f " + pwd_file); |
michael@0 | 209 | return [noise_file, pwd_file] |
michael@0 | 210 | |
michael@0 | 211 | def generate_ca_cert(db_dir, dest_dir, noise_file, name, version, do_bc): |
michael@0 | 212 | """ |
michael@0 | 213 | Creates a new CA certificate in an sql NSS database and as a der file |
michael@0 | 214 | Arguments: |
michael@0 | 215 | db_dir -- the location of the nss database (in sql format) |
michael@0 | 216 | dest_dir -- the location of for the output file |
michael@0 | 217 | noise_file -- the location of a noise file. |
michael@0 | 218 | name -- the nickname of the new certificate in the database and the |
michael@0 | 219 | common name of the certificate |
michael@0 | 220 | version -- the version number of the certificate (valid certs must use |
michael@0 | 221 | 3) |
michael@0 | 222 | do_bc -- if the certificate should include the basic constraints |
michael@0 | 223 | (valid ca's should be true) |
michael@0 | 224 | output: |
michael@0 | 225 | outname -- the location of the der file. |
michael@0 | 226 | """ |
michael@0 | 227 | out_name = dest_dir + "/" + name + ".der" |
michael@0 | 228 | base_exec_string = ("certutil -S -z " + noise_file + " -g 2048 -d " + |
michael@0 | 229 | db_dir + "/ -n " + name + " -v 120 -s 'CN=" + name + |
michael@0 | 230 | ",O=PSM Testing,L=Mountain View,ST=California,C=US'" + |
michael@0 | 231 | " -t C,C,C -x --certVersion=" + str(int(version))) |
michael@0 | 232 | if (do_bc): |
michael@0 | 233 | child = pexpect.spawn(base_exec_string + " -2") |
michael@0 | 234 | child.logfile = sys.stdout |
michael@0 | 235 | child.expect('Is this a CA certificate \[y/N\]?') |
michael@0 | 236 | child.sendline('y') |
michael@0 | 237 | child.expect('Enter the path length constraint, enter to skip \[<0 for unlimited path\]: >') |
michael@0 | 238 | child.sendline('') |
michael@0 | 239 | child.expect('Is this a critical extension \[y/N\]?') |
michael@0 | 240 | child.sendline('') |
michael@0 | 241 | child.expect(pexpect.EOF) |
michael@0 | 242 | else: |
michael@0 | 243 | os.system(base_exec_string) |
michael@0 | 244 | os.system("certutil -d " + db_dir + "/ -L -n " + name + " -r > " + |
michael@0 | 245 | out_name) |
michael@0 | 246 | return out_name |
michael@0 | 247 | |
michael@0 | 248 | def generate_child_cert(db_dir, dest_dir, noise_file, name, ca_nick, version, |
michael@0 | 249 | do_bc, is_ee, ocsp_url): |
michael@0 | 250 | """ |
michael@0 | 251 | Creates a new child certificate in an sql NSS database and as a der file |
michael@0 | 252 | Arguments: |
michael@0 | 253 | db_dir -- the location of the nss database (in sql format) |
michael@0 | 254 | dest_dir -- the location of for the output file |
michael@0 | 255 | noise_file -- the location of a noise file. |
michael@0 | 256 | name -- the nickname of the new certificate in the database and the |
michael@0 | 257 | common name of the certificate |
michael@0 | 258 | ca_nick -- the nickname of the isser of the new certificate |
michael@0 | 259 | version -- the version number of the certificate (valid certs must use |
michael@0 | 260 | 3) |
michael@0 | 261 | do_bc -- if the certificate should include the basic constraints |
michael@0 | 262 | (valid ca's should be true) |
michael@0 | 263 | is_ee -- is this and End Entity cert? false means intermediate |
michael@0 | 264 | ocsp_url -- optional location of the ocsp responder for this certificate |
michael@0 | 265 | this is included only if do_bc is set to True |
michael@0 | 266 | output: |
michael@0 | 267 | outname -- the location of the der file. |
michael@0 | 268 | """ |
michael@0 | 269 | |
michael@0 | 270 | out_name = dest_dir + "/" + name + ".der" |
michael@0 | 271 | base_exec_string = ("certutil -S -z " + noise_file + " -g 2048 -d " + |
michael@0 | 272 | db_dir + "/ -n " + name + " -v 120 -m " + |
michael@0 | 273 | str(random.randint(100, 40000000)) + " -s 'CN=" + name + |
michael@0 | 274 | ",O=PSM Testing,L=Mountain View,ST=California,C=US'" + |
michael@0 | 275 | " -t C,C,C -c " + ca_nick + " --certVersion=" + |
michael@0 | 276 | str(int(version))) |
michael@0 | 277 | if (do_bc): |
michael@0 | 278 | extra_arguments = " -2" |
michael@0 | 279 | if (ocsp_url): |
michael@0 | 280 | extra_arguments += " --extAIA" |
michael@0 | 281 | child = pexpect.spawn(base_exec_string + extra_arguments) |
michael@0 | 282 | child.logfile = sys.stdout |
michael@0 | 283 | child.expect('Is this a CA certificate \[y/N\]?') |
michael@0 | 284 | if (is_ee): |
michael@0 | 285 | child.sendline('N') |
michael@0 | 286 | else: |
michael@0 | 287 | child.sendline('y') |
michael@0 | 288 | child.expect('Enter the path length constraint, enter to skip \[<0 for unlimited path\]: >') |
michael@0 | 289 | child.sendline('') |
michael@0 | 290 | child.expect('Is this a critical extension \[y/N\]?') |
michael@0 | 291 | child.sendline('') |
michael@0 | 292 | if (ocsp_url): |
michael@0 | 293 | child.expect('\s+Choice >') |
michael@0 | 294 | child.sendline('2') |
michael@0 | 295 | child.expect('\s+Choice: >') |
michael@0 | 296 | child.sendline('7') |
michael@0 | 297 | child.expect('Enter data:') |
michael@0 | 298 | child.sendline(ocsp_url) |
michael@0 | 299 | child.expect('\s+Choice: >') |
michael@0 | 300 | child.sendline('0') |
michael@0 | 301 | child.expect('Add another location to the Authority Information Access extension \[y/N\]') |
michael@0 | 302 | child.sendline('') |
michael@0 | 303 | child.expect('Is this a critical extension \[y/N\]?') |
michael@0 | 304 | child.sendline('') |
michael@0 | 305 | child.expect(pexpect.EOF) |
michael@0 | 306 | else: |
michael@0 | 307 | os.system(base_exec_string) |
michael@0 | 308 | os.system("certutil -d " + db_dir + "/ -L -n " + name + " -r > " + |
michael@0 | 309 | out_name) |
michael@0 | 310 | return out_name |
michael@0 | 311 |