Metadata-Version: 2.1
Name: http-signature-client
Version: 0.0.23
Summary: Implementation of the client side of the IETF draft "Signing HTTP Messages"
Home-page: https://github.com/michalc/python-http-signature-client
Author: Michal Charemza
Author-email: michal@charemza.name
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.6.3
Description-Content-Type: text/markdown

# http-signature-client  [![CircleCI](https://circleci.com/gh/michalc/python-http-signature-client.svg?style=shield)](https://circleci.com/gh/michalc/python-http-signature-client) [![Test Coverage](https://api.codeclimate.com/v1/badges/fcf6ed3ac0c04d3878a8/test_coverage)](https://codeclimate.com/github/michalc/python-http-signature-client/test_coverage)

Utility function with an HTTP client agnostic Python implementation of the client side of the [IETF draft "Signing HTTP Messages"](https://tools.ietf.org/html/draft-ietf-httpbis-message-signatures-00). No dependencies other than the standard library, but [cryptography](https://github.com/pyca/cryptography) would typically be required in client code to load a private key.

See [http-signature-server](https://github.com/michalc/python-http-signature-server) for a compatible server-side implementation.


## Installation

```python
pip install http-signature-client
```

## Usage

```python
from http_signature_client import sign_headers

def sign(data):
    # Return a signature of `data`, for example using a private key

signed_headers = sign_headers(key_id, sign, method, path, headers_to_sign)
```

## Recipe: HTTPX with PEM-encoded private key and SHA-512 body digest

```python
from base64 import b64encode
import hashlib

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import httpx

from http_signature_client import sign_headers

class HttpSignature(httpx.Auth):
    requires_request_body = True

    def __init__(self, key_id, pem_private_key):
        self.key_id = key_id
        self.private_key = load_pem_private_key(
            pem_private_key, password=None, backend=default_backend())

    def auth_flow(self, request):
        body_sha512 = b64encode(hashlib.sha512(r.content).digest()).decode('ascii')
        headers_to_sign = tuple(request.headers.items()) + (('digest', f'SHA512={body_sha512}'),)
        request.headers = httpx.Headers(sign_headers(
            self.key_id, self.private_key.sign, request.method,
            request.url.full_path, headers_to_sign))
        yield r

# In real cases, take credentials from environment variables/secret store
response = httpx.post('https://postman-echo.com/post', data=b'The bytes', auth=HttpSignature(
    key_id='my-key',
    pem_private_key= \
        b'-----BEGIN PRIVATE KEY-----\n' \
        b'MC4CAQAwBQYDK2VwBCIEINQG5lNt1bE8TZa68mV/WZdpqsXaOXBHvgPQGm5CcjHp\n' \
        b'-----END PRIVATE KEY-----\n',
    )
)
```


## Recipe: Python requests with PEM-encoded private key and SHA-512 body digest

```python
from base64 import b64encode
import hashlib

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import requests
import urllib3

from http_signature_client import sign_headers

def HttpSignature(key_id, pem_private_key):
    private_key = load_pem_private_key(
            pem_private_key, password=None, backend=default_backend())

    def sign(r):
        body_sha512 = b64encode(hashlib.sha512(r.body).digest()).decode('ascii')
        headers_to_sign = tuple(r.headers.items()) + (('digest', f'SHA512={body_sha512}'),)
        parsed_url = urllib3.util.url.parse_url(r.path_url)
        path = parsed_url.path + (f'?{parsed_url.query}' if parsed_url.query else '')
        r.headers = dict(sign_headers(
            key_id, private_key.sign, r.method, path, headers_to_sign))
        return r

    return sign

# In real cases, take credentials from environment variables/secret store
response = requests.post('https://postman-echo.com/post', data=b'The bytes', auth=HttpSignature(
    key_id='my-key',
    pem_private_key= \
        b'-----BEGIN PRIVATE KEY-----\n' \
        b'MC4CAQAwBQYDK2VwBCIEINQG5lNt1bE8TZa68mV/WZdpqsXaOXBHvgPQGm5CcjHp\n' \
        b'-----END PRIVATE KEY-----\n',
    )
)
```


## Recipe: Create an Ed25519 public/private key pair

```python
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat

private_key = Ed25519PrivateKey.generate()
print(private_key.private_bytes(encoding=Encoding.PEM, format=PrivateFormat.PKCS8, encryption_algorithm=NoEncryption()))
print(private_key.public_key().public_bytes(encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo))
```


## API

### Parameters

- `key_id` - The keyId parameter sent with the signature. Typically, the server treats this as the claimed identity of the client.

- `sign` - A function that signs the request bytes once canonicalised. Typically, this would be a function that uses a private key.

- `method` - The HTTP method of the request, such as `GET` or `POST`.

- `path` - The full path of the request, including any query string.

- `headers_to_sign` - A tuple of (key, value) pairs of HTTP headers to sign.

- `headers_to_ignore` - A `frozenset` of HTTP header names to _not_ be signed, even if passed in `headers_to_sign`. These default to hop-by-hop-headers that are typically set by intermediaries.

### Returns

The `headers_to_sign` argument concatanated with an `signature` header containing the HTTP signature.


## What's implemented

A deliberate subset of the signature algorithm is implemented:

- the `(request-target)` pseudo-header is sent and signed [to allow the server to verify the method and path];
- the `created` parameter is sent and signed as the `(created)` pseudo-header [to allow the server to decide to reject if the skew is too large];
- the `headers` parameter is sent and signed [to allow the server to verify headers and pseudo-headers];
- the `expires` parameter is _not_ sent [the server can decide this using the created parameter];
- the `algorithm` parameter is _not_ sent [it should not be used by the server to choose the algorithm].

The `(request-target)` and `(created)` pseudo-headers are always prepended to the list of real HTTP headers before canonicalisation.


