Testing

The Cavage Draft Standard suggests several tests, which we can recreate.

Server

Setting Up

Install

$ pip install drf-keypair-permissions
$ ./manage.py startapp test_app
$ ./manage.py makemigrations
$ ./manage.py migrate
$ ./manage.py createsuperuser
$ ./manage.py runserver

Set up routes

settings.py:

# ...

ALLOWED_HOSTS = [
    'example.com',
    '127.0.0.1',
]
# ...
INSTALLED_APPS = [
    # ...
    'rest_framework',
    'keypair_permissions',
    'test_app',
]
# ...

test_app/urls.py:

from django.urls import path
from .views import (
    AuthTestApiView
)

app_name = 'test_app'

urlpatterns = [
    path('foo', AuthTestApiView.as_view()),
]

test_app/views.py:

from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from keypair_permissions.permissions import HasHttpCrypoAuthorization


class AuthTestApiView(GenericAPIView):
    """Test Crypto Auth Mixin."""

    permission_classes = [HasHttpCrypoAuthorization]

    def get(self, request):
        """GET method."""
        return Response(request.body)

    def post(self, request):
        """POST method."""
        return Response(request.data)

urls.py:

# ...
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('test_app.urls')),
]
# ...

Register public keys

You will have to go to the admin to register these public keys:

RSA Public Key

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
oYi+1hqp1fIekaxsyQIDAQAB
-----END PUBLIC KEY-----

Signing Algorithm: RSA

Hashing Algorithm: SHA256

Set the public_key_id to Test

ECDSA-P256 Public Key

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIG65UDNLeeH2M0FJMq5sS66Zgbfo5HmeiYvSF0rvx+fLoAoGCCqGSM49
AwEHoUQDQgAE+YwQJ7xak48kmy4IhOLo3krj998lCeN95dCTA72TWaHQtwMraLPO
Kc2Z9V6olwQNiezfiSNq83Ln7EL3AOpp9g==
-----END EC PRIVATE KEY-----

Signing Algorithm: ECDSA-P256

Hashing Algorithm: SHA512

Set the public_key_id to P256Test

Client

Setting Up

To run the tests, first we must include some libraries

import requests
import base64
import json
from collections import OrderedDict
from urllib.parse import urlparse

# for RSA-based signing
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256, SHA512
from Crypto.Signature import pkcs1_15

# For digests
import hashlib

# for FastECDSA-based signing
from fastecdsa import curve, ecdsa, keys
from fastecdsa.encoding.pem import PEMEncoder
from fastecdsa.encoding.der import DEREncoder

From here we will build a text-based web browser that knows how to build the Authorization header:

