[Python-experimental] Use DER encoding for ECDSA signatures, add parameter to configure hash algorithm (#5924)

* Use DER encoding for ECDSA signatures

* Use DER encoding for ECDSA signatures

* Use DER encoding for ECDSA signatures

* Use DER encoding for ECDSA signatures

* fix python unit tests for http message signature

* Fix error message

* format python code

* format python code
This commit is contained in:
Sebastien Rosset
2020-04-15 10:43:33 -07:00
committed by GitHub
parent d57ceb86bf
commit 827904f732
3 changed files with 91 additions and 24 deletions

View File

@@ -53,6 +53,10 @@ ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS = {
ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979
}
# The cryptographic hash algorithm for the message signature.
HASH_SHA256 = 'sha256'
HASH_SHA512 = 'sha512'
class HttpSigningConfiguration(object):
"""The configuration parameters for the HTTP signature security scheme.
@@ -98,9 +102,15 @@ class HttpSigningConfiguration(object):
Supported values are:
1. For RSA keys: RSASSA-PSS, RSASSA-PKCS1-v1_5.
2. For ECDSA keys: fips-186-3, deterministic-rfc6979.
The default value is inferred from the private key.
The default value for RSA keys is RSASSA-PSS.
The default value for ECDSA keys is fips-186-3.
If None, the signing algorithm is inferred from the private key.
The default signing algorithm for RSA keys is RSASSA-PSS.
The default signing algorithm for ECDSA keys is fips-186-3.
:param hash_algorithm: The hash algorithm for the signature. Supported values are
sha256 and sha512.
If the signing_scheme is rsa-sha256, the hash algorithm must be set
to None or sha256.
If the signing_scheme is rsa-sha512, the hash algorithm must be set
to None or sha512.
:param signature_max_validity: The signature max validity, expressed as
a datetime.timedelta value. It must be a positive value.
"""
@@ -108,6 +118,7 @@ class HttpSigningConfiguration(object):
private_key_passphrase=None,
signed_headers=None,
signing_algorithm=None,
hash_algorithm=None,
signature_max_validity=None):
self.key_id = key_id
if signing_scheme not in {SCHEME_HS2019, SCHEME_RSA_SHA256, SCHEME_RSA_SHA512}:
@@ -118,6 +129,24 @@ class HttpSigningConfiguration(object):
self.private_key_path = private_key_path
self.private_key_passphrase = private_key_passphrase
self.signing_algorithm = signing_algorithm
self.hash_algorithm = hash_algorithm
if signing_scheme == SCHEME_RSA_SHA256:
if self.hash_algorithm is None:
self.hash_algorithm = HASH_SHA256
elif self.hash_algorithm != HASH_SHA256:
raise Exception("Hash algorithm must be sha256 when security scheme is %s" %
SCHEME_RSA_SHA256)
elif signing_scheme == SCHEME_RSA_SHA512:
if self.hash_algorithm is None:
self.hash_algorithm = HASH_SHA512
elif self.hash_algorithm != HASH_SHA512:
raise Exception("Hash algorithm must be sha512 when security scheme is %s" %
SCHEME_RSA_SHA512)
elif signing_scheme == SCHEME_HS2019:
if self.hash_algorithm is None:
self.hash_algorithm = HASH_SHA256
elif self.hash_algorithm not in {HASH_SHA256, HASH_SHA512}:
raise Exception("Invalid hash algorithm")
if signature_max_validity is not None and signature_max_validity.total_seconds() < 0:
raise Exception("The signature max validity must be a positive value")
self.signature_max_validity = signature_max_validity
@@ -309,14 +338,14 @@ class HttpSigningConfiguration(object):
The prefix is a string that identifies the cryptographc hash. It is used
to generate the 'Digest' header as specified in RFC 3230.
"""
if self.signing_scheme in {SCHEME_RSA_SHA512, SCHEME_HS2019}:
if self.hash_algorithm == HASH_SHA512:
digest = SHA512.new()
prefix = 'SHA-512='
elif self.signing_scheme == SCHEME_RSA_SHA256:
elif self.hash_algorithm == HASH_SHA256:
digest = SHA256.new()
prefix = 'SHA-256='
else:
raise Exception("Unsupported signing algorithm: {0}".format(self.signing_scheme))
raise Exception("Unsupported hash algorithm: {0}".format(self.hash_algorithm))
digest.update(data)
return digest, prefix
@@ -340,7 +369,10 @@ class HttpSigningConfiguration(object):
if sig_alg is None:
sig_alg = ALGORITHM_ECDSA_MODE_FIPS_186_3
if sig_alg in ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS:
signature = DSS.new(self.private_key, sig_alg).sign(digest)
# draft-ietf-httpbis-message-signatures-00 does not specify the ECDSA encoding.
# Issue: https://github.com/w3c-ccg/http-signatures/issues/107
signature = DSS.new(key=self.private_key, mode=sig_alg,
encoding='der').sign(digest)
else:
raise Exception("Unsupported signature algorithm: {0}".format(sig_alg))
else:

View File

@@ -61,6 +61,10 @@ ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS = {
ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979
}
# The cryptographic hash algorithm for the message signature.
HASH_SHA256 = 'sha256'
HASH_SHA512 = 'sha512'
class HttpSigningConfiguration(object):
"""The configuration parameters for the HTTP signature security scheme.
@@ -106,9 +110,15 @@ class HttpSigningConfiguration(object):
Supported values are:
1. For RSA keys: RSASSA-PSS, RSASSA-PKCS1-v1_5.
2. For ECDSA keys: fips-186-3, deterministic-rfc6979.
The default value is inferred from the private key.
The default value for RSA keys is RSASSA-PSS.
The default value for ECDSA keys is fips-186-3.
If None, the signing algorithm is inferred from the private key.
The default signing algorithm for RSA keys is RSASSA-PSS.
The default signing algorithm for ECDSA keys is fips-186-3.
:param hash_algorithm: The hash algorithm for the signature. Supported values are
sha256 and sha512.
If the signing_scheme is rsa-sha256, the hash algorithm must be set
to None or sha256.
If the signing_scheme is rsa-sha512, the hash algorithm must be set
to None or sha512.
:param signature_max_validity: The signature max validity, expressed as
a datetime.timedelta value. It must be a positive value.
"""
@@ -116,6 +126,7 @@ class HttpSigningConfiguration(object):
private_key_passphrase=None,
signed_headers=None,
signing_algorithm=None,
hash_algorithm=None,
signature_max_validity=None):
self.key_id = key_id
if signing_scheme not in {SCHEME_HS2019, SCHEME_RSA_SHA256, SCHEME_RSA_SHA512}:
@@ -126,6 +137,24 @@ class HttpSigningConfiguration(object):
self.private_key_path = private_key_path
self.private_key_passphrase = private_key_passphrase
self.signing_algorithm = signing_algorithm
self.hash_algorithm = hash_algorithm
if signing_scheme == SCHEME_RSA_SHA256:
if self.hash_algorithm is None:
self.hash_algorithm = HASH_SHA256
elif self.hash_algorithm != HASH_SHA256:
raise Exception("Hash algorithm must be sha256 when security scheme is %s" %
SCHEME_RSA_SHA256)
elif signing_scheme == SCHEME_RSA_SHA512:
if self.hash_algorithm is None:
self.hash_algorithm = HASH_SHA512
elif self.hash_algorithm != HASH_SHA512:
raise Exception("Hash algorithm must be sha512 when security scheme is %s" %
SCHEME_RSA_SHA512)
elif signing_scheme == SCHEME_HS2019:
if self.hash_algorithm is None:
self.hash_algorithm = HASH_SHA256
elif self.hash_algorithm not in {HASH_SHA256, HASH_SHA512}:
raise Exception("Invalid hash algorithm")
if signature_max_validity is not None and signature_max_validity.total_seconds() < 0:
raise Exception("The signature max validity must be a positive value")
self.signature_max_validity = signature_max_validity
@@ -317,14 +346,14 @@ class HttpSigningConfiguration(object):
The prefix is a string that identifies the cryptographc hash. It is used
to generate the 'Digest' header as specified in RFC 3230.
"""
if self.signing_scheme in {SCHEME_RSA_SHA512, SCHEME_HS2019}:
if self.hash_algorithm == HASH_SHA512:
digest = SHA512.new()
prefix = 'SHA-512='
elif self.signing_scheme == SCHEME_RSA_SHA256:
elif self.hash_algorithm == HASH_SHA256:
digest = SHA256.new()
prefix = 'SHA-256='
else:
raise Exception("Unsupported signing algorithm: {0}".format(self.signing_scheme))
raise Exception("Unsupported hash algorithm: {0}".format(self.hash_algorithm))
digest.update(data)
return digest, prefix
@@ -348,7 +377,10 @@ class HttpSigningConfiguration(object):
if sig_alg is None:
sig_alg = ALGORITHM_ECDSA_MODE_FIPS_186_3
if sig_alg in ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS:
signature = DSS.new(self.private_key, sig_alg).sign(digest)
# draft-ietf-httpbis-message-signatures-00 does not specify the ECDSA encoding.
# Issue: https://github.com/w3c-ccg/http-signatures/issues/107
signature = DSS.new(key=self.private_key, mode=sig_alg,
encoding='der').sign(digest)
else:
raise Exception("Unsupported signature algorithm: {0}".format(sig_alg))
else:

