krb5 commit: Use memory replay cache for DO_TIME auth contexts

Greg Hudson <[email protected]>
Newsgroups gmane.comp.encryption.kerberos.cvs
Message-ID <[email protected]>
https://github.com/krb5/krb5/commit/0b1c78fcbdef574fb6835f0cd0a89388f4098cb5
commit 0b1c78fcbdef574fb6835f0cd0a89388f4098cb5
Author: Greg Hudson <[email protected]>
Date:   Thu Feb 28 21:28:43 2019 -0500

    Use memory replay cache for DO_TIME auth contexts
    
    Instead of requiring the caller to set up a persistent replay cache
    for KRB-PRIV/KRB-SAFE/KRB-CRED messages produced in DO_TIME auth
    contexts, use an in-memory replay cache.
    
    Update the API documentation for the affected functions and correct
    some inaccuracies.
    
    ticket: 8785 (new)

 .gitignore                    |    1 +
 src/include/k5-int.h          |    6 ++
 src/include/krb5/krb5.hin     |  138 ++++++++++++++++++++-------------
 src/lib/krb5/krb/auth_con.c   |    1 +
 src/lib/krb5/krb/auth_con.h   |    3 +
 src/lib/krb5/krb/int-proto.h  |   11 ++-
 src/lib/krb5/krb/mk_cred.c    |   28 +++++--
 src/lib/krb5/krb/mk_priv.c    |   21 +++--
 src/lib/krb5/krb/mk_safe.c    |   23 ++++--
 src/lib/krb5/krb/privsafe.c   |   40 +++++-----
 src/lib/krb5/krb/rd_cred.c    |   17 ++---
 src/lib/krb5/krb/rd_priv.c    |   22 +++--
 src/lib/krb5/krb/rd_safe.c    |   31 +++++---
 src/lib/krb5/rcache/rc_conv.c |   20 +++++
 src/tests/Makefile.in         |   14 ++-
 src/tests/replay.c            |  172 +++++++++++++++++++++++++++++++++++++++++
 src/tests/t_replay.py         |    6 ++
 17 files changed, 414 insertions(+), 140 deletions(-)

diff --git a/.gitignore b/.gitignore
index 8117674..f39dd6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -438,6 +438,7 @@ local.properties
 /src/tests/localauth
 /src/tests/plugorder
 /src/tests/rdreq
+/src/tests/replay
 /src/tests/responder
 /src/tests/s2p
 /src/tests/s4u2proxy
