minato_auth (minato v0.18.6)
View SourceThe authentication exchange, as a fold over the messages the server sends.
minato_protocol decodes an Authentication* message; this module decides
what to answer with. It does no I/O and starts no process, so the whole cycle,
including the ones that need several round trips, can be driven in a test
without a socket.
State = minato_auth:init(#{user => ~"minato", password => ~"pencil"}),
{send, Reply, Next} = minato_auth:step(Message, State),
%% write minato_protocol:encode(Reply), read the next message, step againstep/2 answers one of three things:
{send, Reply, State}to writeReplyand wait for the next message{continue, State}when the server's message needed no answer, which is whatAuthenticationSASLFinalisauthenticatedonAuthenticationOk, after which the caller readsParameterStatus,BackendKeyDataandReadyForQueryas usual
An ErrorResponse from the server is not handled here. It is an ordinary
decoded message and the caller branches on it: a wrong password arrives that
way, not as an exception.
A started SASL exchange has to finish
AuthenticationOk is refused while a SCRAM exchange is outstanding, and that
refusal is load bearing rather than tidiness. The check that authenticates the
server to the client lives in AuthenticationSASLFinal, so a server that cannot
produce a valid ServerSignature has an obvious move available: send
AuthenticationOk instead and never send the final message at all. A client
that accepted it would have proved itself to an impostor and gone on to send it
queries, which is exactly the outcome verifying the signature is meant to
prevent. Only an exchange that reached verified, or one that never started
because the server asked for no password, ends in authenticated.
Changing method part way through is refused for the same reason. A server that
opened with AuthenticationSASL and then asks for a cleartext password, or for
MD5, is asking the client to hand over something weaker than the exchange it
already began, which is what a man in the middle who cannot finish SCRAM would
do next.
That guard is worth having but it is not a policy. A server that asks for
cleartext in its very first message is answered, because that is what
supporting cleartext means, and minato has no way yet to be told which methods
are acceptable. A require_auth style option belongs with the connection layer,
along with TLS, since both are decisions about the transport rather than about
the exchange.
Methods
| Server asks for | minato answers with |
|---|---|
AuthenticationOk | nothing, the cycle is over |
AuthenticationCleartextPassword | the password, unmodified |
AuthenticationMD5Password | the salted MD5 digest |
AuthenticationSASL | SCRAM-SHA-256, through minato_scram |
SCRAM-SHA-256 is the one that matters: it is what password_encryption has
defaulted to since PostgreSQL 14, and CloudNativePG issues nothing else. The
other two are here because a PostgreSQL is not always a recent PostgreSQL, and
they cost a handful of lines each.
MD5 is broken as a hash and PostgreSQL has deprecated it, so a server asking for it is one to reconfigure rather than one to trust. minato answers anyway, because refusing would only mean a connection that cannot be made at all, whereas the server has already decided what it will accept.
GSSAPI, SSPI and the rest of the Authentication codes never reach this module:
minato_protocol models the four above and raises on any other code as it
decodes, because there is no useful reply to build and a message silently passed
on would look to a caller like a hung connection. A message that is not an
authentication message at all raises here, for the same reason.
Channel binding
SCRAM-SHA-256-PLUS is chosen when the server offers it and the caller supplied
channel_binding_data, which is the hash of the server's certificate and comes
from the connection layer, since only it has the TLS session.
channel_binding => require refuses to authenticate at all without it. prefer
takes it when both sides have it, and falls back to plain SCRAM with a y,,
header, which tells a server that does support binding that its mechanism list
was stripped on the way. disable never binds. See minato_scram for what
each header means.
Summary
Types
What to do about channel binding.
Who to authenticate as.
The map carried by an error({minato_auth, Report}) exception.
The exchange so far. Never holds anything derived from the password.
What to do with the message that was just decoded.
Types
-type channel_binding() :: prefer | require | disable.
What to do about channel binding.
prefer uses SCRAM-SHA-256-PLUS when the server offers it and there is
binding data to use, and plain SCRAM otherwise. require refuses to
authenticate without it. disable never binds.
prefer still tells the truth in the gs2 header: a client with binding data
whose server did not offer -PLUS sends y,,, which is how a server that did
offer it detects the mechanism list being stripped on the way.
-type credentials() :: #{user := binary(), password => binary(), scram => minato_scram:opts(), channel_binding => channel_binding(), channel_binding_data => binary()}.
Who to authenticate as.
#{user := binary(),
password => binary(),
scram => minato_scram:opts()}password is absent for a server that asks for none, and a server that then
asks for one raises rather than sending an empty string. scram is passed to
minato_scram and is only worth setting for a server deliberately configured
away from its iteration count defaults.
The map carried by an error({minato_auth, Report}) exception.
-opaque state()
The exchange so far. Never holds anything derived from the password.
-type step() :: {send, minato_protocol:frontend(), state()} | {continue, state()} | authenticated.
What to do with the message that was just decoded.
Functions
-spec init(credentials()) -> state().
Begin an authentication exchange.
Nothing is sent and nothing is derived yet: the server has not said what it wants, and every method but one needs something from it first.
-spec step(minato_protocol:backend(), state()) -> step().
Answer one Authentication* message.
Raises error({minato_auth, t:report/0}) when the server asks for a method
minato does not implement, when it asks for a password that was not configured,
when it offers no SASL mechanism minato can speak, or when a SASL message
arrives outside an exchange. Raises error({minato_scram, _}) for a server that
breaks SCRAM or fails to prove itself; see minato_scram.