6d164ae
#!/usr/bin/python
6d164ae
# vim:set et sw=4:
6d164ae
#
6d164ae
# certdata2pem.py - splits certdata.txt into multiple files
6d164ae
#
6d164ae
# Copyright (C) 2009 Philipp Kern <pkern@debian.org>
6d164ae
# Copyright (C) 2013 Kai Engert <kaie@redhat.com>
6d164ae
#
6d164ae
# This program is free software; you can redistribute it and/or modify
6d164ae
# it under the terms of the GNU General Public License as published by
6d164ae
# the Free Software Foundation; either version 2 of the License, or
6d164ae
# (at your option) any later version.
6d164ae
#
6d164ae
# This program is distributed in the hope that it will be useful,
6d164ae
# but WITHOUT ANY WARRANTY; without even the implied warranty of
6d164ae
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
6d164ae
# GNU General Public License for more details.
6d164ae
#
6d164ae
# You should have received a copy of the GNU General Public License
6d164ae
# along with this program; if not, write to the Free Software
6d164ae
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301,
6d164ae
# USA.
6d164ae
6d164ae
import base64
6d164ae
import os.path
6d164ae
import re
6d164ae
import sys
6d164ae
import textwrap
6d164ae
import subprocess
6d164ae
import getopt
6d164ae
import asn1
6d164ae
from cryptography import x509
6d164ae
from cryptography.hazmat.primitives import hashes
c4c1a32
from datetime import datetime
c4c1a32
from dateutil.parser import parse
6d164ae
6d164ae
objects = []
6d164ae
6d164ae
pemcerts = []
6d164ae
6d164ae
certdata='./certdata.txt'
6d164ae
pem='./cert.pem'
6d164ae
output='./certdata_out.txt'
6d164ae
trust='CKA_TRUST_CODE_SIGNING'
6d164ae
merge_label="Non-Mozilla Object Signing Only Certificate"
c4c1a32
dateString='thisyear'
6d164ae
6d164ae
trust_types = {
6d164ae
  "CKA_TRUST_SERVER_AUTH",
6d164ae
  "CKA_TRUST_EMAIL_PROTECTION",
6d164ae
  "CKA_TRUST_CODE_SIGNING"
6d164ae
}
6d164ae
6d164ae
attribute_types = {
6d164ae
    "CKA_CLASS" : "CK_OBJECT_CLASS",
6d164ae
    "CKA_TOKEN" : "CK_BBOOL",
6d164ae
    "CKA_PRIVATE" : "CK_BBOOL",
6d164ae
    "CKA_MODIFIABLE" : "CK_BBOOL",
6d164ae
    "CKA_LABEL" : "UTF8",
6d164ae
    "CKA_CERTIFICATE_TYPE" : "CK_CERTIFICATE_TYPE",
6d164ae
    "CKA_SUBJECT" : "MULTILINE_OCTAL",
6d164ae
    "CKA_ID" : "UTF8",
6d164ae
    "CKA_CERT_SHA1_HASH" : "MULTILINE_OCTAL",
6d164ae
    "CKA_CERT_MD5_HASH" : "MULTILINE_OCTAL",
6d164ae
    "CKA_ISSUER" : "MULTILINE_OCTAL",
6d164ae
    "CKA_SERIAL_NUMBER" : "MULTILINE_OCTAL",
6d164ae
    "CKA_VALUE" : "MULTILINE_OCTAL",
6d164ae
    "CKA_NSS_MOZILLA_CA_POLICY" : "CK_BBOOL",
6d164ae
    "CKA_NSS_SERVER_DISTRUST_AFTER" : "Distrust",
6d164ae
    "CKA_NSS_EMAIL_DISTRUST_AFTER" : "Distrust",
6d164ae
    "CKA_TRUST_SERVER_AUTH" : "CK_TRUST",
6d164ae
    "CKA_TRUST_EMAIL_PROTECTION" : "CK_TRUST",
6d164ae
    "CKA_TRUST_CODE_SIGNING" : "CK_TRUST",
6d164ae
    "CKA_TRUST_STEP_UP_APPROVED" : "CK_BBOOL"
6d164ae
}
6d164ae
6d164ae
def printable_serial(obj):
6d164ae
  return ".".join([str(x) for x in obj['CKA_SERIAL_NUMBER']])
