pyauth / http-message-signatures Goto Github PK
View Code? Open in Web Editor NEWAn implementation of RFC 9421, HTTP Message Signatures
License: Apache License 2.0
An implementation of RFC 9421, HTTP Message Signatures
License: Apache License 2.0
I'm looking at Content-Digest verification. Would it make sense to implement this as a helper in this library?
I'm trying to check that the signature verify result includes certain fields, for example on a POST with a body, the content-digest should be covered. I discovered that the keys in the VerifyResult covered_components OrderedDict are quoted; i.e., start and end with double-quote characters. Is this essential? The values are not double-quoted so I am wondering if maybe this was an oversight. Please see example:
OrderedDict([('"@method"', 'POST'), ('"@authority"', 'localhost'), ('"@target-uri"', 'http://localhost/some/path'),
('"content-digest"', 'sha-256=:yUnXGcm2X2HRcRX87e2yhRNdlvZHIIggm6zgJgJiCYw=:'), ('"content-length"', '657'),
('"@signature-params"', '("@method" "@authority" "@target-uri" "content-digest" "content-length");created=1658067851;keyid="test-key";alg="rsa-pss-sha512"')])
Just one more thing, would you please use the verify result in the test.py
tests? I'm sure I'm not the only person who wants to check the covered components, so this seems like it would be a very helpful demonstration.
I'm new to signatures, and am trying to implement a check of signatures on HTTP messages that arrive at a Flask-based REST server (also using connexion, not sure that matters). The tests for that server use a Flask client test fixture, as shown below. I think I can contribute an example of a tox test case that uses a signed Request, and that allows checking of the content-digest.
One wrinkle is the URL. A typical Flask test doesn't use a full URL like "https://blah.company.com/some/path" but rather just the suffix '/some/path'. Using good old localhost
worked here. I believe the server starts on some high-numbered port; the port number is not in the checked fields (component IDs).
To start at the very beginning, I created a public-private key pair like this:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tests/rsa_private.pem
openssl rsa -in rsa_private.pem -pubout -out tests/rsa_public.pem
I am using a Python test fixture that yields a Flask client, in file tests/conftest.py
:
@pytest.fixture
def client():
flask_app.config["TESTING"] = True
cl = flask_app.test_client()
yield cl
Here's the supporting code for test cases, in a file like tests/test_controller.py
:
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from http_message_signatures import algorithms, HTTPMessageSigner, HTTPSignatureKeyResolver
import hashlib
import http_sfv
import json
import requests
class TestHTTPSignatureKeyResolver(HTTPSignatureKeyResolver):
"""
Provides a method to get a private key from a file,
as required by the signature generator method.
"""
def __init__(self):
"""
Fetch the private key from the test file.
"""
with open("tests/rsa_private.pem", "rb") as fh:
self.private_key = load_pem_private_key(fh.read(), password=None)
logger.debug('__init__: private key %s', self.private_key)
def resolve_public_key(self, key_id: str):
"""
Signature generation does not use a private key.
"""
logger.debug('resolve_public_key: key_id %s', key_id)
raise NotImplementedError('No public key')
def resolve_private_key(self, key_id: str):
logger.debug('resolve_private_key: key_id %s', key_id)
return self.private_key
def _create_signature_headers(
method: str, url_base: str,
component_ids: tuple = None,
data: bytes = None) -> dict:
"""
Prepend 'http://localhost' to the URL, then build and sign a request.
If data is not None, compute and add a content-digest header for signing.
Return a dict with these headers:
Content-Digest (optional)
Signature-Input
Signature
:param method: HTTP method; e.g., 'POST'
:param url_base: endpoint path with leading slash; e.g., /controller/entity
:param component_ids: iterable of HTTP component names to sign;
if None, defaults to @method, @authority, @target-uri
:param data: bytes, an encoded JSON string; defaults to None
"""
logger.debug('_create_signature_headers: method %s url %s components %s',
method, url_base, component_ids)
con_dig = 'Content-Digest'
sig_ipt = 'Signature-Input'
sig = 'Signature'
url = 'http://localhost' + url_base
request = requests.Request(method=method, url=url, data=data)
request = request.prepare()
signature_headers = {}
if data is not None:
request.headers[con_dig] = str(http_sfv.Dictionary({"sha-256":
hashlib.sha256(data).digest()}))
signature_headers[con_dig] = request.headers[con_dig]
signer = HTTPMessageSigner(signature_algorithm=algorithms.RSA_PSS_SHA512,
key_resolver=TestHTTPSignatureKeyResolver())
# sign returns nothing
if component_ids is None:
signer.sign(request, key_id='anykey')
else:
signer.sign(request, key_id='anykey',
covered_component_ids=component_ids)
# extract the signature input and signature
signature_headers[sig_ipt] = request.headers[sig_ipt],
signature_headers[sig] = request.headers[sig]
logger.debug('_create_signature_headers: return %s', signature_headers)
return signature_headers
Finally here's a Flask test case POST-ing to a controller method with the signature headers. It creates a request body as bytes and specifies the content-type. Usually I delegate both those actions to the client's post
method by using the json parameter. Doing it this way ensures that the client sends exactly the body that was signed; for example, no re-ordering of the JSON keys.
def test_ctlr_msg_sig(client):
simdata = {'some': 'thing'}
request_json = json.dumps(simdata, sort_keys=True)
request_bytes = request_json.encode('utf-8')
url = '/some/path'
hdrs = _create_signature_headers(
method='POST', url_base=url,
data=request_bytes,
component_ids=("@method", "@authority", "@target-uri",
"content-digest", "content-length"))
hdrs['content-type'] = 'application/json'
res = client.post(url, headers=hdrs, data=request_bytes)
assert res.status_code == 201
If there's an easier way please say!
I am trying to use a plain RSA public/private key pair to sign a message (not an RSA-PSS pair). I hit problems and looked at test.py for how it loads the public and private keys from files test-key-rsa.pem, test-key-rsa.key. Then I realized, those files are not currently used in a test. Please tell me, is that just an oversight, or is there something about plain (non-PSS) RSA keys that makes this impossible?
If I succeed in creating a test I'll post the code here.
On Python 3.10, when attempt to verify the message, the following exception will occur:
File "/home/test/.virtualenvs/httpsig/lib/python3.10/site-packages/http_message_signatures/signatures.py", line 173, in verify raise InvalidSignature(e) from e http_message_signatures.exceptions.InvalidSignature: 'URL' object has no attribute 'decode'
A further trace:
Traceback (most recent call last): File "/home/test/.virtualenvs/httpsig/lib/python3.10/site-packages/http_message_signatures/signatures.py", line 167, in verify sig_base, sig_params_node, sig_elements = self._build_signature_base( File "/home/test/.virtualenvs/httpsig/lib/python3.10/site-packages/http_message_signatures/signatures.py", line 44, in _build_signature_base component_value = component_resolver.resolve(component_id) File "/home/test/.virtualenvs/httpsig/lib/python3.10/site-packages/http_message_signatures/resolvers.py", line 38, in resolve return resolver(**component_node.params) File "/home/test/.virtualenvs/httpsig/lib/python3.10/site-packages/http_message_signatures/resolvers.py", line 52, in get_authority return urllib.parse.urlsplit(self.url).netloc.lower() File "/opt/python310/lib/python3.10/urllib/parse.py", line 458, in urlsplit url, scheme, _coerce_result = _coerce_args(url, scheme) File "/opt/python310/lib/python3.10/urllib/parse.py", line 128, in _coerce_args return _decode_args(args) + (_encode_result,) File "/opt/python310/lib/python3.10/urllib/parse.py", line 112, in _decode_args return tuple(x.decode(encoding, errors) if x else '' for x in args) File "/opt/python310/lib/python3.10/urllib/parse.py", line 112, in <genexpr> return tuple(x.decode(encoding, errors) if x else '' for x in args) AttributeError: 'URL' object has no attribute 'decode'
The url
assigned to self.url
in HTTPSignatureComponentResolver
is a type yarl.URL
:
https://github.com/pyauth/http-message-signatures/blob/main/http_message_signatures/resolvers.py#L29
I propose to always ensure self.url
is always a string type as follows:
self.url = str(message.url)
Will this be an acceptable solution?
Thanks for the considerations.
Hello!
I am opening this issue to discuss an improvement that could be made to the created
timestamp verification.
It's been our experience that even when clocks are in sync between systems in low-latency networks, the client might cross a 1-second boundary before the server, which then mistakenly refuses the signature claiming that the timestamp is in the future; because of this check https://github.com/pyauth/http-message-signatures/blob/main/http_message_signatures/signatures.py#L135-L138
now = datetime.datetime.now()
if "created" in sig_input.params:
if self._parse_integer_timestamp(sig_input.params["created"], field_name="created") > now:
raise InvalidSignature('Signature "created" parameter is set to a time in the future')
As a workaround, we can disable setting the created
field on the client side, but this isn't ideal.
An alternative could be introducing a tolerance in comparing the created
timestamp and now
. For example, a 1-second default tolerance should cover most use cases without significantly reducing safety. Making this configurable could also be a useful improvement if a default isn't desired.
I'm happy to send a pull request to submit the fix; let me know what you think!
Hi,
I'm looking at changes to http_sfv
, and noticed that this library depends upon it.
See a summary of how it would work. Basically, it's moving from an object-based approach to a set of functions that produce datastructures. This should be more performant and I think more straightforward to use.
I'm writing here to ask:
0.10.1
, FWIW.I'm using this library indirectly via the requests-http-signature
package, and utilizing the RSA_V1_5_SHA256
algorithm. There is a bug with that implementation that gets fixed by a newer commit:
52ddf88#diff-84caf8052a46837dd235b3342402fe123788dbc49034f1541ae36ce77f365a5fL59
Would it be possible to cut a new release of this library and requests-http-signature
that contains this fix?
Thank you.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.