Giter VIP home page Giter VIP logo

http-message-signatures's People

Contributors

achille-roussel avatar kislyuk avatar romanek-adam avatar romanek-adam-b2c2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

http-message-signatures's Issues

Should the keys in the VerifyResult covered_components OrderedDict be double quoted?

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.

Please show example of sending message signature headers to Flask using its test client

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!

Test.py does not use key pair test-key-rsa.pem?

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.

AttributeError: 'URL' object has no attribute 'decode'` when attempt verify

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.

Verification of the created field is too strict

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!

Potential API changes in http_sfv (dependency)

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:

  1. If you have any thoughts / preferences about the API.
  2. If the new API is merged, how you'd like to manage the dependency. I'd probably version it as 0.10.1, FWIW.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.