Introducing libsigv4: AWS SigV4 Signatures in Portable C with Kernel Compatibility
When working with AWS Service APIs, like S3 from constrained or non-standard environments, generating SigV4 signatures is unavoidable. AWS provides an official SigV4 for AWS IoT embedded SDK, but as we integrated it into our projects, we quickly ran into challenges:
- System header portability: The official library assumes certain C library functions and headers will be available, which isn’t always true in kernel space or other restricted C environments.
- Redundant parsing: We already parse HTTP headers in our stack, but the AWS SDK parses them again internally, leading to inefficiency.
- Flexibility: We needed a library that could fit into both embedded systems and kernel-level code with tight integration requirements.
That’s why we built libsigv4
.
What is AWS SigV4?
AWS Signature Version 4 (SigV4) is the authentication protocol used by AWS services. Instead of sending credentials directly in a request, SigV4 computes a cryptographic signature over the request details, ensuring authentication, integrity, and time-bounded validity.
At a high level, SigV4 works like this:
- Collect request data (method, path, query string, headers, payload hash).
- Create a canonical request: normalized string form.
- Derive a signing key using HMAC-SHA256 and your AWS secret key.
- Compute the final signature.
- Attach the signature to the request (as an
Authorization
header or presigned URL).
Whether you’re in userspace or kernel space, you need these steps to securely talk to AWS.
Why Another SigV4 Library?
Our Use Case
In our setup, applications make ordinary HTTP requests without being aware of AWS SigV4. Underneath, in kernel space, we intercept and parse these designated requests. Using libsigv4, we then inject the required AWS authorization headers before forwarding the requests upstream.
This approach allows applications to remain completely unchanged — they don’t need to know anything about AWS authentication or SigV4 signing. All of the complexity of parsing, signing, and securely managing credentials is handled transparently in the kernel.
Current C SigV4 Landscape
There are already several implementations of SigV4 in C:
- The AWS official embedded SDK
- sidbai/aws-sigv4-c, a lightweight but abandoned version
- libcurl’s SigV4 API for userspace networking
- Inspiration also came from nanopb, which shows how C libraries can be minimal and portable
But none of these struck the right balance for kernel compatibility, portability, and avoiding duplicate parsing.
Design Goals of libisgv4
- Zero dynamic allocations
- Works on user-provided buffers.
- Critical for kernel code or environments where
malloc
is unavailable or undesirable.
- Pluggable crypto backends
- Compatible with OpenSSL, mbedTLS, TinyCrypt, or even the Linux Kernel Crypto API.
- Choose the right backend depending on your environment.
- System header portability
- Minimal reliance on libc.
- Compiles cleanly in restricted header environments (including kernel space).
- No duplicate parsing
- Integrates with your existing HTTP request parsing.
- Doesn’t re-parse headers you already parsed.
Example: Signing a Simple Request
Here’s a minimal example of using libsigv4
with OpenSSL to sign a GET request to an AWS S3 endpoint:
#include <stdio.h>
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include "sigv4.h"
int HMAC_SHA256(const unsigned char *data, size_t data_len,
const unsigned char *key, size_t key_len,
unsigned char *out, size_t *out_len)
{
unsigned int len = 0;
char *ac = HMAC(EVP_sha256(), key, key_len, data, data_len, out, &len);
*out_len = len;
return (ac != NULL) ? 0 : -1;
}
int main()
{
aws_sigv4_params_t sigv4_params = {
.access_key_id = aws_sigv4_string("your_access_key"),
.secret_access_key = aws_sigv4_string("your_secret_key"),
.method = aws_sigv4_string("GET"),
.uri = aws_sigv4_string("/"),
.query_str = aws_sigv4_string("encoding-type=url"),
.host = aws_sigv4_string("riptides-sigv4.s3.eu-central-1.amazonaws.com"),
.region = aws_sigv4_string("eu-central-1"),
.service = aws_sigv4_string("s3"),
.x_amz_date = aws_sigv4_string("20250815T071550Z"),
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
};
char auth_buf[AWS_SIGV4_AUTH_HEADER_MAX_LEN] = {0};
aws_sigv4_header_t auth_header = {
.value = aws_sigv4_string(auth_buf)};
int status = aws_sigv4_sign(&sigv4_params, &auth_header);
if (status == AWS_SIGV4_OK)
printf("Signature: %s\\n", auth_header.value.data);
else
printf("Failed to sign request, status: %d\\n", status);
return 0;
}
This produces an Authorization
header you can attach directly to your HTTP request. Because everything works on user-provided buffers, you control exactly how much memory is used.
Get Started
You can check out the source here: 👉 https://github.com/riptideslabs/libsigv4
We’d love feedback, especially from embedded developers running into the same challenges.
Closing Thoughts
For many IoT and embedded projects, the hardest part isn’t AWS itself, but fitting AWS’s tools into constrained environments. With libsigv4
, we wanted to remove just enough friction to let developers stay focused on their applications.
If you’re working on SigV4 in embedded C, give it a try - and let us know what environments you’re using it in.
Ready to replace secrets
with trusted identities?
Build with trust at the core.