Skip to content

Commit

Permalink
[Python-experimental] Use DER encoding for ECDSA signatures, add para…
Browse files Browse the repository at this point in the history
…meter to configure hash algorithm (OpenAPITools#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
  • Loading branch information
sebastien-rosset authored and michaelpro1 committed May 7, 2020
1 parent 66fce9c commit 973d0aa
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -98,16 +102,23 @@ 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.
"""
def __init__(self, key_id, signing_scheme, private_key_path,
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}:
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
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.
Expand Down Expand Up @@ -106,16 +110,23 @@ 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.
"""
def __init__(self, key_id, signing_scheme, private_key_path,
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}:
Expand All @@ -126,6 +137,24 @@ def __init__(self, key_id, signing_scheme, private_key_path,
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
Expand Down Expand Up @@ -317,14 +346,14 @@ def _get_message_digest(self, data):
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

Expand All @@ -348,7 +377,10 @@ def _sign_digest(self, digest):
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,12 @@ def _validate_authorization_header(self, request_target, actual_headers, authori
"{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()

Expand All @@ -182,10 +182,12 @@ def _validate_authorization_header(self, request_target, actual_headers, authori
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))
Expand Down Expand Up @@ -295,7 +297,7 @@ def test_valid_http_signature(self):
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)

Expand Down Expand Up @@ -326,7 +328,7 @@ def test_valid_http_signature_with_defaults(self):
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)

Expand Down Expand Up @@ -362,7 +364,7 @@ def test_valid_http_signature_rsassa_pkcs1v15(self):
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)

Expand Down Expand Up @@ -398,7 +400,7 @@ def test_valid_http_signature_rsassa_pss(self):
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)

Expand All @@ -411,6 +413,7 @@ def test_valid_http_signature_ec_p521(self):
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,
Expand All @@ -433,7 +436,7 @@ def test_valid_http_signature_ec_p521(self):
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)

Expand Down

0 comments on commit 973d0aa

Please sign in to comment.