]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
- xfr-tsig, tsig_parse_verify_reply_xfr and tsig_sign_reply_xfr.
authorW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Fri, 5 Sep 2025 12:55:36 +0000 (14:55 +0200)
committerW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Fri, 5 Sep 2025 12:55:36 +0000 (14:55 +0200)
util/tsig.c
util/tsig.h

index bd6e4c11e0cd012c1b4ea17a49b4b366aba04f5b..eb49ea459ef76ac76d67cacf2a5add0a49287f72 100644 (file)
@@ -60,6 +60,9 @@
 
 /** Fudge time to allow in signed time for TSIG records. In seconds. */
 #define TSIG_FUDGE_TIME 300
+/** Maximum number of unsigned TSIG packets. +3 for interoperability, off by
+ * one errors. */
+#define TSIG_MAX_UNSIGNED 103
 
 /**
  * The list of TSIG algorithms. It has short_name, wireformat_name,
@@ -76,6 +79,10 @@ static struct tsig_algorithm tsig_algorithm_table[] = {
        { "hmac-sha512", (uint8_t*)"\x0Bhmac-sha512\x00", 13, "sha512", 64 }
 };
 
+/** Delete the tsig calc state. */
+static void tsig_calc_state_delete(
+       struct tsig_calc_state_crypto* calc_state_crypto);
+
 int
 tsig_key_compare(const void* v1, const void* v2)
 {
@@ -467,6 +474,7 @@ tsig_create(struct tsig_key_table* key_table, uint8_t* name, size_t namelen)
        tsig->error = 0;
        tsig->other_len = 0;
        tsig->other_time = 0;
+       tsig->every_nth = 1;
        return tsig;
 }
 
@@ -486,6 +494,10 @@ void
 tsig_delete(struct tsig_data* tsig)
 {
        if(!tsig) return;
+       if(tsig->calc_state) {
+               tsig_calc_state_delete(tsig->calc_state);
+               tsig->calc_state = NULL;
+       }
        free(tsig->key_name);
        free(tsig->algo_name);
        free(tsig->mac);
@@ -1405,6 +1417,7 @@ tsig_lookup_key(struct tsig_key_table* key_table,
        }
        tsig->fudge = TSIG_FUDGE_TIME; /* seconds */
        tsig->klass = LDNS_RR_CLASS_ANY;
+       tsig->every_nth = 1;
        if(region)
                tsig->key_name = regional_alloc(region, rr->key_name_len);
        else
@@ -2049,3 +2062,364 @@ tsig_sign_shared(sldns_buffer* pkt, const uint8_t* name, const uint8_t* alg,
                key.algo->wireformat_name_len, tsig.mac, tsig.mac_size);
        return 0;
 }
