Eagle Auth - Lightweight PKI for REST Authentication
====================================================
WARNING: still just crazy thoughts :)
Changes to Consider:
- Do we need a clientId? Why not just use the publicKey?
- there would be no need to lookup a clientId
- request handler just gets the publicKey it was authenticated with and can then validate authorization based on that
- Key terminology, pick something sane:
- clientId
- public key
- private key
- accessToken
- secretKey
- seed
- signing key
- Nonces: to be or not to be?
- Useless in distributed apps
- Hard to validate
- Nice to have if someday we need to improve security
- Must be optional (reusability of signed URLs is a feature)
- Url-safe base64, base64 or hex?
Why not Hawk: (and why not do it as part of Hawk)
- HMAC requires symmetric keys
- Hawk has a few complications:
- Port is required (hard behind reverse proxies)
- Response signatures (server can sign it's reply)
- Defaults to 1 min clock skew
- Offers complicated time message
- Nonces are really annoying to store
- Hawk aims at being safe over HTTP
All in all, hawk is pretty good. Most complaints above have
pros and cons. Eagle is mostly of you want an asymmetric
cryptographic scheme, which makes it safer to share keys between
different servers.
Things to look at:
Eagle Goals:
- Authenticate HTTPS requests
- Avoid symmetric keys
- Not require well-synced clocks
- No nonce negotiation (like digest)
- Facilitate authorization schemes
As opposed to hawk, eagle does not aim to secure HTTP requests.
Hawk makes HTTP reasonably secure with the following features:
- Nonces
- Protect against replay attacks
- Not efficient as number of server scale up
- Under-approximations degrades as number of server increase.
- Response signatures
- Only possible because of symmetric keys
- For asymmetric just use a self-signed SSL certificate
- Tight clock synchronization requirements
- Protects against reply attacks
- Causes hawk to offer timestamp messages
- Hard to implement correctly
- Payload hash (optional)
Eagle Auth Overview:
As header:
authentication: Eagle Version=1
Credential=<clientId (url-safe)>,
Headers=host;x-eagle-start;...,
Signature=<url-safe base64 signature>\n
x-eagle-date: <ISO 8601 Extended format>\n
x-eagle-expires: <number of seconds>
x-eagle-content-hash: SHA512 <url-safe base64 hash>\n
Or using query string:
?x-eagle-version=1
&x-eagle-credential=...
&x-eagle-headers=...
&x-eagle-signature=...
&x-eagle-date=<ISO 8601 Extended format>
&x-eagle-expires=<number of seconds>
&x-eagle-content-hash=SHA512 <url-safe base64 hash>
Values:
Credential: clientId (must be url-safe)
Headers: list of headers: host;x-eagle-content-hash;...
(also called list of signed headers)
Signature: url-safe base64 of ed25519.sign(stringToSign, key)
StringToSign:
eagle.1\n
verb:<VERB>\n
resource:<CanonicalURI>\n
query:<CanonicalQueryString>\n
\n
{<header.lowercase()>:<value>\n foreach in Headers}
\n
headers from "Headers" are included in the order they are listed
in Headers. Their header key is always in lower case.
CanonicalURI:
In short this is everything after the domain (and port) and before
the querystring.
CanonicalQueryString:
Querystring without the key: "x-eagle-signature"
Configuration Options:
- requiredHeaders: (must be present in all request)
- mustBeSignedHeader: (must be signed if present)
- host
- x-eagle-date
- x-eagle-expires (providing it is optional)
- x-eagle-content-hash (providing it is optional)
- x-eagle-nonce (providing and validation should optional)
- headersCanBeQueryString: (headers that can be in querystring)
- x-eagle-date
- x-eagle-expires
- x-eagle-content-hash
- x-eagle-nonce
- Content-hash Schemes:
- Max "expires" (defaults 31 days)
- Max clock skew tolerance (default 15min)
Using the configuration, you can set the max life-time of a
signed URL (or a signature for a request). You can also specify
which headers can be provided by query string. But note that
query string and headers cannot conflict. So you maybe not wish
to be the "range" into headersCanBeQueryString as this would
prevent a request with the header range (meaning one thing)
and the querystring range (meaning another).
Hence, do only use "headersCanBeQueryString" for custom headers
that are properly prefixed "x-<app-name>-"
Notice:
You are always welcome to sign more headers than the ones
required. But notice that some headers must be signed if present.
Modifying this behaviour makes it easy to add custom headers that
you want to make sure are always signed.
In most cases sign as many headers as possible with XHR in a
browser there are headers you can't set and, thus, cannot sign.
The "mustBeSignedHeader" allows you to ensure that some extra
(optional) headers are covered by the signature. It's very
similar the hawk "ext" data, which is covered by the HMAC.
Client Responsibilities
-----------------------
We leave it up to the client to decide how many security measures
to employ. By tweaking server configuration, one can force the
client to include more information in the stringToSign.
Payload Hashing:
In the default configuration "x-eagle-content-hash" is not a
required header. If present it must, however, be amongst the
signed headers.
Obviously, the client should specify this as often as possible,
as doing so makes the signature harder to reuse.
But we also recognize that with query string authentication it
is sometimes hard to do this.
Clock Synchronization:
Again in the default configuration allows the interval
x-eagle-expires to 31 days. This is useful for
signed URLs that allows you to GET a resource. But it's a very
big interval for requests that takes place immediately.
However, we do not require x-eagle-expires to be specified, if
not specified we'll assume the signature was written at
x-eagle-date and meant to be used immediately, thus only accepting
the configured clock skew tolerance.
Nonces:
Client should probably include a nonce. In distributed servers
validating it will be hard and complicated, so one may choose not
to do this. But including it is basically free.
General Security Considerations
-------------------------------
- Relies on the client in default configuration
- Replay-ability depends on size signature validity interval
- Payload hash is optional (but encouraged)
- Should always be used with SSL
- More or less equivalent of AWS signatures version 4
- Except they do crazy hashing (which seems questionable)
- Well suited for idempotent operations
- replay-attacks are less interesting for such APIs
Authorization with Eagle Scopes
===============================
Eagle only handles authentication, this is packaged as eagle-auth.
The package eagle-scopes implements an authorization scheme with
scopes and temporary credentials on top of eagle-auth. Using this
authorization scheme is completely optional.
With eagle scopes a client has the following properties:
client:
- clientId (must be url-safe, slugid or uuid recommended)
- secretKey (private key, only known by the client (seed in nacl))
- publicKey (public key, known by the server)
- scopes (whitespace-free strings possibly ending with "*")
The following headers are added:
x-eagle-authorized-scopes: <scope1>;<scope2>;<scope3>\n
(where <scopeX> is URI encoded)
x-eagle-certificate: Key=<publicKey> Signature=...
x-eagle-certificate-version:
x-eagle-certificate-start:
x-eagle-certificate-end:
x-eagle-certificate-scopes:
stringToSign for the certificate signature:
version:1\n
key:<temporary-publicKey>\n
start:<ISO 8601 Extended format>\n
end:<ISO 8601 Extended format>\n
scopes:\n
<scope1>\n
<scope2>\n
...
<scopeN>\n
\n
Note to self:
I'm not sure I want to split the certificate into multiple
headers. It would be nice if the certificate was just this one
string that had to be included.
However, it would also be nice if scopes was separate, because
headers length is fairly limited.