View File

@@ -151,12 +151,12 @@ class MockPoolManager(object):
"{0}: {1}".format(key.lower(), value) for key, value in signed_headers_list]
string_to_sign = "\n".join(header_items)
digest = None
if self.signing_cfg.signing_scheme in {signing.SCHEME_RSA_SHA512, signing.SCHEME_HS2019}:
if self.signing_cfg.hash_algorithm == signing.HASH_SHA512:
digest = SHA512.new()
elif self.signing_cfg.signing_scheme == signing.SCHEME_RSA_SHA256:
elif self.signing_cfg.hash_algorithm == signing.HASH_SHA256:
digest = SHA256.new()
else:
self._tc.fail("Unsupported signature scheme: {0}".format(self.signing_cfg.signing_scheme))
self._tc.fail("Unsupported hash algorithm: {0}".format(self.signing_cfg.hash_algorithm))
digest.update(string_to_sign.encode())
b64_body_digest = base64.b64encode(digest.digest()).decode()
@@ -182,10 +182,12 @@ class MockPoolManager(object):
elif signing_alg == signing.ALGORITHM_RSASSA_PSS:
pss.new(self.pubkey).verify(digest, signature)
elif signing_alg == signing.ALGORITHM_ECDSA_MODE_FIPS_186_3:
verifier = DSS.new(self.pubkey, signing.ALGORITHM_ECDSA_MODE_FIPS_186_3)
verifier = DSS.new(key=self.pubkey, mode=signing.ALGORITHM_ECDSA_MODE_FIPS_186_3,
encoding='der')
verifier.verify(digest, signature)
elif signing_alg == signing.ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979:
verifier = DSS.new(self.pubkey, signing.ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979)
verifier = DSS.new(key=self.pubkey, mode=signing.ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979,
encoding='der')
verifier.verify(digest, signature)
else:
self._tc.fail("Unsupported signing algorithm: {0}".format(signing_alg))
@@ -295,7 +297,7 @@ class PetApiTests(unittest.TestCase):
headers={'Content-Type': r'application/json',
'Authorization': r'Signature keyId="my-key-id",algorithm="hs2019",created=[0-9]+,'
r'headers="\(request-target\) \(created\) host date digest content-type",'
r'signature="[a-zA-Z0-9+/]+="',
r'signature="[a-zA-Z0-9+/=]+"',
'User-Agent': r'OpenAPI-Generator/1.0.0/python'},
preload_content=True, timeout=None)
@@ -326,7 +328,7 @@ class PetApiTests(unittest.TestCase):
headers={'Content-Type': r'application/json',
'Authorization': r'Signature keyId="my-key-id",algorithm="hs2019",created=[0-9]+,'
r'headers="\(created\)",'
r'signature="[a-zA-Z0-9+/]+="',
r'signature="[a-zA-Z0-9+/=]+"',
'User-Agent': r'OpenAPI-Generator/1.0.0/python'},
preload_content=True, timeout=None)
@@ -362,7 +364,7 @@ class PetApiTests(unittest.TestCase):
headers={'Content-Type': r'application/json',
'Authorization': r'Signature keyId="my-key-id",algorithm="hs2019",created=[0-9]+,'
r'headers="\(request-target\) \(created\)",'
r'signature="[a-zA-Z0-9+/]+="',
r'signature="[a-zA-Z0-9+/=]+"',
'User-Agent': r'OpenAPI-Generator/1.0.0/python'},
preload_content=True, timeout=None)
@@ -398,7 +400,7 @@ class PetApiTests(unittest.TestCase):
headers={'Content-Type': r'application/json',
'Authorization': r'Signature keyId="my-key-id",algorithm="hs2019",created=[0-9]+,'
r'headers="\(request-target\) \(created\)",'
r'signature="[a-zA-Z0-9+/]+="',
r'signature="[a-zA-Z0-9+/=]+"',
'User-Agent': r'OpenAPI-Generator/1.0.0/python'},
preload_content=True, timeout=None)
@@ -411,6 +413,7 @@ class PetApiTests(unittest.TestCase):
signing_scheme=signing.SCHEME_HS2019,
private_key_path=privkey_path,
private_key_passphrase=self.private_key_passphrase,
hash_algorithm=signing.HASH_SHA512,
signed_headers=[
signing.HEADER_REQUEST_TARGET,
signing.HEADER_CREATED,
@@ -433,7 +436,7 @@ class PetApiTests(unittest.TestCase):
headers={'Content-Type': r'application/json',
'Authorization': r'Signature keyId="my-key-id",algorithm="hs2019",created=[0-9]+,'
r'headers="\(request-target\) \(created\)",'
r'signature="[a-zA-Z0-9+/]+"',
r'signature="[a-zA-Z0-9+/=]+"',
'User-Agent': r'OpenAPI-Generator/1.0.0/python'},
preload_content=True, timeout=None)