6d164ae
6d164ae
def getSerial(cert):
6d164ae
    encoder = asn1.Encoder()
6d164ae
    encoder.start()
6d164ae
    encoder.write(cert.serial_number)
6d164ae
    return encoder.output()
6d164ae
6d164ae
def dumpOctal(f,value):
6d164ae
    for i in range(len(value)) :
6d164ae
        if  i % 16 == 0 :
6d164ae
            f.write("\n")
6d164ae
        f.write("\\%03o"%int.from_bytes(value[i:i+1],sys.byteorder))
6d164ae
    f.write("\nEND\n")
6d164ae
c4c1a32
# in python 3.8 this can be replaced with return byteval.hex(':',1)
6d164ae
def formatHex(byteval) :
6d164ae
    string=byteval.hex()
6d164ae
    string_out=""
6d164ae
    for i in range(0,len(string)-2,2) :
6d164ae
         string_out += string[i:i+2] + ':'
6d164ae
    string_out += string[-2:]
6d164ae
    return string_out
6d164ae
c4c1a32
def getdate(dateString):
c4c1a32
    print("dateString= %s"%dateString)
c4c1a32
    if dateString.upper() == "THISYEAR":
c4c1a32
        return datetime(datetime.today().year,12,31,11,59,59,9999)
c4c1a32
    if dateString.upper() == "TODAY":
c4c1a32
        return datetime.today()
c4c1a32
    return parse(dateString, fuzzy=True);
c4c1a32
c4c1a32
def getTrust(objlist, serial, issuer) :
c4c1a32
    for obj in objlist:
c4c1a32
        if obj['CKA_CLASS'] == 'CKO_NSS_TRUST' and obj['CKA_SERIAL_NUMBER'] == serial and obj['CKA_ISSUER'] == issuer:
c4c1a32
            return obj
c4c1a32
    return None
c4c1a32
c4c1a32
def isDistrusted(obj) :
c4c1a32
    if (obj == None):
c4c1a32
        return False
c4c1a32
    return obj['CKA_TRUST_SERVER_AUTH'] == 'CKT_NSS_NOT_TRUSTED' and obj['CKA_TRUST_EMAIL_PROTECTION'] == 'CKT_NSS_NOT_TRUSTED' and obj['CKA_TRUST_CODE_SIGNING'] == 'CKT_NSS_NOT_TRUSTED'
c4c1a32
6d164ae
try:
c4c1a32
    opts, args = getopt.getopt(sys.argv[1:],"c:o:p:t:l:x:",)
6d164ae
except getopt.GetoptError as err:
6d164ae
    print(err)
6d164ae
    print(sys.argv[0] + ' [-c certdata] [-p pem] [-o certdata_target] [-t trustvalue] [-l merge_label]')
6d164ae
    print('-c certdata         certdata file to merge to (default="'+certdata+'")');
6d164ae
    print('-p pem              pem file with CAs to merge from (default="'+pem+'")');
6d164ae
    print('-o certdata_target  resulting output file (default="'+output+'")');
6d164ae
    print('-t trustvalue       what these CAs are trusted for (default="'+trust+'")');
6d164ae
    print('-l merge_label      what label CAs that aren\'t in certdata (default="'+merge_label+'")');
c4c1a32
    print('-x date             remove all certs that expire before data (default='+dateString+')');
6d164ae
    sys.exit(2)
6d164ae
6d164ae
for opt, arg in opts:
6d164ae
    if opt == '-c' :
6d164ae
        certdata = arg
6d164ae
    elif opt == '-p' :
6d164ae
        pem = arg
6d164ae
    elif opt == '-o' :
6d164ae
        output = arg
6d164ae
    elif opt == '-t' :
6d164ae
        trust = arg
6d164ae
    elif opt == '-l' :
6d164ae
        merge_label = arg
c4c1a32
    elif opt == '-x' :
c4c1a32
        dateString = arg
c4c1a32
c4c1a32
# parse dateString
c4c1a32
verifyDate = True
c4c1a32
if dateString.upper() == "NEVER":
c4c1a32
   verifyDate = False
