Skip to content

HTTP signature

Goal

To ensure the authenticity of the request made on our API, you need to sign any request with a Ed25519 or RSA private key. Combined with the key id, it's also allow us to identify the client.

Generate a key

To generate a new key, please go on our manager:

Then go in API Keys menu, choose a name and an algorithm and then click on Generate API key. Once you generated it, please store it in a safe place and make a copy of it. It's impossible to recovery the key it as we only keep the public key to check the signature.

Manually sign a request

The key signing process is similar to the one described in the IETF draft.

1. Data normalization

Data used for the signature: headers, method and path. The first thing to do is to clean and select the required data. A list of headers to be used is selected and each value of these headers is used in the signature. A missing header value should throw an error.

A special header name is used to refer to the method and path of the request: (request-target). The value of this header is the method in lowercase followed by a space character and the path. ex: get /test/1.

Headers should be treated as case insensitive.
If the given list of headers contains the same header multiple times an error should be thrown. If the same header as multiple value they should all be used (we use an array to store all values).

After this step we have an object containing the requested header values.

Sample Before:

{
    data:{
        headers:{
            'UsedHeader': 'sample\n l2',
            'UsedHeader': 'sample2',
            'UnusedHeader': 'hello',
            'AnotherHeader': 'bye'
        },
        method: 'GET',
        path: '/test/1'
    },
    headers: ['AnotherHeader', 'UnusedHeader', '(request-target)']
}

After:

{
    headers:[{
        name: 'anotherheader',
        values: ['bye']
    },{
        name: 'usedheader',
        values: ['sample\n l2', 'sample2']
    },{
        name: '(request-target)',
        values: ['get /test/1']
    }]
}

2. Object -> String

Each value of a header is treated as a string, and all value should be aggregated in the order given by the header list.
Each value is split in lines at the line breack character (regex: \r?\n|\r), then each line is trimed of empty character on both ends.
ex: 'sample\n l2' -> ['sample', 'l2'] The lines of a value are then combined into a string, every line separated by a space character. ex: ['sample', 'l2'] -> 'sample l2'
In case the resulting value is empty (length = 0) we put a single space character instead.
ex: '\n\n\n\n ' -> ' '
Then every values of headers are combined into a single string, each value separated by a comma followed by a space character.
ex: ['sample l2', 'sample2'] -> 'sample l2, sample2' Every value is combined with the header name with a colon (:) followed by a space character.
ex:

{
    name: 'usedheader',
    value: 'sample l2, sample2'
}

-> usedheader: sample l2, sample2 Finally every headers are combined with a new line character (\n) still in the order given by the headers list.

Sample Before:

{
    headers:[{
        name: 'anotherheader',
        values: ['bye']
    },{
        name: 'usedheader',
        values: ['sample\n l2', 'sample2']
    },{
        name: '(request-target)',
        values: ['get /test/1']
    }]
}

After

'anotherheader: bye\nusedheader: sample l2, sample2\n(request-target): get /test/1'

3. Creating the signature

The signature is created with the string generated previously, a given algorithm + hash and a private key.
Available algorithms are following: rsa, dsa and ecdsa. KNOT uses ecdsa for the keys it generate but the others algorithm are compatible.
Available hash algorithms are following: sha256 and sha512. KNOT uses sha256 but sha512 is compatible.
The private key is either given by KNOT for calls made to the KNOT's API or generated by the customer for calls made by KNOT to it's endpoints. (A default key is used in case the customer do not want to make one).
The signature should be base64 encoded. Making the signature depends on your programming language and environment:

NodeJs:

return crypto.createSign(hash).update(data).sign(privateKey, 'base64');

Python (with cryptography library):

key = load_pem_private_key(privateKey, password=None, backend=default_backend())
if hash == 'sha256':
    hasher = SHA256()
elif hash == 'sha512':
    hasher = SHA512()
else:
    raise ValueError("Invalid hash value (should be sha256 or sha512)")

if algorithm == 'ecdsa':
    sig = key.sign(data.encode(), ec.ECDSA(algorithm=hasher))
else:
    sig = key.sign(data.encode(), PKCS1v15(), hasher)

return base64.b64encode(sig).decode()

4. Making the Authorization header

