DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (b6d82b1a6b02)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
#!/usr/bin/env 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/.

"""
Given a directory of files, packages them up and signs the
resulting zip file. Mainly for creating test inputs to the
nsIX509CertDB.openSignedAppFileAsync API.
"""
from base64 import b64encode
from cbor2 import dumps
from cbor2.types import CBORTag
from hashlib import sha1, sha256
import StringIO
import argparse
import os
import pycert
import pycms
import pykey
import re
import zipfile


ES256 = -7
ES384 = -35
ES512 = -36
KID = 4
ALG = 1
COSE_Sign = 98


def coseAlgorithmToPykeyHash(algorithm):
    """Helper function that takes one of (ES256, ES384, ES512)
    and returns the corresponding pykey.HASH_* identifier."""
    if algorithm == ES256:
        return pykey.HASH_SHA256
    elif algorithm == ES384:
        return pykey.HASH_SHA384
    elif algorithm == ES512:
        return pykey.HASH_SHA512
    else:
        raise UnknownCOSEAlgorithmError(algorithm)

# COSE_Signature = [
#     protected : serialized_map,
#     unprotected : {},
#     signature : bstr
# ]


def coseSignature(payload, algorithm, signingKey, signingCertificate,
                  bodyProtected):
    """Returns a COSE_Signature structure.
    payload is a string representing the data to be signed
    algorithm is one of (ES256, ES384, ES512)
    signingKey is a pykey.ECKey to sign the data with
    signingCertificate is a byte string
    bodyProtected is the serialized byte string of the protected body header
    """
    protected = {ALG: algorithm, KID: signingCertificate}
    protectedEncoded = dumps(protected)
    # Sig_structure = [
    #     context : "Signature"
    #     body_protected : bodyProtected
    #     sign_protected : protectedEncoded
    #     external_aad : nil
    #     payload : bstr
    # ]
    sigStructure = [u'Signature', bodyProtected, protectedEncoded, None,
                    payload]
    sigStructureEncoded = dumps(sigStructure)
    pykeyHash = coseAlgorithmToPykeyHash(algorithm)
    signature = signingKey.signRaw(sigStructureEncoded, pykeyHash)
    return [protectedEncoded, {}, signature]

# COSE_Sign = [
#     protected : serialized_map,
#     unprotected : {},
#     payload : nil,
#     signatures : [+ COSE_Signature]
# ]


def coseSig(payload, intermediates, signatures):
    """Returns the entire (tagged) COSE_Sign structure.
    payload is a string representing the data to be signed
    intermediates is an array of byte strings
    signatures is an array of (algorithm, signingKey,
               signingCertificate) triplets to be passed to
               coseSignature
    """
    protected = {KID: intermediates}
    protectedEncoded = dumps(protected)
    coseSignatures = []
    for (algorithm, signingKey, signingCertificate) in signatures:
        coseSignatures.append(coseSignature(payload, algorithm, signingKey,
                                            signingCertificate,
                                            protectedEncoded))
    tagged = CBORTag(COSE_Sign, [protectedEncoded, {}, None, coseSignatures])
    return dumps(tagged)


def walkDirectory(directory):
    """Given a relative path to a directory, enumerates the
    files in the tree rooted at that location. Returns a list
    of pairs of paths to those files. The first in each pair
    is the full path to the file. The second in each pair is
    the path to the file relative to the directory itself."""
    paths = []
    for path, dirs, files in os.walk(directory):
        for f in files:
            fullPath = os.path.join(path, f)
            internalPath = re.sub(r'^/', '', fullPath.replace(directory, ''))
            paths.append((fullPath, internalPath))
    return paths


def addManifestEntry(filename, hashes, contents, entries):
    """Helper function to fill out a manifest entry.
    Takes the filename, a list of (hash function, hash function name)
    pairs to use, the contents of the file, and the current list
    of manifest entries."""
    entry = 'Name: %s\n' % filename
    for (hashFunc, name) in hashes:
        base64hash = b64encode(hashFunc(contents).digest())
        entry += '%s-Digest: %s\n' % (name, base64hash)
    entries.append(entry)


