Skip to content

Signed Headers

1. Introduction

Signed HTTP headers are a security mechanism used to protect the integrity and authenticity of HTTP requests and responses by digitally signing specific headers and their values. This approach ensures that the headers have not been tampered with in transit and verifies the sender’s authenticity. Signed HTTP headers are commonly used in APIs to enhance security, especially when transmitting sensitive information or ensuring that requests come from trusted sources. This chapter explores the concept of signed HTTP headers, how they work, their use cases, and best practices for implementing and validating them to avoid security issues.

2. What are Signed HTTP Headers?

Signed HTTP headers involve digitally signing one or more headers in an HTTP request or response using cryptographic algorithms. The signature is usually included as an additional header, such as Signature, containing the signed value along with metadata about the signing algorithm and key used.

Key Components of Signed HTTP Headers:

  1. Headers to Be Signed: Specific headers (e.g., Date, Host, Content-Type) are selected and signed to ensure they are authentic and have not been altered.
  2. Digital Signature: A cryptographic signature generated using a private key. It covers the selected headers and their values, protecting them against tampering.
  3. Signature Header: Contains the actual signature along with metadata such as the algorithm used, key identifier, and the signed headers.

How Signed HTTP Headers Work:

  1. Request Signing: The sender signs selected headers using a private key, generating a signature that is included in the request as a new header.
  2. Request Transmission: The request is sent to the server along with the signed headers and the Signature header containing the digital signature.
  3. Signature Validation: The server verifies the signature using the sender’s public key. If the signature is valid, the request is processed; otherwise, it is rejected as tampered or unauthorized.

3. Use Cases for Signed HTTP Headers

  1. Ensuring Data Integrity

    • Signed HTTP headers protect critical request information from being modified in transit. This is crucial in scenarios where headers control important aspects of the request, such as authorization, request routing, or content validation.

    • Example: An API signs the Date and Host headers to prevent attackers from replaying or redirecting requests maliciously.

  2. Authentication and Authorization

    • Signed headers are often used to authenticate requests, ensuring that they originate from a trusted source. This is particularly useful in APIs where traditional credentials like API keys or tokens are insufficient.

    • Example: An API gateway signs the Authorization header to validate requests made by microservices communicating within a secure architecture.

  3. Protecting Sensitive Information

    • Signing headers ensures that sensitive information, such as user identifiers or session tokens embedded within headers, is secure from tampering, reducing the risk of unauthorized access.

    • Example: A service signs headers containing custom tokens or session information to secure inter-service communication within a microservices architecture.

  4. Non-Repudiation

    • Digital signatures provide non-repudiation, meaning the sender cannot deny sending the signed request. This is important in legal or financial transactions where proof of authenticity is required.

    • Example: A financial application signs transaction details in headers to ensure that all requests are legitimate and traceable.

