]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
Cookie secret file (#1090)
authorWouter Wijngaards <wcawijngaards@users.noreply.github.com>
Fri, 2 Aug 2024 11:32:08 +0000 (13:32 +0200)
committerGitHub <noreply@github.com>
Fri, 2 Aug 2024 11:32:08 +0000 (13:32 +0200)
* - cookie-secret-file, define struct.

* - cookie-secret-file, add config option, create, read and delete struct.

* - cookie-secret-file, check cookie secrets for cookie validation.

* - cookie-secret-file, unbound-control add_cookie_secret, drop_cookie_secret,
  activate_cookie_secret and print_cookie_secrets.

* - cookie-secret-file, test and fix locks, renew writes a fresh cookie,
  staging cookies get a fresh cookie and spelling in error message.

* - cookie-secret-file, remove unused variable from cookie file unit test.

* Remove unshare and faketime dependencies for cookie_file test; documentation nits.

---------

Co-authored-by: Yorgos Thessalonikefs <yorgos@nlnetlabs.nl>
25 files changed:
Makefile.in
daemon/daemon.c
daemon/daemon.h
daemon/remote.c
daemon/worker.c
doc/example.conf.in
doc/unbound-control.8.in
doc/unbound.conf.5.in
smallapp/unbound-control.c
testcode/unitmain.c
testdata/cookie_file.tdir/cookie_file.conf [new file with mode: 0644]
testdata/cookie_file.tdir/cookie_file.dsc [new file with mode: 0644]
testdata/cookie_file.tdir/cookie_file.post [new file with mode: 0644]
testdata/cookie_file.tdir/cookie_file.pre [new file with mode: 0644]
testdata/cookie_file.tdir/cookie_file.test [new file with mode: 0644]
util/config_file.c
util/config_file.h
util/configlexer.lex
util/configparser.y
util/data/msgparse.c
util/data/msgparse.h
util/edns.c
util/edns.h
util/net_help.c
util/net_help.h

index e265a93132427118d8e76596de3524c1d1e794e1..672435e01e9f02aa65a7ceb7f10633cffee780eb 100644 (file)
@@ -1298,7 +1298,7 @@ remote.lo remote.o: $(srcdir)/daemon/remote.c config.h $(srcdir)/daemon/remote.h
  $(srcdir)/validator/val_anchor.h $(srcdir)/iterator/iterator.h $(srcdir)/services/outbound_list.h \
  $(srcdir)/iterator/iter_fwd.h $(srcdir)/iterator/iter_hints.h $(srcdir)/iterator/iter_delegpt.h \
  $(srcdir)/services/outside_network.h $(srcdir)/sldns/str2wire.h $(srcdir)/sldns/parseutil.h \
- $(srcdir)/sldns/wire2str.h
+ $(srcdir)/sldns/wire2str.h $(srcdir)/util/edns.h
 stats.lo stats.o: $(srcdir)/daemon/stats.c config.h $(srcdir)/daemon/stats.h $(srcdir)/util/timehist.h \
  $(srcdir)/libunbound/unbound.h $(srcdir)/daemon/worker.h $(srcdir)/libunbound/worker.h $(srcdir)/sldns/sbuffer.h \
  $(srcdir)/util/data/packed_rrset.h $(srcdir)/util/storage/lruhash.h $(srcdir)/util/locks.h $(srcdir)/util/log.h \
index d81bec84441114c2c10b5ac4e17725924610bd2e..72b4a43be1ad8d0d91c4bb1af03f81e7f6647f67 100644 (file)
@@ -735,6 +735,14 @@ daemon_fork(struct daemon* daemon)
                                   "dnscrypt support");
 #endif
        }
+       if(daemon->cfg->cookie_secret_file &&
+               daemon->cfg->cookie_secret_file[0]) {
+               if(!(daemon->cookie_secrets = cookie_secrets_create()))
+                       fatal_exit("Could not create cookie_secrets: out of memory");
+               if(!cookie_secrets_apply_cfg(daemon->cookie_secrets,
+                       daemon->cfg->cookie_secret_file))
+                       fatal_exit("Could not setup cookie_secrets");
+       }
        /* create global local_zones */
        if(!(daemon->local_zones = local_zones_create()))
                fatal_exit("Could not create local zones: out of memory");
@@ -929,6 +937,7 @@ daemon_delete(struct daemon* daemon)
        acl_list_delete(daemon->acl);
        acl_list_delete(daemon->acl_interface);
        tcl_list_delete(daemon->tcl);
+       cookie_secrets_delete(daemon->cookie_secrets);
        listen_desetup_locks();
        free(daemon->chroot);
        free(daemon->pidfile);
index a6b6391cc48b6d3409d494a47d3a901155b3c34e..5c3a114cc7e61559fb8bc0f262126dcf02a04908 100644 (file)
@@ -58,6 +58,7 @@ struct ub_randstate;
 struct daemon_remote;
 struct respip_set;
 struct shm_main_info;
+struct cookie_secrets;
 
 #include "dnstap/dnstap_config.h"
 #ifdef USE_DNSTAP
