How to create message signatures
This guide will show you how to implement message signatures for API requests. Message signatures allow us to verify that API requests come from a valid source and ensure the integrity of each request.
There are two steps to creating a message signature:
- Create a key pair: only users with the Team admin role can do this.
- Sign a message: anyone sending API requests can do this.
Create a key pair
Only users with the Team admin role can add public keys.
Message signatures use asymmetric encryption, which requires a pair of keys:
- A private key to generate signatures that are attached to each request.
- A public key shared with Griffin and used to validate the signature of each request.
Create a public and private key
You can generate keys using the openssl
command line tool, or a cryptographic library such as Bouncy Castle.
Generate a private and public key, using the ed25519
algorithm.
openssl genpkey -algorithm ed25519 -out private.pem -outpubkey public.pem
You can use any filename for the keys, but using the .pem
extension is recommended.
Add the public key via the app
- Log in to app.griffin.com using an account with the Team admin role.
- Navigate to Settings > Message signatures and select Add public key.
- Complete the public key form:
- Title (optional): give your key a human readable name.
- SHA: an exact copy of the contents of the public key file, as shown in the example below.
-----BEGIN PUBLIC KEY-----
MCxwxQYxKxVwAxxAxTxD1x2xcxExOxxxatx/JxWxVxYxxxVzxxxkxLkxVxw=
-----END PUBLIC KEY-----
Griffin will generate a unique keyid
for each public key. Use this keyid
in the signature base when generating message signatures.
You must use at least one key pair per organization. Do not use this key pair with any other service or platform. We recommend these security practices for managing your private keys.
View your public keys
You can view all public keys added to your organization on the Message signatures page in the app.
For each public key, you can view:
Title
: optional name for the keykeyid
: unique identifier generated by GriffinSHA
: the hash valueAdded by
: the user who added the key and the date it was added
Sign a message
You can implement message signatures either with a programming language which has cryptographic libraries, or by calling out to openssl
command line tooling.
Create a digest of the request body
The content-digest
is the hash value of the request message body. The content-digest
should be included in the signature base to ensure the body cannot be tampered with.
- Generate a SHA-512 digest of the request body.
- Add the
content-digest
header to the request. - Include the
content-digest
in the signature base.
"content-digest": sha-512=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Here are some suggested message content digest libraries.
Generate a unique request ID (nonce)
The nonce
is a unique value that provides extra security for the request. The nonce
should be included in the signature base.
- Generate a
nonce
value using 128-bit UUID version 4 (random), variant 1. - Include the
nonce
in the signature base.
nonce=a68aaca6-776c-44a3-adfd-37d7d5036f92
Create a signature base
A signature base defines the components of the message to be included when generating the signature.
To create a signature base:
- Add an
alg
parameter to the signature base.
This can only be the ed25519
algorithm.
alg="ed25519"
Generate a signature
created
time stampUse the current time, formatted as a Unix time stamp.
created=1618884479
- Set the
expires
time stamp
The signature should have an expiration time no greater than 300 seconds (five minutes) after the signature created
time stamp.
expires=1618885579
- Add a
keyId
parameter to the signature base
Use the keyId
value associated with your public key.
keyid="hs.123456abcdef"
- Assemble all components into the signature base.
Add the standard component for a HTTP request message, and add the signature parameters.
"@signature-params": ("@authority" "content-digest" "content-length" "content-type"
"date" "@method" "@path" "@query");
alg="ed25519";
created=1724311276;
expires=1724311576;
keyid="hs.123456abcdef";
nonce="019178f6-a7f5-4edb-9ddc-b1488ed84af9"
Components in the signature base are included in the signature-input
header of the signed request message.
Generate the message signature and attach headers
You can now generate a message signature based on your signature base, either by using a programming language with cryptographic library or calling out to the openssl
command.
Attach the signature to the request message using these headers:
- The
content-digest
header verifies the body of the request using SHA-512. - The
signature
header attaches the signature value generated from the signature base to the request message using a signature label name. - The
signature-input
header should use the same signature label name, and lists components from the signature base.
"content-digest": sha-512=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
signature: unique-label=:Z1cxalMURtkA94LnchyyFASdEFH1+IY5joho7FhXjVPAhzyImT6NWqSV/vc+KBIPFM/JwJbGk/7k69VGIMcyAg==:
signature-input:
unique-label=("@authority" "content-digest" "content-length" "content-type"
"date" "@method" "@path" "@query");
alg="ed25519";
created=1724311276;
expires=1724311576;
keyid="hs.123456abcdef";
nonce="019178f6-a7f5-4edb-9ddc-b1488ed84af9""
A request message can include a multiple signatures. Each signature uses a unique signature label to match the relevant signature-input
and signature
header values.
Using multiple signatures is useful when rotating cryptographic keys.
Test your implementation
Use the /v0/security/message-signature/verify
endpoint to test your signed messages. If unsuccessful, the endpoint will provide feedback to help you troubleshoot.
Once message signatures are correctly generated, they can be used with all other Griffin API endpoints.
A successful response looks like this:
Status Code: 200
Response Headers:
Date: Mon, 23 Sep 2024 08:52:47 GMT
Content-Type: application/json;charset=utf-8
Content-Length: 59
Server: Jetty(9.4.53.v20231009)
Response Body:
{"busy":true,"message":"Your call is very important to us"}
Signature Verification Result:
Label: sig1
Algorithm: <class 'http_message_signatures._algorithms.ED25519'>
Covered Components:
"@method": POST
"@authority": api.griffin.com
"@path": /v0/security/message-signature/debug
"content-type": application/json
"content-length": 18
"date": Mon, 23 Sep 2024 08:52:47 GMT
"content-digest": sha-512=:WZDPaVn/7xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==:
"@signature-params": ("@method" "@authority" "@path" "content-type" "content-length" "date" "content-digest");created=1727081567;keyid="acme-corp-2024"
Parameters:
created: 1727081567
keyid: test-key-ed25519
Signature verified successfully!
If an invalid keyid
is used to sign the message, you'll get a 401 Unauthorized response.
Status Code: 401
Response Body:
{"errors":[{"title":"Unauthorized","status":"401","detail":"Unknown signing key","code":"unknown-signing-key-id"}]}
You'll also get a 401 if one of the components is missing, e.g the date as shown below.
Status Code: 401
Response Body:
{"errors":[{"title":"Unauthorized","status":"401","detail":["content-digest","date"],"code":"missing-required-components"}]}
Reference
Cryptographic libraries
While we don't endorse specific libraries, here are some status notes on available options:
Python
- http-message-signatures
- v0.5.0 was verified compatible, used in our example implementation below
- Recommended for testing compatibility of your own implementation
.NET
- Unisys NSign
- Version 1.1.0 reported working by customers
- When using NSign, ensure your
Content-Type
header does not include charset parameters (e.g., useapplication/json
instead ofapplication/json; charset=utf-8
)
Not tested
- Python: cryptography.io and Ed25519 Signing
- Java, C#, .Net, Kotlin: Bouncy Castle Open Source cryptographic APIs (FIPS Certified)
- JavaScript / Typescript: node-forge and Ed25519 signing
- Go: Yaron httpsign library
Always evaluate libraries thoroughly before integrating them into your project.
Message component definitions
alg
is the the algorithm used to create the signature. This is always ed25519
as mandated by Griffin.
keyid
is the unique id for your public key. It is generated by Griffin when you add a public key to your organization via the app.
content-digest
is the SHA-512 hash of a message body.
content-length
indicates the size of the message body in bytes.
content-type
specifies the type of content sent to the API endpoint, e.g. application/json
.
created
shows the time stamp for when the signature was created.
date
shows the date stamp of the request message.
expires
is a time stamp which is a maximum of 300 seconds (five minutes) after the created
time stamp.
nonce
is a unique request id as an 128-bit UUID value, e.g. 6e5a16e1-c8c6-42da-b367-644d82360e8c
.
@authority
is the host header, e.g. api.griffin.com.
@method
is POST
, GET
, etc.
@path
is the API endpoint being called.
Message content digest libraries
The following library provides SHA-512 hash encoding for the request message body.
- JavaScript / Typescript: node-forge
Example Python implementation
We provide an example Python implementation below to demonstrate HTTP message signatures. This example uses the third-party http-message-signatures
library and shows a complete workflow including signature generation and verification.
To run this example, save this to httpsig.py
:
from http_message_signatures import HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureKeyResolver, algorithms
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import requests
import base64
import hashlib
import time
import uuid
from datetime import datetime
# Test Ed25519 private key
# TODO Don't use this in production! Replace with your own private key
PRIVATE_KEY_PEM = """
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF
-----END PRIVATE KEY-----
"""
# Corresponding public key (You'd share this with the server)
# both values are taken from the ED25519 values form the RFC
# TODO replace with your generated key
PUBLIC_KEY_PEM = """
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=
-----END PUBLIC KEY-----
"""
# Key ID, taken from the RFC
# TODO take the value returned by Griffin from the UI
KEY_ID = "test-key-ed25519"
# TODO: Replace this with your actual API key
API_KEY = ""
url = "https://api.griffin.com/v0/security/message-signature/verify"
class MyHTTPSignatureKeyResolver(HTTPSignatureKeyResolver):
def __init__(self):
self.private_key = serialization.load_pem_private_key(PRIVATE_KEY_PEM.encode(), password=None)
self.public_key = serialization.load_pem_public_key(PUBLIC_KEY_PEM.encode())
def resolve_public_key(self, key_id: str):
return self.public_key
def resolve_private_key(self, key_id: str):
return self.private_key
# generate a unique nonce for every request
nonce = str(uuid.uuid4())
body = '{"hello": "world"}'
created = datetime.fromtimestamp(time.time())
headers = {
"Host": "api.griffin.com",
"Date": time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()),
"Content-Type": "application/json",
# the API KEY is still required, message signatures are in addition to that
"Authorization": f"GriffinAPIKey {API_KEY}"
}
request = requests.Request('POST', url, data=body, headers=headers)
request = request.prepare()
# Calculate and add Content-Digest
digest = base64.b64encode(hashlib.sha512(request.body.encode()).digest()).decode()
request.headers["Content-Digest"] = f"sha-512=:{digest}:"
# Sign the request
signer = HTTPMessageSigner(signature_algorithm=algorithms.ED25519, key_resolver=MyHTTPSignatureKeyResolver())
signer.sign(request, key_id=KEY_ID, label="sig1", nonce=nonce, include_alg=False, created=created, covered_component_ids=(
"@method", "@authority", "@path", "content-type", "content-length", "date", "content-digest", "@query"
))
# Print the signed request details
print("Signed Headers:")
for header, value in request.headers.items():
print(f"{header}: {value}")
# Verify the signature (this would typically be done on the server side)
verifier = HTTPMessageVerifier(signature_algorithm=algorithms.ED25519, key_resolver=MyHTTPSignatureKeyResolver())
try:
verify_results = verifier.verify(request)
print("\nSignature Verification Result:")
for result in verify_results:
print(f"Label: {result.label}")
print(f"Algorithm: {result.algorithm}")
print("Covered Components:")
for component, value in result.covered_components.items():
print(f" {component}: {value}")
print("Parameters:")
for param, value in result.parameters.items():
print(f" {param}: {value}")
print("\nSignature verified successfully")
except Exception as e:
print(f"\nSignature verification failed: {str(e)}")
# Send the request to the actual server
print("\nSending the request to the Griffin server")
response = requests.Session().send(request)
# Print the response
print(f"\nStatus Code: {response.status_code}")
print("\nResponse Headers:")
for header, value in response.headers.items():
print(f"{header}: {value}")
print("\nResponse Body:")
print(response.text)
When run successfully, this example will:
- Generate a signature for an example request
- Verify the signature locally
- Send the signed request to Griffin's message signature verification endpoint
- Display the response, showing successful verification
The example includes placeholder values that you'll need to replace for production use:
- Replace the example private/public key pair with your own Ed25519 keys
- Update the KEY_ID with the value from your Griffin dashboard
- Add your Griffin API key
The code above uses test keys from the HTTP Message Signatures RFC (RFC 9421) for demonstration purposes only. These keys are publicly known and should never be used in production. When implementing message signatures in your application:
- Always use your own securely generated Ed25519 key pairs
- Evaluate and verify any third-party libraries before use in production
- Follow your organization's security practices and requirements
To execute the script, set up a Python virtual environment and install dependencies:
python -m venv myenv
source myenv/bin/activate
pip install requests http-message-signatures
python httpsig.py
When executed, the script will show the complete request signing process, including:
- The signed HTTP headers
- Local signature verification details
- The response from Griffin's verification endpoint
Signed Headers:
Host: api.griffin.com
Date: Mon, 04 Nov 2024 10:41:39 GMT
Content-Type: application/json
Authorization: GriffinAPIKey <redacted>
Content-Length: 18
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Signature-Input: sig1=("@method" "@authority" "@path" "content-type" "content-length" "date" "content-digest");created=1730716899;keyid="test-key-ed25519";nonce="01f66b12-72bf-4607-8aa9-c87fb32a153c"
Signature: sig1=:redacted=:
Signature Verification Result:
Label: sig1
Algorithm: <class 'http_message_signatures._algorithms.ED25519'>
Covered Components:
"@method": POST
"@authority": api.griffin.com
"@path": /v0/security/message-signature/verify
"content-type": application/json
"content-length": 18
"date": Mon, 04 Nov 2024 10:41:39 GMT
"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
"@signature-params": ("@method" "@authority" "@path" "content-type" "content-length" "date" "content-digest");created=1730716899;keyid="test-key-ed25519";nonce="01f66b12-72bf-4607-8aa9-c87fb32a153c"
Parameters:
created: 1730716899
keyid: test-key-ed25519
nonce: 01f66b12-72bf-4607-8aa9-c87fb32a153c
Signature verified successfully
Sending the request to the Griffin server
Status Code: 200
Response Headers:
Date: Mon, 04 Nov 2024 10:41:39 GMT
Content-Type: application/json;charset=utf-8
Content-Length: 59
Server: Jetty(9.4.56.v20240826)
Response Body:
{"busy":true,"message":"Your call is very important to us"}
(myenv)
This example is intended as a reference implementation to help you understand the HTTP message signatures workflow. Your production implementation may differ based on your specific requirements and technology stack.