summaryrefslogtreecommitdiffstats
path: root/security/ct/tests/gtest/createSTHTestData.py
blob: ab61d4ba0eef58a9dcc8d221d1fbe84ebb847e6c (plain)
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
#!/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/.

"""
This utility is used by the build system to create test inputs for the
signed tree head decoding and verification implementation. The format is
generally lines of <key>:<value> pairs except for the to-be-signed
section, which consists of one or more lines of hex bytes. Comments may
appear at the end of lines and begin with '//'.
The following keys are valid:
signingKey: A pykey key identifier to use to sign the to-be-signed data.
            Required.
spki: A pykey key identifier to create an encoded SubjectPublicKeyInfo
      to be included with the test data. The tests will use this spki to
      validate the signature. Required.
prefix: Hex bytes to include at the beginning of the signed tree head
        data. This data is not covered by the signature (typically this
        is used for the log_id field). Optional. Defaults to the empty
        string.
hash: The name of a hash algorithm to use when signing. Optional.
      Defaults to 'sha256'.
"""

import binascii
import os
import sys

from pyasn1.codec.der import encoder

sys.path.append(
    os.path.join(os.path.dirname(__file__), "..", "..", "..", "manager", "tools")
)
import pykey


def sign(signingKey, hashAlgorithm, hexToSign):
    """Given a pykey, the name of a hash function, and hex bytes to
    sign, signs the data (as binary) and returns a hex string consisting
    of the signature."""
    # key.sign returns a hex string in the format "'<hex bytes>'H",
    # so we have to strip off the "'"s and trailing 'H'
    return signingKey.sign(binascii.unhexlify(hexToSign), "hash:%s" % hashAlgorithm)[
        1:-2
    ]


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

    pass


class UnknownParameterTypeError(Error):
    """Base class for handling unexpected input in this module."""

    def __init__(self, value):
        super(Error, self).__init__()
        self.value = value
        self.category = "key"

    def __str__(self):
        return 'Unknown %s type "%s"' % (self.category, repr(self.value))


class InputTooLongError(Error):
    """Helper exception type for inputs that are too long."""

    def __init__(self, length):
        super(InputTooLongError, self).__init__()
        self.length = length

    def __str__(self):
        return "Input too long: %s > 65535" % self.length


def getTwoByteLenAsHex(callLenOnMe):
    """Given something that len can be called on, returns a hex string
    representing the two-byte length of the something, in network byte
    order (the length must be less than or equal to 65535)."""
    length = len(callLenOnMe)
    if length > 65535:
        raise InputTooLongError(length)
    return bytes([length // 256, length % 256]).hex()


def createSTH(configStream):
    """Given a stream that will provide the specification for a signed
    tree head (see the comment at the top of this file), creates the
    corresponding signed tree head. Returns a string that can be
    compiled as C/C++ that declares two const char*s kSTHHex and
    kSPKIHex corresponding to the hex encoding of the signed tree head
    and the hex encoding of the subject public key info from the
    specification, respectively."""
    toSign = ""
    prefix = ""
    hashAlgorithm = "sha256"
    for line in configStream.readlines():
        if ":" in line:
            param = line.split(":")[0]
            arg = line.split(":")[1].split("//")[0].strip()
            if param == "signingKey":
                signingKey = pykey.keyFromSpecification(arg)
            elif param == "spki":
                spki = pykey.keyFromSpecification(arg)
            elif param == "prefix":
                prefix = arg
            elif param == "hash":
                hashAlgorithm = arg
            else:
                raise UnknownParameterTypeError(param)
        else:
            toSign = toSign + line.split("//")[0].strip()
    signature = sign(signingKey, hashAlgorithm, toSign)
    lengthBytesHex = getTwoByteLenAsHex(binascii.unhexlify(signature))
    sth = prefix + toSign + lengthBytesHex + signature
    spkiHex = encoder.encode(spki.asSubjectPublicKeyInfo()).hex()
    return 'const char* kSTHHex = "%s";\nconst char* kSPKIHex = "%s";\n' % (
        sth,
        spkiHex,
    )


def main(output, inputPath):
    """Given a file-like output and the path to a signed tree head
    specification (see the comment at the top of this file), reads the
    specification, creates the signed tree head, and outputs test data
    that can be included by a gtest corresponding to the
    specification."""
    with open(inputPath) as configStream:
        output.write(createSTH(configStream))