Bug 1228794 - Convert test_getchain.js to generate certificates at build time. r=keeler

With this change, CertUtils.py is no longer needed.
This commit is contained in:
Cykesiopka 2015-12-01 00:28:00 +01:00
parent aea0abf4da
commit 48152475d6
13 changed files with 85 additions and 438 deletions

View File

@ -16,6 +16,7 @@ TEST_DIRS += [
'test_cert_trust',
'test_cert_version',
'test_ev_certs',
'test_getchain',
'test_intermediate_basic_usage_constraints',
'test_keysize',
'test_keysize_ev',

View File

@ -1,344 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This file requires openssl 1.0.0 at least
import os
import random
import pexpect
import time
import sys
default_validity_in_days = 10 * 365
def generate_cert_generic(db_dir, dest_dir, serial_num, key_type, name,
ext_text, signer_key_filename = "",
signer_cert_filename = "",
subject_string = "",
key_size = '2048',
validity_in_days = default_validity_in_days):
"""
Generate an x509 certificate with a sha256 signature
Arguments:
db_dir -- location of the temporary params for the certificate
dest_dir -- location of the x509 cert
serial_num -- serial number for the cert (must be unique for each signer
key)
key_type -- the type of key generated: potential values: 'rsa' or any
of the curves found by 'openssl ecparam -list_curves'
name -- the common name for the cert, will match the prefix of the
output cert
ext_text -- the text for the x509 extensions to be added to the
certificate
signer_key_filename -- the filename of the key from which the cert will
be signed if null the cert will be self signed (think CA
roots).
signer_cert_filename -- the certificate that will sign the certificate
(used to extract signer info) it must be in DER format.
key_size -- public key size for RSA certs
validity_in_days -- the number of days the cert will be valid for
output:
key_name -- the filename of the key file (PEM format)
cert_name -- the filename of the output certificate (DER format)
"""
key_name = db_dir + "/"+ name + ".key"
if key_type == 'rsa':
os.system ("openssl genpkey -algorithm RSA -out " + key_name +
" -pkeyopt rsa_keygen_bits:" + key_size)
else:
#assume is ec
os.system("openssl ecparam -out " + key_name + " -name "+ key_type +
" -genkey");
csr_name = db_dir + "/"+ name + ".csr"
if not subject_string:
subject_string = '/CN=' + name
os.system ("openssl req -new -key " + key_name + " -days 3650" +
" -extensions v3_ca -batch -out " + csr_name +
" -utf8 -subj '" + subject_string + "'")
extensions_filename = db_dir + "/openssl-exts"
f = open(extensions_filename,'w')
f.write(ext_text)
f.close()
cert_name = dest_dir + "/"+ name + ".der"
if not signer_key_filename:
signer_key_filename = key_name;
os.system ("openssl x509 -req -sha256 -in " + csr_name +
" -days " + str(validity_in_days) +
" -signkey " + signer_key_filename +
" -set_serial " + str(serial_num) +
" -extfile " + extensions_filename +
" -outform DER -out "+ cert_name)
else:
os.system ("openssl x509 -req -sha256 -in " + csr_name +
" -CAkey " + signer_key_filename +
" -CA " + signer_cert_filename + " -CAform DER " +
" -days " + str(validity_in_days) +
" -set_serial " + str(serial_num) + " -out " + cert_name +
" -outform DER -extfile " + extensions_filename)
return key_name, cert_name
def generate_int_and_ee(db_dir, dest_dir, ca_key, ca_cert, name, int_ext_text,
ee_ext_text, key_type = 'rsa',
ee_validity_in_days = default_validity_in_days):
"""
Generate an intermediate and ee signed by the generated intermediate. The
name of the intermediate files will be the name '.der' or '.key'. The name
of the end entity files with be "ee-"+ name plus the appropiate prefixes.
The serial number will be generated radomly so it is potentially possible
to have problem (but very unlikely).
Arguments:
db_dir -- location of the temporary params for the certificate
dest_dir -- location of the x509 cert
ca_key -- The filename of the key that will be used to sign the
intermediate (PEM FORMAT)
ca_cert -- The filename of the cert that will be used to sign the
intermediate, it MUST be the private key for the ca_key.
The file must be in DER format.
name -- the common name for the intermediate, will match the prefix
of the output intermediate. The ee will have the name
prefixed with "ee-"
int_ext_text -- the text for the x509 extensions to be added to the
intermediate certificate
ee_ext_text -- the text for the x509 extensions to be added to the
end entity certificate
key_type -- the type of key generated: potential values: 'rsa' or any
of the curves found by 'openssl ecparam -list_curves'
ee_validity_in_days -- the number of days the end-entity cert will be
valid for
output:
int_key -- the filename of the intermeidate key file (PEM format)
int_cert -- the filename of the intermediate certificate (DER format)
ee_key -- the filename of the end entity key file (PEM format)
ee_cert -- the filename of the end entity certficate (DER format)
"""
[int_key, int_cert] = generate_cert_generic(db_dir, dest_dir,
random.randint(100,40000000),
key_type, "int-" + name,
int_ext_text,
ca_key, ca_cert)
[ee_key, ee_cert] = generate_cert_generic(
db_dir,
dest_dir,
random.randint(100,40000000),
key_type,
name,
ee_ext_text,
int_key,
int_cert,
validity_in_days = ee_validity_in_days)
return int_key, int_cert, ee_key, ee_cert
def generate_pkcs12(db_dir, dest_dir, der_cert_filename, key_pem_filename,
prefix):
"""
Generate a pkcs12 file for a given certificate name (in der format) and
a key filename (key in pem format). The output file will have an empty
password.
Arguments:
input:
db_dir -- location of the temporary params for the certificate
dest_dir -- location of the x509 cert
der_cert_filename -- the filename of the certificate to be included in the
output file (DER format)
key_pem_filename -- the filename of the private key of the certificate to
(PEM format)
prefix -- the name to be prepended to the output pkcs12 file.
output:
pk12_filename -- the filename of the outgoing pkcs12 output file
"""
#make pem cert file from der filename
pem_cert_filename = db_dir + "/" + prefix + ".pem"
pk12_filename = dest_dir + "/" + prefix + ".p12"
os.system("openssl x509 -inform der -in " + der_cert_filename + " -out " +
pem_cert_filename )
#now make pkcs12 file
child = pexpect.spawn("openssl pkcs12 -export -in " + pem_cert_filename +
" -inkey " + key_pem_filename + " -out " +
pk12_filename)
child.expect('Enter Export Password:')
child.sendline('')
child.expect('Verifying - Enter Export Password:')
child.sendline('')
child.expect(pexpect.EOF)
return pk12_filename
def print_cert_info(cert_filename):
"""
Prints out information (such as fingerprints) for the given cert.
The information printed is sufficient for enabling EV for the given cert
if necessary.
Note: The utility 'pp' is available as part of NSS.
Arguments:
cert_filename -- the filename of the cert in DER format
"""
os.system('pp -t certificate-identity -i ' + cert_filename)
def init_nss_db(db_dir):
"""
Remove the current nss database in the specified directory and create a new
nss database with the sql format.
Arguments
db_dir -- the desired location of the new database
output
noise_file -- the path to a noise file suitable to generate TEST
certificates. This does not have enough entropy for a real
secret
pwd_file -- the patch to the secret file used for the database.
this file should be empty.
"""
nss_db_files = ["cert9.db", "key4.db", "pkcs11.txt"]
for file in nss_db_files:
if os.path.isfile(file):
os.remove(file)
# create noise file
noise_file = db_dir + "/noise"
nf = open(noise_file, 'w')
nf.write(str(time.time()))
nf.close()
# create pwd file
pwd_file = db_dir + "/pwfile"
pf = open(pwd_file, 'w')
pf.write("\n")
pf.close()
# create nss db
os.system("certutil -d sql:" + db_dir + " -N -f " + pwd_file);
return [noise_file, pwd_file]
def generate_self_signed_cert(db_dir, dest_dir, noise_file, name, version, do_bc, is_ca):
"""
Creates a new self-signed certificate in an sql NSS database and as a der file
Arguments:
db_dir -- the location of the nss database (in sql format)
dest_dir -- the location of for the output file
noise_file -- the location of a noise file.
name -- the nickname of the new certificate in the database and the
common name of the certificate
version -- the version number of the certificate (valid certs must use
3)
do_bc -- if the certificate should include the basic constraints
(valid ca's should be true)
is_ca -- mark the extenstion true or false
output:
outname -- the location of the der file.
"""
out_name = dest_dir + "/" + name + ".der"
base_exec_string = ("certutil -S -z " + noise_file + " -g 2048 -d sql:" +
db_dir + "/ -n " + name + " -v 120 -s 'CN=" + name +
",O=PSM Testing,L=Mountain View,ST=California,C=US'" +
" -t C,C,C -x --certVersion=" + str(int(version)))
if (do_bc):
child = pexpect.spawn(base_exec_string + " -2")
child.logfile = sys.stdout
child.expect('Is this a CA certificate \[y/N\]?')
if (is_ca):
child.sendline('y')
else:
child.sendline('N')
child.expect('Enter the path length constraint, enter to skip \[<0 for unlimited path\]: >')
child.sendline('')
child.expect('Is this a critical extension \[y/N\]?')
child.sendline('')
child.expect(pexpect.EOF)
else:
os.system(base_exec_string)
os.system("certutil -d sql:" + db_dir + "/ -L -n " + name + " -r > " +
out_name)
return out_name
def generate_ca_cert(db_dir, dest_dir, noise_file, name, version, do_bc):
"""
Creates a new CA certificate in an sql NSS database and as a der file
Arguments:
db_dir -- the location of the nss database (in sql format)
dest_dir -- the location of for the output file
noise_file -- the location of a noise file.
name -- the nickname of the new certificate in the database and the
common name of the certificate
version -- the version number of the certificate (valid certs must use
3)
do_bc -- if the certificate should include the basic constraints
(valid ca's should be true)
output:
outname -- the location of the der file.
"""
return generate_self_signed_cert(db_dir, dest_dir, noise_file, name, version, do_bc, True)
def generate_child_cert(db_dir, dest_dir, noise_file, name, ca_nick, version,
do_bc, is_ee, ocsp_url):
"""
Creates a new child certificate in an sql NSS database and as a der file
Arguments:
db_dir -- the location of the nss database (in sql format)
dest_dir -- the location of for the output file
noise_file -- the location of a noise file.
name -- the nickname of the new certificate in the database and the
common name of the certificate
ca_nick -- the nickname of the isser of the new certificate
version -- the version number of the certificate (valid certs must use
3)
do_bc -- if the certificate should include the basic constraints
(valid ca's should be true)
is_ee -- is this and End Entity cert? false means intermediate
ocsp_url -- optional location of the ocsp responder for this certificate
this is included only if do_bc is set to True
output:
outname -- the location of the der file.
"""
out_name = dest_dir + "/" + name + ".der"
base_exec_string = ("certutil -S -z " + noise_file + " -g 2048 -d sql:" +
db_dir + "/ -n " + name + " -v 120 -m " +
str(random.randint(100, 40000000)) + " -s 'CN=" + name +
",O=PSM Testing,L=Mountain View,ST=California,C=US'" +
" -t C,C,C -c " + ca_nick + " --certVersion=" +
str(int(version)))
if (do_bc):
extra_arguments = " -2"
if (ocsp_url):
extra_arguments += " --extAIA"
child = pexpect.spawn(base_exec_string + extra_arguments)
child.logfile = sys.stdout
child.expect('Is this a CA certificate \[y/N\]?')
if (is_ee):
child.sendline('N')
else:
child.sendline('y')
child.expect('Enter the path length constraint, enter to skip \[<0 for unlimited path\]: >')
child.sendline('')
child.expect('Is this a critical extension \[y/N\]?')
child.sendline('')
if (ocsp_url):
child.expect('\s+Choice >')
child.sendline('2')
child.expect('\s+Choice: >')
child.sendline('7')
child.expect('Enter data:')
child.sendline(ocsp_url)
child.expect('\s+Choice: >')
child.sendline('0')
child.expect('Add another location to the Authority Information Access extension \[y/N\]')
child.sendline('')
child.expect('Is this a critical extension \[y/N\]?')
child.sendline('')
child.expect(pexpect.EOF)
else:
os.system(base_exec_string)
os.system("certutil -d sql:" + db_dir + "/ -L -n " + name + " -r > " +
out_name)
return out_name

View File

@ -18,6 +18,7 @@ subject:<subject distinguished name specification>
[subjectKey:<key specification>]
[signature:{sha256WithRSAEncryption,sha1WithRSAEncryption,
md5WithRSAEncryption,ecdsaWithSHA256}]
[serialNumber:<integer in the interval [1, 127]>]
[extension:<extension name:<extension-specific data>>]
[...]
@ -72,8 +73,11 @@ If an extension name has '[critical]' after it, it will be marked as
critical. Otherwise (by default), it will not be marked as critical.
TLSFeature values can either consist of a named value (currently only
'OCSPMustStaple' which corresponds to status_request) or a numeric tls feature
value (see rfc7633 for more information).
'OCSPMustStaple' which corresponds to status_request) or a numeric TLS
feature value (see rfc7633 for more information).
If a serial number is not explicitly specified, it is automatically
generated based on the contents of the certificate.
"""
from pyasn1.codec.der import decoder
@ -113,7 +117,12 @@ class NameConstraints(univ.Sequence):
)
class UnknownBaseError(Exception):
class Error(Exception):
"""Base class for exceptions in this module."""
pass
class UnknownBaseError(Error):
"""Base class for handling unexpected input in this module."""
def __init__(self, value):
super(UnknownBaseError, self).__init__()
@ -196,6 +205,7 @@ class UnknownNSCertTypeError(UnknownBaseError):
UnknownBaseError.__init__(self, value)
self.category = 'nsCertType'
class UnknownTLSFeature(UnknownBaseError):
"""Helper exception type to handle unknown TLS Features."""
@ -204,6 +214,17 @@ class UnknownTLSFeature(UnknownBaseError):
self.category = 'TLSFeature'
class InvalidSerialNumber(Error):
"""Exception type to handle invalid serial numbers."""
def __init__(self, value):
super(InvalidSerialNumber, self).__init__()
self.value = value
def __str__(self):
return repr(self.value)
def getASN1Tag(asn1Type):
"""Helper function for returning the base tag value of a given
type from the pyasn1 package"""
@ -309,6 +330,16 @@ def datetimeToTime(dt):
time.setComponentByName('generalTime', useful.GeneralizedTime(dt.strftime('%Y%m%d%H%M%SZ')))
return time
def serialBytesToString(serialBytes):
"""Takes a list of integers in the interval [0, 255] and returns
the corresponding serial number string."""
serialBytesLen = len(serialBytes)
if serialBytesLen > 127:
raise InvalidSerialNumber("{} bytes is too long".format(serialBytesLen))
# Prepend the ASN.1 INTEGER tag and length bytes.
stringBytes = [getASN1Tag(univ.Integer), serialBytesLen] + serialBytes
return ''.join(chr(b) for b in stringBytes)
class Certificate(object):
"""Utility class for reading a certificate specification and
generating a signed x509 certificate"""
@ -326,8 +357,12 @@ class Certificate(object):
self.extensions = None
self.subjectKey = pykey.keyFromSpecification('default')
self.issuerKey = pykey.keyFromSpecification('default')
self.serialNumber = None
self.decodeParams(paramStream)
self.serialNumber = self.generateSerialNumber()
# If a serial number wasn't specified, generate one based on
# the certificate contents.
if not self.serialNumber:
self.serialNumber = self.generateSerialNumber()
def generateSerialNumber(self):
"""Generates a serial number for this certificate based on its
@ -353,10 +388,7 @@ class Certificate(object):
# significant byte is set (to prevent a leading zero byte,
# which also wouldn't be valid).
serialBytes[0] |= 0x01
# Now prepend the ASN.1 INTEGER tag and length bytes.
serialBytes.insert(0, len(serialBytes))
serialBytes.insert(0, getASN1Tag(univ.Integer))
return ''.join(chr(b) for b in serialBytes)
return serialBytesToString(serialBytes)
def decodeParams(self, paramStream):
for line in paramStream.readlines():
@ -381,6 +413,13 @@ class Certificate(object):
self.setupKey('subject', value)
elif param == 'signature':
self.signature = value
elif param == 'serialNumber':
serialNumber = int(value)
# Ensure only serial numbers that conform to the rules listed in
# generateSerialNumber() are permitted.
if serialNumber < 1 or serialNumber > 127:
raise InvalidSerialNumber(value)
self.serialNumber = serialBytesToString([serialNumber])
else:
raise UnknownParameterTypeError(param)
@ -553,13 +592,12 @@ class Certificate(object):
def addTLSFeature(self, features, critical):
namedFeatures = {'OCSPMustStaple': 5}
featureList = [f.strip() for f in features.split(',')]
print "FeatureList is",featureList
sequence = univ.Sequence()
for feature in featureList:
featureValue = 0
try:
featureValue = int(feature)
except:
except ValueError:
try:
featureValue = namedFeatures[feature]
except:

View File

@ -8,19 +8,13 @@
do_get_profile(); // must be called before getting nsIX509CertDB
const certdb = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
// This is the list of certificates needed for the test
// The certificates prefixed by 'int-' are intermediates
// This is the list of certificates needed for the test.
var certList = [
'ee',
'ca-1',
'ca-2',
];
function load_cert(cert_name, trust_string) {
var cert_filename = cert_name + ".der";
addCertFromFile(certdb, "test_getchain/" + cert_filename, trust_string);
}
// Since all the ca's are identical expect for the serial number
// I have to grab them by enumerating all the certs and then finding
// the ones that I am interested in.
@ -31,7 +25,7 @@ function get_ca_array() {
while (enumerator.hasMoreElements()) {
let cert = enumerator.getNext().QueryInterface(Ci.nsIX509Cert);
if (cert.commonName == 'ca') {
ret_array[parseInt(cert.serialNumber)] = cert;
ret_array[parseInt(cert.serialNumber, 16)] = cert;
}
}
return ret_array;
@ -53,7 +47,7 @@ function check_matching_issuer_and_getchain(expected_issuer_serial, cert) {
function check_getchain(ee_cert, ssl_ca, email_ca){
// A certificate should first build a chain/issuer to
// a SSL trust domain, then an EMAIL trust domain and then
// and object signer trust domain
// an object signer trust domain.
const nsIX509Cert = Components.interfaces.nsIX509Cert;
certdb.setCertTrust(ssl_ca, nsIX509Cert.CA_CERT,
@ -73,8 +67,8 @@ function run_test() {
clearOCSPCache();
clearSessionCache();
for (let i = 0 ; i < certList.length; i++) {
load_cert(certList[i], ',,');
for (let cert of certList) {
addCertFromFile(certdb, `test_getchain/${cert}.pem`, ",,");
}
let ee_cert = certdb.findCertByNickname(null, 'ee');

View File

@ -0,0 +1,5 @@
issuer:ca
subject:ca
extension:basicConstraints:cA,
extension:keyUsage:cRLSign,keyCertSign
serialNumber:1

View File

@ -0,0 +1,5 @@
issuer:ca
subject:ca
extension:basicConstraints:cA,
extension:keyUsage:cRLSign,keyCertSign
serialNumber:2

View File

@ -0,0 +1,2 @@
issuer:ca
subject:ee

View File

@ -1,73 +0,0 @@
#!/usr/bin/python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import tempfile, os, sys
import random
import pexpect
import subprocess
import shutil
libpath = os.path.abspath('../psm_common_py')
sys.path.append(libpath)
import CertUtils
srcdir = os.getcwd()
db = tempfile.mkdtemp()
CA_basic_constraints = "basicConstraints = critical, CA:TRUE\n"
EE_basic_constraints = "basicConstraints = CA:FALSE\n"
CA_full_ku = ("keyUsage = digitalSignature, nonRepudiation, keyEncipherment, " +
"dataEncipherment, keyAgreement, keyCertSign, cRLSign\n")
CA_eku = ("extendedKeyUsage = critical, serverAuth, clientAuth, " +
"emailProtection, codeSigning\n")
authority_key_ident = "authorityKeyIdentifier = keyid, issuer\n"
subject_key_ident = "subjectKeyIdentifier = hash\n"
def self_sign_csr(db_dir, dst_dir, csr_name, key_file, serial_num, ext_text,
out_prefix):
extensions_filename = db_dir + "/openssl-exts"
f = open(extensions_filename, 'w')
f.write(ext_text)
f.close()
cert_name = dst_dir + "/" + out_prefix + ".der"
os.system ("openssl x509 -req -sha256 -days 3650 -in " + csr_name +
" -signkey " + key_file +
" -set_serial " + str(serial_num) +
" -extfile " + extensions_filename +
" -outform DER -out " + cert_name)
def generate_certs():
key_type = 'rsa'
ca_ext = CA_basic_constraints + CA_full_ku + subject_key_ident + CA_eku;
ee_ext_text = (EE_basic_constraints + authority_key_ident)
[ca_key, ca_cert] = CertUtils.generate_cert_generic(db,
srcdir,
1,
key_type,
'ca',
ca_ext)
CertUtils.generate_cert_generic(db,
srcdir,
100,
key_type,
'ee',
ee_ext_text,
ca_key,
ca_cert)
shutil.copy(ca_cert, srcdir + "/" + "ca-1.der")
self_sign_csr(db, srcdir, db + "/ca.csr", ca_key, 2, ca_ext, "ca-2")
os.remove(ca_cert);
generate_certs()

View File

@ -0,0 +1,19 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
test_certificates = (
'ca-1.pem',
'ca-2.pem',
'ee.pem',
)
for test_certificate in test_certificates:
input_file = test_certificate + '.certspec'
GENERATED_FILES += [test_certificate]
props = GENERATED_FILES[test_certificate]
props.script = '../pycert.py'
props.inputs = [input_file]
TEST_HARNESS_FILES.xpcshell.security.manager.ssl.tests.unit.test_getchain += ['!%s' % test_certificate]