Skip to content

Commit bb248a9

Browse files
Golang-style salt lengths to verify RSA PSS sigs
Signed-off-by: Trishank Karthik Kuppusamy <[email protected]>
1 parent 6f88f63 commit bb248a9

File tree

2 files changed

+222
-144
lines changed

2 files changed

+222
-144
lines changed

securesystemslib/rsa_keys.py

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
# Import pyca/cryptography routines needed to generate and load cryptographic
6969
# keys in PEM format.
7070
from cryptography.hazmat.primitives import serialization
71-
from cryptography.hazmat.backends.interfaces import PEMSerializationBackend
7271
from cryptography.hazmat.primitives.serialization import load_pem_private_key
7372
from cryptography.hazmat.backends import default_backend
7473

@@ -85,24 +84,25 @@
8584
# pyca/cryptography requires hash objects to generate PKCS#1 PSS
8685
# signatures (i.e., padding.PSS). The 'hmac' module is needed to verify
8786
# ciphertexts in encrypted key files.
88-
from cryptography.hazmat.primitives import hashes
89-
from cryptography.hazmat.primitives import hmac
87+
from cryptography.hazmat.primitives import hashes, hmac
9088

9189
# RSA's probabilistic signature scheme with appendix (RSASSA-PSS).
9290
# PKCS#1 v1.5 is available for compatibility with existing applications, but
9391
# RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates
94-
# a random salt to ensure the signature generated is probabilistic rather than
95-
# deterministic (e.g., PKCS#1 v1.5).
92+
# a random salt to ensure the signature generated is probabilistic rather
93+
# than deterministic (e.g., PKCS#1 v1.5).
9694
# http://en.wikipedia.org/wiki/RSA-PSS#Schemes
9795
# https://tools.ietf.org/html/rfc3447#section-8.1
9896
# The 'padding' module is needed for PSS signatures.
9997
from cryptography.hazmat.primitives.asymmetric import padding
10098

10199
# Import pyca/cryptography's Key Derivation Function (KDF) module.
102-
# 'securesystemslib.keys.py' needs this module to derive a secret key according
103-
# to the Password-Based Key Derivation Function 2 specification. The derived
104-
# key is used as the symmetric key to encrypt securesystemslib key information.
105-
# PKCS#5 v2.0 PBKDF2 specification: http://tools.ietf.org/html/rfc2898#section-5.2
100+
# 'securesystemslib.keys.py' needs this module to derive a secret key
101+
# according to the Password-Based Key Derivation Function 2 specification.
102+
# The derived key is used as the symmetric key to encrypt securesystemslib
103+
# key information.
104+
# PKCS#5 v2.0 PBKDF2 specification:
105+
# http://tools.ietf.org/html/rfc2898#section-5.2
106106
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
107107

108108
# pyca/cryptography's AES implementation available in 'ciphers.Cipher. and
@@ -112,11 +112,12 @@
112112
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
113113

114114
# The mode of operation is presently set to CTR (CounTeR Mode) for symmetric
115-
# block encryption (AES-256, where the symmetric key is 256 bits). 'modes' can
116-
# be used as an argument to 'ciphers.Cipher' to specify the mode of operation
117-
# for the block cipher. The initial random block, or initialization vector
118-
# (IV), can be set to begin the process of incrementing the 128-bit blocks and
119-
# allowing the AES algorithm to perform cipher block operations on them.
115+
# block encryption (AES-256, where the symmetric key is 256 bits). 'modes'
116+
# can be used as an argument to 'ciphers.Cipher' to specify the mode of
117+
# operation for the block cipher. The initial random block, or
118+
# initialization vector (IV), can be set to begin the process of
119+
# incrementing the 128-bit blocks and allowing the AES algorithm to perform
120+
# cipher block operations on them.
120121
from cryptography.hazmat.primitives.ciphers import modes
121122
except ImportError:
122123
CRYPTO = False
@@ -246,7 +247,54 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS):
246247

247248

248249

