Module nh_shared_secret

NervesHub shared-secret authentication headers.

Description

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.

Data Types

option()

option() = {digest, sha256 | sha384 | sha512} | {iterations, pos_integer()} | {key_length, pos_integer()} | {max_age, pos_integer()} | {signed_at, integer()}

Function Index

headers/3Build the authentication headers for a device.
headers/4
payload/3The external term format encoding of {Identifier, SignedAtMs, MaxAge}.
salt/3The salt the key is derived from.
token/2Sign a payload the way Plug.Crypto.MessageVerifier does.

Function Details

headers/3

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/4

headers(Identifier::binary(), Key::binary(), Secret::binary(), Opts::[option()]) -> [{binary(), binary()}]

payload/3

payload(Identifier::binary(), SignedAtMs::integer(), MaxAge::integer()) -> binary()

The external term format encoding of {Identifier, SignedAtMs, MaxAge}.

salt/3

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: changing x-nh-time in flight changes the salt, so the key no longer derives and the signature no longer verifies.

token/2

token(SigningKey::binary(), Payload::binary()) -> binary()

Sign a payload the way Plug.Crypto.MessageVerifier does.


Generated by EDoc