Add support for SHA384 signatures (#20)
This commit is contained in:
Родитель
81d4c7fca0
Коммит
ec7f494f37
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -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 =
|
||||
|
|
Загрузка…
Ссылка в новой задаче