NervesHub shared-secret authentication headers.
NervesHub authenticates a device either with a client certificate or with a
shared secret. This builds the headers for the second, which the server reads
in NervesHub.DeviceLink.Authentication.
The signature is a Plug.Crypto token, so the device has to reproduce what
Plug.Crypto.sign/4 would have produced:
Salt = "NH1:device-socket:shared-secret:connect\n\nx-nh-alg=..\nx-nh-key=..\nx-nh-time=..\n"
Key = PBKDF2-HMAC(Digest, Secret, Salt, Iterations, KeyLength)
Payload = term_to_binary({Identifier, SignedAtMs, MaxAge})
Token = "SFMyNTY." ++ b64url(Payload) ++ "." ++ b64url(HMAC(Key, "SFMyNTY." ++ b64url(Payload)))
Every step is a primitive AtomVM has, except term_to_binary/1. That one is
written out by hand here rather than called: the payload's shape is fixed, and
relying on AtomVM's encoder to agree with OTP's byte for byte — in particular
on how it writes a millisecond timestamp, which is too large for INTEGER_EXT
and lands in SMALL_BIG_EXT — would be a silent dependency. A mismatch fails
as an unexplained unauthorized at the socket, which is a bad thing to debug
on a device.
option() = {digest, sha256 | sha384 | sha512} | {iterations, pos_integer()} | {key_length, pos_integer()} | {max_age, pos_integer()} | {signed_at, integer()}
| headers/3 | Build the authentication headers for a device. |
| headers/4 | |
| payload/3 | The external term format encoding of {Identifier, SignedAtMs, MaxAge}. |
| salt/3 | The salt the key is derived from. |
| token/2 | Sign a payload the way Plug.Crypto.MessageVerifier does. |
headers(Identifier::binary(), Key::binary(), Secret::binary()) -> [{binary(), binary()}]
Build the authentication headers for a device.
Identifier is the device identifier, Key the shared secret key (nhd_..
for a device secret, nhp_.. for a product one) and Secret its secret.
headers(Identifier::binary(), Key::binary(), Secret::binary(), Opts::[option()]) -> [{binary(), binary()}]
payload(Identifier::binary(), SignedAtMs::integer(), MaxAge::integer()) -> binary()
The external term format encoding of {Identifier, SignedAtMs, MaxAge}.
salt(Alg::binary(), Key::binary(), SignedAt::integer()) -> binary()
The salt the key is derived from.
It repeats the headers, which is what binds the signature to them: changingx-nh-time in flight changes the salt, so the key no longer derives and the
signature no longer verifies.
token(SigningKey::binary(), Payload::binary()) -> binary()
Sign a payload the way Plug.Crypto.MessageVerifier does.
Generated by EDoc