Packages

Elixir bindings to macOS Kerberos via GSS.framework. Provides kinit, credential listing, TGT validation, and ticket management for Active Directory environments.

Current section

Files

Jump to
ex_krb5 lib ex_krb5.ex
Raw

lib/ex_krb5.ex

defmodule ExKrb5 do
@moduledoc """
Elixir bindings to macOS Kerberos via GSS.framework.
Provides native macOS Kerberos integration using Apple's public GSS APIs,
including `gss_aapl_initial_cred()` and `gss_aapl_change_password()`.
Tickets are stored in the system KCM credential cache, visible to
Finder, Safari, and all other macOS applications.
This is the same approach used by Jamf's NoMAD — no shelling out to
`kinit`, no private APIs.
## Quick Start
# Authenticate (equivalent to kinit)
:ok = ExKrb5.kinit("user@CORP.EXAMPLE.COM", "password123")
# List cached credentials
{:ok, creds} = ExKrb5.list_credentials()
# Check ticket status
{:ok, info} = ExKrb5.credential_info("user@CORP.EXAMPLE.COM")
info.time_remaining #=> 36000 (seconds)
# Change AD password
:ok = ExKrb5.change_password("user@CORP.EXAMPLE.COM", "old_pass", "new_pass")
# Generate a Kerberos token for HTTP Negotiate auth
{:ok, token} = ExKrb5.init_sec_context("user@CORP.EXAMPLE.COM", "HTTP/intranet.corp.example.com")
## macOS Integration
All operations go through Apple's `GSS.framework`, which talks to the
system `GSSCred` daemon via Mach IPC. This means:
- Tickets land in the system KCM credential cache
- `klist` on the command line shows tickets acquired by this library
- Finder can use the TGT to mount SMB shares
- Safari can use the TGT for SPNEGO authentication
- The GSSCred daemon handles automatic renewal when credentials are held
## Platform
macOS only. Requires GSS.framework (available since macOS 10.7).
"""
use Rustler,
otp_app: :ex_krb5,
crate: "ex_krb5"
@typedoc "Kerberos principal name (e.g., \"user@REALM\")"
@type principal :: String.t()
@typedoc "Service principal name (e.g., \"HTTP/host.example.com\")"
@type spn :: String.t()
@typedoc "Credential information map"
@type credential_info :: %{
principal: String.t(),
time_remaining: non_neg_integer(),
is_expired: boolean()
}
# ============================================================
# Authentication — kinit equivalent
# ============================================================
@doc """
Acquire a Kerberos TGT using a password.
Equivalent to `kinit principal`. Uses `gss_aapl_initial_cred()` to
authenticate against the KDC and store the resulting TGT in the
system credential cache.
## Options
* `:forwardable` - Request a forwardable ticket (default: `true`)
* `:renewable` - Request a renewable ticket (default: `true`)
## Examples
iex> ExKrb5.kinit("user@CORP.EXAMPLE.COM", "password123")
:ok
iex> ExKrb5.kinit("user@CORP.EXAMPLE.COM", "wrong_password")
{:error, "KDC: Preauthentication failed"}
"""
@spec kinit(principal(), String.t(), keyword()) :: :ok | {:error, String.t()}
def kinit(_principal, _password, _opts \\ []),
do: :erlang.nif_error(:not_loaded)
# ============================================================
# Password Management
# ============================================================
@doc """
Change a Kerberos password.
Uses `gss_aapl_change_password()` to change the password on the KDC.
The old password must be correct. The new password must meet the
KDC's password policy requirements.
## Examples
iex> ExKrb5.change_password("user@CORP.EXAMPLE.COM", "old_pass", "new_pass")
:ok
iex> ExKrb5.change_password("user@CORP.EXAMPLE.COM", "old_pass", "weak")
{:error, "Password does not meet policy requirements"}
"""
@spec change_password(principal(), String.t(), String.t()) ::
:ok | {:error, String.t()}
def change_password(_principal, _old_password, _new_password),
do: :erlang.nif_error(:not_loaded)
# ============================================================
# Credential Cache Operations
# ============================================================
@doc """
List all Kerberos credentials in the system cache.
Uses `gss_iter_creds_f()` to enumerate all cached credentials.
Returns a list of credential info maps.
## Examples
iex> ExKrb5.list_credentials()
{:ok, [
%{principal: "user@CORP.EXAMPLE.COM", time_remaining: 36000, is_expired: false}
]}
"""
@spec list_credentials() :: {:ok, [credential_info()]} | {:error, String.t()}
def list_credentials(),
do: :erlang.nif_error(:not_loaded)
@doc """
Get information about a specific credential.
## Examples
iex> ExKrb5.credential_info("user@CORP.EXAMPLE.COM")
{:ok, %{principal: "user@CORP.EXAMPLE.COM", time_remaining: 36000, is_expired: false}}
iex> ExKrb5.credential_info("nobody@NOWHERE")
{:error, "No credentials found for principal"}
"""
@spec credential_info(principal()) :: {:ok, credential_info()} | {:error, String.t()}
def credential_info(_principal),
do: :erlang.nif_error(:not_loaded)
# ============================================================
# Credential Lifecycle
# ============================================================
@doc """
Renew a Kerberos TGT.
> Note: Not currently available via public GSS.framework API.
> Use `kinit/2` to re-authenticate, or enable the Kerberos SSO
> Extension for automatic renewal.
## Examples
iex> ExKrb5.renew("user@CORP.EXAMPLE.COM")
{:error, "Renewal requires re-authentication..."}
"""
@spec renew(principal()) :: :ok | {:error, String.t()}
def renew(_principal),
do: :erlang.nif_error(:not_loaded)
@doc """
Hold (pin) a credential in the cache.
> Note: `gss_cred_hold` is not available in the public GSS.framework API.
> Credentials acquired via `kinit/2` are automatically managed by
> the GSSCred daemon.
"""
@spec hold(principal()) :: :ok | {:error, String.t()}
def hold(_principal),
do: :erlang.nif_error(:not_loaded)
@doc """
Unhold (unpin) a credential.
> Note: `gss_cred_unhold` is not available in the public GSS.framework API.
"""
@spec unhold(principal()) :: :ok | {:error, String.t()}
def unhold(_principal),
do: :erlang.nif_error(:not_loaded)
@doc """
Destroy a credential from the cache.
Equivalent to `kdestroy` for a specific principal.
## Examples
iex> ExKrb5.destroy("user@CORP.EXAMPLE.COM")
:ok
"""
@spec destroy(principal()) :: :ok | {:error, String.t()}
def destroy(_principal),
do: :erlang.nif_error(:not_loaded)
# ============================================================
# GSSAPI Security Context
# ============================================================
@doc """
Initialize a GSSAPI security context (generate a Kerberos token).
Generates a token that can be used for HTTP Negotiate authentication,
SMB authentication, etc.
The `target` should be a service principal name like
`"HTTP/intranet.corp.example.com"` or `"cifs/fileserver.corp.example.com"`.
## Options
* `:delegate` - Allow credential delegation (default: `false`)
* `:mutual` - Request mutual authentication (default: `false`)
## Examples
iex> {:ok, token} = ExKrb5.init_sec_context("user@CORP.EXAMPLE.COM", "HTTP/intranet.corp.example.com")
iex> byte_size(token) > 0
true
# Use in HTTP header:
# Authorization: Negotiate \#{Base.encode64(token)}
"""
@spec init_sec_context(principal(), spn(), keyword()) ::
{:ok, binary()} | {:error, String.t()}
def init_sec_context(_principal, _target, _opts \\ []),
do: :erlang.nif_error(:not_loaded)
# ============================================================
# Convenience / Query
# ============================================================
@doc """
Check if a valid (non-expired) TGT exists for the given principal.
## Examples
iex> ExKrb5.has_valid_tgt?("user@CORP.EXAMPLE.COM")
true
"""
@spec has_valid_tgt?(principal()) :: boolean()
def has_valid_tgt?(principal) do
case credential_info(principal) do
{:ok, %{is_expired: false}} -> true
_ -> false
end
end
@doc """
Get the time remaining (in seconds) for a credential.
Returns `0` if no valid credential is found.
## Examples
iex> ExKrb5.time_remaining("user@CORP.EXAMPLE.COM")
36000
"""
@spec time_remaining(principal()) :: non_neg_integer()
def time_remaining(principal) do
case credential_info(principal) do
{:ok, %{time_remaining: t}} -> t
_ -> 0
end
end
end