Content Signature HTTP Header

Introduction

Whilst the IETF draft standard on Signing HTTP Messages brings a relatively simple approach to signing HTTP requests and responses, it is nevertheless too complex for the simple task of providing a signature over HTTP entity body content. In many cases we want to consider the entity bytes as a document that we can reuse in other contexts whilst retaining the digital signature over the document.

If we use the HTTP Signature approach this reuse of the entity as a signed document is made quite difficult. Signature validation means additional effort.

  • saving the signed headers including the (optional) Digest header and the pseudo-header (request-target)
  • ensuring the Digest header is actually supplied (and matches the document bytes)
  • concatenating the headers correctly to produce the input to the signature function

In sum we see how two concerns are being conflated: validating HTTP traffic on the one hand, signatures over the entity body on the other, and these concerns leak into subsequent document and signature processing.

Therefore, drawing on existing work we therefore define a new HTTP entity header: Content-Signature.

Building on Content-MD5

One of the most important conceptual stepping stones is the existing Content-MD5 header. Quoting the HTTP standard:

The Content-MD5 entity-header field, as defined in RFC 1864 [23], is an MD5 digest of the entity-body for the purpose of providing an end-to-end message integrity check (MIC) of the entity-body. (Note: a MIC is good for detecting accidental modification of the entity-body in transit, but is not proof against malicious attacks.)

   Content-MD5   = "Content-MD5" ":" md5-digest
   md5-digest   = <base64 of 128 bit MD5 digest as per RFC 1864>

The Content-MD5 header field MAY be generated by an origin server or client to function as an integrity check of the entity-body. Only origin servers or clients MAY generate the Content-MD5 header field; proxies and gateways MUST NOT generate it, as this would defeat its value as an end-to-end integrity check. Any recipient of the entity- body, including gateways and proxies, MAY check that the digest value in this header field matches that of the entity-body as received.

The MD5 digest is computed based on the content of the entity-body, including any content-coding that has been applied, but not including any transfer-encoding applied to the message-body. If the message is received with a transfer-encoding, that encoding MUST be removed prior to checking the Content-MD5 value against the received entity.

This has the result that the digest is computed on the octets of the entity-body exactly as, and in the order that, they would be sent if no transfer-encoding were being applied.

HTTP extends RFC 1864 to permit the digest to be computed for MIME composite media-types (e.g., multipart/* and message/rfc822), but this does not change how the digest is computed as defined in the preceding paragraph.

There are several consequences of this. The entity-body for composite types MAY contain many body-parts, each with its own MIME and HTTP headers (including Content-MD5, Content-Transfer-Encoding, and Content-Encoding headers). If a body-part has a Content-Transfer- Encoding or Content-Encoding header, it is assumed that the content of the body-part has had the encoding applied, and the body-part is included in the Content-MD5 digest as is – i.e., after the application. The Transfer-Encoding header field is not allowed within body-parts .

Reference to http-signatures

Significant work on authenticating HTTP requests and responses using digital signatures has been described in a draft standard. It describes the scheme for a Signature header as follows:

Creating a Signature

In order to create a signature, a client MUST:

  1. Use the contents of the HTTP message, the headers value, and
    the Signature String Construction algorithm to create the
    signature string.

  2. The algorithm and key associated with keyId must then be used
    to generate a digital signature on the signature string.

  3. The signature is then generated by base 64 encoding the output
    of the digital signature algorithm.

Synthesising Content-Signature

If we take the insights from both of these standards we synthesise a new header, Content-Signature.

The digest part of the signature is computed as per Content-MD5 over the entity body bytes using the algorithm specified, before any transfer encoding (such as compression) is applied.

This digest is then signed and the data about the signature made available in the Content-Signature header which correspondingly requires the fields as defined:

  • algorithm
  • keyId
  • signature

Algorithms

Cavage points out that a signature algorithm registry should be created at IANA. At the time of writing (May 2015) this has not yet happened. The TLS spec names the following hash algorithms:

  • md5
  • sha1
  • sha224
  • sha256
  • sha384
  • sha512

Note that the use of md5 and sha1 is strongly discouraged due to collision attacks.

The spec also names the following signature algorithms:

  • rsa
  • dsa
  • ecdsa

We therefore recommend that when naming algorithms the following simple scheme be used:

$signatureAlgorithm-$hashAlgorithm

We also recommend that only the above algorithm names be used (for the time being). For example, a RSA signature over a sha256 hash would be named rsa-sha256.

Example

Let us assume that we have 2048 bit RSA private and public keys. For the purposes of this exercise we will use the alias lotteries-io.

We also have a file which will be the HTTP entity body. Its contents are:

This is an example.

We can compute the sha-256 hash over this using:
openssl dgst -sha256 -binary example.txt | base64 > digest

This gives:

yAqXBB8VuhZrmj6PwrCXJtd4vDvZM41L7+NLRnB+vuw=

Signing the file using our private key is done, for example, as follows:

openssl dgst -sha256 -binary -sign private_key.pem example.txt | base64 > signature.base64

This gives:
UNtlSkR94FjgGstW238OcfxGSvEAtCJ8wikagpPdympgO7kjiM8PFpQ06vfKOtM3hGqMhGkrEI85 pErk94ou6E/pY8N7XGYgWdrvc3I1j0yaWAfUn3yCezl7slXfIs+Ph2zP+0LGgX3bVJrhYat+65bH LC2Fr5q2aEBWCdSfe2U80NhzFk7zCZKFcMi2xftz+m/qcJ4uEq1knABo6JMAGukgwcrgiRmu+sBD 6OEZFm8pM5eoA/akzB+j5IkgkTK1bXryJb60DOKYiB01hvKdfkxMk+X335+/n5nAuhQr990dg3mw zFaC/g19zjVkwQ87kKZn/yA2wEI5Ni6xFHpXCg==

Note that if we decode the base64 to raw bytes using base64 --decode signature.base64 > signature.raw we can then verify the signature using:

openssl dgst -sha256 -binary -verify public_key.pem -signature signature.raw example.txt

This gives:

Verified OK

Given all this, the Content-Signature header would now look like:

Content-Signature: keyId="lotteries-io",algorithm="rsa-sha256",signature="UNtlSkR94FjgGstW238OcfxGSvEAtCJ8wikagpPdympgO7kjiM8PFpQ06vfKOtM3hGqMhGkrEI85pErk94ou6E/pY8N7XGYgWdrvc3I1j0yaWAfUn3yCezl7slXfIs+Ph2zP+0LGgX3bVJrhYat+65bHLC2Fr5q2aEBWCdSfe2U80NhzFk7zCZKFcMi2xftz+m/qcJ4uEq1knABo6JMAGukgwcrgiRmu+sBD6OEZFm8pM5eoA/akzB+j5IkgkTK1bXryJb60DOKYiB01hvKdfkxMk+X335+/n5nAuhQr990dg3mwzFaC/g19zjVkwQ87kKZn/yA2wEI5Ni6xFHpXCg=="

Summarizing

In summary, given the following variables with syntax and semantics as defined above:

  • keyId
  • algorithm
  • signature

we construct Content-Signature as follows:

Content-Signature: keyId=$keyId,algorithm=$algorithm,signature=$signature