249-
def create_rsa_signature(private_key, data, scheme='rsassa-pss-sha256'):
250+
# The RSA-PSS scheme allows for choosing the salt length:
251+
# https://crypto.stackexchange.com/questions/1217/rsa-pss-salt-size
252+
# For compatibility with Golang, we borrow the salt lengths typically used in
253+
# its crypto package.
254+
# https://github.com/golang/go/blob/11f92e9dae96939c2d784ae963fa7763c300660b/src/crypto/rsa/pss.go#L225-L232
255+
# FIXME: really, we should encode the salt length as part of the metadata on
256+
# how to use the RSA-PSS public key.
257+
class SaltLengthType:
258+
"""A class to represent common salt lengths for RSA-PSS."""
259+
260+
@classmethod
261+
def get_salt_length(self, algorithm):
262+
"""Get the salt length as integer."""
263+
raise NotImplementedError
264+
265+
266+
267+
268+
269+
# NOTE: This is what used to be the standard behaviour.
270+
class HashSaltLengthType(SaltLengthType):
271+
"""Salt length to equal the length of the hash used in the signature."""
272+
273+
@classmethod
274+
def get_salt_length(cls, algorithm):
275+
"""Get the salt length as integer."""
276+
return algorithm.digest_size
277+
278+
279+
280+
281+
282+
class MaxSaltLengthType(SaltLengthType):
283+
"""Salt length in a PSS signature to be as large as possible when
284+
signing, and to be auto-detected when verifying."""
285+
286+
@classmethod
287+
def get_salt_length(cls, algorithm):
288+
"""Get the salt length as integer."""
289+
# NOTE: We disregard algorithm here.
290+
return padding.PSS.MAX_LENGTH
291+
292+
293+
294+
295+
296+
def create_rsa_signature(private_key, data, scheme='rsassa-pss-sha256',
297+
salt_length_type=HashSaltLengthType):
250298
"""
251299
<Purpose>
252300
Generate a 'scheme' signature. The signature, and the signature scheme
@@ -279,6 +327,10 @@ def create_rsa_signature(private_key, data, scheme='rsassa-pss-sha256'):
279327
scheme:
280328
The signature scheme used to generate the signature.
281329
330+
salt_length_type:
331+
The strategy for determining the length of the salt used in RSA-PSS, one of
332+
HashSaltLengthType or MaxSaltLengthType.
333+
282334
<Exceptions>
283335
securesystemslib.exceptions.FormatError, if 'private_key' is improperly
284336
formatted.
@@ -337,9 +389,12 @@ def create_rsa_signature(private_key, data, scheme='rsassa-pss-sha256'):
337389
# Generate an RSSA-PSS signature. Raise
338390
# 'securesystemslib.exceptions.CryptoError' for any of the expected
339391
# exceptions raised by pyca/cryptography.
340-
signature = private_key_object.sign(
341-
data, padding.PSS(mgf=padding.MGF1(digest_obj.algorithm),
342-
salt_length=digest_obj.algorithm.digest_size), digest_obj.algorithm)
392+
signature = private_key_object.sign(data,
393+
padding.PSS(
394+
mgf=padding.MGF1(digest_obj.algorithm),
395+
salt_length=salt_length_type.get_salt_length(
396+
digest_obj.algorithm)),
397+
digest_obj.algorithm)
343398

344399
elif scheme.startswith('rsa-pkcs1v15'):
345400
# Generate an RSA-PKCS1v15 signature. Raise
@@ -382,7 +437,8 @@ def create_rsa_signature(private_key, data, scheme='rsassa-pss-sha256'):
382437

383438

384439

385-
def verify_rsa_signature(signature, signature_scheme, public_key, data):
440+
def verify_rsa_signature(signature, signature_scheme, public_key, data,
441+
salt_length_type=HashSaltLengthType):
386442
"""
387443
<Purpose>
388444
Determine whether the corresponding private key of 'public_key' produced
@@ -415,6 +471,9 @@ def verify_rsa_signature(signature, signature_scheme, public_key, data):
415471
Data used by securesystemslib.keys.create_signature() to generate
416472
'signature'. 'data' (a string) is needed here to verify 'signature'.
417473
474+
salt_length_type:
475+
The length of salt used in RSA-PSS.
476+
418477
<Exceptions>
419478
securesystemslib.exceptions.FormatError, if 'signature',
420479
'signature_scheme', 'public_key', or 'data' are improperly formatted.
@@ -455,11 +514,6 @@ def verify_rsa_signature(signature, signature_scheme, public_key, data):
455514
# What about 'data'?
456515
securesystemslib.formats.DATA_SCHEMA.check_match(data)
457516