diff --git a/src/include/k5-int.h b/src/include/k5-int.h
index 7d3c129..71dce73 100644
--- a/src/include/k5-int.h
+++ b/src/include/k5-int.h
@@ -1989,6 +1989,12 @@ krb5_error_code krb5_auth_to_rep(krb5_context, krb5_tkt_authent *,
 krb5_error_code krb5_rc_hash_message(krb5_context context,
                                      const krb5_data *message, char **out);
 
+/* Set *tag_out to the integrity tag of *enc.  (Does not allocate memory;
+ * returned buffer is a subrange of *ctext.) */
+krb5_error_code
+k5_rc_tag_from_ciphertext(krb5_context context, const krb5_enc_data *enc,
+                          krb5_data *tag_out);
+
 krb5_error_code KRB5_CALLCONV
 krb5_rc_initialize(krb5_context, krb5_rcache, krb5_deltat);
 
diff --git a/src/include/krb5/krb5.hin b/src/include/krb5/krb5.hin
index a67eec1..eb31607 100644
--- a/src/include/krb5/krb5.hin
+++ b/src/include/krb5/krb5.hin
@@ -3316,25 +3316,24 @@ krb5_rd_error(krb5_context context, const krb5_data *enc_errbuf,
  * This function parses a @c KRB-SAFE message, verifies its integrity, and
  * stores its data into @a userdata_out.
  *
- * @note The @a rdata_out argument is required if #KRB5_AUTH_CONTEXT_RET_TIME
- * or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set in the @a auth_context.
+ * @note The @a rdata_out argument is required if the
+ * #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set
+ * in @a auth_context.
  *
- * @note @a auth_context must have a remote address set.  This address will be
- * used to verify the sender address in the KRB-SAFE message.  If @a
- * auth_context has a local address set, it will be used to verify the receiver
- * address in the KRB-SAFE message if the message contains one.  Both addresses
- * must use type @c ADDRTYPE_ADDRPORT.
+ * If @a auth_context has a remote address set, the address will be used to
+ * verify the sender address in the KRB-SAFE message.  If @a auth_context has a
+ * local address set, it will be used to verify the receiver address in the
+ * KRB-SAFE message if the message contains one.
  *
  * If the #KRB5_AUTH_CONTEXT_DO_SEQUENCE flag is set in @a auth_context, the
  * sequence number of the KRB-SAFE message is checked against the remote
  * sequence number field of @a auth_context.  Otherwise, the sequence number is
  * not used.
  *
- * If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context,
- * then two additional checks are performed:
- * @li The timestamp in the message must be within the permitted clock skew
- *     (which is usually five minutes).
- * @li The message must not be a replayed message field in @a auth_context.
+ * If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context, then the
+ * timestamp in the message is verified to be within the permitted clock skew
+ * of the current time, and the message is checked against an in-memory replay
+ * cache to detect reflections or replays.
  *
  * Use krb5_free_data_contents() to free @a userdata_out when it is no longer
  * needed.
@@ -3358,25 +3357,24 @@ krb5_rd_safe(krb5_context context, krb5_auth_context auth_context,
  * This function parses a @c KRB-PRIV message, verifies its integrity, and
  * stores its unencrypted data into @a userdata_out.
  *
- * @note If the #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE
- * flag is set in @a auth_context, @a rdata_out is required.
+ * @note The @a rdata_out argument is required if the
+ * #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set
+ * in @a auth_context.
  *
- * @note @a auth_context must have a remote address set.  This address will be
- * used to verify the sender address in the KRB-PRIV message.  If @a
- * auth_context has a local address set, it will be used to verify the receiver
- * address in the KRB-PRIV message if the message contains one.  Both addresses
- * must use type @c ADDRTYPE_ADDRPORT.
+ * If @a auth_context has a remote address set, the address will be used to
+ * verify the sender address in the KRB-PRIV message.  If @a auth_context has a
+ * local address set, it will be used to verify the receiver address in the
+ * KRB-PRIV message if the message contains one.
  *
  * If the #KRB5_AUTH_CONTEXT_DO_SEQUENCE flag is set in @a auth_context, the
- * sequence number of the KRB-SAFE message is checked against the remote
+ * sequence number of the KRB-PRIV message is checked against the remote
  * sequence number field of @a auth_context.  Otherwise, the sequence number is
  * not used.
  *
- * If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context,
- * then two additional checks are performed:
- * @li The timestamp in the message must be within the permitted clock skew
- *     (which is usually five minutes).
- * @li The message must not be a replayed message field in @a auth_context.
+ * If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context, then the
+ * timestamp in the message is verified to be within the permitted clock skew
+ * of the current time, and the message is checked against an in-memory replay
+ * cache to detect reflections or replays.
  *
  * Use krb5_free_data_contents() to free @a userdata_out when it is no longer
  * needed.
@@ -5263,18 +5261,22 @@ krb5_kt_read_service_key(krb5_context context, krb5_pointer keyprocarg,
  * optional; if specified, it will be used to form the receiver address used in
  * the message.
  *
- * If #KRB5_AUTH_CONTEXT_DO_TIME flag is set in the @a auth_context, an entry
- * describing the message is entered in the replay cache @a
- * auth_context->rcache which enables the caller to detect if this message is
- * reflected by an attacker.  If #KRB5_AUTH_CONTEXT_DO_TIME is not set, the
- * replay cache is not used.
+ * @note The @a rdata_out argument is required if the
+ * #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set
+ * in @a auth_context.
  *
- * If either #KRB5_AUTH_CONTEXT_DO_SEQUENCE or
- * #KRB5_AUTH_CONTEXT_RET_SEQUENCE is set, the @a auth_context local sequence
- * number will be placed in @a rdata_out as its sequence number.
+ * If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context, a
+ * timestamp is included in the KRB-SAFE message, and an entry for the message
+ * is entered in an in-memory replay cache to detect if the message is
+ * reflected by an attacker.  If #KRB5_AUTH_CONTEXT_DO_TIME is not set, no
+ * replay cache is used.  If #KRB5_AUTH_CONTEXT_RET_TIME is set in @a
+ * auth_context, a timestamp is included in the KRB-SAFE message and is stored
+ * in @a rdata_out.
  *
- * @note The @a rdata_out argument is required if #KRB5_AUTH_CONTEXT_RET_TIME
- * or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set in the @a auth_context.
+ * If either #KRB5_AUTH_CONTEXT_DO_SEQUENCE or #KRB5_AUTH_CONTEXT_RET_SEQUENCE
+ * is set, the @a auth_context local sequence number is included in the
+ * KRB-SAFE message and then incremented.  If #KRB5_AUTH_CONTEXT_RET_SEQUENCE
+ * is set, the sequence number used is stored in @a rdata_out.
  *
  * Use krb5_free_data_contents() to free @a der_out when it is no longer
  * needed.
@@ -5293,30 +5295,35 @@ krb5_mk_safe(krb5_context context, krb5_auth_context auth_context,
  * @param [in]  auth_context    Authentication context
  * @param [in]  userdata        User data for @c KRB-PRIV message
  * @param [out] der_out         Formatted @c KRB-PRIV message
- * @param [out] rdata_out       Replay cache handle (NULL if not needed)
+ * @param [out] rdata_out       Replay data (NULL if not needed)
  *
  * This function is similar to krb5_mk_safe(), but the message is encrypted and
  * integrity-protected, not just integrity-protected.
  *
  * The local address in @a auth_context must be set, and is used to form the
- * sender address used in the KRB-SAFE message.  The remote address is
+ * sender address used in the KRB-PRIV message.  The remote address is
  * optional; if specified, it will be used to form the receiver address used in
  * the message.
  *
- * @note If the #KRB5_AUTH_CONTEXT_RET_TIME or
- * #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set in @a auth_context, the @a
- * rdata_out is required.
+ * @note The @a rdata_out argument is required if the
+ * #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set
+ * in @a auth_context.
  *
- * @note The flags from @a auth_context specify whether sequence numbers or
- * timestamps will be used to identify the message.  Valid values are:
+ * If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context, a
+ * timestamp is included in the KRB-PRIV message, and an entry for the message
+ * is entered in an in-memory replay cache to detect if the message is
+ * reflected by an attacker.  If #KRB5_AUTH_CONTEXT_DO_TIME is not set, no
+ * replay cache is used.  If #KRB5_AUTH_CONTEXT_RET_TIME is set in @a
+ * auth_context, a timestamp is included in the KRB-PRIV message and is stored
+ * in @a rdata_out.
  *
- * @li #KRB5_AUTH_CONTEXT_DO_TIME      - Use timestamps in @a outdata
- * @li #KRB5_AUTH_CONTEXT_RET_TIME     - Copy timestamp to @a outdata.
- * @li #KRB5_AUTH_CONTEXT_DO_SEQUENCE  - Use local sequence numbers from
- *                                       @a auth_context in replay cache.
- * @li #KRB5_AUTH_CONTEXT_RET_SEQUENCE - Use local sequence numbers from
- *                                       @a auth_context as a sequence number
- *                                       in the encrypted message @a der_out.
+ * If either #KRB5_AUTH_CONTEXT_DO_SEQUENCE or #KRB5_AUTH_CONTEXT_RET_SEQUENCE
+ * is set, the @a auth_context local sequence number is included in the
+ * KRB-PRIV message and then incremented.  If #KRB5_AUTH_CONTEXT_RET_SEQUENCE
+ * is set, the sequence number used is stored in @a rdata_out.
+ *
+ * Use krb5_free_data_contents() to free @a der_out when it is no longer
+ * needed.
  *
  * @retval 0 Success; otherwise - Kerberos error codes
  */
@@ -5448,11 +5455,33 @@ krb5_recvauth_version(krb5_context context,
  * This function takes an array of credentials @a creds and formats
  * a @c KRB-CRED message @a der_out to pass to krb5_rd_cred().
  *
- * @note If the #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE
- * flag is set in @a auth_context, @a rdata_out is required.
+ * The local and remote addresses in @a auth_context are optional; if either is
+ * specified, they are used to form the sender and receiver addresses in the
+ * KRB-CRED message.
+ *
+ * @note The @a rdata_out argument is required if the
+ * #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set
+ * in @a auth_context.
+ *
+ * If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context, an entry
+ * for the message is entered in an in-memory replay cache to detect if the
+ * message is reflected by an attacker.  If #KRB5_AUTH_CONTEXT_DO_TIME is not
+ * set, no replay cache is used.  If #KRB5_AUTH_CONTEXT_RET_TIME is set in @a
+ * auth_context, the timestamp used for the KRB-CRED message is stored in @a
+ * rdata_out.
+ *
+ * If either #KRB5_AUTH_CONTEXT_DO_SEQUENCE or #KRB5_AUTH_CONTEXT_RET_SEQUENCE
+ * is set, the @a auth_context local sequence number is included in the
+ * KRB-CRED message and then incremented.  If #KRB5_AUTH_CONTEXT_RET_SEQUENCE
+ * is set, the sequence number used is stored in @a rdata_out.
+ *
+ * Use krb5_free_data_contents() to free @a der_out when it is no longer
+ * needed.
  *
  * The message will be encrypted using the send subkey of @a auth_context if it
- * is present, or the session key otherwise.
+ * is present, or the session key otherwise.  If neither key is present, the
+ * credentials will not be encrypted, and the message should only be sent over
+ * a secure channel.  No replay cache entry is used in this case.
  *
  * @retval
  *  0 Success
@@ -5503,8 +5532,9 @@ krb5_mk_1cred(krb5_context context, krb5_auth_context auth_context,
  * @param [out] creds_out       Null-terminated array of forwarded credentials
  * @param [out] rdata_out       Replay data (NULL if not needed)
  *
- * @note The @a rdata_out argument is required if #KRB5_AUTH_CONTEXT_RET_TIME
- * or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set in the @a auth_context.`
+ * @note The @a rdata_out argument is required if the
+ * #KRB5_AUTH_CONTEXT_RET_TIME or #KRB5_AUTH_CONTEXT_RET_SEQUENCE flag is set
+ * in @a auth_context.`
  *
  * @a creddata will be decrypted using the receiving subkey if it is present in
  * @a auth_context, or the session key if the receiving subkey is not present
diff --git a/src/lib/krb5/krb/auth_con.c b/src/lib/krb5/krb/auth_con.c
index c86a4af..a8a97eb 100644
--- a/src/lib/krb5/krb/auth_con.c
+++ b/src/lib/krb5/krb/auth_con.c
@@ -78,6 +78,7 @@ krb5_auth_con_free(krb5_context context, krb5_auth_context auth_context)
     if (auth_context->ad_context)
         krb5_authdata_context_free(context, auth_context->ad_context);
     free(auth_context);
+    k5_memrcache_free(context, auth_context->memrcache);
     return 0;
 }
 
diff --git a/src/lib/krb5/krb/auth_con.h b/src/lib/krb5/krb/auth_con.h
index 821b41e..a010ae4 100644
--- a/src/lib/krb5/krb/auth_con.h
+++ b/src/lib/krb5/krb/auth_con.h
@@ -3,6 +3,8 @@
 #ifndef KRB5_AUTH_CONTEXT
 #define KRB5_AUTH_CONTEXT
 
+#include "../rcache/memrcache.h"
+
 struct _krb5_auth_context {
     krb5_magic          magic;
     krb5_address      * remote_addr;
@@ -21,6 +23,7 @@ struct _krb5_auth_context {
     krb5_cksumtype      safe_cksumtype;         /* mk_safe, ... */
     krb5_data           cstate;                 /* mk_priv, rd_priv only */
     krb5_rcache         rcache;
+    k5_memrcache        memrcache;
     krb5_enctype      * permitted_etypes;       /* rd_req */
     krb5_mk_req_checksum_func checksum_func;
     void *checksum_func_data;
diff --git a/src/lib/krb5/krb/int-proto.h b/src/lib/krb5/krb/int-proto.h
index 3e80241..d45adab 100644
--- a/src/lib/krb5/krb/int-proto.h
+++ b/src/lib/krb5/krb/int-proto.h
@@ -159,13 +159,16 @@ k5_privsafe_gen_addrs(krb5_context context, krb5_auth_context authcon,
                       krb5_address *lstorage, krb5_address *rstorage,
                       krb5_address **local_out, krb5_address **remote_out);
 
-/* If the DO_TIME flag is set in authcon, store a replay record.  If check_time
- * is true, also check that the timestamp is within clock skew. */
+/*
+ * If the DO_TIME flag is set in authcon, store a replay record in a memory
+ * replay cache (initializing one if necessary).  Either enc or cksum must be
+ * non-null.  If rdata is not null, also check that its timestamp is within
+ * clock skew.
+ */
 krb5_error_code
 k5_privsafe_check_replay(krb5_context context, krb5_auth_context authcon,
-                         krb5_address *addr, const char *uniq,
                          const krb5_replay_data *rdata,
-                         krb5_boolean check_time);
+                         const krb5_enc_data *enc, const krb5_checksum *cksum);
 
 krb5_boolean
 k5_privsafe_check_seqnum(krb5_context ctx, krb5_auth_context ac,
diff --git a/src/lib/krb5/krb/mk_cred.c b/src/lib/krb5/krb/mk_cred.c
index 003a0fd..62224f1 100644
--- a/src/lib/krb5/krb/mk_cred.c
+++ b/src/lib/krb5/krb/mk_cred.c
@@ -66,14 +66,16 @@ encrypt_credencpart(krb5_context context, krb5_cred_enc_part *encpart,
 
 /*
  * Marshal a KRB-CRED message into der_out, encrypted with key (or unencrypted
- * if key is NULL).  Use the timestamp and sequence number from rdata and the
- * addresses from local_addr and remote_addr (either of which may be NULL).
- * der_out should be freed by the caller when finished.
+ * if key is NULL).  Store the ciphertext in enc_out.  Use the timestamp and
+ * sequence number from rdata and the addresses from local_addr and remote_addr
+ * (either of which may be NULL).  der_out and enc_out should be freed by the
+ * caller when finished.
  */
 static krb5_error_code
 create_krbcred(krb5_context context, krb5_creds **creds, krb5_key key,
                const krb5_replay_data *rdata, krb5_address *local_addr,
-               krb5_address *remote_addr, krb5_data **der_out)
+               krb5_address *remote_addr, krb5_data **der_out,
+               krb5_enc_data *enc_out)
 {
     krb5_error_code ret;
     krb5_cred_enc_part credenc;
@@ -84,6 +86,7 @@ create_krbcred(krb5_context context, krb5_creds **creds, krb5_key key,
     size_t i, ncreds;
 
     *der_out = NULL;
+    memset(enc_out, 0, sizeof(*enc_out));
     memset(&enc, 0, sizeof(enc));
 
     for (ncreds = 0; creds[ncreds] != NULL; ncreds++);
@@ -137,6 +140,9 @@ create_krbcred(krb5_context context, krb5_creds **creds, krb5_key key,
     if (ret)
         goto cleanup;
 
+    *enc_out = enc;
+    memset(&enc, 0, sizeof(enc));
+
 cleanup:
     krb5_free_tickets(context, tickets);
     krb5_free_data_contents(context, &enc.ciphertext);
@@ -154,9 +160,11 @@ krb5_mk_ncred(krb5_context context, krb5_auth_context authcon,
     krb5_key key;
     krb5_replay_data rdata;
     krb5_data *der_krbcred = NULL;
+    krb5_enc_data enc;
     krb5_address *local_addr, *remote_addr, lstorage, rstorage;
 
     *der_out = NULL;
+    memset(&enc, 0, sizeof(enc));
     memset(&lstorage, 0, sizeof(lstorage));
     memset(&rstorage, 0, sizeof(rstorage));
 
@@ -180,14 +188,15 @@ krb5_mk_ncred(krb5_context context, krb5_auth_context authcon,
 
     key = (authcon->send_subkey != NULL) ? authcon->send_subkey : authcon->key;
     ret = create_krbcred(context, creds, key, &rdata, local_addr, remote_addr,
-                         &der_krbcred);
+                         &der_krbcred, &enc);
     if (ret)
         goto cleanup;
 
-    ret = k5_privsafe_check_replay(context, authcon, authcon->local_addr,
-                                   "_forw", &rdata, FALSE);
-    if (ret)
-        goto cleanup;
+    if (key != NULL) {
+        ret = k5_privsafe_check_replay(context, authcon, NULL, &enc, NULL);
+        if (ret)
+            goto cleanup;
+    }
 
     *der_out = der_krbcred;
     der_krbcred = NULL;
@@ -196,6 +205,7 @@ krb5_mk_ncred(krb5_context context, krb5_auth_context authcon,
         authcon->local_seq_number++;
 
 cleanup:
+    krb5_free_data_contents(context, &enc.ciphertext);
     free(lstorage.contents);
     free(rstorage.contents);
     if (der_krbcred != NULL) {
diff --git a/src/lib/krb5/krb/mk_priv.c b/src/lib/krb5/krb/mk_priv.c
index d66ab8a..d023169 100644
--- a/src/lib/krb5/krb/mk_priv.c
+++ b/src/lib/krb5/krb/mk_priv.c
@@ -35,16 +35,16 @@
 #include "auth_con.h"
 
 /*
- * Marshal a KRB-PRIV message into der_out, encrypted with key.  Use the
- * timestamp and sequence number from rdata and the addresses from local_addr
- * and remote_addr (the second of which may be NULL).  der_out should be freed
- * by the caller when finished.
+ * Marshal a KRB-PRIV message into der_out, encrypted with key.  Store the
+ * ciphertext in enc_out.  Use the timestamp and sequence number from rdata and
+ * the addresses from local_addr and remote_addr (the second of which may be
+ * NULL).  der_out and enc_out should be freed by the caller when finished.
  */
 static krb5_error_code
 create_krbpriv(krb5_context context, const krb5_data *userdata,
                krb5_key key, const krb5_replay_data *rdata,
                krb5_address *local_addr, krb5_address *remote_addr,
-               krb5_data *cstate, krb5_data *der_out)
+               krb5_data *cstate, krb5_data *der_out, krb5_enc_data *enc_out)
 {
     krb5_enctype enctype = krb5_k_key_enctype(context, key);
     krb5_error_code ret;
@@ -91,6 +91,9 @@ create_krbpriv(krb5_context context, const krb5_data *userdata,
     *der_out = *der_krbpriv;
     free(der_krbpriv);
 
+    *enc_out = privmsg.enc_part;
+    memset(&privmsg.enc_part, 0, sizeof(privmsg.enc_part));
+
 cleanup:
     zapfree(privmsg.enc_part.ciphertext.data,
             privmsg.enc_part.ciphertext.length);
@@ -111,9 +114,11 @@ krb5_mk_priv(krb5_context context, krb5_auth_context authcon,
     krb5_key key;
     krb5_replay_data rdata;
     krb5_data der_krbpriv = empty_data();
+    krb5_enc_data enc;
     krb5_address *local_addr, *remote_addr, lstorage, rstorage;
 
     *der_out = empty_data();
+    memset(&enc, 0, sizeof(enc));
     memset(&lstorage, 0, sizeof(lstorage));
     memset(&rstorage, 0, sizeof(rstorage));
     if (!authcon->local_addr)
@@ -130,12 +135,11 @@ krb5_mk_priv(krb5_context context, krb5_auth_context authcon,
 
     key = (authcon->send_subkey != NULL) ? authcon->send_subkey : authcon->key;
     ret = create_krbpriv(context, userdata, key, &rdata, local_addr,
-                         remote_addr, &authcon->cstate, &der_krbpriv);
+                         remote_addr, &authcon->cstate, &der_krbpriv, &enc);
     if (ret)
         goto cleanup;
 
-    ret = k5_privsafe_check_replay(context, authcon, authcon->local_addr,
-                                   "_priv", &rdata, FALSE);
+    ret = k5_privsafe_check_replay(context, authcon, NULL, &enc, NULL);
     if (ret)
         goto cleanup;
 
@@ -147,6 +151,7 @@ krb5_mk_priv(krb5_context context, krb5_auth_context authcon,
 
 cleanup:
     krb5_free_data_contents(context, &der_krbpriv);
+    zapfree(enc.ciphertext.data, enc.ciphertext.length);
     free(lstorage.contents);
     free(rstorage.contents);
     return ret;
diff --git a/src/lib/krb5/krb/mk_safe.c b/src/lib/krb5/krb/mk_safe.c
index 02ee725..8a10616 100644
--- a/src/lib/krb5/krb/mk_safe.c
+++ b/src/lib/krb5/krb/mk_safe.c
@@ -36,15 +36,16 @@
 
 /*
  * Marshal a KRB-SAFE message into der_out, with a keyed checksum of type
- * sumtype.  Use the timestamp and sequence number from rdata and the addresses
- * from local_addr and remote_addr (the second of which may be NULL).  der_out
- * should be freed by the caller when finished.
+ * sumtype.  Store the checksum in cksum_out.  Use the timestamp and sequence
+ * number from rdata and the addresses from local_addr and remote_addr (the
+ * second of which may be NULL).  der_out and cksum_out should be freed by the
+ * caller when finished.
  */
 static krb5_error_code
 create_krbsafe(krb5_context context, const krb5_data *userdata, krb5_key key,
                const krb5_replay_data *rdata, krb5_address *local_addr,
                krb5_address *remote_addr, krb5_cksumtype sumtype,
-               krb5_data *der_out)
+               krb5_data *der_out, krb5_checksum *cksum_out)
 {
     krb5_error_code ret;
     krb5_safe safemsg;
@@ -85,12 +86,14 @@ create_krbsafe(krb5_context context, const krb5_data *userdata, krb5_key key,
     /* Encode the message again with the real checksum. */
     safemsg.checksum = &safe_checksum;
     ret = encode_krb5_safe(&safemsg, &der_krbsafe);
-    krb5_free_checksum_contents(context, &safe_checksum);
-    if (ret)
+    if (ret) {
+        krb5_free_checksum_contents(context, &safe_checksum);
         return ret;
+    }
 
     *der_out = *der_krbsafe;
     free(der_krbsafe);
+    *cksum_out = safe_checksum;
     return 0;
 }
 
@@ -126,10 +129,12 @@ krb5_mk_safe(krb5_context context, krb5_auth_context authcon,
     krb5_key key;
     krb5_replay_data rdata;
     krb5_data der_krbsafe = empty_data();
+    krb5_checksum cksum;
     krb5_address *local_addr, *remote_addr, lstorage, rstorage;
     krb5_cksumtype sumtype;
 
     *der_out = empty_data();
+    memset(&cksum, 0, sizeof(cksum));
     memset(&lstorage, 0, sizeof(lstorage));
     memset(&rstorage, 0, sizeof(rstorage));
     if (authcon->local_addr == NULL)
@@ -147,12 +152,11 @@ krb5_mk_safe(krb5_context context, krb5_auth_context authcon,
     key = (authcon->send_subkey != NULL) ? authcon->send_subkey : authcon->key;
     sumtype = safe_cksumtype(context, authcon, key->keyblock.enctype);
     ret = create_krbsafe(context, userdata, key, &rdata, local_addr,
-                         remote_addr, sumtype, &der_krbsafe);
+                         remote_addr, sumtype, &der_krbsafe, &cksum);
     if (ret)
         goto cleanup;
 
-    ret = k5_privsafe_check_replay(context, authcon, authcon->local_addr,
-                                   "_safe", &rdata, FALSE);
+    ret = k5_privsafe_check_replay(context, authcon, NULL, NULL, &cksum);
     if (ret)
         goto cleanup;
 
@@ -164,6 +168,7 @@ krb5_mk_safe(krb5_context context, krb5_auth_context authcon,
 
 cleanup:
     krb5_free_data_contents(context, &der_krbsafe);
+    krb5_free_checksum_contents(context, &cksum);
     free(lstorage.contents);
     free(rstorage.contents);
     return ret;
diff --git a/src/lib/krb5/krb/privsafe.c b/src/lib/krb5/krb/privsafe.c
index a1e5230..461e11f 100644
--- a/src/lib/krb5/krb/privsafe.c
+++ b/src/lib/krb5/krb/privsafe.c
@@ -106,38 +106,38 @@ k5_privsafe_gen_addrs(krb5_context context, krb5_auth_context authcon,
 
 krb5_error_code
 k5_privsafe_check_replay(krb5_context context, krb5_auth_context authcon,
-                         krb5_address *addr, const char *uniq,
                          const krb5_replay_data *rdata,
-                         krb5_boolean check_time)
+                         const krb5_enc_data *enc, const krb5_checksum *cksum)
 {
     krb5_error_code ret;
-    krb5_donot_replay replay;
-    char *client = NULL;
+    krb5_data tag;
+
+    assert(enc != NULL || cksum != NULL);
 
     if (!(authcon->auth_context_flags & KRB5_AUTH_CONTEXT_DO_TIME))
         return 0;
 
-    if (authcon->rcache == NULL)
-        return KRB5_RC_REQUIRED;
-
-    if (check_time) {
+    if (rdata != NULL) {
         ret = krb5_check_clockskew(context, rdata->timestamp);
         if (ret)
             return ret;
     }
 
-    ret = krb5_gen_replay_name(context, addr, uniq, &client);
-    if (ret)
-        return ret;
-
-    replay.client = client;
-    replay.server = "";
-    replay.msghash = NULL;
-    replay.cusec = rdata->usec;
-    replay.ctime = rdata->timestamp;
-    ret = krb5_rc_store(context, authcon->rcache, &replay);
-    free(replay.client);
-    return ret;
+    if (enc != NULL) {
+        ret = k5_rc_tag_from_ciphertext(context, enc, &tag);
+        if (ret)
+            return ret;
+    } else {
+        tag = make_data(cksum->contents, cksum->length);
+    }
+
+    if (authcon->memrcache == NULL) {
+        ret = k5_memrcache_create(context, &authcon->memrcache);
+        if (ret)
+            return ret;
+    }
+
+    return k5_memrcache_store(context, authcon->memrcache, &tag);
 }
 
 /*
diff --git a/src/lib/krb5/krb/rd_cred.c b/src/lib/krb5/krb/rd_cred.c
index 7fdd103..720147c 100644
--- a/src/lib/krb5/krb/rd_cred.c
+++ b/src/lib/krb5/krb/rd_cred.c
@@ -158,9 +158,6 @@ krb5_rd_cred(krb5_context context, krb5_auth_context authcon,
         replaydata_out == NULL)
         return KRB5_RC_REQUIRED;
 
-    if ((flags & KRB5_AUTH_CONTEXT_DO_TIME) && authcon->rcache == NULL)
-        return KRB5_RC_REQUIRED;
-
     ret = decode_krb5_cred(creddata, &krbcred);
     if (ret)
         goto cleanup;
@@ -173,13 +170,13 @@ krb5_rd_cred(krb5_context context, krb5_auth_context authcon,
     if (ret)
         goto cleanup;
 
-    rdata.timestamp = encpart->timestamp;
-    rdata.usec = encpart->usec;
-    rdata.seq = encpart->nonce;
-    ret = k5_privsafe_check_replay(context, authcon, authcon->remote_addr,
-                                   "_forw", &rdata, TRUE);
-    if (ret)
-        goto cleanup;
+    if (authcon->recv_subkey != NULL || authcon->key != NULL) {
+        rdata.timestamp = encpart->timestamp;
+        ret = k5_privsafe_check_replay(context, authcon, &rdata,
+                                       &krbcred->enc_part, NULL);
+        if (ret)
+            goto cleanup;
+    }
 
     if (flags & KRB5_AUTH_CONTEXT_DO_SEQUENCE) {
         if (authcon->remote_seq_number != (uint32_t)encpart->nonce) {
diff --git a/src/lib/krb5/krb/rd_priv.c b/src/lib/krb5/krb/rd_priv.c
index 34eb656..29a9746 100644
--- a/src/lib/krb5/krb/rd_priv.c
+++ b/src/lib/krb5/krb/rd_priv.c
@@ -36,13 +36,15 @@
 
 /*
  * Unmarshal a KRB-PRIV message from der_krbpriv, placing the confidential user
- * data in *userdata_out and replay data in *rdata_out.  The caller should free
- * *userdata_out when finished.
+ * data in *userdata_out, ciphertext in *enc_out, and replay data in
+ * *rdata_out.  The caller should free *userdata_out and *enc_out when
+ * finished.
  */
 static krb5_error_code
 read_krbpriv(krb5_context context, krb5_auth_context authcon,
              const krb5_data *der_krbpriv, const krb5_key key,
-             krb5_replay_data *rdata_out, krb5_data *userdata_out)
+             krb5_replay_data *rdata_out, krb5_data *userdata_out,
+             krb5_enc_data *enc_out)
 {
     krb5_error_code ret;
     krb5_priv *privmsg = NULL;
@@ -84,6 +86,9 @@ read_krbpriv(krb5_context context, krb5_auth_context authcon,
     *userdata_out = encpart->user_data;
     encpart->user_data.data = NULL;
 
+    *enc_out = privmsg->enc_part;
+    memset(&privmsg->enc_part, 0, sizeof(privmsg->enc_part));
+
 cleanup:
     krb5_free_priv_enc_part(context, encpart);
     krb5_free_priv(context, privmsg);
@@ -99,26 +104,24 @@ krb5_rd_priv(krb5_context context, krb5_auth_context authcon,
     krb5_error_code ret;
     krb5_key key;
     krb5_replay_data rdata;
+    krb5_enc_data enc;
     krb5_data userdata = empty_data();
     const krb5_int32 flags = authcon->auth_context_flags;
 
     *userdata_out = empty_data();
+    memset(&enc, 0, sizeof(enc));
 
     if (((flags & KRB5_AUTH_CONTEXT_RET_TIME) ||
          (flags & KRB5_AUTH_CONTEXT_RET_SEQUENCE)) && rdata_out == NULL)
         return KRB5_RC_REQUIRED;
 
-    if ((flags & KRB5_AUTH_CONTEXT_DO_TIME) && authcon->remote_addr == NULL)
-        return KRB5_REMOTE_ADDR_REQUIRED;
-
     key = (authcon->recv_subkey != NULL) ? authcon->recv_subkey : authcon->key;
     memset(&rdata, 0, sizeof(rdata));
-    ret = read_krbpriv(context, authcon, inbuf, key, &rdata, &userdata);
+    ret = read_krbpriv(context, authcon, inbuf, key, &rdata, &userdata, &enc);
     if (ret)
         goto cleanup;
 
-    ret = k5_privsafe_check_replay(context, authcon, authcon->remote_addr,
-                                   "_priv", &rdata, TRUE);
+    ret = k5_privsafe_check_replay(context, authcon, &rdata, &enc, NULL);
     if (ret)
         goto cleanup;
 
@@ -141,6 +144,7 @@ krb5_rd_priv(krb5_context context, krb5_auth_context authcon,
     userdata = empty_data();
 
 cleanup:
+    krb5_free_data_contents(context, &enc.ciphertext);
     krb5_free_data_contents(context, &userdata);
     return ret;
 }
diff --git a/src/lib/krb5/krb/rd_safe.c b/src/lib/krb5/krb/rd_safe.c
index eab7f6d..17f2b27 100644
--- a/src/lib/krb5/krb/rd_safe.c
+++ b/src/lib/krb5/krb/rd_safe.c
@@ -36,23 +36,26 @@
 
 /*
  * Unmarshal a KRB-SAFE message from der_krbsafe, placing the
- * integrity-protected user data in *userdata_out and replay data in
- * *rdata_out.  The caller should free *userdata_out when finished.
+ * integrity-protected user data in *userdata_out, replay data in *rdata_out,
+ * and checksum in *cksum_out.  The caller should free *userdata_out and
+ * *cksum_out when finished.
  */
 static krb5_error_code
 read_krbsafe(krb5_context context, krb5_auth_context ac,
              const krb5_data *der_krbsafe, krb5_key key,
-             krb5_replay_data *rdata_out, krb5_data *userdata_out)
+             krb5_replay_data *rdata_out, krb5_data *userdata_out,
+             krb5_checksum **cksum_out)
 {
     krb5_error_code ret;
     krb5_safe *krbsafe;
     krb5_data *safe_body = NULL, *der_zerosafe = NULL;
-    krb5_checksum zero_cksum, *safe_cksum;
+    krb5_checksum zero_cksum, *safe_cksum = NULL;
     krb5_octet zero_octet = 0;
     krb5_boolean valid;
     struct krb5_safe_with_body swb;
 
     *userdata_out = empty_data();
+    *cksum_out = NULL;
     if (!krb5_is_krb_safe(der_krbsafe))
         return KRB5KRB_AP_ERR_MSG_TYPE;
 
@@ -75,7 +78,8 @@ read_krbsafe(krb5_context context, krb5_auth_context ac,
     if (ret)
         goto cleanup;
 
-    /* Regenerate the KRB-SAFE message without the checksum. */
+    /* Regenerate the KRB-SAFE message without the checksum.  Save the message
+     * checksum to verify. */
     safe_cksum = krbsafe->checksum;
     zero_cksum.length = 0;
     zero_cksum.checksum_type = 0;
@@ -84,7 +88,7 @@ read_krbsafe(krb5_context context, krb5_auth_context ac,
     swb.body = safe_body;
     swb.safe = krbsafe;
     ret = encode_krb5_safe_with_body(&swb, &der_zerosafe);
-    krbsafe->checksum = safe_cksum;
+    krbsafe->checksum = NULL;
     if (ret)
         goto cleanup;
 
@@ -109,6 +113,9 @@ read_krbsafe(krb5_context context, krb5_auth_context ac,
     *userdata_out = krbsafe->user_data;
     krbsafe->user_data.data = NULL;
 
+    *cksum_out = safe_cksum;
+    safe_cksum = NULL;
+
 cleanup:
     if (der_zerosafe != NULL) {
         zap(der_zerosafe->data, der_zerosafe->length);
@@ -116,6 +123,7 @@ cleanup:
     }
     krb5_free_data(context, safe_body);
     krb5_free_safe(context, krbsafe);
+    krb5_free_checksum(context, safe_cksum);
     return ret;
 }
 
@@ -128,6 +136,7 @@ krb5_rd_safe(krb5_context context, krb5_auth_context authcon,
     krb5_key key;
     krb5_replay_data rdata;
     krb5_data userdata = empty_data();
+    krb5_checksum *cksum;
     const krb5_int32 flags = authcon->auth_context_flags;
 
     *userdata_out = empty_data();
@@ -136,17 +145,14 @@ krb5_rd_safe(krb5_context context, krb5_auth_context authcon,
          (flags & KRB5_AUTH_CONTEXT_RET_SEQUENCE)) && rdata_out == NULL)
         return KRB5_RC_REQUIRED;
 
-    if ((flags & KRB5_AUTH_CONTEXT_DO_TIME) && authcon->remote_addr == NULL)
-        return KRB5_REMOTE_ADDR_REQUIRED;
-
     key = (authcon->recv_subkey != NULL) ? authcon->recv_subkey : authcon->key;
     memset(&rdata, 0, sizeof(rdata));
-    ret = read_krbsafe(context, authcon, inbuf, key, &rdata, &userdata);
+    ret = read_krbsafe(context, authcon, inbuf, key, &rdata, &userdata,
+                       &cksum);
     if (ret)
         goto cleanup;
 
-    ret = k5_privsafe_check_replay(context, authcon, authcon->remote_addr,
-                                   "_safe", &rdata, TRUE);
+    ret = k5_privsafe_check_replay(context, authcon, &rdata, NULL, cksum);
     if (ret)
         goto cleanup;
 
@@ -170,5 +176,6 @@ krb5_rd_safe(krb5_context context, krb5_auth_context authcon,
 
 cleanup:
     krb5_free_data_contents(context, &userdata);
+    krb5_free_checksum(context, cksum);
     return ret;
 }
diff --git a/src/lib/krb5/rcache/rc_conv.c b/src/lib/krb5/rcache/rc_conv.c
index f2fe528..ff33a97 100644
--- a/src/lib/krb5/rcache/rc_conv.c
+++ b/src/lib/krb5/rcache/rc_conv.c
@@ -74,3 +74,23 @@ krb5_rc_hash_message(krb5_context context, const krb5_data *message,
     *out = hash;
     return 0;
 }
+
+krb5_error_code
+k5_rc_tag_from_ciphertext(krb5_context context, const krb5_enc_data *enc,
+                          krb5_data *tag_out)
+{
+    krb5_error_code ret;
+    const krb5_data *cdata = &enc->ciphertext;
+    unsigned int len;
+
+    *tag_out = empty_data();
+
+    ret = krb5_c_crypto_length(context, enc->enctype,
+                               KRB5_CRYPTO_TYPE_CHECKSUM, &len);
+    if (ret)
+        return ret;
+    if (cdata->length < len)
+        return EINVAL;
+    *tag_out = make_data(cdata->data + cdata->length - len, len);
+    return 0;
+}
diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in
index 0eec2b6..5afbe21 100644
--- a/src/tests/Makefile.in
+++ b/src/tests/Makefile.in
@@ -8,10 +8,10 @@ RUN_DB_TEST = $(RUN_SETUP) KRB5_KDC_PROFILE=kdc.conf KRB5_CONFIG=krb5.conf \
 
 OBJS= adata.o etinfo.o forward.o gcred.o hist.o hooks.o hrealm.o \
 	icinterleave.o icred.o kdbtest.o localauth.o plugorder.o rdreq.o \
-	responder.o s2p.o s4u2proxy.o unlockiter.o
+	replay.o responder.o s2p.o s4u2proxy.o unlockiter.o
 EXTRADEPSRCS= adata.c etinfo.c forward.c gcred.c hist.c hooks.c hrealm.c \
-	icinterleave.c icred.c kdbtest.c localauth.c plugorder.c rdreq.o \
-	responder.c s2p.c s4u2proxy.c unlockiter.c
+	icinterleave.c icred.c kdbtest.c localauth.c plugorder.c rdreq.c \
+	replay.c responder.c s2p.c s4u2proxy.c unlockiter.c
 
 TEST_DB = ./testdb
 TEST_REALM = FOO.TEST.REALM
@@ -63,6 +63,9 @@ plugorder: plugorder.o $(KRB5_BASE_DEPLIBS)
 rdreq: rdreq.o $(KRB5_BASE_DEPLIBS)
 	$(CC_LINK) -o $@ rdreq.o $(KRB5_BASE_LIBS)
 
+replay: replay.o $(KRB5_BASE_DEPLIBS)
+	$(CC_LINK) -o $@ replay.o $(KRB5_BASE_LIBS)
+
 responder: responder.o $(KRB5_BASE_DEPLIBS)
 	$(CC_LINK) -o $@ responder.o $(KRB5_BASE_LIBS)
 
@@ -119,7 +122,7 @@ kdb_check: kdc.conf krb5.conf
 	$(RM) $(TEST_DB)* stash_file
 
 check-pytests: adata etinfo forward gcred hist hooks hrealm icinterleave icred
-check-pytests: kdbtest localauth plugorder rdreq responder s2p s4u2proxy
+check-pytests: kdbtest localauth plugorder rdreq replay responder s2p s4u2proxy
 check-pytests: unlockiter
 	$(RUNPYTEST) $(srcdir)/t_general.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_hooks.py $(PYTESTFLAGS)
@@ -178,10 +181,11 @@ check-pytests: unlockiter
 	$(RUNPYTEST) $(srcdir)/t_kdcpolicy.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_u2u.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_kdcoptions.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_replay.py $(PYTESTFLAGS)
 
 clean:
 	$(RM) adata etinfo forward gcred hist hooks hrealm icinterleave icred
-	$(RM) kdbtest localauth plugorder rdreq responder s2p s4u2proxy
+	$(RM) kdbtest localauth plugorder rdreq replay responder s2p s4u2proxy
 	$(RM) unlockiter
 	$(RM) krb5.conf kdc.conf
 	$(RM) -rf kdc_realm/sandbox ldap
diff --git a/src/tests/replay.c b/src/tests/replay.c
new file mode 100644
index 0000000..1703123
--- /dev/null
+++ b/src/tests/replay.c
@@ -0,0 +1,172 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* tests/replay.c - test replay cache using libkrb5 functions */
+/*
+ * Copyright (C) 2019 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "k5-int.h"
+
+int
+main(int argc, char **argv)
+{
+    krb5_error_code ret;
+    krb5_context ctx;
+    krb5_auth_context c_authcon, s_authcon, s_authcon2;
+    krb5_rcache rc;
+    krb5_ccache cc;
+    krb5_principal client, server;
+    krb5_creds mcred, *cred, **tmpcreds;
+    krb5_data der_apreq, der_krbsafe, der_krbpriv, *der_krbcred, tmpdata;
+    krb5_address addr;
+    struct in_addr inaddr;
+    const char *server_name;
+
+    assert(argc == 2);
+    server_name = argv[1];
+
+    /* Create client and server auth contexts.  (They will use a replay cache
+     * by default.) */
+    ret = krb5_init_context(&ctx);
+    assert(ret == 0);
+    ret = krb5_auth_con_init(ctx, &c_authcon);
+    assert(ret == 0);
+    ret = krb5_auth_con_init(ctx, &s_authcon);
+    assert(ret == 0);
+
+    /* Set dummy addresses for the auth contexts. */
+    memset(&inaddr, 0, sizeof(inaddr));
+    addr.addrtype = ADDRTYPE_INET;
+    addr.length = sizeof(inaddr);
+    addr.contents = (uint8_t *)&inaddr;
+    ret = krb5_auth_con_setaddrs(ctx, c_authcon, &addr, &addr);
+    assert(ret == 0);
+    ret = krb5_auth_con_setaddrs(ctx, s_authcon, &addr, &addr);
+    assert(ret == 0);
+
+    /* Set up replay caches for the auth contexts. */
+    tmpdata = string2data("testclient");
+    ret = krb5_get_server_rcache(ctx, &tmpdata, &rc);
+    assert(ret == 0);
+    ret = krb5_auth_con_setrcache(ctx, c_authcon, rc);
+    assert(ret == 0);
+    tmpdata = string2data("testserver");
+    ret = krb5_get_server_rcache(ctx, &tmpdata, &rc);
+    assert(ret == 0);
+    ret = krb5_auth_con_setrcache(ctx, s_authcon, rc);
+    assert(ret == 0);
+
+    /* Construct the client and server principal names. */
+    ret = krb5_cc_default(ctx, &cc);
+    assert(ret == 0);
+    ret = krb5_cc_get_principal(ctx, cc, &client);
+    assert(ret == 0);
+    ret = krb5_parse_name(ctx, server_name, &server);
+    assert(ret == 0);
+
+    /* Get credentials for the client. */
+    memset(&mcred, 0, sizeof(mcred));
+    mcred.client = client;
+    mcred.server = server;
+    ret = krb5_get_credentials(ctx, 0, cc, &mcred, &cred);
+    assert(ret == 0);
+
+    /* Send an AP-REP to establish the sessions. */
+    ret = krb5_mk_req_extended(ctx, &c_authcon, 0, NULL, cred, &der_apreq);
+    assert(ret == 0);
+    ret = krb5_rd_req(ctx, &s_authcon, &der_apreq, NULL, NULL, NULL, NULL);
+    assert(ret == 0);
+
+    /* Set up another server auth context with the same rcache name and replay
+     * the AP-REQ. */
+    ret = krb5_auth_con_init(ctx, &s_authcon2);
+    assert(ret == 0);
+    tmpdata = string2data("testserver");
+    ret = krb5_get_server_rcache(ctx, &tmpdata, &rc);
+    assert(ret == 0);
+    ret = krb5_auth_con_setrcache(ctx, s_authcon2, rc);
+    assert(ret == 0);
+    ret = krb5_rd_req(ctx, &s_authcon2, &der_apreq, NULL, NULL, NULL, NULL);
+    assert(ret == KRB5KRB_AP_ERR_REPEAT);
+    krb5_auth_con_free(ctx, s_authcon2);
+
+    /* Make a KRB-SAFE message with the client auth context. */
+    tmpdata = string2data("safemsg");
+    ret = krb5_mk_safe(ctx, c_authcon, &tmpdata, &der_krbsafe, NULL);
+    assert(ret == 0);
+    /* Play it back to the client to detect a reflection. */
+    ret = krb5_rd_safe(ctx, c_authcon, &der_krbsafe, &tmpdata, NULL);
+    assert(ret == KRB5KRB_AP_ERR_REPEAT);
+    /* Send it to the server auth context twice, to detect a replay. */
+    ret = krb5_rd_safe(ctx, s_authcon, &der_krbsafe, &tmpdata, NULL);
+    assert(ret == 0);
+    krb5_free_data_contents(ctx, &tmpdata);
+    ret = krb5_rd_safe(ctx, s_authcon, &der_krbsafe, &tmpdata, NULL);
+    assert(ret == KRB5KRB_AP_ERR_REPEAT);
+
+    /* Make a KRB-PRIV message with the client auth context. */
+    tmpdata = string2data("safemsg");
+    ret = krb5_mk_priv(ctx, c_authcon, &tmpdata, &der_krbpriv, NULL);
+    assert(ret == 0);
+    /* Play it back to the client to detect a reflection. */
+    ret = krb5_rd_priv(ctx, c_authcon, &der_krbpriv, &tmpdata, NULL);
+    assert(ret == KRB5KRB_AP_ERR_REPEAT);
+    /* Send it to the server auth context twice, to detect a replay. */
+    ret = krb5_rd_priv(ctx, s_authcon, &der_krbpriv, &tmpdata, NULL);
+    assert(ret == 0);
+    krb5_free_data_contents(ctx, &tmpdata);
+    ret = krb5_rd_priv(ctx, s_authcon, &der_krbpriv, &tmpdata, NULL);
+    assert(ret == KRB5KRB_AP_ERR_REPEAT);
+
+    /* Make a KRB-CRED message with the client auth context. */
+    tmpdata = string2data("safemsg");
+    ret = krb5_mk_1cred(ctx, c_authcon, cred, &der_krbcred, NULL);
+    assert(ret == 0);
+    /* Play it back to the client to detect a reflection. */
+    ret = krb5_rd_cred(ctx, c_authcon, der_krbcred, &tmpcreds, NULL);
+    assert(ret == KRB5KRB_AP_ERR_REPEAT);
+    /* Send it to the server auth context twice, to detect a replay. */
+    ret = krb5_rd_cred(ctx, s_authcon, der_krbcred, &tmpcreds, NULL);
+    assert(ret == 0);
+    krb5_free_tgt_creds(ctx, tmpcreds);
+    ret = krb5_rd_cred(ctx, s_authcon, der_krbcred, &tmpcreds, NULL);
+    assert(ret == KRB5KRB_AP_ERR_REPEAT);
+
+    krb5_free_data_contents(ctx, &der_apreq);
+    krb5_free_data_contents(ctx, &der_krbsafe);
+    krb5_free_data_contents(ctx, &der_krbpriv);
+    krb5_free_data(ctx, der_krbcred);
+    krb5_free_creds(ctx, cred);
+    krb5_cc_close(ctx, cc);
+    krb5_free_principal(ctx, client);
+    krb5_free_principal(ctx, server);
+    krb5_auth_con_free(ctx, c_authcon);
+    krb5_auth_con_free(ctx, s_authcon);
+    krb5_free_context(ctx);
+    return 0;
+}
diff --git a/src/tests/t_replay.py b/src/tests/t_replay.py
new file mode 100644
index 0000000..6ad58fe
--- /dev/null
+++ b/src/tests/t_replay.py
@@ -0,0 +1,6 @@
+from k5test import *
+
+realm = K5Realm()
+realm.run(['./replay', realm.host_princ])
+
+success('Replay tests')
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.