c4c1a32
else:
c4c1a32
   date = getdate(dateString)
c4c1a32
6d164ae
6d164ae
# read the pem file
6d164ae
in_cert, certvalue = False, ""
6d164ae
for line in open(pem, 'r'):
6d164ae
    if not in_cert:
6d164ae
       if line.find("BEGIN CERTIFICATE") != -1:
6d164ae
            in_cert = True;
6d164ae
       continue
6d164ae
    # Ignore comment lines and blank lines.
6d164ae
    if line.startswith('#'):
6d164ae
        continue
6d164ae
    if len(line.strip()) == 0:
6d164ae
        continue
6d164ae
    if line.find("END CERTIFICATE") != -1 :
6d164ae
       pemcerts.append(certvalue);
6d164ae
       certvalue = "";
6d164ae
       in_cert = False;
6d164ae
       continue
6d164ae
    certvalue += line;
6d164ae
6d164ae
# read the certdata.txt file
6d164ae
in_data, in_multiline, in_obj = False, False, False
6d164ae
field, ftype, value, binval, obj = None, None, None, bytearray(), dict()
6d164ae
header, comment = "", ""
6d164ae
for line in open(certdata, 'r'):
6d164ae
    # Ignore the file header.
6d164ae
    if not in_data:
6d164ae
        header += line
6d164ae
        if line.startswith('BEGINDATA'):
6d164ae
            in_data = True
6d164ae
        continue
6d164ae
    # Ignore comment lines. 
6d164ae
    if line.startswith('#'):
6d164ae
        comment += line
6d164ae
        continue
6d164ae
6d164ae
    # Empty lines are significant if we are inside an object.
6d164ae
    if in_obj and len(line.strip()) == 0:
6d164ae
        # collect all the inline comments in this object
6d164ae
        obj['Comment'] += comment
6d164ae
        comment = ""
6d164ae
        objects.append(obj)
6d164ae
        obj = dict()
6d164ae
        in_obj = False
6d164ae
        continue
6d164ae
    if len(line.strip()) == 0:
6d164ae
        continue
6d164ae
    if in_multiline:
6d164ae
        if not line.startswith('END'):
6d164ae
            if ftype == 'MULTILINE_OCTAL':
6d164ae
                line = line.strip()
6d164ae
                for i in re.finditer(r'\\([0-3][0-7][0-7])', line):
6d164ae
                    integ = int(i.group(1), 8)
6d164ae
                    binval.extend((integ).to_bytes(1, sys.byteorder))
6d164ae
                obj[field] = binval
6d164ae
            else:
6d164ae
                value += line
6d164ae
                obj[field] = value
6d164ae
            continue
6d164ae
        in_multiline = False
6d164ae
        continue
6d164ae
    if line.startswith('CKA_CLASS'):
6d164ae
        in_obj = True
6d164ae
        obj['Comment'] = comment
6d164ae
        comment = ""
6d164ae
    line_parts = line.strip().split(' ', 2)
6d164ae
    if len(line_parts) > 2:
6d164ae
        field, ftype = line_parts[0:2]
6d164ae
        value = ' '.join(line_parts[2:])
6d164ae
    elif len(line_parts) == 2:
6d164ae
        field, ftype = line_parts
6d164ae
        value = None
6d164ae
    else:
6d164ae
        raise NotImplementedError('line_parts < 2 not supported.\n' + line)
6d164ae
    if ftype == 'MULTILINE_OCTAL':
6d164ae
        in_multiline = True
6d164ae
        value = ""
6d164ae
        binval = bytearray()
6d164ae
        continue
6d164ae
    obj[field] = value
6d164ae
if len(list(obj.items())) > 0:
6d164ae
    objects.append(obj)
6d164ae
c4c1a32
# strip out expired certificates from certdata.txt
c4c1a32
if verifyDate :
c4c1a32
    for obj in objects:
c4c1a32
        if obj['CKA_CLASS'] == 'CKO_CERTIFICATE' :
c4c1a32
            cert = x509.load_der_x509_certificate(obj['CKA_VALUE'])