def getCert(subject, keyName, issuerName, ee, issuerKey=""):
    """Helper function to create an X509 cert from a specification.
    Takes the subject, the subject key name to use, the issuer name,
    a bool whether this is an EE cert or not, and optionally an issuer key
    name."""
    certSpecification = 'issuer:%s\n' % issuerName + \
        'subject:' + subject + '\n' + \
        'subjectKey:%s\n' % keyName
    if ee:
        certSpecification += 'extension:keyUsage:digitalSignature'
    else:
        certSpecification += 'extension:basicConstraints:cA,\n' + \
            'extension:keyUsage:cRLSign,keyCertSign'
    if issuerKey:
        certSpecification += '\nissuerKey:%s' % issuerKey
    certSpecificationStream = StringIO.StringIO()
    print >>certSpecificationStream, certSpecification
    certSpecificationStream.seek(0)
    return pycert.Certificate(certSpecificationStream)


def coseAlgorithmToSignatureParams(coseAlgorithm, issuerName):
    """Given a COSE algorithm ('ES256', 'ES384', 'ES512') and an issuer
    name, returns a (algorithm id, pykey.ECCKey, encoded certificate)
    triplet for use with coseSig.
    """
    if coseAlgorithm == 'ES256':
        keyName = 'secp256r1'
        algId = ES256
    elif coseAlgorithm == 'ES384':
        keyName = 'secp384r1'
        algId = ES384
    elif coseAlgorithm == 'ES512':
        keyName = 'secp521r1'  # COSE uses the hash algorithm; this is the curve
        algId = ES512
    else:
        raise UnknownCOSEAlgorithmError(coseAlgorithm)
    key = pykey.ECCKey(keyName)
    # The subject must differ to avoid errors when importing into NSS later.
    ee = getCert('xpcshell signed app test signer ' + keyName,
                 keyName, issuerName, True, 'default')
    return (algId, key, ee.toDER())


def signZip(appDirectory, outputFile, issuerName, rootName, manifestHashes,
            signatureHashes, pkcs7Hashes, coseAlgorithms, emptySignerInfos,
            headerPaddingFactor):
    """Given a directory containing the files to package up,
    an output filename to write to, the name of the issuer of
    the signing certificate, the name of trust anchor, a list of hash algorithms
    to use in the manifest file, a similar list for the signature file,
    a similar list for the pkcs#7 signature, a list of COSE signature algorithms
    to include, whether the pkcs#7 signer info should be kept empty, and how
    many MB to pad the manifests by (to test handling large manifest files),
    packages up the files in the directory and creates the output as
    appropriate."""
    # The header of each manifest starts with the magic string
    # 'Manifest-Version: 1.0' and ends with a blank line. There can be
    # essentially anything after the first line before the blank line.
    mfEntries = ['Manifest-Version: 1.0']
    if headerPaddingFactor > 0:
        # In this format, each line can only be 72 bytes long. We make
        # our padding 50 bytes per line (49 of content and one newline)
        # so the math is easy.
        singleLinePadding = 'a' * 49
        # 1000000 / 50 = 20000
        allPadding = [singleLinePadding] * (headerPaddingFactor * 20000)
        mfEntries.extend(allPadding)
    # Append the blank line.
    mfEntries.append('')

    with zipfile.ZipFile(outputFile, 'w', zipfile.ZIP_DEFLATED) as outZip:
        for (fullPath, internalPath) in walkDirectory(appDirectory):
            with open(fullPath) as inputFile:
                contents = inputFile.read()
            outZip.writestr(internalPath, contents)

            # Add the entry to the manifest we're building
            addManifestEntry(internalPath, manifestHashes, contents, mfEntries)

        if len(coseAlgorithms) > 0:
            coseManifest = '\n'.join(mfEntries)
            outZip.writestr('META-INF/cose.manifest', coseManifest)
            addManifestEntry('META-INF/cose.manifest', manifestHashes,
                             coseManifest, mfEntries)
            intermediates = []
            coseIssuerName = issuerName
            if rootName:
                coseIssuerName = 'xpcshell signed app test issuer'
                intermediate = getCert(coseIssuerName, 'default', rootName, False)
                intermediate = intermediate.toDER()
                intermediates.append(intermediate)
            signatures = map(lambda coseAlgorithm:
                             coseAlgorithmToSignatureParams(coseAlgorithm, coseIssuerName),
                             coseAlgorithms)
            coseSignatureBytes = coseSig(coseManifest, intermediates, signatures)
            outZip.writestr('META-INF/cose.sig', coseSignatureBytes)
            addManifestEntry('META-INF/cose.sig', manifestHashes,
                             coseSignatureBytes, mfEntries)

        if len(pkcs7Hashes) != 0 or emptySignerInfos:
            mfContents = '\n'.join(mfEntries)
            sfContents = 'Signature-Version: 1.0\n'
            for (hashFunc, name) in signatureHashes:
                base64hash = b64encode(hashFunc(mfContents).digest())
                sfContents += '%s-Digest-Manifest: %s\n' % (name, base64hash)

            cmsSpecification = ''
            for name in pkcs7Hashes:
                hashFunc, _ = hashNameToFunctionAndIdentifier(name)
                cmsSpecification += '%s:%s\n' % (name,
                                                 hashFunc(sfContents).hexdigest())
            cmsSpecification += 'signer:\n' + \
                'issuer:%s\n' % issuerName + \
                'subject:xpcshell signed app test signer\n' + \
                'extension:keyUsage:digitalSignature'
            cmsSpecificationStream = StringIO.StringIO()
            print >>cmsSpecificationStream, cmsSpecification
            cmsSpecificationStream.seek(0)
            cms = pycms.CMS(cmsSpecificationStream)
            p7 = cms.toDER()
            outZip.writestr('META-INF/A.RSA', p7)
            outZip.writestr('META-INF/A.SF', sfContents)
            outZip.writestr('META-INF/MANIFEST.MF', mfContents)