@@ -148,6 +149,8 @@ struct daemon {
 #endif
        /** reuse existing cache on reload if other conditions allow it. */
        int reuse_cache;
+       /** the EDNS cookie secrets from the cookie-secret-file */
+       struct cookie_secrets* cookie_secrets;
 };
 
 /**
index a5db4330c5a71275188e2f463611cba13531dce5..855b1f963b2396fe178630ca8854b654baebfea9 100644 (file)
@@ -88,6 +88,7 @@
 #include "sldns/wire2str.h"
 #include "sldns/sbuffer.h"
 #include "util/timeval_func.h"
+#include "util/edns.h"
 #ifdef USE_CACHEDB
 #include "cachedb/cachedb.h"
 #endif
@@ -3195,6 +3196,210 @@ do_rpz_disable(RES* ssl, struct worker* worker, char* arg)
     do_rpz_enable_disable(ssl, worker, arg, 0);
 }
 
+/** Write the cookie secrets to file, returns `0` on failure.
+ * Caller has to hold the lock. */
+static int
+cookie_secret_file_dump(RES* ssl, struct worker* worker) {
+       char const* secret_file = worker->env.cfg->cookie_secret_file;
+       struct cookie_secrets* cookie_secrets = worker->daemon->cookie_secrets;
+       char secret_hex[UNBOUND_COOKIE_SECRET_SIZE * 2 + 1];
+       FILE* f;
+       size_t i;
+       if(secret_file == NULL || secret_file[0]==0) {
+               (void)ssl_printf(ssl, "error: no cookie secret file configured\n");
+               return 0;
+       }
+       log_assert( secret_file != NULL );
+
+       /* open write only and truncate */
+       if((f = fopen(secret_file, "w")) == NULL ) {
+               (void)ssl_printf(ssl, "unable to open cookie secret file %s: %s",
+                                secret_file, strerror(errno));
+               return 0;
+       }
+       if(cookie_secrets == NULL) {
+               /* nothing to write */
+               fclose(f);
+               return 1;
+       }
+
+       for(i = 0; i < cookie_secrets->cookie_count; i++) {
+               struct cookie_secret const* cs = &cookie_secrets->
+                       cookie_secrets[i];
+               ssize_t const len = hex_ntop(cs->cookie_secret,
+                       UNBOUND_COOKIE_SECRET_SIZE, secret_hex,
+                       sizeof(secret_hex));
+               (void)len; /* silence unused variable warning with -DNDEBUG */
+               log_assert( len == UNBOUND_COOKIE_SECRET_SIZE * 2 );
+               secret_hex[UNBOUND_COOKIE_SECRET_SIZE * 2] = '\0';
+               fprintf(f, "%s\n", secret_hex);
+       }
+       explicit_bzero(secret_hex, sizeof(secret_hex));
+       fclose(f);
+       return 1;
+}
+
+/** Activate cookie secret */
+static void
+do_activate_cookie_secret(RES* ssl, struct worker* worker) {
+       char const* secret_file = worker->env.cfg->cookie_secret_file;
+       struct cookie_secrets* cookie_secrets = worker->daemon->cookie_secrets;
+
+       if(secret_file == NULL || secret_file[0] == 0) {
+               (void)ssl_printf(ssl, "error: no cookie secret file configured\n");
+               return;
+       }
+       if(cookie_secrets == NULL) {
+               (void)ssl_printf(ssl, "error: there are no cookie_secrets.");
+               return;
+       }
+       lock_basic_lock(&cookie_secrets->lock);
+
+       if(cookie_secrets->cookie_count <= 1 ) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               (void)ssl_printf(ssl, "error: no staging cookie secret to activate\n");
+               return;
+       }
+       /* Only the worker 0 writes to file, the others update state. */
+       if(worker->thread_num == 0 && !cookie_secret_file_dump(ssl, worker)) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               (void)ssl_printf(ssl, "error: writing to cookie secret file: \"%s\"\n",
+                               secret_file);
+               return;
+       }
+       activate_cookie_secret(cookie_secrets);
+       if(worker->thread_num == 0)
+               (void)cookie_secret_file_dump(ssl, worker);
+       lock_basic_unlock(&cookie_secrets->lock);
+       send_ok(ssl);
+}
+
+/** Drop cookie secret */
+static void
+do_drop_cookie_secret(RES* ssl, struct worker* worker) {
+       char const* secret_file = worker->env.cfg->cookie_secret_file;
+       struct cookie_secrets* cookie_secrets = worker->daemon->cookie_secrets;
+
+       if(secret_file == NULL || secret_file[0] == 0) {
+               (void)ssl_printf(ssl, "error: no cookie secret file configured\n");
+               return;
+       }
+       if(cookie_secrets == NULL) {
+               (void)ssl_printf(ssl, "error: there are no cookie_secrets.");
+               return;
+       }
+       lock_basic_lock(&cookie_secrets->lock);
+
+       if(cookie_secrets->cookie_count <= 1 ) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               (void)ssl_printf(ssl, "error: can not drop the currently active cookie secret\n");
+               return;
+       }
+       /* Only the worker 0 writes to file, the others update state. */
+       if(worker->thread_num == 0 && !cookie_secret_file_dump(ssl, worker)) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               (void)ssl_printf(ssl, "error: writing to cookie secret file: \"%s\"\n",
+                               secret_file);
+               return;
+       }
+       drop_cookie_secret(cookie_secrets);
+       if(worker->thread_num == 0)
+               (void)cookie_secret_file_dump(ssl, worker);
+       lock_basic_unlock(&cookie_secrets->lock);
+       send_ok(ssl);
+}
+
+/** Add cookie secret */
+static void
+do_add_cookie_secret(RES* ssl, struct worker* worker, char* arg) {
+       uint8_t secret[UNBOUND_COOKIE_SECRET_SIZE];
+       char const* secret_file = worker->env.cfg->cookie_secret_file;
+       struct cookie_secrets* cookie_secrets = worker->daemon->cookie_secrets;
+
+       if(secret_file == NULL || secret_file[0] == 0) {
+               (void)ssl_printf(ssl, "error: no cookie secret file configured\n");
+               return;
+       }
+       if(cookie_secrets == NULL) {
+               worker->daemon->cookie_secrets = cookie_secrets_create();
+               if(!worker->daemon->cookie_secrets) {
+                       (void)ssl_printf(ssl, "error: out of memory");
+                       return;
+               }
+               cookie_secrets = worker->daemon->cookie_secrets;
+       }
+       lock_basic_lock(&cookie_secrets->lock);
+
+       if(*arg == '\0') {
+               lock_basic_unlock(&cookie_secrets->lock);
+               (void)ssl_printf(ssl, "error: missing argument (cookie_secret)\n");
+               return;
+       }
+       if(strlen(arg) != 32) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               explicit_bzero(arg, strlen(arg));
+               (void)ssl_printf(ssl, "invalid cookie secret: invalid argument length\n");
+               (void)ssl_printf(ssl, "please provide a 128bit hex encoded secret\n");
+               return;
+       }
+       if(hex_pton(arg, secret, UNBOUND_COOKIE_SECRET_SIZE) !=
+               UNBOUND_COOKIE_SECRET_SIZE ) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               explicit_bzero(secret, UNBOUND_COOKIE_SECRET_SIZE);
+               explicit_bzero(arg, strlen(arg));
+               (void)ssl_printf(ssl, "invalid cookie secret: parse error\n");
+               (void)ssl_printf(ssl, "please provide a 128bit hex encoded secret\n");
+               return;
+       }
+       /* Only the worker 0 writes to file, the others update state. */
+       if(worker->thread_num == 0 && !cookie_secret_file_dump(ssl, worker)) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               explicit_bzero(secret, UNBOUND_COOKIE_SECRET_SIZE);
+               explicit_bzero(arg, strlen(arg));
+               (void)ssl_printf(ssl, "error: writing to cookie secret file: \"%s\"\n",
+                               secret_file);
+               return;
+       }
+       add_cookie_secret(cookie_secrets, secret, UNBOUND_COOKIE_SECRET_SIZE);
+       explicit_bzero(secret, UNBOUND_COOKIE_SECRET_SIZE);
+       if(worker->thread_num == 0)
+               (void)cookie_secret_file_dump(ssl, worker);
+       lock_basic_unlock(&cookie_secrets->lock);
+       explicit_bzero(arg, strlen(arg));
+       send_ok(ssl);
+}
+
+/** Print cookie secrets */
+static void
+do_print_cookie_secrets(RES* ssl, struct worker* worker) {
+       struct cookie_secrets* cookie_secrets = worker->daemon->cookie_secrets;
+       char secret_hex[UNBOUND_COOKIE_SECRET_SIZE * 2 + 1];
+       int i;
+
+       if(!cookie_secrets)
+               return; /* Output is empty. */
+       lock_basic_lock(&cookie_secrets->lock);
+       for(i = 0; (size_t)i < cookie_secrets->cookie_count; i++) {
+               struct cookie_secret const* cs = &cookie_secrets->
+                       cookie_secrets[i];
+               ssize_t const len = hex_ntop(cs->cookie_secret,
+                       UNBOUND_COOKIE_SECRET_SIZE, secret_hex,
+                       sizeof(secret_hex));
+               (void)len; /* silence unused variable warning with -DNDEBUG */
+               log_assert( len == UNBOUND_COOKIE_SECRET_SIZE * 2 );
+               secret_hex[UNBOUND_COOKIE_SECRET_SIZE * 2] = '\0';
+               if (i == 0)
+                       (void)ssl_printf(ssl, "active : %s\n",  secret_hex);
+               else if (cookie_secrets->cookie_count == 2)
+                       (void)ssl_printf(ssl, "staging: %s\n",  secret_hex);
+               else
+                       (void)ssl_printf(ssl, "staging[%d]: %s\n", i,
+                               secret_hex);
+       }
+       lock_basic_unlock(&cookie_secrets->lock);
+       explicit_bzero(secret_hex, sizeof(secret_hex));
+}
+
 /** check for name with end-of-string, space or tab after it */
 static int
 cmdcmp(char* p, const char* cmd, size_t len)
@@ -3327,6 +3532,9 @@ execute_cmd(struct daemon_remote* rc, RES* ssl, char* cmd,
        } else if(cmdcmp(p, "view_local_datas", 16)) {
                do_view_datas_add(rc, ssl, worker, skipwhite(p+16));
                return;
+       } else if(cmdcmp(p, "print_cookie_secrets", 20)) {
+               do_print_cookie_secrets(ssl, worker);
+               return;
        }
 
 #ifdef THREADS_DISABLED
