Blob Blame History Raw
#!/usr/bin/python
# vim:set et sw=4:
#
# certdata2pem.py - splits certdata.txt into multiple files
#
# Copyright (C) 2009 Philipp Kern <pkern@debian.org>
# Copyright (C) 2013 Kai Engert <kaie@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301,
# USA.

import base64
import os.path
import re
import sys
import textwrap
import subprocess
import getopt
import asn1
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from datetime import datetime
from dateutil.parser import parse

objects = []

pemcerts = []

certdata='./certdata.txt'
pem='./cert.pem'
output='./certdata_out.txt'
trust='CKA_TRUST_CODE_SIGNING'
merge_label="Non-Mozilla Object Signing Only Certificate"
dateString='thisyear'

trust_types = {
  "CKA_TRUST_SERVER_AUTH",
  "CKA_TRUST_EMAIL_PROTECTION",
  "CKA_TRUST_CODE_SIGNING"
}

attribute_types = {
    "CKA_CLASS" : "CK_OBJECT_CLASS",
    "CKA_TOKEN" : "CK_BBOOL",
    "CKA_PRIVATE" : "CK_BBOOL",
    "CKA_MODIFIABLE" : "CK_BBOOL",
    "CKA_LABEL" : "UTF8",
    "CKA_CERTIFICATE_TYPE" : "CK_CERTIFICATE_TYPE",
    "CKA_SUBJECT" : "MULTILINE_OCTAL",
    "CKA_ID" : "UTF8",
    "CKA_CERT_SHA1_HASH" : "MULTILINE_OCTAL",
    "CKA_CERT_MD5_HASH" : "MULTILINE_OCTAL",
    "CKA_ISSUER" : "MULTILINE_OCTAL",
    "CKA_SERIAL_NUMBER" : "MULTILINE_OCTAL",
    "CKA_VALUE" : "MULTILINE_OCTAL",
    "CKA_NSS_MOZILLA_CA_POLICY" : "CK_BBOOL",
    "CKA_NSS_SERVER_DISTRUST_AFTER" : "Distrust",
    "CKA_NSS_EMAIL_DISTRUST_AFTER" : "Distrust",
    "CKA_TRUST_SERVER_AUTH" : "CK_TRUST",
    "CKA_TRUST_EMAIL_PROTECTION" : "CK_TRUST",
    "CKA_TRUST_CODE_SIGNING" : "CK_TRUST",
    "CKA_TRUST_STEP_UP_APPROVED" : "CK_BBOOL"
}

def printable_serial(obj):
  return ".".join([str(x) for x in obj['CKA_SERIAL_NUMBER']])

def getSerial(cert):
    encoder = asn1.Encoder()
    encoder.start()
    encoder.write(cert.serial_number)
    return encoder.output()

def dumpOctal(f,value):
    for i in range(len(value)) :
        if  i % 16 == 0 :
            f.write("\n")
        f.write("\\%03o"%int.from_bytes(value[i:i+1],sys.byteorder))
    f.write("\nEND\n")

# in python 3.8 this can be replaced with return byteval.hex(':',1)
def formatHex(byteval) :
    string=byteval.hex()
    string_out=""
    for i in range(0,len(string)-2,2) :
         string_out += string[i:i+2] + ':'
    string_out += string[-2:]
    return string_out

def getdate(dateString):
    print("dateString= %s"%dateString)
    if dateString.upper() == "THISYEAR":
        return datetime(datetime.today().year,12,31,11,59,59,9999)
    if dateString.upper() == "TODAY":
        return datetime.today()
    return parse(dateString, fuzzy=True);

def getTrust(objlist, serial, issuer) :
    for obj in objlist:
        if obj['CKA_CLASS'] == 'CKO_NSS_TRUST' and obj['CKA_SERIAL_NUMBER'] == serial and obj['CKA_ISSUER'] == issuer:
            return obj
    return None

def isDistrusted(obj) :
    if (obj == None):
        return False
    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'

try:
    opts, args = getopt.getopt(sys.argv[1:],"c:o:p:t:l:x:",)
except getopt.GetoptError as err:
    print(err)
    print(sys.argv[0] + ' [-c certdata] [-p pem] [-o certdata_target] [-t trustvalue] [-l merge_label]')
    print('-c certdata         certdata file to merge to (default="'+certdata+'")');
    print('-p pem              pem file with CAs to merge from (default="'+pem+'")');
    print('-o certdata_target  resulting output file (default="'+output+'")');
    print('-t trustvalue       what these CAs are trusted for (default="'+trust+'")');
    print('-l merge_label      what label CAs that aren\'t in certdata (default="'+merge_label+'")');
    print('-x date             remove all certs that expire before data (default='+dateString+')');
    sys.exit(2)