458-
# Verify whether the private key of 'public_key' produced 'signature'.
459-
# Before returning the 'valid_signature' Boolean result, ensure 'RSASSA-PSS'
460-
# was used as the signature scheme.
461-
valid_signature = False
462-
463517
# Verify the RSASSA-PSS signature with pyca/cryptography.
464518
try:
465519
public_key_object = serialization.load_pem_public_key(
@@ -474,19 +528,22 @@ def verify_rsa_signature(signature, signature_scheme, public_key, data):
474528
try:
475529
if signature_scheme.startswith('rsassa-pss'):
476530
public_key_object.verify(signature, data,
477-
padding.PSS(mgf=padding.MGF1(digest_obj.algorithm),
478-
salt_length=digest_obj.algorithm.digest_size),
531+
padding.PSS(
532+
mgf=padding.MGF1(digest_obj.algorithm),
533+
salt_length=salt_length_type.get_salt_length(
534+
digest_obj.algorithm)),
479535
digest_obj.algorithm)
480536

481537
elif signature_scheme.startswith('rsa-pkcs1v15'):
482538
public_key_object.verify(signature, data, padding.PKCS1v15(),
483539
digest_obj.algorithm)
484540

485-
# The RSA_SCHEME_SCHEMA.check_match() above should have validated 'scheme'.
486-
# This is a defensive check check..
541+
# The RSA_SCHEME_SCHEMA.check_match() above should have validated
542+
# 'scheme'. This is a defensive check.
487543
else: # pragma: no cover
488-
raise securesystemslib.exceptions.UnsupportedAlgorithmError('Unsupported'
489-
' signature scheme is specified: ' + repr(scheme))
544+
raise securesystemslib.exceptions.UnsupportedAlgorithmError(
545+
'Unsupported signature scheme is specified: ' + \
546+
repr(signature_scheme))
490547

491548
return True
492549

@@ -496,7 +553,8 @@ def verify_rsa_signature(signature, signature_scheme, public_key, data):
496553
# Raised by load_pem_public_key().
497554
except (ValueError, cryptography.exceptions.UnsupportedAlgorithm) as e:
498555
raise securesystemslib.exceptions.CryptoError('The PEM could not be'
499-
' decoded successfully, or contained an unsupported key type: ' + str(e))
556+
' decoded successfully, or contained an unsupported key type: ' + \
557+
str(e))
500558

501559

502560