@@ -3391,6 +3599,12 @@ execute_cmd(struct daemon_remote* rc, RES* ssl, char* cmd,
                do_rpz_enable(ssl, worker, skipwhite(p+10));
        } else if(cmdcmp(p, "rpz_disable", 11)) {
                do_rpz_disable(ssl, worker, skipwhite(p+11));
+       } else if(cmdcmp(p, "add_cookie_secret", 17)) {
+               do_add_cookie_secret(ssl, worker, skipwhite(p+17));
+       } else if(cmdcmp(p, "drop_cookie_secret", 18)) {
+               do_drop_cookie_secret(ssl, worker);
+       } else if(cmdcmp(p, "activate_cookie_secret", 22)) {
+               do_activate_cookie_secret(ssl, worker);
        } else {
                (void)ssl_printf(ssl, "error unknown command '%s'\n", p);
        }
index dd14a5a3cd5c5b86ae205166163e31ba99553746..84e18f2d018fed3be2693aa187dced0494eaf030 100644 (file)
@@ -1573,7 +1573,8 @@ worker_handle_request(struct comm_point* c, void* arg, int error,
        if((ret=parse_edns_from_query_pkt(
                        c->buffer, &edns, worker->env.cfg, c, repinfo,
                        (worker->env.now ? *worker->env.now : time(NULL)),
-                       worker->scratchpad)) != 0) {
+                       worker->scratchpad,
+                       worker->daemon->cookie_secrets)) != 0) {
                struct edns_data reply_edns;
                verbose(VERB_ALGO, "worker parse edns: formerror.");
                log_addr(VERB_CLIENT, "from", &repinfo->client_addr,
index 19aa599523fc5f336474e5b72a14ecd837076666..9f3fc0dce4ccabf0e916c21a5e132ff632410257 100644 (file)
@@ -1044,6 +1044,11 @@ server:
        # example value "000102030405060708090a0b0c0d0e0f".
        # cookie-secret: <128 bit random hex string>
 
+       # File with cookie secrets, the 'cookie-secret:' option is ignored
+       # and the file can be managed to have staging and active secrets
+       # with remote control commands. Disabled with "". Default is "".
+       # cookie-secret-file: "/usr/local/etc/unbound_cookiesecrets.txt"
+
        # Enable to attach Extended DNS Error codes (RFC8914) to responses.
        # ede: no
 
index 8d98d05c84672cd63c894cc0a1bacabdda7e1052..17073f9388ffee109d336d3c2c9716fc487912e0 100644 (file)
@@ -350,6 +350,41 @@ Remove a list of \fIlocal_data\fR for given view from stdin. Like local_datas_re
 .TP
 .B view_local_datas \fIview\fR
 Add a list of \fIlocal_data\fR for given view from stdin.  Like local_datas.
+.TP
+.B add_cookie_secret <secret>
+Add or replace a cookie secret persistently. <secret> needs to be an 128 bit
+hex string.
+.IP
+Cookie secrets can be either \fIactive\fR or \fIstaging\fR. \fIActive\fR cookie
+secrets are used to create DNS Cookies, but verification of a DNS Cookie
+succeeds with any of the \fIactive\fR or \fIstaging\fR cookie secrets. The
+state of the current cookie secrets can be printed with the
+\fBprint_cookie_secrets\fR command.
+.IP
+When there are no cookie secrets configured yet, the <secret> is added as
+\fIactive\fR. If there is already an \fIactive\fR cookie secret, the <secret>
+is added as \fIstaging\fR or replacing an existing \fIstaging\fR secret.
+.IP
+To "roll" a cookie secret used in an anycast set. The new secret has to be
+added as staging secret to \fBall\fR nodes in the anycast set. When \fBall\fR
+nodes can verify DNS Cookies with the new secret, the new secret can be
+activated with the \fBactivate_cookie_secret\fR command. After \fBall\fR nodes
+have the new secret \fIactive\fR for at least one hour, the previous secret can
+be dropped with the \fBdrop_cookie_secret\fR command.
+.IP
+Persistence is accomplished by writing to a file which if configured with the
+\fBcookie\-secret\-file\fR option in the server section of the config file.
+This is disabled by default, "".
+.TP
+.B drop_cookie_secret
+Drop the \fIstaging\fR cookie secret.
+.TP
+.B activate_cookie_secret
+Make the current \fIstaging\fR cookie secret \fIactive\fR, and the current
+\fIactive\fR cookie secret \fIstaging\fR.
+.TP
+.B print_cookie_secrets
+Show the current configured cookie secrets with their status.
 .SH "EXIT CODE"
 The unbound\-control program exits with status code 1 on error, 0 on success.
 .SH "SET UP"
index 90e109ad76189b6c09d26ac7f1471d75f5c95de1..d6d9c905cc9fd1bb8f8a389112c6748d83b01011 100644 (file)
@@ -1983,6 +1983,20 @@ Useful to explicitly set for servers in an anycast deployment that need to
 share the secret in order to verify each other's Server Cookies.
 An example hex string would be "000102030405060708090a0b0c0d0e0f".
 Default is a 128 bits random secret generated at startup time.
+This option is ignored if a \fBcookie\-secret\-file\fR is
+present.  In that case the secrets from that file are used in DNS Cookie
+calculations.
+.TP 5
+.B cookie\-secret\-file: \fI<filename>
+File from which the secrets are read used in DNS Cookie calculations. When this
+file exists, the secrets in this file are used and the secret specified by the
+\fBcookie-secret\fR option is ignored.
+Enable it by setting a filename, like "/usr/local/etc/unbound_cookiesecrets.txt".
+The content of this file must be manipulated with the \fBadd_cookie_secret\fR,
+\fBdrop_cookie_secret\fR and \fBactivate_cookie_secret\fR commands to the
+\fIunbound\-control\fR(8) tool. Please see that manpage on how to perform a
+safe cookie secret rollover.
+Default is "" (disabled).
 .TP 5
 .B edns\-client\-string: \fI<IP netblock> <string>
 Include an EDNS0 option containing configured ascii string in queries with
index 50a465bd51bc53a789f6363281ebc59c70bc4a23..21e7eb82d5ef0725a64e13a636e98c3832824bf9 100644 (file)
@@ -186,6 +186,10 @@ usage(void)
        printf("  rpz_enable zone               Enable the RPZ zone if it had previously\n");
        printf("                                been disabled\n");
        printf("  rpz_disable zone              Disable the RPZ zone\n");
+       printf("  add_cookie_secret <secret>    add (or replace) a new cookie secret <secret>\n");
+       printf("  drop_cookie_secret            drop a staging cookie secret\n");
+       printf("  activate_cookie_secret        make a staging cookie secret active\n");
+       printf("  print_cookie_secrets          show all cookie secrets with their status\n");
        printf("Version %s\n", PACKAGE_VERSION);
        printf("BSD licensed, see LICENSE in source package for details.\n");
        printf("Report bugs to %s\n", PACKAGE_BUGREPORT);
index 1ddc56750d93911f069fac1b926107eef7f940cf..084c12b93b4fab97f9a6a18d2a024c7cdb0d7bb7 100644 (file)
@@ -1117,7 +1117,7 @@ static void edns_ede_encode_encodedecode(struct query_info* qinfo,
        sldns_buffer_skip(pkt, 2 + 2);
        /* decode */
        unit_assert(parse_edns_from_query_pkt(pkt, edns, NULL, NULL, NULL, 0,
-               region) == 0);
+               region, NULL) == 0);
 }
 
 static void edns_ede_encode_check(struct edns_data* edns, int* found_ede,
diff --git a/testdata/cookie_file.tdir/cookie_file.conf b/testdata/cookie_file.tdir/cookie_file.conf
new file mode 100644 (file)
index 0000000..25dd93f
--- /dev/null
@@ -0,0 +1,19 @@
+server:
+       verbosity: 7
+       use-syslog: no
+       directory: ""
+       pidfile: "unbound.pid"
+       chroot: ""
+       username: ""
+       do-not-query-localhost: no
+       use-caps-for-id: no
+       port: @SERVER_PORT@
+       interface: 127.0.0.1
+       cookie-secret-file: "cookie_secrets.txt"
+       answer-cookie: yes
+       access-control: 127.0.0.0/8 allow_cookie  # BADCOOKIE for incomplete/invalid cookies
+
+remote-control:
+       control-enable: yes
+       control-port: @CONTROL_PORT@
+       control-use-cert: no
diff --git a/testdata/cookie_file.tdir/cookie_file.dsc b/testdata/cookie_file.tdir/cookie_file.dsc
new file mode 100644 (file)
index 0000000..4f321bd
--- /dev/null
@@ -0,0 +1,16 @@
+BaseName: cookie_file
+Version: 1.0
+Description: Check the cookie rollover
+CreationDate: Fri 14 Jun 11:00:00 CEST 2024
+Maintainer:
+Category:
+Component:
+CmdDepends:
+Depends:
+Help:
+Pre: cookie_file.pre
+Post: cookie_file.post
+Test: cookie_file.test
+AuxFiles:
+Passed:
+Failure:
diff --git a/testdata/cookie_file.tdir/cookie_file.post b/testdata/cookie_file.tdir/cookie_file.post
new file mode 100644 (file)
index 0000000..b64af9c
--- /dev/null
@@ -0,0 +1,10 @@
+# #-- cookie_file.post --#
+# source the master var file when it's there
+[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master
+# source the test var file when it's there
+[ -f .tpkg.var.test ] && source .tpkg.var.test
+#
+# do your teardown here
+. ../common.sh
+kill_from_pidfile "unbound.pid"
+cat unbound.log
diff --git a/testdata/cookie_file.tdir/cookie_file.pre b/testdata/cookie_file.tdir/cookie_file.pre
new file mode 100644 (file)
index 0000000..61da542
--- /dev/null
@@ -0,0 +1,24 @@
+# #-- cookie_file.pre--#
+PRE="../.."
+. ../common.sh
+
+get_random_port 2
+SERVER_PORT=$RND_PORT
+CONTROL_PORT=$(($RND_PORT + 1))
+echo "SERVER_PORT=$SERVER_PORT" >> .tpkg.var.test
+echo "CONTROL_PORT=$CONTROL_PORT" >> .tpkg.var.test
+
+# make config file
+sed \
+       -e 's/@SERVER_PORT\@/'$SERVER_PORT'/' \
+       -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' \
+       < cookie_file.conf > ub.conf
+
+# empty cookie file
+touch cookie_secrets.txt
+
+# start unbound in the background
+$PRE/unbound -d -c ub.conf > unbound.log 2>&1 &
+
+cat .tpkg.var.test
+wait_unbound_up unbound.log
diff --git a/testdata/cookie_file.tdir/cookie_file.test b/testdata/cookie_file.tdir/cookie_file.test
new file mode 100644 (file)
index 0000000..7da4fa6
--- /dev/null
@@ -0,0 +1,248 @@
+# #-- cookie_file.test --#
+# source the master var file when it's there
+[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master
+# use .tpkg.var.test for in test variable passing
+[ -f .tpkg.var.test ] && source .tpkg.var.test
+PRE="../.."
+. ../common.sh
+
+first_secret=dd3bdf9344b678b185a6f5cb60fca715
+second_secret=445536bcd2513298075a5d379663c962
+
+
+teststep "Add first secret"
+echo ">> add_cookie_secret $first_secret"
+$PRE/unbound-control -c ub.conf add_cookie_secret $first_secret
+# check secret is persisted
+outfile=cookie_secrets.1
+$PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+if ! grep -q "$first_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "$first_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "$first_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "^active.*$first_secret" $outfile
+then
+       cat $outfile
+       echo "First secret was not provisioned"
+       exit 1
+fi
+echo ">> print_cookie_secrets"
+cat $outfile
+
+
+teststep "Get a valid cookie for this secret"
+outfile=dig.output.1
+dig version.server ch txt @127.0.0.1 -p $SERVER_PORT +cookie=3132333435363738 > $outfile
+if ! grep -q "BADCOOKIE" $outfile
+then
+       cat $outfile
+       echo "Did not get a BADCOOKIE response for a client-only cookie"
+       exit 1
+fi
+if ! grep -q "COOKIE: 3132333435363738" $outfile
+then
+       cat $outfile
+       echo "Did not get a cookie in the response"
+       exit 1
+fi
+first_cookie=$(grep "; COOKIE:" $outfile | cut -d ' ' -f 3)
+cat $outfile
+echo "first cookie: $first_cookie"
+
+
+teststep "Verify the first cookie can be reused"
+outfile=dig.output.2
+dig version.server ch txt @127.0.0.1 -p $SERVER_PORT +cookie=$first_cookie > $outfile
+if grep -q "BADCOOKIE" $outfile
+then
+       cat $outfile
+       echo "Got BADCOOKIE response for a valid cookie"
+       exit 1
+fi
+if ! grep -q "COOKIE: $first_cookie" $outfile
+then
+       cat $outfile
+       echo "Did not get the same first cookie in the response"
+       exit 1
+fi
+
+
+teststep "Add second secret"
+outfile=cookie_secrets.2
+echo ">> add_cookie_secret $second_secret"
+$PRE/unbound-control -c ub.conf add_cookie_secret $second_secret
+$PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+if ! grep -q "$second_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "$second_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "$second_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "^staging.*$second_secret" $outfile \
+       || ! grep -q "^active.*$first_secret" $outfile
+then
+               cat $outfile
+               echo "Secrets were not provisioned"
+               exit 1
+fi
+echo ">> print_cookie_secrets"
+cat $outfile
+echo ">> cookie_secrets.txt"
+cat cookie_secrets.txt
+
+
+teststep "Verify the first cookie can be reused"
+outfile=dig.output.3
+dig version.server ch txt @127.0.0.1 -p $SERVER_PORT +cookie=$first_cookie > $outfile
+if grep -q "BADCOOKIE" $outfile
+then
+       cat $outfile
+       echo "Got BADCOOKIE response for a valid cookie"
+       exit 1
+fi
+if ! grep -q "COOKIE: $first_cookie" $outfile
+then
+       cat $outfile
+       echo "Did not get the same first cookie in the response"
+       exit 1
+fi
+
+
+teststep "Secret rollover"
+outfile=cookie_secrets.3
+$PRE/unbound-control -c ub.conf activate_cookie_secret
+$PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+if ! grep -q "^active.*$second_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "^active.*$second_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "^active.*$second_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if ! grep -q "^active.*$second_secret" $outfile \
+       || ! grep -q "^staging.*$first_secret" $outfile
+then
+       cat $outfile
+       echo "Second secret was not activated"
+       exit 1
+fi
+echo ">> activate cookie secret, printout"
+cat $outfile
+echo ">> cookie_secrets.txt"
+cat cookie_secrets.txt
+
+
+teststep "Verify the first cookie can be reused but a new cookie is returned from the second secret"
+outfile=dig.output.4
+dig version.server ch txt @127.0.0.1 -p $SERVER_PORT +cookie=$first_cookie > $outfile
+if grep -q "BADCOOKIE" $outfile
+then
+       cat $outfile
+       echo "Got BADCOOKIE response for a valid cookie"
+       exit 1
+fi
+if ! grep -q "COOKIE: 3132333435363738" $outfile
+then
+       cat $outfile
+       echo "Did not get a cookie in the response"
+       exit 1
+fi
+if grep -q "COOKIE: $first_cookie" $outfile
+then
+       cat $outfile
+       echo "Got the same first cookie in the response while the second secret is active"
+       exit 1
+fi
+second_cookie=$(grep "; COOKIE:" $outfile | cut -d ' ' -f 3)
+cat $outfile
+echo "second cookie: $second_cookie"
+
+
+teststep "Drop cookie secret"
+outfile=cookie_secrets.4
+$PRE/unbound-control -c ub.conf drop_cookie_secret
+$PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+if grep -q "^staging.*$first_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if grep -q "^staging.*$first_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if grep -q "^staging.*$first_secret" $outfile
+then
+       sleep 1
+       $PRE/unbound-control -c ub.conf print_cookie_secrets > $outfile
+fi
+if grep -q "^staging.*$first_secret" $outfile
+then
+       cat $outfile
+       echo "First secret was not dropped"
+       exit 1
+fi
+echo ">> drop cookie secret, printout"
+cat $outfile
+echo ">> cookie_secrets.txt"
+cat cookie_secrets.txt
+
+
+teststep "Verify the first cookie can not be reused and the second cookie is returned instead"
+outfile=dig.output.4
+dig version.server ch txt @127.0.0.1 -p $SERVER_PORT +cookie=$first_cookie > $outfile
+if ! grep -q "BADCOOKIE" $outfile
+then
+       cat $outfile
+       echo "Did not get BADCOOKIE response for an invalid cookie"
+       exit 1
+fi
+if ! grep -q "COOKIE: 3132333435363738" $outfile
+then
+       cat $outfile
+       echo "Did not get a cookie in the response"
+       exit 1
+fi
+if grep -q "COOKIE: $first_cookie" $outfile
+then
+       cat $outfile
+       echo "Got the same first cookie in the response while the second secret is active"
+       exit 1
+fi
+if ! grep -q "COOKIE: $second_cookie" $outfile
+then
+       cat $outfile
+       echo "Did not get the same second cookie in the response"
+       exit 1
+fi
+
+exit 0
index 6c42a80d07115b14e1fd8652c554e221b0232769..9a93befd3061a05e5892538e040125998e4d8a4d 100644 (file)
@@ -387,6 +387,7 @@ config_create(void)
        memset(cfg->cookie_secret, 0, sizeof(cfg->cookie_secret));
        cfg->cookie_secret_len = 16;
        init_cookie_secret(cfg->cookie_secret, cfg->cookie_secret_len);
+       cfg->cookie_secret_file = NULL;
 #ifdef USE_CACHEDB
        if(!(cfg->cachedb_backend = strdup("testframe"))) goto error_exit;
        if(!(cfg->cachedb_secret = strdup("default"))) goto error_exit;
@@ -839,6 +840,8 @@ int config_set_option(struct config_file* cfg, const char* opt,
        { IS_NUMBER_OR_ZERO; cfg->ipsecmod_max_ttl = atoi(val); }
        else S_YNO("ipsecmod-strict:", ipsecmod_strict)
 #endif
+       else S_YNO("answer-cookie:", do_answer_cookie)
+       else S_STR("cookie-secret-file:", cookie_secret_file)
 #ifdef USE_CACHEDB
        else S_YNO("cachedb-no-store:", cachedb_no_store)
        else S_YNO("cachedb-check-when-serve-expired:", cachedb_check_when_serve_expired)
@@ -1336,6 +1339,8 @@ config_get_option(struct config_file* cfg, const char* opt,
        else O_LST(opt, "ipsecmod-whitelist", ipsecmod_whitelist)
        else O_YNO(opt, "ipsecmod-strict", ipsecmod_strict)
 #endif
+       else O_YNO(opt, "answer-cookie", do_answer_cookie)
+       else O_STR(opt, "cookie-secret-file", cookie_secret_file)
 #ifdef USE_CACHEDB
        else O_STR(opt, "backend", cachedb_backend)
        else O_STR(opt, "secret-seed", cachedb_secret)
@@ -1721,6 +1726,7 @@ config_delete(struct config_file* cfg)
        free(cfg->ipsecmod_hook);
        config_delstrlist(cfg->ipsecmod_whitelist);
 #endif
+       free(cfg->cookie_secret_file);
 #ifdef USE_CACHEDB
        free(cfg->cachedb_backend);
        free(cfg->cachedb_secret);
index cca71412737552701419fb861f449e1daa589f84..23aacc67aa77ac6920853d7af0c7d9d6f6358e80 100644 (file)
@@ -750,6 +750,8 @@ struct config_file {
        uint8_t cookie_secret[40];
        /** cookie secret length */
        size_t  cookie_secret_len;
+       /** path to cookie secret store */
+       char* cookie_secret_file;
 
        /* ipset module */
 #ifdef USE_IPSET
index 31a37d50de9a23102d37f6763a166e5b999dd1a3..cd506209229acf6e813e8f9245201bbcdb6421d0 100644 (file)
@@ -582,6 +582,7 @@ udp-upstream-without-downstream{COLON} { YDVAR(1, VAR_UDP_UPSTREAM_WITHOUT_DOWNS
 tcp-connection-limit{COLON}    { YDVAR(2, VAR_TCP_CONNECTION_LIMIT) }
 answer-cookie{COLON}           { YDVAR(1, VAR_ANSWER_COOKIE ) }
 cookie-secret{COLON}           { YDVAR(1, VAR_COOKIE_SECRET) }
+cookie-secret-file{COLON}      { YDVAR(1, VAR_COOKIE_SECRET_FILE) }
 edns-client-string{COLON}      { YDVAR(2, VAR_EDNS_CLIENT_STRING) }
 edns-client-string-opcode{COLON} { YDVAR(1, VAR_EDNS_CLIENT_STRING_OPCODE) }
 nsid{COLON}                    { YDVAR(1, VAR_NSID ) }
index cf026bdad54bd33030cd601bb9af73cc25ee3604..b650b810918bf82af220b7001df0ae9428c2127e 100644 (file)
@@ -205,6 +205,7 @@ extern struct config_parser_state* cfg_parser;
 %token VAR_PROXY_PROTOCOL_PORT VAR_STATISTICS_INHIBIT_ZERO
 %token VAR_HARDEN_UNKNOWN_ADDITIONAL VAR_DISABLE_EDNS_DO VAR_CACHEDB_NO_STORE
 %token VAR_LOG_DESTADDR VAR_CACHEDB_CHECK_WHEN_SERVE_EXPIRED
+%token VAR_COOKIE_SECRET_FILE
 
 %%
 toplevelvars: /* empty */ | toplevelvars toplevelvar ;
@@ -342,7 +343,7 @@ content_server: server_num_threads | server_verbosity | server_port |
        server_interface_automatic_ports | server_ede |
        server_proxy_protocol_port | server_statistics_inhibit_zero |
        server_harden_unknown_additional | server_disable_edns_do |
-       server_log_destaddr
+       server_log_destaddr | server_cookie_secret_file
        ;
 stubstart: VAR_STUB_ZONE
        {
@@ -3998,45 +3999,52 @@ server_cookie_secret: VAR_COOKIE_SECRET STRING_ARG
                free($2);
        }
        ;
-       ipsetstart: VAR_IPSET
-               {
-                       OUTYY(("\nP(ipset:)\n"));
-                       cfg_parser->started_toplevel = 1;
-               }
-               ;
-       contents_ipset: contents_ipset content_ipset
-               | ;
-       content_ipset: ipset_name_v4 | ipset_name_v6
-               ;
-       ipset_name_v4: VAR_IPSET_NAME_V4 STRING_ARG
-               {
-               #ifdef USE_IPSET
-                       OUTYY(("P(name-v4:%s)\n", $2));
-                       if(cfg_parser->cfg->ipset_name_v4)
-                               yyerror("ipset name v4 override, there must be one "
-                                       "name for ip v4");
-                       free(cfg_parser->cfg->ipset_name_v4);
-                       cfg_parser->cfg->ipset_name_v4 = $2;
-               #else
-                       OUTYY(("P(Compiled without ipset, ignoring)\n"));
-                       free($2);
-               #endif
-               }
+server_cookie_secret_file: VAR_COOKIE_SECRET_FILE STRING_ARG
+       {
+               OUTYY(("P(cookie_secret_file:%s)\n", $2));
+               free(cfg_parser->cfg->cookie_secret_file);
+               cfg_parser->cfg->cookie_secret_file = $2;
+       }
        ;
-       ipset_name_v6: VAR_IPSET_NAME_V6 STRING_ARG
-       {
-               #ifdef USE_IPSET
-                       OUTYY(("P(name-v6:%s)\n", $2));
-                       if(cfg_parser->cfg->ipset_name_v6)
-                               yyerror("ipset name v6 override, there must be one "
-                                       "name for ip v6");
-                       free(cfg_parser->cfg->ipset_name_v6);
-                       cfg_parser->cfg->ipset_name_v6 = $2;
-               #else
-                       OUTYY(("P(Compiled without ipset, ignoring)\n"));
-                       free($2);
-               #endif
-               }
+ipsetstart: VAR_IPSET
+       {
+               OUTYY(("\nP(ipset:)\n"));
+               cfg_parser->started_toplevel = 1;
+       }
+       ;
+contents_ipset: contents_ipset content_ipset
+       | ;
+content_ipset: ipset_name_v4 | ipset_name_v6
+       ;
+ipset_name_v4: VAR_IPSET_NAME_V4 STRING_ARG
+       {
+       #ifdef USE_IPSET
+               OUTYY(("P(name-v4:%s)\n", $2));
+               if(cfg_parser->cfg->ipset_name_v4)
+                       yyerror("ipset name v4 override, there must be one "
+                               "name for ip v4");
+               free(cfg_parser->cfg->ipset_name_v4);
+               cfg_parser->cfg->ipset_name_v4 = $2;
+       #else
+               OUTYY(("P(Compiled without ipset, ignoring)\n"));
+               free($2);
+       #endif
+       }
+       ;
+ipset_name_v6: VAR_IPSET_NAME_V6 STRING_ARG
+       {
+       #ifdef USE_IPSET
+               OUTYY(("P(name-v6:%s)\n", $2));
+               if(cfg_parser->cfg->ipset_name_v6)
+                       yyerror("ipset name v6 override, there must be one "
+                               "name for ip v6");
+               free(cfg_parser->cfg->ipset_name_v6);
+               cfg_parser->cfg->ipset_name_v6 = $2;
+       #else
+               OUTYY(("P(Compiled without ipset, ignoring)\n"));
+               free($2);
+       #endif
+       }
        ;
 %%
 
index ab1e0b557f09ddbc79f3248a664dd454dce052f5..6963d850171ee5d59736004e05419b74f153b277 100644 (file)
@@ -947,7 +947,8 @@ parse_packet(sldns_buffer* pkt, struct msg_parse* msg, struct regional* region)
 static int
 parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len,
        struct edns_data* edns, struct config_file* cfg, struct comm_point* c,
-       struct comm_reply* repinfo, uint32_t now, struct regional* region)
+       struct comm_reply* repinfo, uint32_t now, struct regional* region,
+       struct cookie_secrets* cookie_secrets)
 {
        /* To respond with a Keepalive option, the client connection must have
         * received one message with a TCP Keepalive EDNS option, and that
@@ -1070,13 +1071,24 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len,
                                        &((struct sockaddr_in6*)&repinfo->remote_addr)->sin6_addr, 16);
                        }
 
-                       cookie_val_status = edns_cookie_server_validate(
-                               rdata_ptr, opt_len, cfg->cookie_secret,
-                               cfg->cookie_secret_len, cookie_is_v4,
-                               server_cookie, now);
+                       if(cfg->cookie_secret_file &&
+                               cfg->cookie_secret_file[0]) {
+                               /* Loop over the active and staging cookies. */
+                               cookie_val_status =
+                                       cookie_secrets_server_validate(
+                                       rdata_ptr, opt_len, cookie_secrets,
+                                       cookie_is_v4, server_cookie, now);
+                       } else {
+                               /* Use the cookie option value to validate. */
+                               cookie_val_status = edns_cookie_server_validate(
+                                       rdata_ptr, opt_len, cfg->cookie_secret,
+                                       cfg->cookie_secret_len, cookie_is_v4,
+                                       server_cookie, now);
+                       }
+                       if(cookie_val_status == COOKIE_STATUS_VALID_RENEW)
+                               edns->cookie_valid = 1;
                        switch(cookie_val_status) {
                        case COOKIE_STATUS_VALID:
-                       case COOKIE_STATUS_VALID_RENEW:
                                edns->cookie_valid = 1;
                                /* Reuse cookie */
                                if(!edns_opt_list_append(
@@ -1093,12 +1105,28 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len,
                                edns->cookie_client = 1;
                                ATTR_FALLTHROUGH
                                /* fallthrough */
+                       case COOKIE_STATUS_VALID_RENEW:
                        case COOKIE_STATUS_FUTURE:
                        case COOKIE_STATUS_EXPIRED:
                        case COOKIE_STATUS_INVALID:
                        default:
-                               edns_cookie_server_write(server_cookie,
-                                       cfg->cookie_secret, cookie_is_v4, now);
+                               if(cfg->cookie_secret_file &&
+                                       cfg->cookie_secret_file[0]) {
+                                       if(!cookie_secrets)
+                                               break;
+                                       lock_basic_lock(&cookie_secrets->lock);
+                                       if(cookie_secrets->cookie_count < 1) {
+                                               lock_basic_unlock(&cookie_secrets->lock);
+                                               break;
+                                       }
+                                       edns_cookie_server_write(server_cookie,
+                                               cookie_secrets->cookie_secrets[0].cookie_secret,
+                                               cookie_is_v4, now);
+                                       lock_basic_unlock(&cookie_secrets->lock);
+                               } else {
+                                       edns_cookie_server_write(server_cookie,
+                                               cfg->cookie_secret, cookie_is_v4, now);
+                               }
                                if(!edns_opt_list_append(&edns->opt_list_out,
                                        LDNS_EDNS_COOKIE, 24, server_cookie,
                                        region)) {
@@ -1240,7 +1268,8 @@ skip_pkt_rrs(sldns_buffer* pkt, int num)
 int 
 parse_edns_from_query_pkt(sldns_buffer* pkt, struct edns_data* edns,
        struct config_file* cfg, struct comm_point* c,
-       struct comm_reply* repinfo, time_t now, struct regional* region)
+       struct comm_reply* repinfo, time_t now, struct regional* region,
+       struct cookie_secrets* cookie_secrets)
 {
        size_t rdata_len;
        uint8_t* rdata_ptr;
@@ -1286,7 +1315,7 @@ parse_edns_from_query_pkt(sldns_buffer* pkt, struct edns_data* edns,
        rdata_ptr = sldns_buffer_current(pkt);
        /* ignore rrsigs */
        return parse_edns_options_from_query(rdata_ptr, rdata_len, edns, cfg,
-               c, repinfo, now, region);
+               c, repinfo, now, region, cookie_secrets);
 }
 
 void
index 656e0d285dcdc974cd8015388eae60324fd63ad5..aebd48efac34f8ba3269b84f8626110aa8328649 100644 (file)
@@ -73,6 +73,7 @@ struct edns_option;
 struct config_file;
 struct comm_point;
 struct comm_reply;
+struct cookie_secrets;
 
 /** number of buckets in parse rrset hash table. Must be power of 2. */
 #define PARSE_TABLE_SIZE 32
@@ -322,12 +323,14 @@ int skip_pkt_rrs(struct sldns_buffer* pkt, int num);
  * @param repinfo: commreply to determine the client address
  * @param now: current time
  * @param region: region to alloc results in (edns option contents)
+ * @param cookie_secrets: the cookie secrets for EDNS COOKIE validation.
  * @return: 0 on success, or an RCODE on error.
  *     RCODE formerr if OPT is badly formatted and so on.
  */
 int parse_edns_from_query_pkt(struct sldns_buffer* pkt, struct edns_data* edns,
        struct config_file* cfg, struct comm_point* c,
-       struct comm_reply* repinfo, time_t now, struct regional* region);
+       struct comm_reply* repinfo, time_t now, struct regional* region,
+       struct cookie_secrets* cookie_secrets);
 
 /**
  * Calculate hash value for rrset in packet.
index 2b4047f0b60067adea4e55174595dae4ae04a40f..ee95a69122093ec6fe4cb692ca8b22079f45c021 100644 (file)
@@ -187,3 +187,189 @@ edns_cookie_server_validate(const uint8_t* cookie, size_t cookie_len,
                return COOKIE_STATUS_VALID_RENEW;
        return COOKIE_STATUS_VALID;
 }
+
+struct cookie_secrets*
+cookie_secrets_create(void)
+{
+       struct cookie_secrets* cookie_secrets = calloc(1,
+               sizeof(*cookie_secrets));
+       if(!cookie_secrets)
+               return NULL;
+       lock_basic_init(&cookie_secrets->lock);
+       lock_protect(&cookie_secrets->lock, &cookie_secrets->cookie_count,
+               sizeof(cookie_secrets->cookie_count));
+       lock_protect(&cookie_secrets->lock, cookie_secrets->cookie_secrets,
+               sizeof(cookie_secret_type)*UNBOUND_COOKIE_HISTORY_SIZE);
+       return cookie_secrets;
+}
+
+void
+cookie_secrets_delete(struct cookie_secrets* cookie_secrets)
+{
+       if(!cookie_secrets)
+               return;
+       lock_basic_destroy(&cookie_secrets->lock);
+       explicit_bzero(cookie_secrets->cookie_secrets,
+               sizeof(cookie_secret_type)*UNBOUND_COOKIE_HISTORY_SIZE);
+       free(cookie_secrets);
+}
+
+/** Read the cookie secret file */
+static int
+cookie_secret_file_read(struct cookie_secrets* cookie_secrets,
+       char* cookie_secret_file)
+{
+       char secret[UNBOUND_COOKIE_SECRET_SIZE * 2 + 2/*'\n' and '\0'*/];
+       FILE* f;
+       int corrupt = 0;
+       size_t count;
+
+       log_assert(cookie_secret_file != NULL);
+       cookie_secrets->cookie_count = 0;
+       f = fopen(cookie_secret_file, "r");
+       /* a non-existing cookie file is not an error */
+       if( f == NULL ) {
+               if(errno != EPERM) {
+                       log_err("Could not read cookie-secret-file '%s': %s",
+                               cookie_secret_file, strerror(errno));
+                       return 0;
+               }
+               return 1;
+       }
+       /* cookie secret file exists and is readable */
+       for( count = 0; count < UNBOUND_COOKIE_HISTORY_SIZE; count++ ) {
+               size_t secret_len = 0;
+               ssize_t decoded_len = 0;
+               if( fgets(secret, sizeof(secret), f) == NULL ) { break; }
+               secret_len = strlen(secret);
+               if( secret_len == 0 ) { break; }
+               log_assert( secret_len <= sizeof(secret) );
+               secret_len = secret[secret_len - 1] == '\n' ? secret_len - 1 : secret_len;
+               if( secret_len != UNBOUND_COOKIE_SECRET_SIZE * 2 ) { corrupt++; break; }
+               /* needed for `hex_pton`; stripping potential `\n` */
+               secret[secret_len] = '\0';
+               decoded_len = hex_pton(secret, cookie_secrets->cookie_secrets[count].cookie_secret,
+                                      UNBOUND_COOKIE_SECRET_SIZE);
+               if( decoded_len != UNBOUND_COOKIE_SECRET_SIZE ) { corrupt++; break; }
+               cookie_secrets->cookie_count++;
+       }
+       fclose(f);
+       return corrupt == 0;
+}
+
+int
+cookie_secrets_apply_cfg(struct cookie_secrets* cookie_secrets,
+       char* cookie_secret_file)
+{
+       if(!cookie_secrets) {
+               if(!cookie_secret_file || !cookie_secret_file[0])
+                       return 1; /* There is nothing to read anyway */
+               log_err("Could not read cookie secrets, no structure alloced");
+               return 0;
+       }
+       if(!cookie_secret_file_read(cookie_secrets, cookie_secret_file))
+               return 0;
+       return 1;
+}
+
+enum edns_cookie_val_status
+cookie_secrets_server_validate(const uint8_t* cookie, size_t cookie_len,
+       struct cookie_secrets* cookie_secrets, int v4,
+       const uint8_t* hash_input, uint32_t now)
+{
+       size_t i;
+       enum edns_cookie_val_status cookie_val_status,
+               last = COOKIE_STATUS_INVALID;
+       if(!cookie_secrets)
+               return COOKIE_STATUS_INVALID; /* There are no cookie secrets.*/
+       lock_basic_lock(&cookie_secrets->lock);
+       if(cookie_secrets->cookie_count == 0) {
+               lock_basic_unlock(&cookie_secrets->lock);
+               return COOKIE_STATUS_INVALID; /* There are no cookie secrets.*/
+       }
+       for(i=0; i<cookie_secrets->cookie_count; i++) {
+               cookie_val_status = edns_cookie_server_validate(cookie,
+                       cookie_len,
+                       cookie_secrets->cookie_secrets[i].cookie_secret,
+                       UNBOUND_COOKIE_SECRET_SIZE, v4, hash_input, now);
+               if(cookie_val_status == COOKIE_STATUS_VALID ||
+                       cookie_val_status == COOKIE_STATUS_VALID_RENEW) {
+                       lock_basic_unlock(&cookie_secrets->lock);
+                       /* For staging cookies, write a fresh cookie. */
+                       if(i != 0)
+                               return COOKIE_STATUS_VALID_RENEW;
+                       return cookie_val_status;
+               }
+               if(last == COOKIE_STATUS_INVALID)
+                       last = cookie_val_status; /* Store more interesting
+                               failure to return. */
+       }
+       lock_basic_unlock(&cookie_secrets->lock);
+       return last;
+}
+
+void add_cookie_secret(struct cookie_secrets* cookie_secrets,
+       uint8_t* secret, size_t secret_len)
+{
+       log_assert(secret_len == UNBOUND_COOKIE_SECRET_SIZE);
+       (void)secret_len;
+       if(!cookie_secrets)
+               return;
+
+       /* New cookie secret becomes the staging secret (position 1)
+        * unless there is no active cookie yet, then it becomes the active
+        * secret.  If the UNBOUND_COOKIE_HISTORY_SIZE > 2 then all staging cookies
+        * are moved one position down.
+        */
+       if(cookie_secrets->cookie_count == 0) {
+               memcpy( cookie_secrets->cookie_secrets->cookie_secret
+                      , secret, UNBOUND_COOKIE_SECRET_SIZE);
+               cookie_secrets->cookie_count = 1;
+               explicit_bzero(secret, UNBOUND_COOKIE_SECRET_SIZE);
+               return;
+       }
+#if UNBOUND_COOKIE_HISTORY_SIZE > 2
+       memmove( &cookie_secrets->cookie_secrets[2], &cookie_secrets->cookie_secrets[1]
+              , sizeof(struct cookie_secret) * (UNBOUND_COOKIE_HISTORY_SIZE - 2));
+#endif
+       memcpy( cookie_secrets->cookie_secrets[1].cookie_secret
+             , secret, UNBOUND_COOKIE_SECRET_SIZE);
+       cookie_secrets->cookie_count = cookie_secrets->cookie_count     < UNBOUND_COOKIE_HISTORY_SIZE
+                         ? cookie_secrets->cookie_count + 1 : UNBOUND_COOKIE_HISTORY_SIZE;
+       explicit_bzero(secret, UNBOUND_COOKIE_SECRET_SIZE);
+}
+
+void activate_cookie_secret(struct cookie_secrets* cookie_secrets)
+{
+       uint8_t active_secret[UNBOUND_COOKIE_SECRET_SIZE];
+       if(!cookie_secrets)
+               return;
+       /* The staging secret becomes the active secret.
+        * The active secret becomes a staging secret.
+        * If the UNBOUND_COOKIE_HISTORY_SIZE > 2 then all staging secrets are moved
+        * one position up and the previously active secret becomes the last
+        * staging secret.
+        */
+       if(cookie_secrets->cookie_count < 2)
+               return;
+       memcpy( active_secret, cookie_secrets->cookie_secrets[0].cookie_secret
+             , UNBOUND_COOKIE_SECRET_SIZE);
+       memmove( &cookie_secrets->cookie_secrets[0], &cookie_secrets->cookie_secrets[1]
+              , sizeof(struct cookie_secret) * (UNBOUND_COOKIE_HISTORY_SIZE - 1));
+       memcpy( cookie_secrets->cookie_secrets[cookie_secrets->cookie_count - 1].cookie_secret
+             , active_secret, UNBOUND_COOKIE_SECRET_SIZE);
+       explicit_bzero(active_secret, UNBOUND_COOKIE_SECRET_SIZE);
+}
+
+void drop_cookie_secret(struct cookie_secrets* cookie_secrets)
+{
+       if(!cookie_secrets)
+               return;
+       /* Drops a staging cookie secret. If there are more than one, it will
+        * drop the last staging secret. */
+       if(cookie_secrets->cookie_count < 2)
+               return;
+       explicit_bzero( cookie_secrets->cookie_secrets[cookie_secrets->cookie_count - 1].cookie_secret
+                     , UNBOUND_COOKIE_SECRET_SIZE);
+       cookie_secrets->cookie_count -= 1;
+}
index 5da0ecb290a711a27ceb9c78fea3446bf06c720b..47ccb1ad2cd3280b8bb3c94d8db7f4a742550bc0 100644 (file)
@@ -43,6 +43,7 @@
 #define UTIL_EDNS_H
 
 #include "util/storage/dnstree.h"
+#include "util/locks.h"
 
 struct edns_data;
 struct config_file;
@@ -75,6 +76,31 @@ struct edns_string_addr {
        size_t string_len;
 };
 
+#define UNBOUND_COOKIE_HISTORY_SIZE 2
+#define UNBOUND_COOKIE_SECRET_SIZE 16
+
+typedef struct cookie_secret cookie_secret_type;
+struct cookie_secret {
+       /** cookie secret */
+       uint8_t cookie_secret[UNBOUND_COOKIE_SECRET_SIZE];
+};
+
+/**
+ * The cookie secrets from the cookie-secret-file.
+ */
+struct cookie_secrets {
+       /** lock on the structure, in case there are modifications
+        * from remote control, this avoids race conditions. */
+       lock_basic_type lock;
+
+       /** how many cookies are there in the cookies array */
+       size_t cookie_count;
+
+       /* keep track of the last `UNBOUND_COOKIE_HISTORY_SIZE`
+        * cookies as per rfc requirement .*/
+       cookie_secret_type cookie_secrets[UNBOUND_COOKIE_HISTORY_SIZE];
+};
+
 enum edns_cookie_val_status {
        COOKIE_STATUS_CLIENT_ONLY = -3,
        COOKIE_STATUS_FUTURE = -2,
@@ -165,4 +191,63 @@ enum edns_cookie_val_status edns_cookie_server_validate(const uint8_t* cookie,
        size_t cookie_len, const uint8_t* secret, size_t secret_len, int v4,
        const uint8_t* hash_input, uint32_t now);
 
+/**
+ * Create the cookie secrets structure.
+ * @return the structure or NULL on failure.
+ */
+struct cookie_secrets* cookie_secrets_create(void);
+
+/**
+ * Delete the cookie secrets.
+ * @param cookie_secrets: the cookie secrets.
+ */
+void cookie_secrets_delete(struct cookie_secrets* cookie_secrets);
+
+/**
+ * Apply configuration to cookie secrets, read them from file.
+ * @param cookie_secrets: the cookie secrets structure.
+ * @param cookie_secret_file: the file name, it is read.
+ * @return false on failure.
+ */
+int cookie_secrets_apply_cfg(struct cookie_secrets* cookie_secrets,
+       char* cookie_secret_file);
+
+/**
+ * Validate the cookie secrets, try all of them.
+ * @param cookie: pointer to the cookie data.
+ * @param cookie_len: the length of the cookie data.
+ * @param cookie_secrets: struct of cookie secrets.
+ * @param v4: if the client IP is v4 or v6.
+ * @param hash_input: pointer to the hash input for validation. It needs to be:
+ *     Client Cookie | Version | Reserved | Timestamp | Client-IP
+ * @param now: the current time.
+ * return edns_cookie_val_status with the cookie validation status i.e.,
+ *     <=0 for invalid, else valid.
+ */
+enum edns_cookie_val_status cookie_secrets_server_validate(
+       const uint8_t* cookie, size_t cookie_len,
+       struct cookie_secrets* cookie_secrets, int v4,
+       const uint8_t* hash_input, uint32_t now);
+
+/**
+ * Add a cookie secret. If there are no secrets yet, the secret will become
+ * the active secret. Otherwise it will become the staging secret.
+ * Active secrets are used to both verify and create new DNS Cookies.
+ * Staging secrets are only used to verify DNS Cookies. Caller has to lock.
+ */
+void add_cookie_secret(struct cookie_secrets* cookie_secrets, uint8_t* secret,
+       size_t secret_len);
+
+/**
+ * Makes the staging cookie secret active and the active secret staging.
+ * Caller has to lock.
+ */
+void activate_cookie_secret(struct cookie_secrets* cookie_secrets);
+
+/**
+ * Drop a cookie secret. Drops the staging secret. An active secret will not
+ * be dropped. Caller has to lock.
+ */
+void drop_cookie_secret(struct cookie_secrets* cookie_secrets);
+
 #endif
index 7723338163752b032cec3fea3b3b785928b500c8..5cf702ef9bcaafd1c60df9fbb27e3dc2bacd5111 100644 (file)
@@ -47,6 +47,7 @@
 #ifdef HAVE_NETIOAPI_H
 #include <netioapi.h>
 #endif
+#include <ctype.h>
 #include "util/net_help.h"
 #include "util/log.h"
 #include "util/data/dname.h"
@@ -1871,3 +1872,42 @@ sock_close(int socket)
        closesocket(socket);
 }
 #  endif /* USE_WINSOCK */
+
+ssize_t
+hex_ntop(uint8_t const *src, size_t srclength, char *target, size_t targsize)
+{
+       static char hexdigits[] = {
+               '0', '1', '2', '3', '4', '5', '6', '7',
+               '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+       };
+       size_t i;
+
+       if (targsize < srclength * 2 + 1) {
+               return -1;
+       }
+
+       for (i = 0; i < srclength; ++i) {
+               *target++ = hexdigits[src[i] >> 4U];
+               *target++ = hexdigits[src[i] & 0xfU];
+       }
+       *target = '\0';
+       return 2 * srclength;
+}
+
+ssize_t
+hex_pton(const char* src, uint8_t* target, size_t targsize)
+{
+       uint8_t *t = target;
+       if(strlen(src) % 2 != 0 || strlen(src)/2 > targsize) {
+               return -1;
+       }
+       while(*src) {
+               if(!isxdigit((unsigned char)src[0]) ||
+                       !isxdigit((unsigned char)src[1]))
+                       return -1;
+               *t++ = sldns_hexdigit_to_int(src[0]) * 16 +
+                       sldns_hexdigit_to_int(src[1]) ;
+               src += 2;
+       }
+       return t-target;
+}
index 1c57b5b7019c38930bd88add35af56a273079434..28245ea0c1eea60806bcc765ccce869548b7ed43 100644 (file)
@@ -572,4 +572,13 @@ char* sock_strerror(int errn);
 /** close the socket with close, or wsa closesocket */
 void sock_close(int socket);
 
+/**
+ * Convert binary data to a string of hexadecimal characters.
+ */
+ssize_t hex_ntop(uint8_t const *src, size_t srclength, char *target,
+                size_t targsize);
+
+/** Convert hexadecimal data to binary. */
+ssize_t hex_pton(const char* src, uint8_t* target, size_t targsize);
+
 #endif /* NET_HELP_H */