class SecureHttpClient:
    """Simple Authorization Client."""

    headers = {
        'Content-Type': 'application/json',
    }
    encoding = 'utf-8'
    data_body = ''
    method = 'GET'
    url = None

    signing_algorithm = None
    hashing_algorithm = None
    digest_algorithm = None
    private_key_string = None
    url_parts = None

    authorization_parameters = OrderedDict({
        'algorithm': 'hs2019'
    })
    required_authorization_headers = []
    signing_string = None

    def set_url(self, url):
        """Set the url."""
        self.url = url
        self.url_parts = urlparse(url)

    def get(self, url):
        """Retrieve a URL."""
        self.method = 'GET'
        self.build_secure_request()
        return requests.post(
            self.url,
            data=self.data_body.encode(self.encoding),
            headers=self.headers
        )

    def post(self):
        """Retrieve a URL."""
        self.method = 'POST'
        self.build_secure_request()
        return requests.post(
            self.url,
            data=self.data_body.encode(self.encoding),
            headers=self.headers
        )

    def build_secure_request(self):
        """Secure the request."""
        if self.digest_algorithm is not None:
            self.set_digest()
        self.build_authorization_header()

    def build_signing_string(self):
        """Build verification string."""
        request_target = '{} {}'.format(self.method.lower(), self.url_parts.path)
        if len(self.url_parts.query) > 0:
            request_target += '?{}'.format(self.url_parts.query)
        signing_rows = []
        if len(self.required_authorization_headers) > 0:
            for required_header in self.required_authorization_headers:
                if required_header[0] == '(':
                    if required_header == '(request-target)':
                        signing_rows.append(
                            '(request-target): {}'.format(request_target)
                        )
                    else:
                        cleaned_header = required_header[1:-1]
                        signing_rows.append(
                            '{}: {}'.format(
                                required_header, self.authorization_parameters[cleaned_header]
                            )
                        )
                else:
                    cleaned_header = required_header.replace('-', ' ')\
                        .title().replace(' ', '-')
                    signing_rows.append('{}: {}'.format(
                        required_header, self.headers[cleaned_header]
                    ))
        else:
            if 'created' in self.authorization_parameters:
                signing_rows.append(
                    '{}: {}'.format('(created)', self.authorization_parameters['created'])
                )
            elif 'Date' in self.headers:
                signing_rows.append(
                    '{}: {}'.format('date', self.headers['Date'])
                )
            else:
                raise Exception("Date or created required")

        signing_string = '\n'.join(signing_rows)
        self.signing_string = signing_string

    def build_authorization_header(self):
        """Build authorization headers."""
        if len(self.required_authorization_headers) > 0:
            self.authorization_parameters['headers'] = '{}'.format(
                ' '.join([
                    header.lower() for header in self.required_authorization_headers
                ])
            )
        self.build_signing_string()
        signing_bytestring = self.signing_string.encode(self.encoding)
        if self.signing_algorithm == 'RSA':
            signer = RSA.import_key(self.private_key_string)
            if self.hashing_algorithm == 'SHA256':
                hash_obj = SHA256.new(signing_bytestring)
            elif self.hashing_algorithm == 'SHA512':
                hash_obj = SHA512.new(signing_bytestring)
            else:
                raise Exception("Invalid key type")
            signature = pkcs1_15.new(signer).sign(hash_obj)
        else:
            private_key, public_key = PEMEncoder.decode_private_key(self.private_key_string)
            if self.hashing_algorithm == 'SHA256':
                hash_function = hashlib.sha256
            elif self.hashing_algorithm == 'SHA512':
                hash_function = hashlib.sha512
            else:
                raise Exception("Invalid key type")

            if self.signing_algorithm.lower() == 'p256':
                r, s = ecdsa.sign(signing_bytestring, private_key, curve=curve.P256, hashfunc=hash_function)
            else:
                raise Exception("Invalid key type")
            signature = DEREncoder.encode_signature(r, s)
        base64_signature = base64.b64encode(signature).decode(self.encoding)
        self.authorization_parameters['signature'] = '{}'.format(base64_signature)
        authorization_rows = []
        for key, value in self.authorization_parameters.items():
            if isinstance(value, str):
                authorization_rows.append('{}="{}"'.format(key, value))
            elif isinstance(value, int) or isinstance(value, float):
                authorization_rows.append('{}={}'.format(key, value))
            elif isinstance(value, bool):
                if value is True:
                    authorization_rows.append('{}=true')
                else:
                    authorization_rows.append('{}=false')
        authorization_header = 'Signature {}'.format(','.join(authorization_rows))
        self.headers['Authorization'] = authorization_header

    def set_digest(self):
        """Create a hash digest from text."""
        if self.digest_algorithm is None:
            return None
        data_string = self.data_body.encode(self.encoding)
        data_string_hash = ''
        if self.digest_algorithm == 'SHA256':
            data_string_hash = hashlib.sha256(data_string).digest()
        elif self.digest_algorithm == 'SHA512':
            data_string_hash = hashlib.sha512(data_string).digest()
        data_string_b64_hash = base64.b64encode(data_string_hash).decode(self.encoding)
        digest = '{}={}'.format(self.digest_algorithm.upper(), data_string_b64_hash)
        self.headers['Digest'] = digest

Elliptic Key Algorithm Tests

The Cavage draft recommendations using the SHA12 algorithm to hash the signing string and the EDCSA-P256 algorithm to sign it. Combined, this algorithm is referred to as hs2019.

The following test data uses the following EDCSA-P256 key, which we will refer to as keyId=P256Test in the following samples:


test_private_key = """-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIG65UDNLeeH2M0FJMq5sS66Zgbfo5HmeiYvSF0rvx+fLoAoGCCqGSM49
AwEHoUQDQgAE+YwQJ7xak48kmy4IhOLo3krj998lCeN95dCTA72TWaHQtwMraLPO
Kc2Z9V6olwQNiezfiSNq83Ln7EL3AOpp9g==
-----END EC PRIVATE KEY-----"""

Default Test

If a list of headers is not included, the date is the only header that is signed by default for hs2019. The string to sign would be:

date: Sun, 05 Jan 2014 21:31:40 GMT

date = 'Sun, 05 Jan 2014 21:31:40 GMT'
url = 'http://127.0.0.1:8002/foo'
data_body = '{"hello": "world"}'