class Error(Exception):
    """Base class for exceptions in this module."""
    pass


class UnknownHashAlgorithmError(Error):
    """Helper exception type to handle unknown hash algorithms."""

    def __init__(self, name):
        super(UnknownHashAlgorithmError, self).__init__()
        self.name = name

    def __str__(self):
        return 'Unknown hash algorithm %s' % repr(self.name)


class UnknownCOSEAlgorithmError(Error):
    """Helper exception type to handle unknown COSE algorithms."""

    def __init__(self, name):
        super(UnknownCOSEAlgorithmError, self).__init__()
        self.name = name

    def __str__(self):
        return 'Unknown COSE algorithm %s' % repr(self.name)


def hashNameToFunctionAndIdentifier(name):
    if name == 'sha1':
        return (sha1, 'SHA1')
    if name == 'sha256':
        return (sha256, 'SHA256')
    raise UnknownHashAlgorithmError(name)


def main(outputFile, appPath, *args):
    """Main entrypoint. Given an already-opened file-like
    object, a path to the app directory to sign, and some
    optional arguments, signs the contents of the directory and
    writes the resulting package to the 'file'."""
    parser = argparse.ArgumentParser(description='Sign an app.')
    parser.add_argument('-i', '--issuer', action='store', help='Issuer name',
                        default='xpcshell signed apps test root')
    parser.add_argument('-r', '--root', action='store', help='Root name',
                        default='')
    parser.add_argument('-m', '--manifest-hash', action='append',
                        help='Hash algorithms to use in manifest',
                        default=[])
    parser.add_argument('-s', '--signature-hash', action='append',
                        help='Hash algorithms to use in signature file',
                        default=[])
    parser.add_argument('-c', '--cose-sign', action='append',
                        help='Append a COSE signature with the given ' +
                             'algorithms (out of ES256, ES384, and ES512)',
                        default=[])
    parser.add_argument('-z', '--pad-headers', action='store', default=0,
                        help='Pad the header sections of the manifests ' +
                             'with X MB of repetitive data')
    group = parser.add_mutually_exclusive_group()
    group.add_argument('-p', '--pkcs7-hash', action='append',
                       help='Hash algorithms to use in PKCS#7 signature',
                       default=[])
    group.add_argument('-e', '--empty-signerInfos', action='store_true',
                       help='Emit pkcs#7 SignedData with empty signerInfos')
    parsed = parser.parse_args(args)
    if len(parsed.manifest_hash) == 0:
        parsed.manifest_hash.append('sha256')
    if len(parsed.signature_hash) == 0:
        parsed.signature_hash.append('sha256')
    signZip(appPath, outputFile, parsed.issuer, parsed.root,
            map(hashNameToFunctionAndIdentifier, parsed.manifest_hash),
            map(hashNameToFunctionAndIdentifier, parsed.signature_hash),
            parsed.pkcs7_hash, parsed.cose_sign, parsed.empty_signerInfos,
            int(parsed.pad_headers))