@@ -679,7 +737,8 @@ def create_rsa_public_and_private_from_pem(pem, passphrase=None):
679737
# Or if the key was encrypted but no password was supplied.
680738
# UnsupportedAlgorithm: If the private key (or if the key is encrypted with
681739
# an unsupported symmetric cipher) is not supported by the backend.
682-
except (ValueError, TypeError, cryptography.exceptions.UnsupportedAlgorithm) as e:
740+
except (ValueError, TypeError,
741+
cryptography.exceptions.UnsupportedAlgorithm) as e:
683742
# Raise 'securesystemslib.exceptions.CryptoError' and pyca/cryptography's
684743
# exception message. Avoid propogating pyca/cryptography's exception trace
685744
# to avoid revealing sensitive error.
@@ -739,7 +798,8 @@ def encrypt_key(key_object, password):
739798
'1f26964cc8d4f7ee5f3c5da2fbb7ab35811169573ac367b860a537e47789f8c4'}}
740799
>>> passphrase = 'secret'
741800
>>> encrypted_key = encrypt_key(ed25519_key, passphrase)
742-
>>> securesystemslib.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key.encode('utf-8'))
801+
>>> securesystemslib.formats.ENCRYPTEDKEY_SCHEMA.matches(
802+
encrypted_key.encode('utf-8'))
743803
True
744804
745805
<Arguments>
@@ -770,7 +830,8 @@ def encrypt_key(key_object, password):
770830
encryption key.
771831
772832
<Returns>
773-
An encrypted string in 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA' format.
833+
An encrypted string in 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA'
834+
format.
774835
"""
775836

776837
if not CRYPTO: # pragma: no cover
@@ -786,7 +847,8 @@ def encrypt_key(key_object, password):
786847
securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
787848

788849
# Ensure the private portion of the key is included in 'key_object'.
789-
if 'private' not in key_object['keyval'] or not key_object['keyval']['private']:
850+
if 'private' not in key_object['keyval'] or \
851+
not key_object['keyval']['private']:
790852
raise securesystemslib.exceptions.FormatError('Key object does not contain'
791853
' a private part.')
792854

@@ -815,10 +877,10 @@ def decrypt_key(encrypted_key, password):
815877
"""
816878
<Purpose>
817879
Return a string containing 'encrypted_key' in non-encrypted form.
818-
The decrypt_key() function can be applied to the encrypted string to restore
819-
the original key object, a securesystemslib key (e.g., RSAKEY_SCHEMA,
820-
ED25519KEY_SCHEMA). This function calls the appropriate cryptography module
821-
(i.e., rsa_keys.py) to perform the decryption.
880+
The decrypt_key() function can be applied to the encrypted string to
881+
restore the original key object, a securesystemslib key (e.g.,
882+
RSAKEY_SCHEMA, ED25519KEY_SCHEMA). This function calls the appropriate
883+
cryptography module (i.e., rsa_keys.py) to perform the decryption.
822884
823885
Encrypted securesystemslib keys use AES-256-CTR-Mode and passwords
824886
strengthened with PBKDF2-HMAC-SHA256 (100K iterations be default, but may
@@ -875,7 +937,8 @@ def decrypt_key(encrypted_key, password):
875937
used to re-derive the encryption/decryption key.
876938
877939
<Returns>
878-
The decrypted key object in 'securesystemslib.formats.ANYKEY_SCHEMA' format.
940+
The decrypted key object in 'securesystemslib.formats.ANYKEY_SCHEMA'
941+
format.
879942
"""
880943

881944
if not CRYPTO: # pragma: no cover
@@ -944,11 +1007,11 @@ def _generate_derived_key(password, salt=None, iterations=None):
9441007

9451008
def _encrypt(key_data, derived_key_information):
9461009
"""
947-
Encrypt 'key_data' using the Advanced Encryption Standard (AES-256) algorithm.
948-
'derived_key_information' should contain a key strengthened by PBKDF2. The
949-
key size is 256 bits and AES's mode of operation is set to CTR (CounTeR Mode).
950-
The HMAC of the ciphertext is generated to ensure the ciphertext has not been
951-
modified.
1010+
Encrypt 'key_data' using the Advanced Encryption Standard (AES-256)
1011+
algorithm. 'derived_key_information' should contain a key strengthened by
1012+
PBKDF2. The key size is 256 bits and AES's mode of operation is set to CTR
1013+
(CounTeR Mode). The HMAC of the ciphertext is generated to ensure the
1014+
ciphertext has not been modified.
9521015
9531016
'key_data' is the JSON string representation of the key. In the case
9541017
of RSA keys, this format would be 'securesystemslib.formats.RSAKEY_SCHEMA':
@@ -984,15 +1047,15 @@ def _encrypt(key_data, derived_key_information):
9841047

9851048
# Encrypt the plaintext and get the associated ciphertext.
9861049
# Do we need to check for any exceptions?
987-
ciphertext = encryptor.update(key_data.encode('utf-8')) + encryptor.finalize()
1050+
ciphertext = encryptor.update(key_data.encode('utf-8')) + \
1051+
encryptor.finalize()
9881052

9891053
# Generate the hmac of the ciphertext to ensure it has not been modified.
9901054
# The decryption routine may verify a ciphertext without having to perform
9911055
# a decryption operation.
9921056
symmetric_key = derived_key_information['derived_key']
9931057
salt = derived_key_information['salt']
994-
hmac_object = \
995-
cryptography.hazmat.primitives.hmac.HMAC(symmetric_key, hashes.SHA256(),
1058+
hmac_object = hmac.HMAC(symmetric_key, hashes.SHA256(),
9961059
backend=default_backend())
9971060
hmac_object.update(ciphertext)
9981061
hmac_value = binascii.hexlify(hmac_object.finalize())
@@ -1031,7 +1094,7 @@ def _decrypt(file_contents, password):
10311094
# separating. Raise 'securesystemslib.exceptions.CryptoError', if
10321095
# 'file_contents' does not contains the expected data layout.
10331096
try:
1034-
salt, iterations, hmac, iv, ciphertext = \
1097+
salt, iterations, observed_hmac, iv, ciphertext = \
10351098
file_contents.split(_ENCRYPTION_DELIMITER)
10361099

10371100
except ValueError:
@@ -1054,14 +1117,14 @@ def _decrypt(file_contents, password):
10541117
# See the encryption routine for why we use the encrypt-then-MAC approach.
10551118
# The decryption routine may verify a ciphertext without having to perform
10561119
# a decryption operation.
1057-
generated_hmac_object = \
1058-
cryptography.hazmat.primitives.hmac.HMAC(symmetric_key, hashes.SHA256(),
1120+
generated_hmac_object = hmac.HMAC(symmetric_key, hashes.SHA256(),
10591121
backend=default_backend())
10601122
generated_hmac_object.update(ciphertext)
10611123
generated_hmac = binascii.hexlify(generated_hmac_object.finalize())
10621124

10631125

1064-
if not securesystemslib.util.digests_are_equal(generated_hmac.decode(), hmac):
1126+
if not securesystemslib.util.digests_are_equal(
1127+
generated_hmac.decode(), observed_hmac):
10651128
raise securesystemslib.exceptions.CryptoError('Decryption failed.')
10661129

10671130
# Construct a Cipher object, with the key and iv.

0 commit comments

Comments
 (0)