Merge pull request #673 from reaperhulk/rsa-pkcs1-signature-only

Add RSA PKCS1 signing (and structure for PSS + verification)
This commit is contained in:
Alex Gaynor 2014-02-25 20:58:19 -08:00
commit d8c8f7cde6
12 changed files with 317 additions and 2 deletions

View file

@ -46,3 +46,7 @@ class InvalidKey(Exception):
class InvalidToken(Exception):
pass
class UnsupportedPadding(Exception):
pass

View file

@ -24,6 +24,7 @@ from cryptography.hazmat.backends.interfaces import (
@utils.register_interface(HashBackend)
@utils.register_interface(HMACBackend)
@utils.register_interface(PBKDF2HMACBackend)
@utils.register_interface(RSABackend)
class MultiBackend(object):
name = "multibackend"
@ -106,3 +107,8 @@ class MultiBackend(object):
for b in self._filtered_backends(RSABackend):
return b.generate_rsa_private_key(public_exponent, key_size)
raise UnsupportedAlgorithm
def create_rsa_signature_ctx(self, private_key, padding, algorithm):
for b in self._filtered_backends(RSABackend):
return b.create_rsa_signature_ctx(private_key, padding, algorithm)
raise UnsupportedAlgorithm

View file

@ -17,7 +17,8 @@ import itertools
from cryptography import utils
from cryptography.exceptions import (
UnsupportedAlgorithm, InvalidTag, InternalError
UnsupportedAlgorithm, InvalidTag, InternalError, AlreadyFinalized,
UnsupportedPadding
)
from cryptography.hazmat.backends.interfaces import (
CipherBackend, HashBackend, HMACBackend, PBKDF2HMACBackend, RSABackend
@ -321,6 +322,23 @@ class Backend(object):
modulus=self._bn_to_int(ctx.n),
)
def _rsa_cdata_from_private_key(self, private_key):
ctx = self._lib.RSA_new()
assert ctx != self._ffi.NULL
ctx = self._ffi.gc(ctx, self._lib.RSA_free)
ctx.p = self._int_to_bn(private_key.p)
ctx.q = self._int_to_bn(private_key.q)
ctx.d = self._int_to_bn(private_key.d)
ctx.e = self._int_to_bn(private_key.e)
ctx.n = self._int_to_bn(private_key.n)
ctx.dmp1 = self._int_to_bn(private_key.dmp1)
ctx.dmq1 = self._int_to_bn(private_key.dmq1)
ctx.iqmp = self._int_to_bn(private_key.iqmp)
return ctx
def create_rsa_signature_ctx(self, private_key, padding, algorithm):
return _RSASignatureContext(self, private_key, padding, algorithm)
class GetCipherByName(object):
def __init__(self, fmt):
@ -572,4 +590,100 @@ class _HMACContext(object):
return self._backend._ffi.buffer(buf)[:]
@utils.register_interface(interfaces.AsymmetricSignatureContext)
class _RSASignatureContext(object):
def __init__(self, backend, private_key, padding, algorithm):
self._backend = backend
self._private_key = private_key
if not isinstance(padding, interfaces.AsymmetricPadding):
raise TypeError(
"Expected provider of interfaces.AsymmetricPadding")
if padding.name == "EMSA-PKCS1-v1_5":
if self._backend._lib.Cryptography_HAS_PKEY_CTX:
self._finalize_method = self._finalize_pkey_ctx
self._padding_enum = self._backend._lib.RSA_PKCS1_PADDING
else:
self._finalize_method = self._finalize_pkcs1
else:
raise UnsupportedPadding(
"{0} is not supported by this backend".format(padding.name)
)
self._padding = padding
self._algorithm = algorithm
self._hash_ctx = _HashContext(backend, self._algorithm)
def update(self, data):
if self._hash_ctx is None:
raise AlreadyFinalized("Context has already been finalized")
self._hash_ctx.update(data)
def finalize(self):
if self._hash_ctx is None:
raise AlreadyFinalized("Context has already been finalized")
evp_pkey = self._backend._lib.EVP_PKEY_new()
assert evp_pkey != self._backend._ffi.NULL
evp_pkey = backend._ffi.gc(evp_pkey, backend._lib.EVP_PKEY_free)
rsa_cdata = backend._rsa_cdata_from_private_key(self._private_key)
res = self._backend._lib.RSA_blinding_on(
rsa_cdata, self._backend._ffi.NULL)
assert res == 1
res = self._backend._lib.EVP_PKEY_set1_RSA(evp_pkey, rsa_cdata)
assert res == 1
evp_md = self._backend._lib.EVP_get_digestbyname(
self._algorithm.name.encode("ascii"))
assert evp_md != self._backend._ffi.NULL
pkey_size = self._backend._lib.EVP_PKEY_size(evp_pkey)
assert pkey_size > 0
return self._finalize_method(evp_pkey, pkey_size, rsa_cdata, evp_md)
def _finalize_pkey_ctx(self, evp_pkey, pkey_size, rsa_cdata, evp_md):
pkey_ctx = self._backend._lib.EVP_PKEY_CTX_new(
evp_pkey, self._backend._ffi.NULL
)
assert pkey_ctx != self._backend._ffi.NULL
res = self._backend._lib.EVP_PKEY_sign_init(pkey_ctx)
assert res == 1
res = self._backend._lib.EVP_PKEY_CTX_set_signature_md(
pkey_ctx, evp_md)
assert res > 0
res = self._backend._lib.EVP_PKEY_CTX_set_rsa_padding(
pkey_ctx, self._padding_enum)
assert res > 0
data_to_sign = self._hash_ctx.finalize()
self._hash_ctx = None
buflen = self._backend._ffi.new("size_t *")
res = self._backend._lib.EVP_PKEY_sign(
pkey_ctx,
self._backend._ffi.NULL,
buflen,
data_to_sign,
len(data_to_sign)
)
assert res == 1
buf = self._backend._ffi.new("unsigned char[]", buflen[0])
res = self._backend._lib.EVP_PKEY_sign(
pkey_ctx, buf, buflen, data_to_sign, len(data_to_sign))
assert res == 1
return self._backend._ffi.buffer(buf)[:]
def _finalize_pkcs1(self, evp_pkey, pkey_size, rsa_cdata, evp_md):
sig_buf = self._backend._ffi.new("char[]", pkey_size)
sig_len = self._backend._ffi.new("unsigned int *")
res = self._backend._lib.EVP_SignFinal(
self._hash_ctx._ctx,
sig_buf,
sig_len,
evp_pkey
)
self._hash_ctx.finalize()
self._hash_ctx = None
assert res == 1
return self._backend._ffi.buffer(sig_buf)[:sig_len[0]]
backend = Backend()

View file

@ -0,0 +1,22 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function
from cryptography import utils
from cryptography.hazmat.primitives import interfaces
@utils.register_interface(interfaces.AsymmetricPadding)
class PKCS1v15(object):
name = "EMSA-PKCS1-v1_5"

View file

@ -135,6 +135,9 @@ class RSAPrivateKey(object):
def generate(cls, public_exponent, key_size, backend):
return backend.generate_rsa_private_key(public_exponent, key_size)
def signer(self, padding, algorithm, backend):
return backend.create_rsa_signature_ctx(self, padding, algorithm)
@property
def key_size(self):
return _bit_length(self.modulus)

View file

@ -42,3 +42,8 @@ Exceptions
This is raised when the verify method of a one time password function's
computed token does not match the expected token.
.. class:: UnsupportedPadding
This is raised when the chosen padding is not supported by the backend.

View file

@ -0,0 +1,10 @@
.. hazmat::
Asymmetric Algorithms
=====================
.. toctree::
:maxdepth: 1
rsa
padding

View file

@ -0,0 +1,20 @@
.. hazmat::
Padding
=======
.. currentmodule:: cryptography.hazmat.primitives.asymmetric.padding
.. warning::
`Padding is critical`_ when signing or encrypting data using RSA. Without
correct padding signatures can be forged, messages decrypted, and private
keys compromised.
.. class:: PKCS1v15()
.. versionadded:: 0.3
PKCS1 v1.5 (also known as simply PKCS1) is a simple padding scheme
developed for use with RSA keys. It is defined in :rfc:`3447`.
.. _`Padding is critical`: http://rdist.root.org/2009/10/06/why-rsa-encryption-padding-is-critical/

View file

@ -50,6 +50,46 @@ RSA
provider.
:return: A new instance of ``RSAPrivateKey``.
.. method:: signer(padding, algorithm, backend)
.. versionadded:: 0.3
Sign data which can be verified later by others using the public key.
.. doctest::
>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.hazmat.primitives import hashes
>>> from cryptography.hazmat.primitives.asymmetric import rsa, padding
>>> private_key = rsa.RSAPrivateKey.generate(
... public_exponent=65537,
... key_size=2048,
... backend=default_backend()
... )
>>> signer = private_key.signer(
... padding.PKCS1v15(),
... hashes.SHA256(),
... default_backend()
... )
>>> signer.update(b"this is some data I'd like")
>>> signer.update(b" to sign")
>>> signature = signer.finalize()
:param padding: An instance of a
:class:`~cryptography.hazmat.primitives.interfaces.AsymmetricPadding`
provider.
:param algorithm: An instance of a
:class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm`
provider.
:param backend: A
:class:`~cryptography.hazmat.backends.interfaces.RSABackend`
provider.
:returns:
:class:`~cryptography.hazmat.primitives.interfaces.AsymmetricSignatureContext`
.. class:: RSAPublicKey(public_exponent, modulus)

View file

@ -11,7 +11,7 @@ Primitives
symmetric-encryption
padding
key-derivation-functions
rsa
asymmetric/index
constant-time
interfaces
twofactor

View file

@ -20,6 +20,7 @@ from cryptography.hazmat.backends.interfaces import (
)
from cryptography.hazmat.backends.multibackend import MultiBackend
from cryptography.hazmat.primitives import hashes, hmac
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@ -85,6 +86,9 @@ class DummyRSABackend(object):
def generate_rsa_private_key(self, public_exponent, private_key):
pass
def create_rsa_signature_ctx(self, private_key, padding, algorithm):
pass
class TestMultiBackend(object):
def test_ciphers(self):
@ -158,6 +162,13 @@ class TestMultiBackend(object):
key_size=1024, public_exponent=65537
)
backend.create_rsa_signature_ctx("private_key", padding.PKCS1v15(),
hashes.MD5())
backend = MultiBackend([])
with pytest.raises(UnsupportedAlgorithm):
backend.generate_rsa_private_key(key_size=1024, public_exponent=3)
with pytest.raises(UnsupportedAlgorithm):
backend.create_rsa_signature_ctx("private_key", padding.PKCS1v15(),
hashes.MD5())

View file

@ -14,16 +14,25 @@
from __future__ import absolute_import, division, print_function
import binascii
import itertools
import os
import pytest
from cryptography import exceptions, utils
from cryptography.hazmat.primitives import hashes, interfaces
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding
from ...utils import load_pkcs1_vectors, load_vectors_from_file
@utils.register_interface(interfaces.AsymmetricPadding)
class DummyPadding(object):
name = "UNSUPPORTED-PADDING"
def _modinv(e, m):
"""
Modular Multiplicative Inverse. Returns x such that: (x*e) mod m == 1
@ -55,6 +64,17 @@ def _check_rsa_private_key(skey):
assert skey.key_size == pkey.key_size
def _flatten_pkcs1_examples(vectors):
flattened_vectors = []
for vector in vectors:
examples = vector[0].pop("examples")
for example in examples:
merged_vector = (vector[0], vector[1], example)
flattened_vectors.append(merged_vector)
return flattened_vectors
def test_modular_inverse():
p = int(
"d1f9f6c09fd3d38987f7970247b85a6da84907753d42ec52bc23b745093f4fff5cff3"
@ -363,3 +383,63 @@ class TestRSA(object):
# Test a public_exponent that is not odd.
with pytest.raises(ValueError):
rsa.RSAPublicKey(public_exponent=6, modulus=15)
@pytest.mark.rsa
class TestRSASignature(object):
@pytest.mark.parametrize(
"pkcs1_example",
_flatten_pkcs1_examples(load_vectors_from_file(
os.path.join(
"asymmetric", "RSA", "pkcs1v15sign-vectors.txt"),
load_pkcs1_vectors
))
)
def test_pkcs1v15_signing(self, pkcs1_example, backend):
private, public, example = pkcs1_example
private_key = rsa.RSAPrivateKey(
p=private["p"],
q=private["q"],
private_exponent=private["private_exponent"],
dmp1=private["dmp1"],
dmq1=private["dmq1"],
iqmp=private["iqmp"],
public_exponent=private["public_exponent"],
modulus=private["modulus"]
)
signer = private_key.signer(padding.PKCS1v15(), hashes.SHA1(), backend)
signer.update(binascii.unhexlify(example["message"]))
signature = signer.finalize()
assert binascii.hexlify(signature) == example["signature"]
def test_use_after_finalize(self, backend):
private_key = rsa.RSAPrivateKey.generate(
public_exponent=65537,
key_size=512,
backend=backend
)
signer = private_key.signer(padding.PKCS1v15(), hashes.SHA1(), backend)
signer.update(b"sign me")
signer.finalize()
with pytest.raises(exceptions.AlreadyFinalized):
signer.finalize()
with pytest.raises(exceptions.AlreadyFinalized):
signer.update(b"more data")
def test_unsupported_padding(self, backend):
private_key = rsa.RSAPrivateKey.generate(
public_exponent=65537,
key_size=512,
backend=backend
)
with pytest.raises(exceptions.UnsupportedPadding):
private_key.signer(DummyPadding(), hashes.SHA1(), backend)
def test_padding_incorrect_type(self, backend):
private_key = rsa.RSAPrivateKey.generate(
public_exponent=65537,
key_size=512,
backend=backend
)
with pytest.raises(TypeError):
private_key.signer("notpadding", hashes.SHA1(), backend)