mirror of
https://github.com/saymrwulf/cryptography.git
synced 2026-05-14 20:37:55 +00:00
Merge pull request #673 from reaperhulk/rsa-pkcs1-signature-only
Add RSA PKCS1 signing (and structure for PSS + verification)
This commit is contained in:
commit
d8c8f7cde6
12 changed files with 317 additions and 2 deletions
|
|
@ -46,3 +46,7 @@ class InvalidKey(Exception):
|
|||
|
||||
class InvalidToken(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedPadding(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
22
cryptography/hazmat/primitives/asymmetric/padding.py
Normal file
22
cryptography/hazmat/primitives/asymmetric/padding.py
Normal 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"
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
10
docs/hazmat/primitives/asymmetric/index.rst
Normal file
10
docs/hazmat/primitives/asymmetric/index.rst
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.. hazmat::
|
||||
|
||||
Asymmetric Algorithms
|
||||
=====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
rsa
|
||||
padding
|
||||
20
docs/hazmat/primitives/asymmetric/padding.rst
Normal file
20
docs/hazmat/primitives/asymmetric/padding.rst
Normal 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/
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ Primitives
|
|||
symmetric-encryption
|
||||
padding
|
||||
key-derivation-functions
|
||||
rsa
|
||||
asymmetric/index
|
||||
constant-time
|
||||
interfaces
|
||||
twofactor
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue