Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ pkg_check_modules(CRYPTO REQUIRED IMPORTED_TARGET libcrypto)
# ImageMagick 7. Version 6 is refused: it does not provide the headers the
# module includes.
find_package(MagickWand7 REQUIRED)

# -- The signing library
#
# The module links it, and so do the command and the tests. It builds first
# because every other target here reaches for it.

add_subdirectory(sign)

# -- The module

add_library(mod_dims MODULE
Expand Down Expand Up @@ -121,6 +129,7 @@ target_include_directories(mod_dims PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src)

target_link_libraries(mod_dims PRIVATE
moddims_sign
PkgConfig::APR
PkgConfig::APRUTIL
PkgConfig::CURL
Expand Down
142 changes: 142 additions & 0 deletions docs/docs/clients/c.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# C library

`libmoddims_sign` holds the `/dims4/` and `/dims5/` signing rules. The module
compiles the same source.

It needs C99 and libcrypto. It does not need APR and it does not need httpd.

## Build

The library installs with the module.

```
cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build
cmake --install build
```

That writes `dims_sign.h` under `include/dims`, `libmoddims_sign.a` and the
shared library under `lib`, `dims-sign` under `bin`, and `dims-sign.pc` under
`lib/pkgconfig`.

`pkg-config` resolves `-lmoddims_sign` to the shared library. Pass
`--static` to link the archive.

```
cc app.c $(pkg-config --cflags --libs dims-sign)
```

## Sign a URL

```c
#include <dims_sign.h>
#include <stdio.h>
#include <stdlib.h>

int
main(void)
{
const char *url =
"https://images.example.com/dims5/resize/100x100/"
"?url=http%3A%2F%2Forigin%3A8080%2Fgrid.png";
char *signed_url;
dims_sign_status status;

status = dims_sign_dims5_url(url, getenv("DIMS_SIGNING_KEY"), NULL,
&signed_url);
if (status != DIMS_SIGN_OK) {
fprintf(stderr, "cannot sign: %s\n", dims_sign_strerror(status));
return 1;
}

puts(signed_url);
dims_sign_free(signed_url);

return 0;
}
```

`dims_sign_dims4_url` uses the client secret instead of the signing key. Four
segments follow the prefix: the client id, the signature, the expiry, and the
commands. Write a placeholder in the signature segment. Its length sets the
length of the signature, from 6 characters to 32.

## The prefix

The third argument is what comes before the commands in the path. `NULL` means
`/dims5/` or `/dims4/`. A caller behind a rewrite passes the public prefix.

```c
dims_sign_dims5_url("https://cdn.example.com/img/resize/100x100/?url=...",
key, "/img/", &signed_url);
```

The commands are `resize/100x100/` either way, so the signature matches what
the module computes after the rewrite.

## Memory

A string of unknown length comes back allocated. Release it with
`dims_sign_free`. A digest of fixed length goes into a caller buffer, because
its size is a compile time constant.

A call that fails leaves the out parameter untouched and does not allocate.

## Thread safety

Every function is safe to call from any thread. No function reads or writes
static state.

## Encrypting the image URL

`eurl` hides the source from a public caller. The signature covers the plain
image URL, so the server verifies the request after it decrypts.

```c
char *signed_url;

dims_sign_dims5_eurl_url(url, key, NULL, &signed_url);
```

That signs the URL and replaces `url` with `eurl` in one call. The `/dims4/`
form takes the cipher the server is configured for:

```c
dims_sign_dims4_eurl_url(url, secret, NULL, DIMS_SIGN_EURL_ECB, &signed_url);
```

| Endpoint | Key | Cipher |
|---|---|---|
| `/dims5/` | HKDF-SHA256 of the signing key, salt `go-dims` | AES-128-GCM |
| `/dims4/` | SHA-1 of the client secret, hex, first 16 characters uppercased | AES-128-ECB, or GCM under [`DimsEncryptionAlgorithm`](/configuration/clients) |

`dims_sign_derive_key` and `dims_sign_eurl_encrypt` do the two steps on their
own. `dims_sign_eurl_decrypt` reads a value back, so a caller can check what it
wrote.

A GCM value is the 12 byte IV, the ciphertext, and the 16 byte tag, base64
encoded. The IV comes from the system random source, so two calls on one URL
produce two values.

## Status codes

| Status | Meaning |
|---|---|
| `DIMS_SIGN_OK` | the call produced a result |
| `DIMS_SIGN_MEMORY` | malloc refused |
| `DIMS_SIGN_BAD_ARGUMENT` | a required argument is NULL or empty |
| `DIMS_SIGN_BAD_URL` | the signer cannot read the URL |
| `DIMS_SIGN_BAD_FIELD` | a signed field holds a control character |
| `DIMS_SIGN_CRYPTO` | libcrypto refused |
| `DIMS_SIGN_BAD_EURL` | an eurl value is not base64, is too short, or fails its tag check |

`dims_sign_strerror` returns a short description of each one.

## What the signer does not do

It does not repair the URL. The caller supplies a valid one. A percent escape
that is not two hex digits gives `DIMS_SIGN_BAD_URL`.

It does not read an image URL out of the path. `/dims4/` also accepts the
image URL as the last path segment. The `url` query parameter is the
documented form, and the signer covers only that form.
98 changes: 98 additions & 0 deletions docs/docs/clients/dims-sign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# dims-sign

A command that signs a URL, prints the message behind one, and compares the
signature a URL holds against the one the key produces.

```
dims-sign (--dims4 | --dims5) [--key-file FILE] [--prefix P] [--eurl]
[--cipher gcm|ecb] [--message | --verify] URL
```

The endpoint is a flag. The path alone does not identify the endpoint.
`--prefix` defaults to the prefix that flag names.

## The key

The key comes from `--key-file`, or from `DIMS_SIGNING_KEY` in the environment
when the flag is absent. A `--key-file` of `-` reads standard input.

There is no `--key` flag. A key on the command line is visible to every user of
the machine through `ps`, and the shell records it in the history file.

## Sign

```
$ dims-sign --dims5 --key-file dims.key \
'https://images.example.com/dims5/resize/100x100/?url=http%3A%2F%2Forigin%3A8080%2Fgrid.png'
https://images.example.com/dims5/resize/100x100/?url=http%3A%2F%2Forigin%3A8080%2Fgrid.png&sig=e9d70afb...
```

A `/dims4/` URL holds a placeholder in the signature segment. Its length sets
the length of the signature.

```
$ DIMS_SIGNING_KEY=a-secret dims-sign --dims4 \
'/dims4/CLIENT/xxxxxx/2147483647/resize/100x100/?url=https%3A%2F%2Fexample.com%2Fcat.jpg'
/dims4/CLIENT/0c0bf3/2147483647/resize/100x100/?url=https%3A%2F%2Fexample.com%2Fcat.jpg
```

## Encrypt the image URL

`--eurl` signs the URL and then replaces `url` with the encrypted source. The
signature covers the plain image URL, so the server verifies the request after
it decrypts.

```
$ dims-sign --dims5 --key-file dims.key --eurl "$url"
/dims5/resize/100x100/?eurl=SbEm%2BgYau0i4Bj%2BP%2FLOgRUf9UG3eeq3DRDh%2F...&sig=e9d70afb...
```

`/dims5/` reads AES-128-GCM. `/dims4/` reads what
[`DimsEncryptionAlgorithm`](/configuration/clients) names, and its default is
AES-128-ECB. `--cipher` names the one to use, and it defaults to the endpoint
default.

Every call writes a fresh IV, so two runs on one URL produce two values.

## Read the message

The server logs a mismatch without the digests. This command runs on a machine
that already holds the key, so it prints the message the key produces.

```
$ dims-sign --dims5 --key-file dims.key --message "$url"
watermark/0.2,0.5,se/
http://origin:8080/grid.png
overlay=http%3A%2F%2Forigin%3A8080%2Foverlay.png
```

The three lines are the commands, the image URL, and the canonical query. Read
each one against [`/dims5/`](/endpoints/dims5) to find the line that differs.

`--message` needs `--dims5`. A `/dims4/` message holds the client secret.

## Check a signature

```
$ dims-sign --dims5 --key-file dims.key --verify "$url"
signature mismatch
wanted e9d70afb0b29520bae7fa47fb3de2d4c62c85f40d89636f6b190ac8055838bff
got 6d3dcb0a1f29520bae7fa47fb3de2d4c62c85f40d89636f6b190ac8055838bff
```

`--verify` accepts both endpoints. It prints digests.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | a signature, or a match |
| `1` | a mismatch |
| `2` | a usage error |
| `3` | a URL the command cannot read |

## Where it installs

`cmake --install` writes `dims-sign` under `bin`. `docker/Dockerfile` copies
only `libmod_dims.so` out of the build stage, so the server image has no
`dims-sign`.
35 changes: 35 additions & 0 deletions docs/docs/clients/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Clients

A client signs a URL before a browser requests it. The signing rules live in
one C library, and the module compiles the same source.

| Client | What it is |
|---|---|
| [C library](/clients/c) | `libmoddims_sign`, the signing rules and the `eurl` ciphers |
| [dims-sign](/clients/dims-sign) | a command that signs a URL and checks one |

## One contract

`test/fixtures/signing.tsv` holds a signed URL, a canonical query, and a
message for each case. The unit suite reads that file and compares the library
output against every field.

An `eurl` record goes the other way: it holds a ciphertext and the plain image
URL it decrypts to. A ciphertext holds a fresh nonce, so the file cannot pin
one a client produces. Each suite round trips its own encrypt through its own
decrypt instead.

The request suite sends each signed URL in that file to a running module, then
reads the signature counters to confirm the module verified all of them. The
file records what the server accepts.

## Which endpoint

Pick the signer that matches the endpoint the server serves.
[`/dims5/`](/endpoints/dims5) signs with HMAC-SHA256 under one key.
[`/dims4/`](/endpoints/dims4) signs with MD5 under a client secret.

An operator picks the location with `SetHandler`, and a reverse proxy in front
can rewrite a public path onto it. The path alone does not identify the
endpoint. A client names the endpoint, and names the prefix when it differs
from the conventional one.
10 changes: 7 additions & 3 deletions docs/docs/endpoints/dims4.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ and the URL is:
```

The commands have a trailing slash in the message. The image URL is percent
encoded in the query string but not in the message.
encoded in the query string but not in the message. Every plus in the image URL
becomes a space in the message, so write `%2B` for a plus that has to survive.

A [client library](/clients/) signs a URL for you.

### Code

Expand Down Expand Up @@ -139,8 +142,9 @@ well, list it in `_keys` and append its value to the message:
message = expires + secret + "watermark/0.2,0.5,se/" + image + overlay
```

Several parameters are appended in the order `_keys` gives, not in the order
they appear in the query string.
Each value goes into the message as it appears in the query string, before it
is decoded. Several parameters are appended in the order `_keys` gives, not in
the order they appear in the query string.

## Expiry

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/endpoints/dims5.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Take the HMAC-SHA256 of that under the signing key, hex encoded and lowercase.
The whole digest is compared, and the comparison reads every byte whatever the
answer.

A [client library](/clients/) signs a URL for you.

### The canonical query

Every signed parameter written `name=value`, percent encoded, ordered by name.
Expand Down
6 changes: 6 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ const sidebars = {
link: {type: 'doc', id: 'endpoints/index'},
items: ['endpoints/dims5', 'endpoints/dims4', 'endpoints/dims3', 'endpoints/status', 'endpoints/metrics', 'endpoints/local'],
},
{
type: 'category',
label: 'Clients',
link: {type: 'doc', id: 'clients/index'},
items: ['clients/c', 'clients/dims-sign'],
},
{
type: 'category',
label: 'Operations',
Expand Down
Loading
Loading