client = SecureHttpClient()
client.headers['Date'] = date
client.signing_algorithm = 'ECDSA-P256'
client.hashing_algorithm = 'SHA512'
client.data_body = data_body
client.authorization_parameters['keyId'] = 'P256Test'
client.authorization_parameters['algorithm'] = 'hs2019'
client.private_key_string = test_private_key
client.set_url(url)

client.build_secure_request()
print(client.signing_string)
print("")
print(client.headers['Authorization'])

print("")
response = client.post()
print(response.status_code)

Output:

date: Sun, 05 Jan 2014 21:31:40 GMT

Signature algorithm="hs2019",keyId="P256Test",signature="MEUCIGGB0P3P/iZCzCbX1fj1Q6AbYPJr9dEBYcsuiLoS3q6uAiEAkEjvmWfuN1UDPmYCkBywnI/MwisCuNEmlAxPB3ZBVgc="

200

Basic Test

The minimum recommended data to sign is the (request-target), host, and date. In this case, the test would look like this:

date = 'Sun, 05 Jan 2014 21:31:40 GMT'
host = 'example.com'
required_headers = ['(request-target)', 'host', 'date']

url = 'http://127.0.0.1:8000/foo?param=value&pet=dog'
data_body = '{"hello": "world"}'

client = SecureHttpClient()
client.headers['Date'] = date
client.headers['Host'] = host
client.signing_algorithm = 'ECDSA-P256'
client.hashing_algorithm = 'SHA512'
client.data_body = data_body
client.authorization_parameters['keyId'] = 'P256Test'
client.authorization_parameters['algorithm'] = 'hs2019'
client.private_key_string = test_private_key
client.required_authorization_headers = required_headers
client.set_url(url)

client.build_secure_request()
print(client.signing_string)
print("")
print(client.headers['Authorization'])

print("")
response = client.post()
print(response.status_code)

Output:

(request-target): post /foo?param=value&pet=dog
host: example.com
date: Sun, 05 Jan 2014 21:31:40 GMT

Signature algorithm="hs2019",keyId="P256Test",headers="(request-target) host date",signature="MEUCIQCtmaLAdg5gTruZntyRo/Wy5qEEeyoq94leGtms0VSHYwIgJ6qux2OnOeYWZ8MS3IuY0fcL0GdgrlGBSPFx9z2KCWM="

200

All Headers Test

A strong signature including all of the headers and a digest of the body of the HTTP request would look like this:

date = 'Sun, 05 Jan 2014 21:31:40 GMT'
host = 'example.com'
required_headers = [
    '(request-target)',
    '(created)',
    '(expires)',
    'host',
    'date',
    'content-type',
    'digest',
    'content-length'
]

url = 'http://127.0.0.1:8002/foo?param=value&pet=dog'
data_body = '{"hello": "world"}'

client = SecureHttpClient()
client.data_body = data_body
client.headers['Date'] = date
client.headers['Host'] = host
client.headers['Content-Type'] = 'application/json'
client.headers['Content-Length'] = str(len(client.data_body))
client.signing_algorithm = 'ECDSA-P256'
client.hashing_algorithm = 'SHA512'
client.digest_algorithm = 'SHA256'
client.authorization_parameters['keyId'] = 'P256Test'
client.authorization_parameters['keyId'] = 'P-256-DAQcDQgAE+-None'
client.authorization_parameters['algorithm'] = 'hs2019'
client.authorization_parameters['created'] = 1402170695
client.authorization_parameters['expires'] = 1402170699
client.private_key_string = test_private_key
client.required_authorization_headers = required_headers
client.set_url(url)

client.build_secure_request()
print(client.signing_string)
print("")
print(client.headers['Digest'])
print("")
print(client.headers['Authorization'])

print("")
response = client.post()
print(response.status_code)
(request-target): post /foo?param=value&pet=dog
(created): 1402170695
(expires): 1402170699
host: application/json
date: Sun, 05 Jan 2014 21:31:40 GMT
content-type: application/json; encoding=utf-8
digest: SHA256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
content-length: 18

Signature algorithm="hs2019",keyId="P256Test",headers="(request-target) (created) (expires) host date content-type digest content-length",signature="MEYCIQDE2WaRqfvu7TvJcGLfrNpPnboin/hGBdWwKr/8WwXJOwIhAKD0uTs5HE5SGKpJLffwy50TfS19F/kiNV51QM3PUBiY",created=1402170695,expires=1402170699

200

The Cavage draft recommendations comes with several tests and also the following warning:

WARNING: THESE TEST VECTORS ARE OLD AND POSSIBLY WRONG. THE NEXT VERSION OF THIS SPECIFICATION WILL CONTAIN THE PROPER TEST VECTORS.