+
+int
+tsig_sign_reply_xfr(struct tsig_data* tsig, struct sldns_buffer* pkt,
+       struct tsig_key_table* key_table, uint64_t now, int last_packet)
+{
+       size_t aftername_pos;
+       uint16_t current_query_id;
+       uint8_t timers_var_buf[64];
+       struct sldns_buffer timers_var;
+
+       sldns_buffer_init_frm_data(&timers_var, timers_var_buf,
+               sizeof(timers_var_buf));
+
+       if(!tsig) {
+               /* For some rcodes, like FORMERR, no tsig data is returned,
+                * and also no TSIG is needed on the reply. */
+               verbose(VERB_ALGO, "tsig_sign_reply: no TSIG on error reply");
+               return 1;
+       }
+       if(LDNS_RCODE_WIRE(sldns_buffer_begin(pkt)) == LDNS_RCODE_SERVFAIL ||
+          LDNS_RCODE_WIRE(sldns_buffer_begin(pkt)) == LDNS_RCODE_FORMERR ||
+          LDNS_RCODE_WIRE(sldns_buffer_begin(pkt)) == LDNS_RCODE_NOTIMPL ||
+          (LDNS_RCODE_WIRE(sldns_buffer_begin(pkt)) == LDNS_RCODE_NOTAUTH &&
+           tsig->error != LDNS_TSIG_ERROR_BADTIME)) {
+               uint8_t* algo_name;
+               size_t algo_name_len;
+               /* No TSIG calculation on error reply. */
+               if(tsig->error == 0)
+                       return 1; /* No TSIG needed for the error. Also,
+                       copying in possible formerr contents is not desired. */
+               if(tsig->algo_name) {
+                       /* For errors, the tsig->algo_name is allocated. */
+                       algo_name = tsig->algo_name;
+                       algo_name_len = tsig->algo_name_len;
+               } else {
+                       /* Robust code in case there is algo name. */
+                       algo_name = (uint8_t*)"\000";
+                       algo_name_len = 1;
+               }
+
+               /* The TSIG can be written straight away */
+               sldns_buffer_write(pkt, tsig->key_name, tsig->key_name_len);
+               aftername_pos = sldns_buffer_position(pkt);
+               tsig_append_rr(tsig, pkt, aftername_pos, algo_name,
+                       algo_name_len, NULL, 0);
+               return 1;
+       }
+
+       if(!tsig->later_packet) {
+               /* First packet is signed as usual */
+               int ret;
+               ret = tsig_sign_reply(tsig, pkt, key_table, now);
+               tsig->later_packet = 1;
+               return ret;
+       }
+
+       if(tsig->num_updates == 0) {
+               /* Init the calc state for the new packet, or for the new
+                * packet sequence. */
+               struct tsig_key* key;
+               if(tsig->calc_state) {
+                       tsig_calc_state_delete(tsig->calc_state);
+                       tsig->calc_state = NULL;
+               }
+               lock_rw_rdlock(&key_table->lock);
+               key = tsig_key_table_search(key_table, tsig->key_name,
+                       tsig->key_name_len);
+               if(!key) {
+                       /* The tsig key has disappeared from the key table. */
+                       lock_rw_unlock(&key_table->lock);
+                       verbose(VERB_ALGO, "tsig_sign_reply_xfr: key not in table");
+                       return 0;
+               }
+
+               tsig->calc_state = tsig_calc_state_init(key);
+               lock_rw_unlock(&key_table->lock);
+               if(!tsig->calc_state) {
+                       verbose(VERB_ALGO, "tsig_sign_reply_xfr: out of memory");
+                       return 0;
+               }
+
+               /* Update with the prior mac. */
+               if(tsig->mac_size != 0 && tsig->mac) {
+                       uint8_t prior_buf[1024];
+                       struct sldns_buffer prior;
+                       sldns_buffer_init_frm_data(&prior, prior_buf,
+                               sizeof(prior_buf));
+                       if(sldns_buffer_remaining(&prior) <
+                               2 /* mac_size */ + tsig->mac_size) {
+                               verbose(VERB_ALGO, "tsig_sign_reply_xfr: prior buffer too small");
+                               return 0;
+                       }
+                       sldns_buffer_write_u16(&prior, tsig->mac_size);
+                       sldns_buffer_write(&prior, tsig->mac, tsig->mac_size);
+                       if(!tsig_calc_state_update(tsig->calc_state, &prior)) {
+                               verbose(VERB_ALGO, "tsig_sign_reply_xfr: failed to update tsig crypto");
+                               return 0;
+                       }
+               }
+       }
+
+       if(tsig->every_nth != 1 && tsig->num_updates+1 < tsig->every_nth &&
+               !last_packet) {
+               /* Update with the packet contents. */
+               current_query_id = sldns_buffer_read_u16_at(pkt, 0);
+               sldns_buffer_write_u16_at(pkt, 0, tsig->original_query_id);
+               if(!tsig_calc_state_update(tsig->calc_state, pkt)) {
+                       sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+                       verbose(VERB_ALGO, "tsig_sign_reply_xfr: failed to update tsig crypto");
+                       return 0;
+               }
+               sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+               tsig->num_updates++;
+               return 1;
+       }
+
+       /* Update with the packet contents. */
+       current_query_id = sldns_buffer_read_u16_at(pkt, 0);
+       sldns_buffer_write_u16_at(pkt, 0, tsig->original_query_id);
+       if(!tsig_calc_state_update(tsig->calc_state, pkt)) {
+               sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+               verbose(VERB_ALGO, "tsig_sign_reply_xfr: failed to update tsig crypto");
+               return 0;
+       }
+       sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+
+       /* Update with the timers part of the variables. */
+       tsig->time_signed = now;
+       sldns_buffer_write_u48(&timers_var, tsig->time_signed);
+       sldns_buffer_write_u16(&timers_var, tsig->fudge);
+       if(!tsig_calc_state_update(tsig->calc_state, &timers_var)) {
+               verbose(VERB_ALGO, "tsig_sign_reply_xfr: failed to update tsig crypto");
+               return 0;
+       }
+
+       /* Finalize the calculation. */
+       if(!tsig_calc_state_final(tsig->calc_state, tsig)) {
+               verbose(VERB_ALGO, "tsig_sign_reply_xfr: failed to make final tsig crypto");
+               return 0;
+       }
+
+       /* Append TSIG record. */
+       if(sldns_buffer_remaining(pkt) < tsig_reserved_space(tsig)) {
+               /* Not enough space in buffer for packet and TSIG. */
+               verbose(VERB_ALGO, "tsig_sign_reply: not enough buffer space");
+               return 0;
+       }
+       sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+       sldns_buffer_write(pkt, tsig->key_name, tsig->key_name_len);
+       aftername_pos = sldns_buffer_position(pkt);
+       tsig_append_rr(tsig, pkt, aftername_pos, tsig->algo_name,
+               tsig->algo_name_len, tsig->mac, tsig->mac_size);
+       tsig->num_updates = 0;
+       return 1;
+}
+
+int
+tsig_verify_reply_xfr(struct tsig_data* tsig, struct sldns_buffer* pkt,
+       struct tsig_record* rr, uint64_t now)
+{
+       uint8_t timers_var_buf[64];
+       struct sldns_buffer timers_var;
+       uint16_t current_query_id;
+
+       sldns_buffer_init_frm_data(&timers_var, timers_var_buf,
+               sizeof(timers_var_buf));
+
+       /* packet with original query ID and ARCOUNT without TSIG. */
+       current_query_id = sldns_buffer_read_u16_at(pkt, 0);
+       sldns_buffer_write_u16_at(pkt, 0, rr->original_query_id);
+       LDNS_ARCOUNT_SET( sldns_buffer_begin(pkt)
+                       , LDNS_ARCOUNT(sldns_buffer_begin(pkt)) - 1);
+       sldns_buffer_set_position(pkt, rr->tsig_pos);
+
+       /* Update with packet */
+       if(!tsig_calc_state_update(tsig->calc_state, pkt)) {
+               sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+               verbose(VERB_ALGO, "tsig_verify_reply_xfr: failed to update tsig crypto");
+               return 0;
+       }
+
+       /* Update with the timers part of the variables. */
+       sldns_buffer_write_u48(&timers_var, rr->signed_time);
+       sldns_buffer_write_u16(&timers_var, rr->fudge_time);
+       if(!tsig_calc_state_update(tsig->calc_state, &timers_var)) {
+               sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+               verbose(VERB_ALGO, "tsig_verify_reply_xfr: failed to update tsig crypto");
+               return 0;
+       }
+
+       /* Finalize the calculation. */
+       if(!tsig_calc_state_final(tsig->calc_state, tsig)) {
+               sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+               verbose(VERB_ALGO, "tsig_verify_reply_xfr: failed to make final tsig crypto");
+               return 0;
+       }
+
+       if(CRYPTO_memcmp(rr->mac_data, tsig->mac, tsig->mac_size) != 0) {
+               /* TSIG has wrong digest. */
+               sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+               if(verbosity >= VERB_ALGO) {
+                       char keynm[256];
+                       dname_str(tsig->key_name, keynm);
+                       verbose(VERB_ALGO, "tsig_verify_reply_xfr: TSIG %s has wrong digest",
+                               keynm);
+               }
+               return 0;
+       }
+       /* The TSIG digest has verified */
+       sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+
+       /* Check the TSIG timestamp. */
+       /* Time is checked after the MAC is verified */
+       if( (now > rr->signed_time && now - rr->signed_time > rr->fudge_time) ||
+           (now < rr->signed_time && rr->signed_time - now > rr->fudge_time)) {
+               /* TSIG has wrong timestamp. */
+               if(verbosity >= VERB_ALGO) {
+                       char keynm[256];
+                       dname_str(tsig->key_name, keynm);
+                       verbose(VERB_ALGO, "tsig_verify_reply_xfr: TSIG %s has wrong timestamp, now=%llu packet time=%llu fudge time=%d",
+                               keynm, (unsigned long long)now,
+                               (unsigned long long)rr->signed_time,
+                               (int)rr->fudge_time);
+                       if(rr->other_size == 6)
+                               verbose(VERB_ALGO, "tsig_verify_reply_xfr: other time is reported at %llu",
+                                       (unsigned long long)rr->other_time);
+               }
+               return 0;
+       }
+
+       return 1;
+}
+
+int
+tsig_parse_verify_reply_xfr(struct tsig_data* tsig, struct sldns_buffer* pkt,
+       struct tsig_key_table* key_table, uint64_t now, int last_packet)
+{
+       struct tsig_key* key;
+       struct tsig_record rr;
+       int ret;
+
+       if(!tsig->later_packet) {
+               /* First packet verifies like normal. */
+               int ret;
+               ret = tsig_parse_verify_reply(tsig, pkt, key_table, now);
+               tsig->later_packet = 1;
+               return ret;
+       }
+
+       /* Init if needed. */
+       if(tsig->num_updates == 0) {
+               /* Init the calc state for the new packet, or for the new
+                * packet sequence. */
+               struct tsig_key* key;
+               if(tsig->calc_state) {
+                       tsig_calc_state_delete(tsig->calc_state);
+                       tsig->calc_state = NULL;
+               }
+               lock_rw_rdlock(&key_table->lock);
+               key = tsig_key_table_search(key_table, tsig->key_name,
+                       tsig->key_name_len);
+               if(!key) {
+                       /* The tsig key has disappeared from the key table. */
+                       lock_rw_unlock(&key_table->lock);
+                       verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: key not in table");
+                       return 0;
+               }
+
+               tsig->calc_state = tsig_calc_state_init(key);
+               lock_rw_unlock(&key_table->lock);
+               if(!tsig->calc_state) {
+                       verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: out of memory");
+                       return 0;
+               }
+
+               /* Update with the prior mac. */
+               if(tsig->mac_size != 0 && tsig->mac) {
+                       uint8_t prior_buf[1024];
+                       struct sldns_buffer prior;
+                       sldns_buffer_init_frm_data(&prior, prior_buf,
+                               sizeof(prior_buf));
+                       if(sldns_buffer_remaining(&prior) <
+                               2 /* mac_size */ + tsig->mac_size) {
+                               verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: prior buffer too small");
+                               return 0;
+                       }
+                       sldns_buffer_write_u16(&prior, tsig->mac_size);
+                       sldns_buffer_write(&prior, tsig->mac, tsig->mac_size);
+                       if(!tsig_calc_state_update(tsig->calc_state, &prior)) {
+                               verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: failed to update tsig crypto");
+                               return 0;
+                       }
+               }
+       }
+
+       /* If there is no TSIG, that is okay, for every_nth. Update
+        * the counters. */
+       /* The ARCOUNT is 0 or we are at end of packet without TSIG. */
+       if(LDNS_ARCOUNT(sldns_buffer_begin(pkt)) < 1 ||
+               sldns_buffer_remaining(pkt) < 1) {
+               uint16_t current_query_id;
+               tsig->num_updates++;
+               if(last_packet) {
+                       verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: last packet missing TSIG.");
+                       return 0;
+               }
+               if(tsig->num_updates > TSIG_MAX_UNSIGNED) {
+                       verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: too many packets without TSIG.");
+                       return 0;
+               }
+               /* Update with the packet contents. */
+               current_query_id = sldns_buffer_read_u16_at(pkt, 0);
+               sldns_buffer_write_u16_at(pkt, 0, tsig->original_query_id);
+               if(!tsig_calc_state_update(tsig->calc_state, pkt)) {
+                       sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+                       verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: failed to update tsig crypto");
+                       return 0;
+               }
+               sldns_buffer_write_u16_at(pkt, 0, current_query_id);
+               return 1;
+       }
+
+       /* Parse the TSIG RR from the query. */
+       ret = tsig_parse(pkt, &rr);
+       if(ret != 0)
+               return 0;
+
+       /* Check it is the same key, same algo, same digest_size */
+       if(dname_pkt_compare(pkt, tsig->key_name, rr.key_name) != 0) {
+               verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: key name wrong");
+               return 0;
+       }
+
+       lock_rw_rdlock(&key_table->lock);
+       key = tsig_key_table_search(key_table, tsig->key_name,
+               tsig->key_name_len);
+       if(!key) {
+               /* The tsig key has disappeared from the key table. */
+               lock_rw_unlock(&key_table->lock);
+               verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: key not in table");
+               return 0;
+       }
+
+       if(query_dname_compare(key->algo->wireformat_name,
+               rr.algorithm_name) != 0) {
+               lock_rw_unlock(&key_table->lock);
+               verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: algorithm wrong");
+               return 0;
+       }
+       if(tsig->mac_size != rr.mac_size) {
+               lock_rw_unlock(&key_table->lock);
+               verbose(VERB_ALGO, "tsig_parse_verify_reply_xfr: mac size wrong");
+               return 0;
+       }
+       lock_rw_unlock(&key_table->lock);
+       tsig->num_updates = 0;
+
+       /* Verify the TSIG. */
+       ret = tsig_verify_reply_xfr(tsig, pkt, &rr, now);
+       return ret;
+}
index 16cbf08004cbdd94c3ba6102f152eb9a043b08cc..6ba9c1ca6d508263e9524f7fb3192ae6571ae08c 100644 (file)
@@ -47,6 +47,7 @@ struct sldns_buffer;
 struct config_file;
 struct config_tsig_key;
 struct regional;
