\documentstyle[12pt,fullpage,changebar]{article}

% $Id$

\setlength{\parskip}{.7\baselineskip}
\setlength{\parindent}{0pt}

\def\secure{OV*Secure}
\def\v#1{\verb+#1+}
\def\k#1{K$_#1$}

\title{OV*Secure Admin Server \\ Implementation Design}
\author{Barry Jaspan}
\date{DRAFT --- \today}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Make _ actually generate an _, and allow line-breaking after it.
\let\underscore=\_
\catcode`_=13
\def_{\underscore\penalty75\relax}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\begin{document}

\maketitle

{\setlength{\parskip}{0pt}\tableofcontents}

\section{Overview}

The admin server is implemented as a nearly-stateless transaction
server, where each admin API function represents a single transaction.
No per-client or per-connection information is stored; only local
database handles are maintained between requests.

The admin API is exported via an RPC interface that hides all details
about network encoding, authentication, and encryption of data on the
wire.  The RPC mechanism does, however, allow the server to access the
underlying authentication credentials for authorization purposes.

The admin server accesses a total of three databases.

\begin{itemize}
\item The master Kerberos database is used to store all the
information that the Kerberos server understands, thus allowing the
greatest functionality with no modifications to a standard KDC.  

\item The admin principal database stores \secure{}-specific per-principal
information. 

\item The policy database stores \secure{} policy information.
\end{itemize}

The per-principal information stored in the admin principal database
consists of the principal's policy name and an array of the
principal's previous keys.  The old keys are stored encrypted in the
key of the special principal ``kadmin/history'' that is created by
ovsec_kadm_create.  Since a change in kadmin/history's key renders
every principal's key history array useless, it can only be changed
using the ovsec_kadm_edit utility; that program will reencrypt every
principal's key history in the new key.  The admin server refuses all
requests to change kdamin/history's key.

\section{Main}

The admin server starts by trapping all fatal signals and directing
them to a cleanup-and-exit function.  It then creates and exports the
RPC interface and enters its main loop.

The main loop dispatches all incoming requests to the RPC mechanism.
After 15 seconds of inactivity, the server closes all open databases;
each database will be automatically reopened by the API function
implementations as necessary.

\section{Remote Procedure Calls}

The RPC for the Admin system will be based on SUNRPC.  SUNRPC is used
because it is a well-known, portable RPC mechanism.  The underlying
external data representation (xdr) mechanisms for wire encapsulation
are well-known and extensible.

Authentication to the admin server will be handled by adding a GSS-API
authentication type within the existing SUNRPC structure.  This will
require code modifications to SUNRPC, but the API and wire protocol do
not need to change.  This may affect whether the RPC will use UDP or
TCP; although all the admin functions are stateless, the GSS-API
authentication binding will not be and it might be easier to use TCP
for this reason.

\section{Database Record Types}
\label{sec:db-types}

\subsection{Admin Principal, osa_princ_ent_t}

The admin principal database stores records of the type
osa_princ_ent_t (declared in $<$ovsec_admin/adb.h$>$), which is the
subset of the ovsec_kadm_principal_ent_t structure that is not stored
in the Kerberos database plus the necessary bookkeeping information.
The records are keyed by the ASCII representation of the principal's
name, including the trailing NULL.

\begin{verbatim}
typedef struct _osa_princ_ent_t {
        krb5_principal name;

        char * policy;
        u_int32 aux_attributes;

        u_int32 num_old_keys;
        u_int32 next_old_key;
        krb5_kvno admin_history_kvno;
        krb5_encrypted_keyblock *old_keys;
} osa_princ_ent_rec, *osa_princ_ent_t;
\end{verbatim}

The fields that are different from ovsec_kadm_principal_ent_t are:

\begin{description}
\item[num_old_keys] The number of previous keys in the old_keys array.
This value must be 0 $\le$ num_old_keys $<$ pw_history_num.

\item[next_old_key] The index into old_keys where the next key should
be inserted.  This value must be 0 $\le$ next_old_key $\le$
num_old_keys.

\item[admin_history_kvno] The key version number of the admin/history
principal's key used to encrypt the values in old_keys.  If the admin
server finds that admin/history's kvno is different from the value in
this field, an error message is logged.  (XXX where?)

\item[old_keys] The array of the principal's previous keys, each
encrypted in the admin/history key.  There are num_old_keys elements.
\end{description}

\subsection{Policy, osa_policy_ent_t}

The policy database stores records of the type osa_policy_ent_t
(declared in $<$ovsec_admin/adb.h$>$) , which is all of
ovsec_kadm_policy_ent_t plus necessary bookkeeping information.  The
records are keyed by the policy name.

\begin{verbatim}
typedef struct _osa_policy_ent_t {
        char *policy;

        u_int32 pw_min_life;
        u_int32 pw_max_life;
        u_int32 pw_min_length;
        u_int32 pw_min_classes;
        u_int32 pw_history_num;

        u_int32 refcnt;
} osa_policy_ent_rec, *osa_policy_ent_t;
\end{verbatim}

\subsection{Kerberos, krb5_db_entry}

The Kerberos database stores records of type krb5_db_entry, which is
defined in the $<$krb5/kdb.h$>$ header file.

\begin{verbatim}
typedef struct _krb5_encrypted_keyblock {
    krb5_keytype keytype;
    int length;
    krb5_octet *contents;
} krb5_encrypted_keyblock;

typedef struct _krb5_db_entry {
    krb5_principal principal;
    krb5_encrypted_keyblock key;
    krb5_kvno kvno;
    krb5_deltat max_life;
    krb5_deltat max_renewable_life;
    krb5_kvno mkvno;
    
    krb5_timestamp expiration;
    krb5_timestamp pw_expiration;
    krb5_timestamp last_pwd_change;
    krb5_timestamp last_success;   
    
    krb5_timestamp last_failed;
    krb5_kvno fail_auth_count;
    
    krb5_principal mod_name;
    krb5_timestamp mod_date;
    krb5_flags attributes;
    krb5_int32 salt_type:8,
               salt_length:24;
    krb5_octet *salt;
    krb5_encrypted_keyblock alt_key;
    krb5_int32 alt_salt_type:8,
               alt_salt_length:24;
    krb5_octet *alt_salt;
    
    krb5_int32 expansion[8];
} krb5_db_entry;
\end{verbatim}

The interpretation of most of these fields is the same as given in the
``Principals, ovsec_kadm_principal_ent_t'' section of the functional
specification.  The fields that are not defined there are not used by
\secure{}; however, the admin server preserves the value of any fields
it does not understand.

\section{Database Access Methods}

\subsection{Principal and Policy Databases}

This section describes the database abstraction used for the admin
principal and policy databases.  Since both databases export
equivalent functionality, the API is only described once.  The
character T is used to represent both ``princ'' and ``policy''. The
location of the principal database is defined by the \#define
PRINCIPAL_DB (``/krb5/ovsec_principal.db'') in $<$ovsec_admin/adb.h$>$. The
location of the policy database is defined by the \#define POLICY_DB
(``/krb5/ovsec_policy.db'') in $<$ovsec_admin/adb.h$>$.

Note that this is {\it only} a database abstraction.  All functional
intelligence, such as maintaining policy reference counts or sanity
checking, must be implemented above this layer.

Prototypes for the osa functions are supplied in
$<$ovsec_admin/adb.h$>$. The routines can be found (in the first
relase) in ``stage/lib/libadb.a''. They require linking with the
Berkely DB library (``stage/lib/libdb.a''). [Note: We needed to remove
the dbm compatibility routines from libdb.a because we want to leave
KDB library alone in case somebody wants to run a stock MIT KDC with
our admin server.]

The database routines use com_err for error codes.  The error code
table name is ``adb'' and the offsets are the same as the order
presented here. The error table header file is
$<$ovsec_admin/adb_err.h$>$. Callers of the OSA routines should first call
init_adb_err_tbl() to initialize the database table.

\begin{description}
\item[OSA_ADB_OK] Operation successful.
\item[OSA_ADB_FAILURE] General failure.
\item[OSA_ADB_DUP] Operation would create a duplicate database entry.
\item[OSA_ADB_NOENT] Named entry not in database.
\item[OSA_ADB_BAD_PRINC] The krb5_principal structure is invalid.
\item[OSA_ADB_BAD_POLICY] The specified policy name is invalid.
\item[OSA_ADB_XDR_FAILURE] The principal or policy structure cannot be
encoded for storage.
\end{description}

Database functions can also return system errors.  Unless otherwise
specified, database functions return OSA_ADB_OK.

\begin{verbatim}
osa_adb_ret_t
osa_adb_open_T(osa_adb_T_t *db, char *filename);
\end{verbatim}
%
Open the database named filename.  Returns OSA_ADB_FAILURE if it
cannot open the database.

\begin{verbatim}
osa_adb_ret_t
osa_adb_close_T(osa_adb_T_t db);
\end{verbatim}
%
Close an open database.

\begin{verbatim}
osa_adb_ret_t
osa_adb_create_T(osa_adb_T_t db, osa_T_ent_t entry);
\end{verbatim}
%
Adds the entry to the database.  All fields are defined.  Returns
OSA_ADB_DUP if it already exists.

\begin{verbatim}
osa_adb_ret_t
osa_adb_destroy_T(osa_adb_T_t db, osa_T_t name);
\end{verbatim}

Removes the named entry from the database.  Returns OSA_ADB_NOENT if
it does not exist.

\begin{verbatim}
osa_adb_ret_t
osa_adb_get_T(osa_adb_T_t db, osa_T_t name,
        osa_princ_ent_t *entry); 
\end{verbatim}

Looks up the named entry in the db, and returns it in *entry in
allocated storage that must be freed with osa_adb_free_T.  Returns
OSA_ADB_NOENT if name does not exist, OSA_ADB_MEM if memory cannot be
allocated.

\begin{verbatim}
osa_adb_ret_t
osadb_adb_put_T(osa_adb_T_t db, osa_T_ent_t entry);
\end{verbatim}

Modifies the existing entry named in entry.  All fields must be filled
in.  Returns OSA_DB_NOENT if the named entry does not exist.  Note
that this cannot be used to rename an entry; rename is implemented by
deleting the old name and creating the new one (NOT ATOMIC!).

\begin{verbatim}
void osa_adb_free_T(osa_T_ent_t);
\end{verbatim}

Frees the memory associated with an osa_T_ent_t allocated by
osa_adb_get_T.

\begin{verbatim}
typedef osa_adb_ret_t (*osa_adb_iter_T_func)(void *data,
				    osa_T_ent_t entry);

osa_adb_ret_t osa_adb_iter_T(osa_adb_T_t db, osa_adb_iter_T_func func, 
		    void *data);
\end{verbatim}

Iterates over every entry in the database.  For each entry ent in the
database db, the function (*func)(data, ent) is called.  If func
returns an error code, osa_adb_iter_T returns an error code.  If all
invokations of func return OSA_ADB_OK, osa_adb_iter_T returns
OSA_ADB_OK.  The function func is permitted to access the database,
but the consequences of modifying the database during the iteration
are undefined.

\subsection{Kerberos Database}

Kerberos uses dbm to store krb5_db_entry records.  It can be accessed
and modified in parallel with the Kerberos server, using functions
that are defined inside the KDC and the libkdb.a.

\subsubsection{Database Manipulation Functions}

The following functions are declared in \v{lib/kdb/kdb_dbm.c} in the
Kerberos sources and are available in libkdb.a.  They can return the
following error codes; error codes that can be returned by any
function are indicated with a ``*'' and are not listed specifically
for each function.

\begin{description}
\item[* KRB5_KDB_NOTINITED] The database is not open; call
krb5_dbm_db_init.
\item[* KRB5_KDB_CANTLOCK_DB] The necessary lock cannot be acquired.  Try
again later.
\item[* system errors] An error occurred accessing the database files.
\item[KRB5_KDB_DB_INUSE] The database was modified without the use
of proper locking.\footnote{This error occurs when the entire database
is swapped out from the under the process, say by a kdb5_edit restore.
It can only be returned by krb5_db_get_principal.  It is not yet clear
what a program should do when it gets this error.}
\item[KRB5_KDB_NOENTRY] The principal to be deleted is not
in the database.
\end{description}

\begin{verbatim}
krb5_dbm_db_init(void)
\end{verbatim}

Opens the Kerberos database file (but does not actually call
dbm_open).  This can be called even if the database is already open,
in which case it just returns success.

\begin{verbatim}
krb5_dbm_db_fini(void)
\end{verbatim}

Closes the database file; this MUST be called before the process
exits.  Returns KRB5_KDB_DBNOTINITED if the database isn't open, but
that isn't really a fatal error.

\begin{verbatim}
krb5_dbm_get_principal(krb5_principal searchfor, 
        krb5_db_entry *entries, int *nentries, krb5_boolean *more)
\end{verbatim}

Search the database for the principal searchfor and write the results
into *entries.  The interface is set up to handle wildcard gets, but
the code doesn't handle it: *nentries is assumed to be 1, and *more is
always returned as 0.

This function does not retry if the database cannot be locked; that is
up to the caller.  

Returns KRB5_KDB_DB_INUSE.

\begin{verbatim}
krb5_dbm_put_principal(krb5_db_entry *entries, int *nentries)
\end{verbatim}

Stores *nentries elements from the entries array into the database.
On return *nentries is set to the number of entries actually written;
the first *nentries entries will have been written, even if an error
pis returned.

This function does not retry if the database cannot be locked; that is
up to the caller.

\begin{verbatim}
krb5_dbm_db_delete_principal(krb5_principal searchfor, int *nentries)
\end{verbatim}

Removes the principal searchfor from the database.  nentries will be
set to 0 or 1 on output, indicating the number of entries deleted (the
code does not currently support wildcards).

Returns KRB5_KDB_NOENTRY.

\begin{verbatim}
typedef krb5_error_code (*iter_func)(krb5_pointer, krb5_db_entry *);

krb5_dbm_db_iterate(iter_func func, krb5_point func_arg)
\end{verbatim}

Calls (*func)(func_arg, entry) for every entry in the database.  If
func returns an error code, the iteration stops and that error code is
returned.

Returns func error codes.

\begin{verbatim}
void krb5_dbm_db_free_principal(krb5_db_entry *entries, int nentries)
\end{verbatim}

Frees entries returned by krb5_dbm_db_get_principal.  nentries entries
in the array entries will be freed.

\subsubsection{Initialization and Key Access}

Keys stored in the Kerberos database are encrypted in the Kerberos
master key.  The admin server will therefore have to acquire the key
before it can perform any key-changing operations, and will have to
decrypt and encrypt the keys retrieved from and placed into the
database via krb5_db_get_principal and _put_principal.  This section
describes the internal admin server API that will be used to perform
these functions.

\begin{verbatim}
krb5_principal master_princ;
krb5_encrypt_block master_encblock;
krb5_keyblock master_keyblock;

void kdc_init_master()
\end{verbatim}

kdc_init_master opens the database and acquires the master key.  It
also sets the global variables master_princ, master_encblock, and
master_keyblock:

\begin{itemize}
\item master_princ is set to the name of the Kerberos master principal
(\v{K/M@REALM}).

\item master_encblock is something I have no idea about.

\item master_keyblock is the Kerberos master key
\end{itemize}

\begin{verbatim}
krb5_error_code kdb_get_entry_and_key(krb5_principal principal,
                                      krb5_db_entry *entry,
                                      krb5_keyblock *key)
\end{verbatim}

kdb_get_entry_and_key retrieves the named principal's entry from the
database in entry, and decrypts its key into key.  The caller must
free entry with krb5_dbm_db_free_principal and free key-$>$contents with
free.\footnote{The caller should also \v{memset(key-$>$contents, 0,
key-$>$length)}.  There should be a function krb5_free_keyblock_contents
for this, but there is not.}

\begin{verbatim}
krb5_error_code kdb_put_entry_pw(krb5_db_entry *entry, char *pw)
\end{verbatim}

kdb_put_entry_pw stores entry in the database.  All the entry values
must already be set; this function does not change any of them except
the key.  pw, the NULL-terminated password string, is converted to a
key using string-to-key with the salt type specified in
entry-$>$salt_type.\footnote{The salt_type should be set based on the
command line arguments to the kadmin server (see the ``Command Line''
section of the functional specification).}

\section{Admin Principal and Policy Database Implementation}

The admin principal and policy databases will each be stored in a
single hash table, implemented by the Berkeley 4.4BSD db library.
Each record will consist of an entire osa_T_ent_t.  The key into the
hash table is the entry name (for principals, the ASCII representation
of the name).  The value is the T entry structure.  Since the key and
data must be self-contained, with no pointers, the Sun xdr mechanisms
will be used to marshal and unmarshal data in the database.

The server in the first release will be single-threaded in that a
request will run to completion (or error) before the next will run,
but multiple connections will be allowed simultaneously.

\section{ACLs, acl_check}

The ACL mechanism described in the ``Authorization ACLs'' section of
the functional specifications will be implemented by the acl_check
function.

\begin{verbatim}
enum access_t {
        ACCESS_DENIED = 0,
        ACCESS_OK = 1,
};

enum access_t acl_check(krb5_principal princ, char *priv);
\end{verbatim}

The priv argument must be one of ``get'', ``add'', ``delete'', or
``modify''.  acl_check returns 1 if the principal princ has the named
privilege, 0 if it does not.

\section{Function Details}

This section discusses specific design issues for Admin API functions
that are not addresed by the functional specifications.

\subsection{ovsec_kadm_create_principal}

If the named principal exists in either the Kerberos or admin
principal database, but not both, return OVSEC_KADM_BAD_DB.

The principal's initial key is not stored in the key history array at
creation time.

\subsection{ovsec_kadm_delete_principal}

If the named principal exists in either the Kerberos or admin
principal database, but not both, return OVSEC_KADM_BAD_DB.

\subsection{ovsec_kadm_modify_principal}

If the named principal exists in either the Kerberos or admin
principal database, but not both, return OVSEC_KADM_BAD_DB.

If pw_history_num changes and the new value $n$ is smaller than the
current value of num_old_keys, old_keys should end up with the $n$
most recent keys; these are found by counting backwards $n$ elements
in old_keys from next_old_key.  next_old_keys should then be reset to
0, the oldest of the saved keys, and num_old_keys set to $n$, the
new actual number of old keys in the array.  

\subsection{ovsec_kadm_chpass_principal, randkey_principal}

The algorithm for determining whether a password is in the principal's
key history is complicated by the use of the kadmin/history \k{h}
encrypting key.  

\begin{enumerate}
\item For ovsec_kadm_chpass_principal, convert the password to a key
using string-to-key and the salt method specified by the command line
arguments.

\item If the POLICY bit is set and pw_history_num is not zero, check
if the new key is in the history.
\begin{enumerate}
\item Retrieve the principal's current key and decrypt it with \k{M}.
If it is the same as the new key, return OVSEC_KADM_PASS_REUSE.
\item Retrieve the kadmin/history key \k{h} and decrypt it with \k{M}.
\item Encrypt the principal's new key in \k{h}.
\item If the principal's new key encrypted in \k{h} is in old_keys,
return OVSEC_KADM_PASS_REUSE.
\item Encrypt the principal's current key in \k{h} and store it in
old_keys.
\item Erase the memory containing \k{h}.
\end{enumerate}

\item Encrypt the principal's new key in \k{M} and store it in the
database.
\item Erase the memory containing \k{M}.
\end{enumerate}

To store the an encrypted key in old_keys, insert it as the
next_old_key element of old_keys, and increment next_old_key by one
modulo pw_history_num.

\subsection{ovsec_kadm_get_principal}

If the named principal exists in either the Kerberos or admin
principal database, but not both, return OVSEC_KADM_BAD_DB.

\end{document}