The following test data uses the following RSA 2048-bit keys, which we will refer to as keyId=Test in the following samples:


test_private_key = """-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
-----END RSA PRIVATE KEY-----"""

Default Test

If a list of headers is not included, the date is the only header that is signed by default for rsa-sha256. The string to sign would be:

date: Sun, 05 Jan 2014 21:31:40 GMT

date = 'Sun, 05 Jan 2014 21:31:40 GMT'
url = 'http://127.0.0.1:8002/foo'
data_body = '{"hello": "world"}'

client = SecureHttpClient()
client.headers['Date'] = date
client.signing_algorithm = 'RSA'
client.hashing_algorithm = 'SHA256'
client.data_body = data_body
client.authorization_parameters['keyId'] = 'Test'
client.authorization_parameters['algorithm'] = 'rsa-sha256'
client.private_key_string = test_private_key
client.set_url(url)

client.build_secure_request()
print(client.signing_string)
print("")
print(client.headers['Authorization'])

print("")
response = client.post()
print(response.status_code)

Output:

date: Sun, 05 Jan 2014 21:31:40 GMT

Signature algorithm="rsa-sha256",keyId="Test",signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="

200

Basic Test

The minimum recommended data to sign is the (request-target), host, and date. We can test this:

date = 'Sun, 05 Jan 2014 21:31:40 GMT'
host = 'example.com'
required_headers = ['(request-target)', 'host', 'date']

url = 'http://127.0.0.1:8000/foo/?param=value&pet=dog'
data_body = '{"hello": "world"}'

client = SecureHttpClient()
client.headers['Date'] = date
client.headers['Host'] = host
client.signing_algorithm = 'RSA'
client.hashing_algorithm = 'SHA256'
client.data_body = data_body
client.authorization_parameters['keyId'] = 'Test'
client.authorization_parameters['algorithm'] = 'rsa-sha256'
client.private_key_string = test_private_key
client.required_authorization_headers = required_headers
client.set_url(url)

client.build_secure_request()
print(client.signing_string)
print("")
print(client.headers['Authorization'])

print("")
response = client.post()
print(response.status_code)

Output:

(request-target): post /foo?param=value&pet=dog
host: example.com
date: Sun, 05 Jan 2014 21:31:40 GMT

Signature algorithm="rsa-sha256",keyId="Test",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0=",headers="(request-target) host date"

200

All Headers Test

A strong signature including all of the headers and a digest of the body of the HTTP request would be build like this:

date = 'Sun, 05 Jan 2014 21:31:40 GMT'
host = 'example.com'
required_headers = [
    '(request-target)',
    '(created)',
    '(expires)',
    'host',
    'date',
    'content-type',
    'digest',
    'content-length'
]

url = 'http://127.0.0.1:8000/foo?param=value&pet=dog'
data_body = '{"hello": "world"}'

client = SecureHttpClient()
client.data_body = data_body
client.headers['Date'] = date
client.headers['Host'] = host
client.headers['Content-Type'] = 'application/json'
client.headers['Content-Length'] = str(len(client.data_body))
client.signing_algorithm = 'RSA'
client.hashing_algorithm = 'SHA256'
client.digest_algorithm = 'SHA256'
client.authorization_parameters['keyId'] = 'Test'
client.authorization_parameters['algorithm'] = 'rsa-sha256'
client.authorization_parameters['created'] = 1402170695
client.authorization_parameters['expires'] = 1402170699
client.private_key_string = test_private_key
client.required_authorization_headers = required_headers
client.set_url(url)

client.build_secure_request()
print(client.signing_string)
print("")
print(client.headers['Digest'])
print("")
print(client.headers['Authorization'])

print("")
response = client.post()
print(response.status_code)
(request-target): post /foo?param=value&pet=dog
(created): 1402170695
(expires): 1402170699
host: application/json
date: Sun, 05 Jan 2014 21:31:40 GMT
content-type: application/json; encoding=utf-8
digest: SHA256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
content-length: 18

Signature algorithm="rsa-sha256",keyId="Test",signature="j3Uo3HDtxBAkSJBYuOyVFfaFZQNNFdyPG/tuzP8sUYWcUoT4DcOBp7W01K96g7tksH/+oqKjUAIXuFIkOHtQK96HPyrIi1LF7sKbm5j4Autl/XfA4q76NRQEnNYujLq6lGI8rju2Jau/yLVd0CSbEji3flVqLLvJZp8jCth0LnY=",created=1402170695,expires=1402170699,headers="(request-target) (created) (expires) host date content-type digest content-length"

200