for opt, arg in opts:
    if opt == '-c' :
        certdata = arg
    elif opt == '-p' :
        pem = arg
    elif opt == '-o' :
        output = arg
    elif opt == '-t' :
        trust = arg
    elif opt == '-l' :
        merge_label = arg
    elif opt == '-x' :
        dateString = arg

# parse dateString
verifyDate = True
if dateString.upper() == "NEVER":
   verifyDate = False
else:
   date = getdate(dateString)


# read the pem file
in_cert, certvalue = False, ""
for line in open(pem, 'r'):
    if not in_cert:
       if line.find("BEGIN CERTIFICATE") != -1:
            in_cert = True;
       continue
    # Ignore comment lines and blank lines.
    if line.startswith('#'):
        continue
    if len(line.strip()) == 0:
        continue
    if line.find("END CERTIFICATE") != -1 :
       pemcerts.append(certvalue);
       certvalue = "";
       in_cert = False;
       continue
    certvalue += line;

# read the certdata.txt file
in_data, in_multiline, in_obj = False, False, False
field, ftype, value, binval, obj = None, None, None, bytearray(), dict()
header, comment = "", ""
for line in open(certdata, 'r'):
    # Ignore the file header.
    if not in_data:
        header += line
        if line.startswith('BEGINDATA'):
            in_data = True
        continue
    # Ignore comment lines. 
    if line.startswith('#'):
        comment += line
        continue

    # Empty lines are significant if we are inside an object.
    if in_obj and len(line.strip()) == 0:
        # collect all the inline comments in this object
        obj['Comment'] += comment
        comment = ""
        objects.append(obj)
        obj = dict()
        in_obj = False
        continue
    if len(line.strip()) == 0:
        continue
    if in_multiline:
        if not line.startswith('END'):
            if ftype == 'MULTILINE_OCTAL':
                line = line.strip()
                for i in re.finditer(r'\\([0-3][0-7][0-7])', line):
                    integ = int(i.group(1), 8)
                    binval.extend((integ).to_bytes(1, sys.byteorder))
                obj[field] = binval
            else:
                value += line
                obj[field] = value
            continue
        in_multiline = False
        continue
    if line.startswith('CKA_CLASS'):
        in_obj = True
        obj['Comment'] = comment
        comment = ""
    line_parts = line.strip().split(' ', 2)
    if len(line_parts) > 2:
        field, ftype = line_parts[0:2]
        value = ' '.join(line_parts[2:])
    elif len(line_parts) == 2:
        field, ftype = line_parts
        value = None
    else:
        raise NotImplementedError('line_parts < 2 not supported.\n' + line)
    if ftype == 'MULTILINE_OCTAL':
        in_multiline = True
        value = ""
        binval = bytearray()
        continue
    obj[field] = value
if len(list(obj.items())) > 0:
    objects.append(obj)

# strip out expired certificates from certdata.txt
if verifyDate :
    for obj in objects:
        if obj['CKA_CLASS'] == 'CKO_CERTIFICATE' :
            cert = x509.load_der_x509_certificate(obj['CKA_VALUE'])
            if (cert.not_valid_after <= date) :
                trust_obj = getTrust(objects,obj['CKA_SERIAL_NUMBER'],obj['CKA_ISSUER'])
                # we don't remove distrusted expired certificates
                if  not isDistrusted(trust_obj) :
                    print("  Remove cert %s"%obj['CKA_LABEL'])
                    print("     Expires: %s"%cert.not_valid_after.strftime("%m/%d/%Y"))
                    print("     Prune time %s: "%date.strftime("%m/%d/%Y"))
                    obj['Comment'] = None;
                    if (trust_obj != None):
                        trust_obj['Comment'] = None;