c4c1a32
            if (cert.not_valid_after <= date) :
c4c1a32
                trust_obj = getTrust(objects,obj['CKA_SERIAL_NUMBER'],obj['CKA_ISSUER'])
c4c1a32
                # we don't remove distrusted expired certificates
c4c1a32
                if  not isDistrusted(trust_obj) :
c4c1a32
                    print("  Remove cert %s"%obj['CKA_LABEL'])
c4c1a32
                    print("     Expires: %s"%cert.not_valid_after.strftime("%m/%d/%Y"))
c4c1a32
                    print("     Prune time %s: "%date.strftime("%m/%d/%Y"))
c4c1a32
                    obj['Comment'] = None;
c4c1a32
                    if (trust_obj != None):
c4c1a32
                        trust_obj['Comment'] = None;
c4c1a32
6d164ae
# now merge the results
6d164ae
for certval in pemcerts:
6d164ae
    certder = base64.b64decode(certval)
6d164ae
    cert = x509.load_der_x509_certificate(certder)
6d164ae
    try:
6d164ae
        label=cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value
6d164ae
    except:
6d164ae
        try:
6d164ae
            label=cert.subject.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_UNIT_NAME)[0].value
6d164ae
        except:
6d164ae
            try:
6d164ae
                label=cert.subject.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_NAME)[0].value
6d164ae
            except:
6d164ae
                label="Unknown Certificate"
c4c1a32
    if cert.not_valid_after <= date:
c4c1a32
        print("  Skipping code signing cert %s"%label)
c4c1a32
        print("     Expires: %s"%cert.not_valid_after.strftime("%m/%d/%Y"))
c4c1a32
        print("     Prune time %s: "%date.strftime("%m/%d/%Y"))
c4c1a32
        continue
c4c1a32
    certhashsha1 = cert.fingerprint(hashes.SHA1())
c4c1a32
    certhashmd5 =  cert.fingerprint(hashes.MD5())
6d164ae
    
6d164ae
    
6d164ae
    found = False
6d164ae
    # see if it exists in certdata.txt
6d164ae
    for obj in objects:
6d164ae
        # we only need to check the trust objects, because
6d164ae
        # that is the object we would modify if it exists
6d164ae
        if obj['CKA_CLASS'] != 'CKO_NSS_TRUST':
6d164ae
            continue
6d164ae
        # explicitly distrusted certs don't have a hash value
6d164ae
        if not 'CKA_CERT_SHA1_HASH' in obj:
6d164ae
            continue
6d164ae
        if obj['CKA_CERT_SHA1_HASH'] != certhashsha1:
6d164ae
            continue
6d164ae
        obj[trust] = 'CKT_NSS_TRUSTED_DELEGATOR'
6d164ae
        found = True
c4c1a32
        print('Updating "'+label+'" with code signing');
6d164ae
        break
6d164ae
    if  found :
6d164ae
        continue
6d164ae
    # append this certificate
6d164ae
    obj=dict()
6d164ae
    time='%a %b %d %H:%M:%S %Y'
6d164ae
    comment  = '# ' + merge_label + '\n# %s "'+label+'"\n'
6d164ae
    comment +=  '# Issuer: ' + cert.issuer.rfc4514_string() + '\n'
6d164ae
    comment +=  '# Serial Number:'
6d164ae
    sn=cert.serial_number
6d164ae
    if sn < 0x100000:
6d164ae
        comment +=  ' %d (0x%x)\n'%(sn,sn)
6d164ae
    else:
6d164ae
        comment +=  formatHex(sn.to_bytes((sn.bit_length()+7)//8,"big")) + '\n'
6d164ae
    comment +=  '# Subject: ' + cert.subject.rfc4514_string() + '\n'
6d164ae
    comment +=  '# Not Valid Before: ' + cert.not_valid_before.strftime(time) + '\n'
6d164ae
    comment +=  '# Not Valid After: ' + cert.not_valid_after.strftime(time) + '\n'
6d164ae
    comment +=  '# Fingerprint (MD5): ' + formatHex(certhashmd5) + '\n'
6d164ae
    comment +=  '# Fingerprint (SHA1): ' + formatHex(certhashsha1) + '\n'
6d164ae
    obj['Comment']= comment%"Certificate"
6d164ae
    obj['CKA_CLASS'] = 'CKO_CERTIFICATE'
6d164ae
    obj['CKA_TOKEN'] = 'CK_TRUE'
6d164ae
    obj['CKA_PRIVATE'] = 'CK_FALSE'
6d164ae
    obj['CKA_MODIFIABLE'] = 'CK_FALSE'
6d164ae
    obj['CKA_LABEL'] = '"' + label + '"'
6d164ae
    obj['CKA_CERTIFICATE_TYPE'] = 'CKC_X_509'
6d164ae
    obj['CKA_SUBJECT'] = cert.subject.public_bytes()
6d164ae
    obj['CKA_ID'] = '"0"'
6d164ae
    obj['CKA_ISSUER'] = cert.issuer.public_bytes()
6d164ae
    obj['CKA_SERIAL_NUMBER'] = getSerial(cert)
6d164ae
    obj['CKA_VALUE'] = certder
6d164ae
    obj['CKA_NSS_MOZILLA_CA_POLICY'] = 'CK_FALSE'
6d164ae
    obj['CKA_NSS_SERVER_DISTRUST_AFTER'] = 'CK_FALSE'
6d164ae
    obj['CKA_NSS_EMAIL_DISTRUST_AFTER'] = 'CK_FALSE'
6d164ae
    objects.append(obj)
6d164ae
6d164ae
    # append the trust values
6d164ae
    obj=dict()
6d164ae
    obj['Comment']= comment%"Trust for"
c4c1a32
    obj['CKA_CLASS'] = 'CKO_NSS_TRUST'
6d164ae
    obj['CKA_TOKEN'] = 'CK_TRUE'
6d164ae
    obj['CKA_PRIVATE'] = 'CK_FALSE'
6d164ae
    obj['CKA_MODIFIABLE'] = 'CK_FALSE'
6d164ae
    obj['CKA_LABEL'] = '"' + label + '"'
6d164ae
    obj['CKA_CERT_SHA1_HASH'] = certhashsha1
6d164ae
    obj['CKA_CERT_MD5_HASH'] = certhashmd5
6d164ae
    obj['CKA_ISSUER'] = cert.issuer.public_bytes()
6d164ae
    obj['CKA_SERIAL_NUMBER'] = getSerial(cert)
6d164ae
    for t in list(trust_types):
6d164ae
       if t == trust:
6d164ae
          obj[t] = 'CKT_NSS_TRUSTED_DELEGATOR'
6d164ae
       else:
6d164ae
          obj[t] = 'CKT_NSS_MUST_VERIFY_TRUST'
6d164ae
    obj['CKA_TRUST_STEP_UP_APPROVED'] = 'CK_FALSE'
6d164ae
    objects.append(obj)
c4c1a32
    print('Adding code signing cert "'+label+'"');
6d164ae
6d164ae
# now dump the results
6d164ae
f = open(output, 'w')
6d164ae
f.write(header)
6d164ae
for obj in objects:
6d164ae
    if 'Comment' in obj:
c4c1a32
        # if comment is None, we've deleted the entry above
c4c1a32
        if obj['Comment'] == None:
c4c1a32
            continue
6d164ae
        f.write(obj['Comment'])
6d164ae
    else:
6d164ae
        print("Object with no comment!!")
6d164ae
        print(obj)
6d164ae
    for field in list(attribute_types.keys()):
6d164ae
        if not field in obj:
6d164ae
            continue
6d164ae
        ftype = attribute_types[field];
6d164ae
        if ftype == 'Distrust':
6d164ae
            if obj[field] == 'CK_FALSE':
6d164ae
                ftype = 'CK_BBOOL'
6d164ae
            else:
6d164ae
                ftype = 'MULTILINE_OCTAL'
6d164ae
        f.write("%s %s"%(field,ftype));
6d164ae
        if ftype == 'MULTILINE_OCTAL':
6d164ae
           dumpOctal(f,obj[field])
6d164ae
        else:
6d164ae
           f.write(" %s\n"%obj[field])
6d164ae
    f.write("\n")
6d164ae
f.close