/kerlberos

Native Erlang library for KerberosV and GSS-API

Primary LanguageErlang

kerlberos

Native Erlang library for KerberosV and GSS-API (no NIFs or dependencies on MIT Kerberos etc).

Features:

  • KerberosV client
    • Authenticate to KDC, obtain a TGT (AS-REQ/AS-REP)
    • Use a TGT to obtain another ticket (TGS-REQ/TGS-REP)
    • Parses /etc/krb5.conf, can use DNS discovery for KDCs, supports fail-over on KDC unavailability, respects DNS TTLs
  • KerberosV service provider
    • Decode MIT-format keytab file, match against a ticket from a client
    • Unpack and parse AD PAC in service ticket
  • GSS-API initiator/acceptor
    • Support for GSS-KerberosV, including mutual auth, MICs and wrap tokens.
    • Works with all AES and 3DES enctypes (including new SHA2 variants). Partial support for RC4 enctypes.
    • Support for SPNEGO negotiating KerberosV.

Compatible with MIT Kerberos and Active Directory (mostly compatible with Heimdal as well).

Examples

Obtaining a TGT:

1> {ok, R} = krb_realm:open("EXAMPLE.COM").
{ok,<0.492.0>}

2> {ok, TGT} = krb_realm:authenticate(R, ["root"], <<"password">>).
{ok,#{authtime => <<"20210621065701Z">>,
      endtime => <<"20210621105701Z">>,
      flags => [renewable,proxiable,forwardable,pre_auth,initial],
      key =>
          {krb_base_key,aes256_hmac_sha384,
                        <<66,183,155,201,218,5,35,117,120,222,203,33,247,255,17,
                          96,147,47,59,207,73,152,199,...>>},
      principal => ["root"],
      realm => "EXAMPLE.COM",renewuntil => <<"20210628065701Z">>,
      svc_principal => ["krbtgt","EXAMPLE.COM"],
      ticket =>
          {'Ticket',5,"EXAMPLE.COM",
                    {'PrincipalName',2,["krbtgt","EXAMPLE.COM"]},
                    {'EncryptedData',aes256_hmac_sha384,1,
                                     <<169,171,187,255,155,17,230,248,77,55,190,65,22,151,
                                       170,115,165,...>>}}}}

Using that TGT to obtain a service ticket:

3> {ok, T} = krb_realm:obtain_ticket(R, TGT, ["host", "kdc.example.com"]).
{ok,#{authtime => <<"20210621065938Z">>,
      endtime => <<"20210621105937Z">>,
      flags =>
          [transited,renewable,proxiable,forwardable,pre_auth],
      key =>
          {krb_base_key,aes256_hmac_sha384,
                        <<77,107,230,242,248,0,143,150,231,49,163,248,222,62,88,
                          85,249,44,198,191,189,155,194,...>>},
      principal => ["root"],
      realm => "EXAMPLE.COM",renewuntil => <<"20210628065938Z">>,
      svc_principal => ["host","kdc.example.com"],
      ticket =>
          {'Ticket',5,"EXAMPLE.COM",
                    {'PrincipalName',2,["host","kdc.example.com"]},
                    {'EncryptedData',aes256_hmac_sha384,2,
                                     <<58,53,165,168,144,253,86,97,106,128,221,126,100,142,
                                       37,159,253,...>>}}}}

A simple GSS-API listener/acceptor (using SPNEGO):

server() ->
    % First, load our keytab from disk
    {ok, KeyTabData} = file:read_file("/etc/krb5.keytab"),
    {ok, KeyTab} = krb_mit_keytab:parse(KeyTabData),

    % Then open our TCP listening socket
    {ok, LSock} = gen_tcp:listen(8082,
        [binary, {active, true}, {packet, line}, {reuseaddr, true}]),
    server(LSock, KeyTab).

server(LSock, KeyTab) ->
    {ok, Sock} = gen_tcp:accept(LSock),
    % In this example, the client and server send GSS-API tokens to each
    % other base64-encoded on one line per token.
    receive
        {tcp, Sock, Data} ->
            Ret = gss_spnego:accept(base64:decode(Data), #{
                keytab => KeyTab,
                chan_bindings => <<0:128/big>>,
                mutual_auth => true}),
            case Ret of
                {ok, S0} ->
                    start_server_loop(Sock, S0);
                {ok, T0, S0} ->
                    gen_tcp:send(Sock, [base64:encode(T0), $\n]),
                    start_server_loop(Sock, S0);
                {continue, T0, S0} ->
                    gen_tcp:send(Sock, [base64:encode(T0), $\n]),
                    server_continue(Sock, S0)
            end;
        {tcp_closed, Sock} ->
            ok
    end,
    server(LSock, KeyTab).

server_continue(Sock, S0) ->
    receive
        {tcp, Sock, Data} ->
            Ret = gss_spnego:continue(base64:decode(Data), S0),
            case Ret of
                {ok, S0} ->
                    start_server_loop(Sock, S0);
                {ok, T0, S0} ->
                    gen_tcp:send(Sock, [base64:encode(T0), $\n]),
                    start_server_loop(Sock, S0);
                {continue, T0, S0} ->
                    gen_tcp:send(Sock, [base64:encode(T0), $\n]),
                    server_continue(Sock, S0)
            end;
        {tcp_closed, Sock} ->
            ok
    end.

start_server_loop(Sock, S0) ->
    {ok, Peer} = gss_spnego:peer_name(S0),
    {ok, Username} = gss_spnego:translate_name(Peer, ?'id-user-name'),
    io:format("peer username: ~s\n", [Username]),
    server_loop(Sock, S0).

server_loop(Sock, S0) ->
    receive
        {tcp_closed, Sock} -> ok;
        {tcp, Sock, Data} ->
            {ok, Msg, S1} = gss_spnego:unwrap(base64:decode(Data), S0),
            io:format("<= ~s\n", [Msg]),
            {ok, T, S2} = gss_spnego:wrap(<<"ok\n">>, S1),
            gen_tcp:send(Sock, [base64:encode(T), $\n]),
            case Msg of
                <<"exit">> ->
                    gen_tcp:close(Sock),
                    ok;
                _ -> server_loop(Sock, S2)
            end
    end.

Installing

Available on hex.pm

API docs

Edoc