The Authorization header always begin with the keyword Signature, followed by a space character and the list of properties of the signature.
Properties:

  • keyId: the key identifier
  • algorithm: which is the key algorithm followed by a dash (-) and the hash algorithm. You can also use hs2019 to hide the algorithm, in this case the server will determine the algorithm from database.
  • headers: the list of headers used to make the signature (in the same order) and separated by spaces characters
  • signature: the value of the signature

A property is represented by it name followed by the equal sign (=), a double quote ("), the value of the property and another double quote (").

Sample: Signature keyId="12345",algorithm="ecdsa-sha256",headers="anotherheader usedheader (request-target)",signature="_b64_sig_value_"

Sign a request using library.

KNOT currently provide a library to sign request for the following environment:

Mail us at dev@knot.city if you want us to support other environments.

The following libraries have been tested by our team:

Manually verify a request

1. Extract information from Authorization header

The first thing to verify is that the Authorization header value begins with the keyword Signature followed by a space.
After removing the keyword and the space character we split each component with the regex ,(?!(?=[^"]*"[^"]*(?:"[^"]*"[^"]*)*$)).
Signature keyId="12345",algorithm="ecdsa-sha256",headers="anotherheader usedheader (request-target)",signature="_b64_sig_value_" -> ['keyId="12345"', 'algorithm="ecdsa-sha256"', 'headers="anotherheader usedheader (request-target)"', 'signature="_b64_sig_value_"'] Each of those components should have an equal sign on which we split into a component name and value, in case it doesn't have the equal sign we throw an error.
'keyId="12345"' -> {name:'keyId', value:'"12345"'}
The value is checked for a double quote at the begining and at the end of the string and both are removed, if a double quote is missing an error is thrown.
{name:'keyId', value:'"12345"'} -> {name:'keyId', value:'12345'} Each component is evaluated according to its name: * keyId: value is kept as it is. * algorithm: value is split on the dash (-) into the key algorithm and the hash algorithm. * headers: value is split on space characters into a list of header (order should be kept as given). * signature: value is kept as it is. * Any other name throws an error as it is invalid. An object is created with each key used as a property.

Sample Before: Signature keyId="12345",algorithm="ecdsa-sha256",headers="anotherheader usedheader (request-target)",signature="_b64_sig_value_" After:

{
    keyId: "12345",
    algorithm: "ecdsa",
    hash: "sha256",
    headers: ["anotherheader", "usedheader", "(request-target)"],
    signature: "_b64_sig_value_"
}

2. Normalize and stringify data

Follow steps 1 and 2 of manually making a signature with the configuration extracted previously.

3. Verify signature

To verify a signature we need the configuration used to create it (step 1), stringified data (step 2) and the public key.
Depending on your programming language and environment the verification step can be different.

NodeJs:

return crypto.createVerify(hash).update(data).verify(publicKey, signature, 'base64');

Python (with cryptography library):

key = load_pem_public_key(publicKey, backend=default_backend())
if hash == 'sha256':
    hasher = SHA256()
elif hash == 'sha512':
    hasher = SHA512()
else:
    raise ValueError("Invalid hash value (should be sha256 or sha512)")

signature = base64.b64decode(sig)
try:
    if algorithm == 'ecdsa':
        key.verify(signature, data.encode(), ec.ECDSA(algorithm=hasher))
    else:
        key.verify(signature, data.encode(), PKCS1v15(), hasher)
except(InvalidSignature):
    return False
return True

Request done by Knot are signed with the following ECDSA key's (with sha256).

  • Development public key:
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA2OtTKd38+O9cOCfsaEa4sLsrH2XE
YozTpHe6cA6YuGI0kjfjCcdS4CgOC5zWlZM8i4P/ltl6Wl8K72Ji410SE7ABnOFW
zmsHm+Hf/BUX5cJEWQcOeJMPFj7Jp67ze5GPu7O92vnLp7oTEuhbkt3XYcDUR8/5
VN+tAA5L+eSqY/2RfvA=
-----END PUBLIC KEY-----
  • Production public key:
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBqB+VLb5rXo5Y1gDdnRxZ9GXbFPle
OSTVDgHGrqJ/tDuh62Nfec4YgCbHe8BI8kGM9N5M0H66jsJD/1AnmDyeiBQAlslo
/EPrr4+7YXwMbSokZvSt+GesH3z8OUwCfPounSI870XbA+IB71BuILSnc6W6oyVS
UxzTiol8Rs1DdSVwBdA=
-----END PUBLIC KEY-----

Verify a request using library

The same library that sign can verify the signature.