+struct tsig_calc_state_crypto;
 
 /**
  * TSIG record, the RR that is in the packet.
@@ -118,6 +119,17 @@ struct tsig_data {
        uint16_t other_len;
        /** if other len 6, this is 48bit time of error. */
        uint64_t other_time;
+       /** For zone transfers, there are several packets and TSIGs,
+        * this keeps track of the tsig calculation state. It is malloced,
+        * and the tsig has to be deleted to free it. */
+       struct tsig_calc_state_crypto* calc_state;
+       /** For the first packet it is 0, for later packets 1. */
+       int later_packet;
+       /** The number of update only packets without a tsig. */
+       int num_updates;
+       /** The number of packets after which to sign with TSIG, 1 is every
+        * time. */
+       int every_nth;
 };
 
 /**
@@ -443,4 +455,45 @@ int tsig_find_rr(struct sldns_buffer* pkt);
  */
 int tsig_in_packet(struct sldns_buffer* pkt);
 
+/**
+ * Sign XFR reply with TSIG. Appends the TSIG record. Call for later
+ * packets too.
+ * @param tsig: the tsig data. It must be malloced for the crypto state.
+ * @param pkt: the packet to sign.
+ * @param key_table: the tsig key table is used to fetch the key details.
+ * @param now: time to sign the query, the current time.
+ * @param last_packet: set to true for the last packet, that needs to be
+ *     TSIG signed.
+ * @return false on failure.
+ */
+int tsig_sign_reply_xfr(struct tsig_data* tsig, struct sldns_buffer* pkt,
+       struct tsig_key_table* key_table, uint64_t now, int last_packet);
+
+/**
+ * Verify XFR reply with TSIG.
+ * @param tsig: the tsig data.
+ * @param pkt: the reply to verify.
+ * @param rr: the tsig record parsed from the reply.
+ * @param now: time to sign the query, the current time.
+ * @return false on failure, like
+ *     alloc failure, wireformat malformed, did not verify.
+ */
+int tsig_verify_reply_xfr(struct tsig_data* tsig, struct sldns_buffer* pkt,
+       struct tsig_record* rr, uint64_t now);
+
+/**
+ * Parse and verify XFR reply with TSIG. Position at the TSIG record, or
+ * at end of packet if no TSIG record.
+ * @param tsig: the tsig data.
+ * @param pkt: the reply to verify.
+ * @param key_table: the tsig key table is used to fetch the key details.
+ * @param now: time to sign the query, the current time.
+ * @param last_packet: set true for the last packet, it must have a TSIG.
+ * @return false on failure, like
+ *     alloc failure, wireformat malformed, did not verify.
+ */
+int tsig_parse_verify_reply_xfr(struct tsig_data* tsig,
+       struct sldns_buffer* pkt, struct tsig_key_table* key_table,
+       uint64_t now, int last_packet);
+
 #endif /* UTIL_TSIG_H */