\documentstyle[12pt,fullpage,changebar,rcsid]{article} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Make _ actually generate an _, and allow line-breaking after it. \let\underscore=\_ \catcode`_=13 \def_{\underscore\penalty75\relax} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \rcs$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\thanks{\rcsId}} \author{Barry Jaspan} \begin{document} \sloppy \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 ``ovsec_adm/history'' that is created by ovsec_adm_create. Since a change in ovsec_adm/history's key renders every principal's key history array useless, it can only be changed using the ovsec_adm_edit utility; that program will reencrypt every principal's key history in the new key.\footnote{ovsec_adm_edit has not yet been implemented, and there are currently no plans to implement it.} The admin server refuses all requests to change ovsec_adm/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}