summaryrefslogtreecommitdiff
path: root/util/pem_extract_pubkey.py
diff options
context:
space:
mode:
authorVincent Palatin <vpalatin@chromium.org>2014-09-26 15:20:42 -0700
committerchrome-internal-fetch <chrome-internal-fetch@google.com>2014-10-02 23:18:25 +0000
commitbeaddbf1a365463cdef3ed9dd1d093ff6ff80d70 (patch)
tree2f6f7aeda02e320b0962da0a901bb67b3bbf753e /util/pem_extract_pubkey.py
parent0330d9adf2602c44201d5e1b842747caf7dd83b1 (diff)
downloadchrome-ec-beaddbf1a365463cdef3ed9dd1d093ff6ff80d70.tar.gz
zinger: check RW firmware signature
The Zinger RW is now signed with 2048-bit RSA key (using SHA-256 as digest). This CL implements the verification mechanism. note: the RSA key used for signing must be provided as a .pem file. The path to .pem file must be provided in the PEM environment variable. By default, it's using the dev key stored in zinger_dev_key.pem. Signed-off-by: Vincent Palatin <vpalatin@chromium.org> BRANCH=samus BUG=chrome-os-partner:28336 TEST=on Zinger, run with properly signed RW firmware and corrupted firmware and check the serial traces. Change-Id: Ia58482458904a3ed72d6b0e95996cae86a0ead83 Reviewed-on: https://chromium-review.googlesource.com/220178 Commit-Queue: Vincent Palatin <vpalatin@chromium.org> Tested-by: Vincent Palatin <vpalatin@chromium.org> Reviewed-by: Alec Berg <alecaberg@chromium.org>
Diffstat (limited to 'util/pem_extract_pubkey.py')
-rwxr-xr-xutil/pem_extract_pubkey.py223
1 files changed, 223 insertions, 0 deletions
diff --git a/util/pem_extract_pubkey.py b/util/pem_extract_pubkey.py
new file mode 100755
index 0000000000..739e1bea57
--- /dev/null
+++ b/util/pem_extract_pubkey.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Extract the public key from a .pem RSA private key file (PKCS#1),
+ and compute the public key coefficients used by the signature verification
+ code.
+
+ Example:
+ ./util/pem_extract_pubkey board/zinger/zinger_dev_key.pem
+
+ Note: to generate a suitable private key :
+ RSA 2048-bit with public exponent F4 (65537)
+ you can use the following OpenSSL command :
+ openssl genrsa -F4 -out private.pem 2048
+"""
+
+import array
+import base64
+import sys
+
+VERSION = '0.0.1'
+
+"""
+RSA Private Key file (PKCS#1) encoding :
+
+It starts and ends with the tags:
+-----BEGIN RSA PRIVATE KEY-----
+BASE64 ENCODED DATA
+-----END RSA PRIVATE KEY-----
+
+The base64 encoded data is using an ASN.1 / DER structure :
+
+RSAPrivateKey ::= SEQUENCE {
+ version Version,
+ modulus INTEGER, -- n
+ publicExponent INTEGER, -- e
+ privateExponent INTEGER, -- d
+ prime1 INTEGER, -- p
+ prime2 INTEGER, -- q
+ exponent1 INTEGER, -- d mod (p-1)
+ exponent2 INTEGER, -- d mod (q-1)
+ coefficient INTEGER, -- (inverse of q) mod p
+ otherPrimeInfos OtherPrimeInfos OPTIONAL
+}
+"""
+
+PEM_HEADER='-----BEGIN RSA PRIVATE KEY-----'
+PEM_FOOTER='-----END RSA PRIVATE KEY-----'
+
+class PEMError(Exception):
+ """Exception class for pem_extract_pubkey utility."""
+
+# "Constructed" bit in DER tag
+DER_C=0x20
+# DER Sequence tag (always constructed)
+DER_SEQUENCE=DER_C|0x10
+# DER Integer tag
+DER_INTEGER=0x02
+
+class DER:
+ """DER encoded binary data storage and parser."""
+ def __init__(self, data):
+ # DER encoded binary data
+ self._data = data
+ # Initialize index in the data stream
+ self._idx = 0
+
+ def get_byte(self):
+ octet = ord(self._data[self._idx])
+ self._idx += 1
+ return octet
+
+ def get_len(self):
+ octet = self.get_byte()
+ if octet == 0x80:
+ raise PEMError('length indefinite form not supported')
+ if octet & 0x80: # Length long form
+ bytecnt = octet & ~0x80
+ total = 0
+ for i in range(bytecnt):
+ total = (total << 8) | self.get_byte()
+ return total
+ else: # Length short form
+ return octet
+
+ def get_tag(self):
+ tag = self.get_byte()
+ length = self.get_len()
+ data = self._data[self._idx:self._idx + length]
+ self._idx += length
+ return {"tag" : tag, "length" : length, "data" : data}
+
+def pem_get_mod(filename):
+ """Extract the modulus from a PEM private key file.
+
+ the PEM file is DER encoded according the structure quoted above.
+
+ Args:
+ filename : Full path to the .pem private key file.
+
+ Raises:
+ PEMError: If unable to parse .pem file or invalid file format.
+ """
+ # Read all the content of the .pem file
+ content = file(filename).readlines()
+ # Check the PEM RSA Private key tags
+ if content[0].strip() != PEM_HEADER:
+ raise PEMError('invalid PEM private key header')
+ if content[-1].strip() != PEM_FOOTER:
+ raise PEMError('invalid PEM private key footer')
+ # Decode the DER binary stream from the base64 data
+ b64 = "".join([l.strip() for l in content[1:-1]])
+ der = DER(base64.b64decode(b64))
+
+ # Parse the DER and fail at the first error
+ # The private key should be a (constructed) sequence
+ seq = der.get_tag()
+ if seq["tag"] != DER_SEQUENCE:
+ raise PEMError('expecting an ASN.1 sequence')
+ seq = DER(seq["data"])
+
+ # 1st field is Version
+ ver = seq.get_tag()
+ if ver["tag"] != DER_INTEGER:
+ raise PEMError('version field should be an integer')
+
+ # 2nd field is Modulus
+ mod = seq.get_tag()
+ if mod["tag"] != DER_INTEGER:
+ raise PEMError('modulus field should be an integer')
+ # 2048 bits + mandatory ASN.1 sign (0) => 257 Bytes
+ if mod["length"] != 257 or mod["data"][0] != '\x00':
+ raise PEMError('Invalid key length (expecting 2048 bits)')
+
+ # 3rd field is Public Exponent
+ exp = seq.get_tag()
+ if exp["tag"] != DER_INTEGER:
+ raise PEMError('exponent field should be an integer')
+ if exp["length"] != 3 or exp["data"] != "\x01\x00\x01":
+ raise PEMError('the public exponent must be F4 (65537)')
+
+ return mod["data"]
+
+def modinv(a, m):
+ """ The multiplicitive inverse of a in the integers modulo m.
+
+ Return b when a * b == 1 mod m
+ """
+ # Extended GCD
+ lastrem, rem = abs(a), abs(m)
+ x, lastx, y, lasty = 0, 1, 1, 0
+ while rem:
+ lastrem, (quotient, rem) = rem, divmod(lastrem, rem)
+ x, lastx = lastx - quotient*x, x
+ y, lasty = lasty - quotient*y, y
+ #
+ if lastrem != 1:
+ raise ValueError
+ x = lastx * (-1 if a < 0 else 1)
+ return x % m
+
+def to_words(n, count):
+ h = '%x' % n
+ s = ('0'*(len(h) % 2) + h).zfill(count*8).decode('hex')
+ return array.array("I", s[::-1])
+
+def compute_mod_parameters(modulus):
+ ''' Prepare/pre-compute coefficients for the RSA public key signature
+ verification code.
+ '''
+ # create an array of uint32_t to store the modulus but skip the sign byte
+ w = array.array("I",modulus[1:])
+ # all integers in DER encoded .pem file are big endian.
+ w.reverse()
+ w.byteswap()
+ # convert the big-endian modulus to a big integer for the computations
+ N = 0
+ for i in range(len(modulus)):
+ N = (N << 8) | ord(modulus[i])
+ # -1 / N[0] mod 2^32
+ B = 0x100000000L
+ n0inv = B - modinv(w[0], B)
+ # R = 2^(modulo size); RR = (R * R) % N
+ RR = pow(2, 4096, N)
+ rr_words = to_words(RR, 64)
+
+ return {'mod':w, 'rr':rr_words, 'n0inv':n0inv}
+
+def print_header(params):
+ print "{\n\t.n = {%s}," % (",".join(["0x%08x" % (i) for i in params['mod']]))
+ print "\t.rr = {%s}," % (",".join(["0x%08x" % (i) for i in params['rr']]))
+ print "\t.n0inv = 0x%08x\n};" % (params['n0inv'])
+
+def dump_blob(params):
+ mod_bin = params['mod'].tostring()
+ rr_bin = params['rr'].tostring()
+ n0inv_bin = array.array("I",[params['n0inv']]).tostring()
+ return mod_bin + rr_bin + n0inv_bin
+
+def extract_pubkey(pemfile, headerMode=True):
+ # Read the modulus in the .pem file
+ mod = pem_get_mod(sys.argv[1])
+ # Pre-compute the parameters used by the verification code
+ p = compute_mod_parameters(mod)
+
+ if headerMode:
+ # Generate a C header file with the parameters
+ print_header(p)
+ else:
+ # Generate the packed structure as a binary blob
+ return dump_blob(p)
+
+if __name__ == '__main__':
+ try:
+ if len(sys.argv) < 2:
+ raise PEMError('Invalid arguments. Usage: ./pem_extract_pubkey priv.pem')
+ extract_pubkey(sys.argv[1])
+ except KeyboardInterrupt:
+ sys.exit(0)
+ except PEMError as e:
+ sys.stderr.write("Error: %s\n" % (e.message))
+ sys.exit(1)