Add support for SHA384 signatures (#20)

This commit is contained in:
Chris AtLee 2016-12-30 14:58:19 -05:00 коммит произвёл GitHub
Родитель 81d4c7fca0
Коммит ec7f494f37
10 изменённых файлов: 254 добавлений и 23 удалений

Просмотреть файл

@ -33,3 +33,7 @@ default_section=THIRDPARTY
forced_separate=test_mardor
not_skip = __init__.py
skip = migrations, south_migrations
[check-manifest]
ignore =
.*.sw?

Просмотреть файл

@ -12,6 +12,8 @@ from argparse import ArgumentParser
import mardor.mozilla
from mardor.reader import MarReader
from mardor.signing import SigningAlgo
from mardor.signing import get_keysize
from mardor.writer import MarWriter
log = logging.getLogger(__name__)
@ -39,6 +41,11 @@ def build_argparser():
parser.add_argument("--verbose", dest="loglevel", action="store_const",
const=logging.DEBUG, default=logging.WARN)
parser.add_argument("--productversion", dest="productversion",
help="product/version string")
parser.add_argument("--channel", dest="channel",
help="channel this MAR file is applicable to")
parser.add_argument("marfile")
parser.add_argument("files", nargs=REMAINDER)
@ -99,11 +106,13 @@ def do_list(marfile):
yield ("{:7d} {:04o} {}".format(e.size, e.flags, e.name))
def do_create(marfile, files, compress):
def do_create(marfile, files, compress, productversion=None, channel=None,
signing_key=None, signing_algorithm=None):
"""Create a new MAR file."""
with open(marfile, 'w+b') as f:
# TODO: extra info, signature
with MarWriter(f) as m:
with MarWriter(f, productversion=productversion, channel=channel,
signing_key=signing_key,
signing_algorithm=signing_algorithm) as m:
for f in files:
m.add(f, compress=compress)
@ -148,7 +157,24 @@ def main(argv=None):
elif args.action == "create":
compress = mardor.writer.Compression.bz2 if args.bz2 else None
do_create(marfile, args.files, compress)
if args.keyfiles:
signing_key = open(args.keyfiles[0], 'rb').read()
bits = get_keysize(signing_key)
if bits == 2048:
signing_algorithm = SigningAlgo.SHA1
elif bits == 4096:
signing_algorithm = SigningAlgo.SHA384
else:
parser.error("Unsupported key size {} from key {}".format(bits, args.keyfiles[0]))
print("Using {} to sign using algorithm {!s}".format(args.keyfiles[0], signing_algorithm))
else:
signing_key = None
signing_algorithm = None
do_create(marfile, args.files, compress,
productversion=args.productversion, channel=args.channel,
signing_key=signing_key, signing_algorithm=signing_algorithm)
else:
parser.error("Unsupported action {}".format(args.action))

Просмотреть файл

@ -13,8 +13,10 @@ from enum import Enum
from cryptography.exceptions import InvalidSignature
from mardor.format import mar
from mardor.signing import SigningAlgo
from mardor.signing import get_signature_data
from mardor.signing import make_verifier_v1
from mardor.signing import make_verifier_v2
from mardor.utils import auto_decompress_stream
from mardor.utils import bz2_decompress_stream
from mardor.utils import file_iter
@ -128,11 +130,16 @@ class MarReader(object):
verifiers = []
for sig in self.mardata.signatures.sigs:
if sig.algorithm_id == 1:
if sig.algorithm_id == SigningAlgo.SHA1:
verifier = make_verifier_v1(verify_key, sig.signature)
verifiers.append(verifier)
elif sig.algorithm_id == SigningAlgo.SHA384:
verifier = make_verifier_v2(verify_key, sig.signature)
verifiers.append(verifier)
else:
raise ValueError('Unsupported algorithm')
raise ValueError('Unsupported algorithm ({})'.format(sig.algorithm_id))
assert len(verifiers) == len(self.mardata.signatures.sigs)
for block in get_signature_data(self.fileobj,
self.mardata.signatures.filesize):

Просмотреть файл

@ -2,6 +2,8 @@
# 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/.
"""Signing, verification and key support for MAR files."""
from enum import IntEnum
from construct import Int32ub
from construct import Int64ub
from cryptography.hazmat.backends import default_backend
@ -14,6 +16,39 @@ from mardor.format import sigs_header
from mardor.utils import file_iter
class SigningAlgo(IntEnum):
"""
Enum representing supported signing algorithms.
SHA1: RSA-PKCS1-SHA1 using 2048 bit key
SHA384: RSA-PKCS1-SHA384 using 4096 bit key
"""
SHA1 = 1
SHA384 = 2
def get_publickey(keydata):
try:
key = serialization.load_pem_public_key(
keydata,
backend=default_backend(),
)
return key
except ValueError:
key = serialization.load_pem_private_key(
keydata,
password=None,
backend=default_backend(),
)
key = key.public_key()
return key
def get_keysize(keydata):
key = get_publickey(keydata)
return key.key_size
def get_signature_data(fileobj, filesize):
"""Read data from MAR file that is required for MAR signatures.
@ -60,10 +95,10 @@ def make_verifier_v1(public_key, signature):
Returns:
A cryptography key verifier object
"""
key = serialization.load_pem_public_key(
public_key,
backend=default_backend(),
)
key = get_publickey(public_key)
if key.key_size != 2048:
raise ValueError('2048 bit RSA key required')
verifier = key.verifier(
signature,
padding.PKCS1v15(),
@ -72,6 +107,27 @@ def make_verifier_v1(public_key, signature):
return verifier
def make_verifier_v2(public_key, signature):
"""Create verifier object to verify a `signature`.
Args:
public_key (str): PEM formatted public key
signature (bytes): signature to verify
Returns:
A cryptography key verifier object
"""
key = get_publickey(public_key)
if key.key_size != 4096:
raise ValueError('2048 bit RSA key required')
verifier = key.verifier(
signature,
padding.PKCS1v15(),
hashes.SHA384(),
)
return verifier
def make_signer_v1(private_key):
"""Create a signer object that signs using `private_key`.
@ -86,6 +142,8 @@ def make_signer_v1(private_key):
password=None,
backend=default_backend(),
)
if key.key_size != 2048:
raise ValueError('2048 bit RSA key required')
signer = key.signer(
padding.PKCS1v15(),
hashes.SHA1(),
@ -93,11 +151,34 @@ def make_signer_v1(private_key):
return signer
def make_rsa_keypair(bits=2048):
def make_signer_v2(private_key):
"""Create a signer object that signs using `private_key`.
Args:
private_key (str): PEM formatted private key
Returns:
A cryptography key signer object
"""
key = serialization.load_pem_private_key(
private_key,
password=None,
backend=default_backend(),
)
if key.key_size != 4096:
raise ValueError('2048 bit RSA key required')
signer = key.signer(
padding.PKCS1v15(),
hashes.SHA384(),
)
return signer
def make_rsa_keypair(bits):
"""Generate an RSA keypair.
Args:
bits (int): number of bits to use for the key. defaults to 2048.
bits (int): number of bits to use for the key.
Returns:
(private_key, public_key) - both as PEM encoded strings

Просмотреть файл

@ -12,8 +12,10 @@ from mardor.format import extras_header
from mardor.format import index_header
from mardor.format import mar_header
from mardor.format import sigs_header
from mardor.signing import SigningAlgo
from mardor.signing import get_signature_data
from mardor.signing import make_signer_v1
from mardor.signing import make_signer_v2
from mardor.utils import bz2_compress_stream
from mardor.utils import write_to_file
@ -41,6 +43,7 @@ class MarWriter(object):
def __init__(self, fileobj,
productversion=None, channel=None,
signing_key=None,
signing_algorithm=SigningAlgo.SHA384,
):
"""Initialize a new MarWriter object.
@ -57,6 +60,7 @@ class MarWriter(object):
channel (str): channel name to encode in the MAR header
productversion and channel must be specified together
signing_key (str): PEM encoded private key used for signing
signing_algorithm (SigningAlgo):
"""
self.fileobj = fileobj
self.entries = []
@ -70,12 +74,16 @@ class MarWriter(object):
self.productversion = productversion
self.channel = channel
self.signing_key = signing_key
self.signing_algorithm = signing_algorithm
if productversion and channel:
self.use_old_format = False
else:
self.use_old_format = True
if self.use_old_format and self.signing_key:
raise ValueError("productversion and channel must be specified when signing_key is")
self.write_header()
if not self.use_old_format:
fake_sigs = self.dummy_signatures()
@ -202,10 +210,14 @@ class MarWriter(object):
signing key was provided.
"""
signers = []
if self.signing_key:
if self.signing_key and self.signing_algorithm == SigningAlgo.SHA1:
# Algorithm 1: 2048 RSA key w/ SHA1 hash
signer = make_signer_v1(self.signing_key)
signers.append(signer)
signers.append((1, signer))
elif self.signing_key and self.signing_algorithm == SigningAlgo.SHA384:
# Algorithm 2: 4096 RSA key w/ SHA384 hash
signer = make_signer_v2(self.signing_key)
signers.append((2, signer))
return signers
def dummy_signatures(self):
@ -219,7 +231,15 @@ class MarWriter(object):
.write_signatures()
"""
signers = self.get_signers()
return [(1, b'0' * 256)] * len(signers)
signatures = []
for algo_id, signer in signers:
if algo_id == 1:
signatures.append((1, b'0' * 256))
elif algo_id == 2:
signatures.append((2, b'0' * 512))
else:
raise ValueError('Unsupported signing algorithm')
return signatures
def calculate_signatures(self):
"""Calculate the signatures for this MAR file.
@ -229,10 +249,9 @@ class MarWriter(object):
"""
signers = self.get_signers()
for block in get_signature_data(self.fileobj, self.filesize):
[sig.update(block) for sig in signers]
[sig.update(block) for (_, sig) in signers]
# NB: This only supports 1 signature of type 1 right now
signatures = [(1, sig.finalize()) for sig in signers]
signatures = [(algo_id, sig.finalize()) for (algo_id, sig) in signers]
return signatures
def write_signatures(self, signatures):

Просмотреть файл

@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import pytest
from pytest import fixture
from pytest import raises
@ -10,6 +11,7 @@ from mardor import cli
from mardor import mozilla
from mardor.reader import Decompression
from mardor.reader import MarReader
from mardor.signing import make_rsa_keypair
TEST_MAR = os.path.join(os.path.dirname(__file__), 'test.mar')
@ -119,6 +121,27 @@ def test_main_create(tmpdir):
cli.main(['-c', 'test.mar', 'hello.txt'])
@pytest.mark.parametrize('key_size', [2048, 4096])
def test_main_create_signed_v1(tmpdir, key_size):
priv, pub = make_rsa_keypair(key_size)
tmpdir.join('hello.txt').write('hello world')
tmpdir.join('key.pem').write(priv)
with tmpdir.as_cwd():
cli.main(['--productversion', 'foo', '--channel', 'bar', '-k',
'key.pem', '-c', 'test.mar', 'hello.txt'])
cli.main(['-t', '-v', '-k', 'key.pem', 'test.mar'])
def test_main_create_signed_badkeysize(tmpdir):
priv, pub = make_rsa_keypair(1024)
tmpdir.join('hello.txt').write('hello world')
tmpdir.join('key.pem').write(priv)
with tmpdir.as_cwd():
with raises(SystemExit):
cli.main(['--productversion', 'foo', '--channel', 'bar', '-k',
'key.pem', '-c', 'test.mar', 'hello.txt'])
def test_main_create_chdir(tmpdir):
tmpdir.join('hello.txt').write('hello world')
tmpmar = tmpdir.join('test.mar')

Просмотреть файл

@ -42,7 +42,7 @@ def test_verify_nosig(mar_cu):
def test_verify_wrongkey():
private, public = make_rsa_keypair()
private, public = make_rsa_keypair(2048)
with MarReader(open(TEST_MAR, 'rb')) as m:
assert not m.verify(public)
@ -50,7 +50,7 @@ def test_verify_wrongkey():
def test_verify_unsupportedalgo():
pubkey = open(TEST_PUBKEY, 'rb').read()
with MarReader(open(TEST_MAR, 'rb')) as m:
m.mardata.signatures.sigs[0].algorithm_id = 2
m.mardata.signatures.sigs[0].algorithm_id = 3
with pytest.raises(ValueError, message='Unsupported algorithm'):
m.verify(pubkey)

45
tests/test_signing.py Normal file
Просмотреть файл

@ -0,0 +1,45 @@
# 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/.
import pytest
from mardor.signing import make_rsa_keypair
from mardor.signing import make_signer_v1
from mardor.signing import make_signer_v2
from mardor.signing import make_verifier_v1
from mardor.signing import make_verifier_v2
@pytest.mark.parametrize('key_size, signer, verifier', [
(2048, make_signer_v1, make_verifier_v1),
(4096, make_signer_v2, make_verifier_v2),
])
def test_good_keysize(key_size, signer, verifier):
priv, pub = make_rsa_keypair(key_size)
assert verifier(pub, b'')
assert signer(priv)
@pytest.mark.parametrize('key_size, signer, verifier', [
(4096, make_signer_v1, make_verifier_v1),
(2048, make_signer_v2, make_verifier_v2),
])
def test_bad_keysize(key_size, signer, verifier):
priv, pub = make_rsa_keypair(key_size)
with pytest.raises(ValueError):
verifier(pub, b'')
with pytest.raises(ValueError):
signer(priv)
@pytest.mark.parametrize('key_size, signer, verifier', [
(2048, make_signer_v1, make_verifier_v1),
(4096, make_signer_v2, make_verifier_v2),
])
def test_verify_with_privatekey(key_size, signer, verifier):
priv, pub = make_rsa_keypair(key_size)
assert verifier(priv, b'')

Просмотреть файл

@ -122,15 +122,39 @@ def test_additional(tmpdir):
b'hello world')
def test_signing(tmpdir):
private_key, public_key = make_rsa_keypair()
def test_signing_v1(tmpdir):
private_key, public_key = make_rsa_keypair(2048)
message_p = tmpdir.join('message.txt')
message_p.write('hello world')
mar_p = tmpdir.join('test.mar')
with mar_p.open('w+b') as f:
with MarWriter(f, signing_key=private_key, channel='release',
productversion='99.9') as m:
productversion='99.9', signing_algorithm=1) as m:
with tmpdir.as_cwd():
m.add('message.txt')
assert mar_p.size() > 0
with mar_p.open('rb') as f:
with MarReader(f) as m:
assert m.mardata.additional.count == 1
assert m.mardata.signatures.count == 1
assert len(m.mardata.index.entries) == 1
assert m.mardata.index.entries[0].name == 'message.txt'
m.extract(str(tmpdir.join('extracted')))
assert (tmpdir.join('extracted', 'message.txt').read('rb') ==
b'hello world')
assert m.verify(public_key)
def test_signing_v2(tmpdir):
private_key, public_key = make_rsa_keypair(4096)
message_p = tmpdir.join('message.txt')
message_p.write('hello world')
mar_p = tmpdir.join('test.mar')
with mar_p.open('w+b') as f:
with MarWriter(f, signing_key=private_key, channel='release',
productversion='99.9', signing_algorithm=2) as m:
with tmpdir.as_cwd():
m.add('message.txt')

Просмотреть файл

@ -73,6 +73,7 @@ commands =
[testenv:coveralls]
deps =
coverage==4.2
coveralls
skip_install = true
commands =
@ -82,6 +83,7 @@ commands =
[testenv:codecov]
deps =
coverage==4.2
codecov
skip_install = true
commands =