Content Signature
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:
Use the contents of the HTTP message, the
headers
value, and
the Signature String Construction algorithm to create the
signature string.The
algorithm
and key associated withkeyId
must then be used
to generate a digital signature on the signature string.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