4. Implementing Signed HTTP Headers

  1. Signing HTTP Headers

    To sign HTTP headers, the sender creates a string representation of the selected headers, signs it using a private key, and includes the resulting signature in the request. Below is an example implementation of signing headers using Node.js and the crypto module.

    Example: Signing HTTP Headers with Node.js

    const crypto = require("crypto");
    // Headers to be signed
    const headers = {
    host: "api.example.com",
    date: new Date().toUTCString(),
    "content-type": "application/json",
    };
    // Create the string to be signed (canonical representation)
    const signedHeaders = ["host", "date", "content-type"];
    const canonicalString = signedHeaders
    .map((header) => `${header}: ${headers[header]}`)
    .join("\n");
    // Private key for signing (in practice, load this securely)
    const privateKey = `-----BEGIN PRIVATE KEY-----
    ... your private key ...
    -----END PRIVATE KEY-----`;
    // Sign the canonical string
    const signer = crypto.createSign("RSA-SHA256");
    signer.update(canonicalString);
    const signature = signer.sign(privateKey, "base64");
    // Include the signature as a header
    headers[
    "signature"
    ] = `keyId="my-key",algorithm="rsa-sha256",headers="${signedHeaders.join(
    " "
    )}",signature="${signature}"`;
    console.log("Signed Headers:", headers);

    Key Points in the Example:

    • Canonical String: A standardized representation of the headers to be signed, created by concatenating the headers and their values.
    • Signing Process: The canonical string is signed using the private key and the RSA-SHA256 algorithm.
    • Signature Header: The Signature header includes metadata about the signing process, such as the key ID, algorithm, headers signed, and the actual signature.
  2. Validating Signed HTTP Headers

    On the receiving end, the server validates the signature using the public key corresponding to the private key used for signing. If the validation succeeds, the server processes the request; otherwise, it rejects it as tampered or unauthorized.

    Example: Validating Signed HTTP Headers

    const crypto = require("crypto");
    // Extract headers from the incoming request (pseudo code)
    const incomingHeaders = {
    host: "api.example.com",
    date: "Mon, 05 Sep 2024 12:00:00 GMT",
    "content-type": "application/json",
    signature:
    'keyId="my-key",algorithm="rsa-sha256",headers="host date content-type",signature="...base64signature..."',
    };
    // Parse the signature header
    const signatureParts = incomingHeaders["signature"].match(
    /keyId="(.*?)",algorithm="(.*?)",headers="(.*?)",signature="(.*?)"/
    );
    const [_, keyId, algorithm, headers, receivedSignature] = signatureParts;
    // Recreate the canonical string
    const signedHeaders = headers.split(" ");
    const canonicalString = signedHeaders
    .map((header) => `${header}: ${incomingHeaders[header]}`)
    .join("\n");
    // Public key for verification (in practice, load from a secure source)
    const publicKey = `-----BEGIN PUBLIC KEY-----
    ... your public key ...
    -----END PUBLIC KEY-----`;
    // Verify the signature
    const verifier = crypto.createVerify(algorithm);
    verifier.update(canonicalString);
    const isValid = verifier.verify(publicKey, receivedSignature, "base64");
    if (isValid) {
    console.log("Signature is valid, processing request...");
    } else {
    console.log("Invalid signature, rejecting request...");
    }

    Key Points in the Validation Example:

    • Parsing Signature Header: The signature is parsed from the Signature header, extracting details about the signing algorithm and headers.
    • Recreating Canonical String: The server recreates the canonical string using the headers and values from the incoming request.
    • Signature Verification: The server uses the public key to verify the signature. If valid, the request is processed.

5. Best Practices for Signed HTTP Headers

  1. Select Headers Carefully

    • Sign headers that control access or carry critical information, such as Authorization, Date, Host, and any custom headers containing sensitive data. Avoid signing headers that may change in transit, such as Content-Length.
  2. Use Strong Cryptographic Algorithms

    • Use robust algorithms like RSA-SHA256 or ECDSA for signing headers, ensuring resistance to cryptographic attacks. Regularly update algorithms to adhere to current security standards.
  3. Rotate Keys Regularly

    • Implement key rotation policies to minimize the impact of potential key compromise. Ensure that clients and servers can seamlessly switch to new keys.
  4. Use Secure Key Storage

    • Store private keys securely using hardware security modules (HSMs), key management services (e.g., AWS KMS), or other secure vaults. Never hard-code keys directly into the application code.
  5. Implement Time-Based Validation

    • Include the Date header in the signed headers and validate its value to prevent replay attacks. Reject requests with timestamps that fall outside a reasonable time window.
  6. Validate Header Values

    • Ensure that the headers signed and received match exactly. Any discrepancies between the canonical string and incoming headers should result in a validation failure.
  7. Log and Monitor Signature Failures

    • Log failed signature validations to detect and respond to potential attacks, such as unauthorized access attempts or tampering.
  8. Test and Validate Implementation

    • Rigorously test the signing and validation processes to ensure consistency and correctness. Simulate various scenarios, including valid, tampered, and expired signatures.

6. Conclusion

Signed HTTP headers provide an additional layer of security for REST APIs by ensuring the integrity and authenticity of requests and responses. By digitally signing key headers, APIs can protect against tampering, unauthorized access, and replay attacks, enhancing the overall security posture. Implementing signed headers with best practices, such as secure key management, robust validation, and comprehensive logging, helps maintain the integrity of your API and fosters trust between clients and servers.