# now merge the results
for certval in pemcerts:
    certder = base64.b64decode(certval)
    cert = x509.load_der_x509_certificate(certder)
    try:
        label=cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value
    except:
        try:
            label=cert.subject.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_UNIT_NAME)[0].value
        except:
            try:
                label=cert.subject.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_NAME)[0].value
            except:
                label="Unknown Certificate"
    if cert.not_valid_after <= date:
        print("  Skipping code signing cert %s"%label)
        print("     Expires: %s"%cert.not_valid_after.strftime("%m/%d/%Y"))
        print("     Prune time %s: "%date.strftime("%m/%d/%Y"))
        continue
    certhashsha1 = cert.fingerprint(hashes.SHA1())
    certhashmd5 =  cert.fingerprint(hashes.MD5())
    
    
    found = False
    # see if it exists in certdata.txt
    for obj in objects:
        # we only need to check the trust objects, because
        # that is the object we would modify if it exists
        if obj['CKA_CLASS'] != 'CKO_NSS_TRUST':
            continue
        # explicitly distrusted certs don't have a hash value
        if not 'CKA_CERT_SHA1_HASH' in obj:
            continue
        if obj['CKA_CERT_SHA1_HASH'] != certhashsha1:
            continue
        obj[trust] = 'CKT_NSS_TRUSTED_DELEGATOR'
        found = True
        print('Updating "'+label+'" with code signing');
        break
    if  found :
        continue
    # append this certificate
    obj=dict()
    time='%a %b %d %H:%M:%S %Y'
    comment  = '# ' + merge_label + '\n# %s "'+label+'"\n'
    comment +=  '# Issuer: ' + cert.issuer.rfc4514_string() + '\n'
    comment +=  '# Serial Number:'
    sn=cert.serial_number
    if sn < 0x100000:
        comment +=  ' %d (0x%x)\n'%(sn,sn)
    else:
        comment +=  formatHex(sn.to_bytes((sn.bit_length()+7)//8,"big")) + '\n'
    comment +=  '# Subject: ' + cert.subject.rfc4514_string() + '\n'
    comment +=  '# Not Valid Before: ' + cert.not_valid_before.strftime(time) + '\n'
    comment +=  '# Not Valid After: ' + cert.not_valid_after.strftime(time) + '\n'
    comment +=  '# Fingerprint (MD5): ' + formatHex(certhashmd5) + '\n'
    comment +=  '# Fingerprint (SHA1): ' + formatHex(certhashsha1) + '\n'
    obj['Comment']= comment%"Certificate"
    obj['CKA_CLASS'] = 'CKO_CERTIFICATE'
    obj['CKA_TOKEN'] = 'CK_TRUE'
    obj['CKA_PRIVATE'] = 'CK_FALSE'
    obj['CKA_MODIFIABLE'] = 'CK_FALSE'
    obj['CKA_LABEL'] = '"' + label + '"'
    obj['CKA_CERTIFICATE_TYPE'] = 'CKC_X_509'
    obj['CKA_SUBJECT'] = cert.subject.public_bytes()
    obj['CKA_ID'] = '"0"'
    obj['CKA_ISSUER'] = cert.issuer.public_bytes()
    obj['CKA_SERIAL_NUMBER'] = getSerial(cert)
    obj['CKA_VALUE'] = certder
    obj['CKA_NSS_MOZILLA_CA_POLICY'] = 'CK_FALSE'
    obj['CKA_NSS_SERVER_DISTRUST_AFTER'] = 'CK_FALSE'
    obj['CKA_NSS_EMAIL_DISTRUST_AFTER'] = 'CK_FALSE'
    objects.append(obj)

    # append the trust values
    obj=dict()
    obj['Comment']= comment%"Trust for"
    obj['CKA_CLASS'] = 'CKO_NSS_TRUST'
    obj['CKA_TOKEN'] = 'CK_TRUE'
    obj['CKA_PRIVATE'] = 'CK_FALSE'
    obj['CKA_MODIFIABLE'] = 'CK_FALSE'
    obj['CKA_LABEL'] = '"' + label + '"'
    obj['CKA_CERT_SHA1_HASH'] = certhashsha1
    obj['CKA_CERT_MD5_HASH'] = certhashmd5
    obj['CKA_ISSUER'] = cert.issuer.public_bytes()
    obj['CKA_SERIAL_NUMBER'] = getSerial(cert)
    for t in list(trust_types):
       if t == trust:
          obj[t] = 'CKT_NSS_TRUSTED_DELEGATOR'
       else:
          obj[t] = 'CKT_NSS_MUST_VERIFY_TRUST'
    obj['CKA_TRUST_STEP_UP_APPROVED'] = 'CK_FALSE'
    objects.append(obj)
    print('Adding code signing cert "'+label+'"');

# now dump the results
f = open(output, 'w')
f.write(header)
for obj in objects:
    if 'Comment' in obj:
        # if comment is None, we've deleted the entry above
        if obj['Comment'] == None:
            continue
        f.write(obj['Comment'])
    else:
        print("Object with no comment!!")
        print(obj)
    for field in list(attribute_types.keys()):
        if not field in obj:
            continue
        ftype = attribute_types[field];
        if ftype == 'Distrust':
            if obj[field] == 'CK_FALSE':
                ftype = 'CK_BBOOL'
            else:
                ftype = 'MULTILINE_OCTAL'
        f.write("%s %s"%(field,ftype));
        if ftype == 'MULTILINE_OCTAL':
           dumpOctal(f,obj[field])
        else:
           f.write(" %s\n"%obj[field])
    f.write("\n")
f.close