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:
That’s why we built libsigv4.
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:
Authorization header or presigned URL).Whether you’re in userspace or kernel space, you need these steps to securely talk to AWS.
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.
There are already several implementations of SigV4 in C:
But none of these struck the right balance for kernel compatibility, portability, and avoiding duplicate parsing.
Zero dynamic allocations
malloc is unavailable or undesirable.Pluggable crypto backends
System header portability
No duplicate parsing
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.
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.
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.