Browse Source

Import upstream version 11

Sergio Correia 2 years ago
parent
commit
b136f0618e

+ 64 - 0
.github/workflows/build.yml

@@ -0,0 +1,64 @@
+---
+name: build
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-18.04
+    continue-on-error: ${{ ! matrix.stable }}
+    strategy:
+      matrix:
+        os:
+          - fedora:latest
+          - centos:7
+          - centos:8
+          - debian:testing
+          - debian:latest
+          - ubuntu:rolling
+          - ubuntu:bionic
+        stable: [true]
+        include:
+          - os: fedora:rawhide
+            stable: false
+          - os: ubuntu:devel
+            stable: false
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Show OS information
+        run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
+
+      - name: Install build dependencies
+        run: bash .github/workflows/install-dependencies
+
+      - name: Build tang
+        run: |
+          mkdir -p build && cd build
+          export ninja=$(command -v ninja)
+          [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
+          meson .. || cat meson-logs/meson-log.txt >&2
+          ${ninja}
+
+      - name: Run tests
+        run: |
+          cd build
+          if ! meson test ; then
+              cat meson-logs/testlog.txt >&2
+              exit -1
+          fi
+
+      - name: Show full test logs
+        run: |
+          if [ -r build/meson-logs/testlog.txt ]; then
+            cat build/meson-logs/testlog.txt >&2
+          else
+            echo "No test log available" >&2
+          fi
+
+    container:
+      image: ${{matrix.os}}
+      env:
+        DISTRO: ${{matrix.os}}
+
+# vim:set ts=2 sw=2 et:

+ 61 - 0
.github/workflows/coverage.yml

@@ -0,0 +1,61 @@
+---
+name: coverage
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-18.04
+    strategy:
+      matrix:
+        os:
+          - ubuntu:latest
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Show OS information
+        run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
+
+      - name: Install build dependencies
+        run: bash .github/workflows/install-dependencies
+
+      - name: Build tang
+        run: |
+          mkdir -p build && cd build
+          export ninja=$(command -v ninja)
+          [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
+          meson .. -Db_coverage=true || cat meson-logs/meson-log.txt >&2
+          ${ninja}
+
+      - name: Run tests
+        run: |
+          cd build
+          meson test || cat meson-logs/testlog.txt >&2
+
+      - name: Show full test logs
+        run: |
+          if [ -r build/meson-logs/testlog.txt ]; then
+            cat build/meson-logs/testlog.txt >&2
+          else
+            echo "No test log available" >&2
+          fi
+
+      - name: Create coverage report
+        run: |
+          cd build
+          export ninja=$(command -v ninja)
+          [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
+          gcovr -r .. -f ../src -f src/ -e ../tests -e tests -x coverage.xml
+
+      - uses: codecov/codecov-action@v1
+        with:
+          file: build/coverage.xml
+          fail_ci_if_error: true # optional (default = false)
+          verbose: true # optional (default = false)
+
+    container:
+      image: ${{matrix.os}}
+      env:
+        DISTRO: ${{matrix.os}}
+
+# vim:set ts=2 sw=2 et:

+ 30 - 0
.github/workflows/install-dependencies

@@ -0,0 +1,30 @@
+#!/bin/sh -ex
+
+case "${DISTRO}" in
+debian:*|ubuntu:*)
+    export DEBIAN_FRONTEND=noninteractive
+    apt clean
+    apt update
+    apt -y install gcc meson pkg-config libjose-dev jose libhttp-parser-dev \
+                   systemd gcovr curl socat
+    ;;
+
+fedora:*)
+    echo 'max_parallel_downloads=10' >> /etc/dnf/dnf.conf
+    dnf -y clean all
+    dnf -y --setopt=deltarpm=0 update
+    dnf -y install gcc meson pkgconfig libjose-devel jose http-parser-devel \
+                   systemd gcovr curl socat
+    ;;
+
+centos:*)
+    yum -y clean all
+    yum -y --setopt=deltarpm=0 update
+    yum install -y yum-utils epel-release
+    yum config-manager -y --set-enabled PowerTools \
+        || yum config-manager -y --set-enabled powertools || :
+    yum -y install meson socat
+    yum-builddep -y tang
+    ;;
+esac
+# vim: set ts=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:

+ 1 - 0
doc/meson.build

@@ -1,4 +1,5 @@
 mans += join_paths(meson.current_source_dir(), 'tang-show-keys.1')
 mans += join_paths(meson.current_source_dir(), 'tang-show-keys.1')
+mans += join_paths(meson.current_source_dir(), 'tangd-rotate-keys.1')
 mans += join_paths(meson.current_source_dir(), 'tang.8')
 mans += join_paths(meson.current_source_dir(), 'tang.8')
 
 
 # vim:set ts=2 sw=2 et:
 # vim:set ts=2 sw=2 et:

+ 19 - 1
doc/tang.8.adoc

@@ -40,7 +40,13 @@ protocol, see the Tang project's homepage.
 
 
 Getting a Tang server up and running is simple:
 Getting a Tang server up and running is simple:
 
 
+ifdef::freebsd[]
+    $ sudo service tangd enable
+    $ sudo service tangd start
+endif::[]
+ifndef::freebsd[]
     $ sudo systemctl enable tangd.socket --now
     $ sudo systemctl enable tangd.socket --now
+endif::[]
 
 
 That's it. The server is now running with a fresh set of cryptographic keys
 That's it. The server is now running with a fresh set of cryptographic keys
 and will automatically start on the next reboot.
 and will automatically start on the next reboot.
@@ -49,8 +55,13 @@ and will automatically start on the next reboot.
 
 
 Tang intends to be a minimal network service and therefore does not have any
 Tang intends to be a minimal network service and therefore does not have any
 configuration. To adjust the network settings, you can override the
 configuration. To adjust the network settings, you can override the
+ifdef::freebsd[]
+variables in the */usr/local/etc/rc.d/tangd* file.
+endif::[]
+ifndef::freebsd[]
 *tangd.socket* unit file using the standard systemd mechanisms. See
 *tangd.socket* unit file using the standard systemd mechanisms. See
 link:systemd.unit.5.adoc[*systemd.unit*(5)] and link:systemd.socket.5.adoc[*systemd.socket*(5)] for more information.
 link:systemd.unit.5.adoc[*systemd.unit*(5)] and link:systemd.socket.5.adoc[*systemd.socket*(5)] for more information.
+endif::[]
 
 
 == KEY ROTATION
 == KEY ROTATION
 
 
@@ -59,6 +70,10 @@ periodically rotate your keys. The precise interval at which you should rotate
 depends upon your application, key sizes and institutional policy. For some
 depends upon your application, key sizes and institutional policy. For some
 common recommendations, see: https://www.keylength.com.
 common recommendations, see: https://www.keylength.com.
 
 
+There is a convenience script to deal with this. See
+link:tangd-rotate-keys.1.adoc[*tangd-rotate-keys*(1)] for more information.
+This can also be performed manually as described below.
+
 To rotate keys, first we need to generate new keys in the key database
 To rotate keys, first we need to generate new keys in the key database
 directory. This is typically */var/db/tang*. For example, you can create
 directory. This is typically */var/db/tang*. For example, you can create
 new signature and exchange keys with the following commands:
 new signature and exchange keys with the following commands:
@@ -130,10 +145,13 @@ Nathaniel McCallum <npmccallum@redhat.com>
 
 
 == SEE ALSO
 == SEE ALSO
 
 
+ifndef::freebsd[]
 link:systemd.unit.5.adoc[*systemd.unit*(5)],
 link:systemd.unit.5.adoc[*systemd.unit*(5)],
 link:systemd.socket.5.adoc[*systemd.socket*(5)],
 link:systemd.socket.5.adoc[*systemd.socket*(5)],
+endif::[]
 link:jose-jwk-gen.1.adoc[*jose-jwk-gen*(1)],
 link:jose-jwk-gen.1.adoc[*jose-jwk-gen*(1)],
-link:tang-show-keys.1.adoc[*tang-show-keys*(1)]
+link:tang-show-keys.1.adoc[*tang-show-keys*(1)],
+link:tangd-rotate-keys.1.adoc[*tangd-rotate-keys*(1)]
 
 
 == FURTHER READING
 == FURTHER READING
 
 

+ 46 - 0
doc/tangd-rotate-keys.1.adoc

@@ -0,0 +1,46 @@
+tangd-rotate-keys(1)
+====================
+:doctype: manpage
+
+== NAME
+
+tangd-rotate-keys - Perform rotation of tang keys
+
+== SYNOPSIS
+
+*tangd-rotate-keys* [-h] [-v] -d <KEYDIR>
+
+== DESCRIPTION
+
+in order to preserve the security of the system over the long run, you need to periodically
+rotate your keys. The precise interval at which you should rotate depends upon your application,
+key sizes and institutional policy. For some common recommendations, see: https://www.keylength.com.
+
+*tangd-rotate-keys* generates new keys in the key database directory given by the *-d* option.
+This is typically */var/db/tang*. It also rename the old keys to have a leading . in order to
+hide them from advertisement.
+
+Tang will immediately pick up all changes. No restart is required.
+
+At this point, new client bindings will pick up the new keys and old clients can continue to
+utilize the old keys. Once you are sure that all the old clients have been migrated to use the
+new keys, you can remove the old keys. Be aware that removing the old keys while clients are
+still using them can result in data loss. You have been warned.
+
+== OPTIONS
+*  *-d* <KEYDIR>:
+  The directory with the keys, e.g. /var/db/tang
+
+* *-h*:
+  Display the usage information
+
+* *-v*:
+  Verbose. Display additional info on keys created/rotated
+
+== AUTHOR
+
+Sergio Correia <scorreia@redhat.com>
+
+== SEE ALSO
+
+link:tang.8.adoc[*tang*(8)]

+ 10 - 4
meson.build

@@ -1,5 +1,5 @@
 project('tang', 'c',
 project('tang', 'c',
-  version: '10',
+  version: '11',
   license: 'GPL3+',
   license: 'GPL3+',
   default_options: [
   default_options: [
     'c_std=c99',
     'c_std=c99',
@@ -16,7 +16,7 @@ sysconfdir = join_paths(get_option('prefix'), get_option('sysconfdir'))
 bindir = join_paths(get_option('prefix'), get_option('bindir'))
 bindir = join_paths(get_option('prefix'), get_option('bindir'))
 systemunitdir = join_paths(get_option('prefix'), 'lib/systemd/system')
 systemunitdir = join_paths(get_option('prefix'), 'lib/systemd/system')
 licensedir = join_paths(get_option('prefix'), 'share', 'licenses', meson.project_name())
 licensedir = join_paths(get_option('prefix'), 'share', 'licenses', meson.project_name())
-if build_machine.system() == 'freebsd'
+if host_machine.system() == 'freebsd'
   licensedir += '-'+meson.project_version()
   licensedir += '-'+meson.project_version()
 endif
 endif
 jwkdir = join_paths(get_option('localstatedir'), 'db', meson.project_name())
 jwkdir = join_paths(get_option('localstatedir'), 'db', meson.project_name())
@@ -26,6 +26,8 @@ data.set('libexecdir', libexecdir)
 data.set('sysconfdir', sysconfdir)
 data.set('sysconfdir', sysconfdir)
 data.set('systemunitdir', systemunitdir)
 data.set('systemunitdir', systemunitdir)
 data.set('jwkdir', jwkdir)
 data.set('jwkdir', jwkdir)
+data.set('user', get_option('user'))
+data.set('group', get_option('group'))
 
 
 add_project_arguments(
 add_project_arguments(
   '-D_POSIX_C_SOURCE=200809L',
   '-D_POSIX_C_SOURCE=200809L',
@@ -54,7 +56,11 @@ compiler = meson.get_compiler('c')
 if not compiler.has_header('http_parser.h',args : '-I/usr/local/include')
 if not compiler.has_header('http_parser.h',args : '-I/usr/local/include')
   error('http-parser devel files not found.')
   error('http-parser devel files not found.')
 endif
 endif
-http_parser = compiler.find_library('http_parser',dirs:['/usr/lib','/usr/local/lib'])
+if host_machine.system() == 'freebsd'
+  http_parser = compiler.find_library('http_parser',dirs : '/usr/local/lib')
+else
+  http_parser = compiler.find_library('http_parser')
+endif
 
 
 licenses = ['COPYING']
 licenses = ['COPYING']
 libexecbins = []
 libexecbins = []
@@ -75,7 +81,7 @@ install_data(licenses, install_dir: licensedir)
 if a2x.found()
 if a2x.found()
   foreach m : mans
   foreach m : mans
     custom_target(m.split('/')[-1], input: m + '.adoc', output: m.split('/')[-1],
     custom_target(m.split('/')[-1], input: m + '.adoc', output: m.split('/')[-1],
-      command: [a2x, '-f', 'manpage', '-D', meson.current_build_dir(), '@INPUT@'],
+      command: [a2x, '--attribute=' + build_machine.system(), '-f', 'manpage', '-D', meson.current_build_dir(), '@INPUT@'],
       install_dir: join_paths(get_option('mandir'), 'man' + m.split('.')[-1]),
       install_dir: join_paths(get_option('mandir'), 'man' + m.split('.')[-1]),
       install: true
       install: true
     )
     )

+ 2 - 0
meson_options.txt

@@ -0,0 +1,2 @@
+option('user', type: 'string', value: 'tang', description: 'Unprivileged user for tang operations')
+option('group', type: 'string', value: 'tang', description: 'Unprivileged group for tang operations')

+ 24 - 16
src/keys.c

@@ -17,6 +17,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
  */
 
 
+#include <sys/stat.h>
 #include <stdlib.h>
 #include <stdlib.h>
 #include <string.h>
 #include <string.h>
 #include <dirent.h>
 #include <dirent.h>
@@ -263,23 +264,10 @@ find_by_thp(struct tang_keys_info* tki, const char* target)
     json_array_foreach(keys, idx, jwk) {
     json_array_foreach(keys, idx, jwk) {
         for (int i = 0; hashes[i]; i++) {
         for (int i = 0; hashes[i]; i++) {
             __attribute__ ((__cleanup__(cleanup_str))) char* thumbprint = jwk_thumbprint(jwk, hashes[i]);
             __attribute__ ((__cleanup__(cleanup_str))) char* thumbprint = jwk_thumbprint(jwk, hashes[i]);
-            if (strcmp(thumbprint, target) != 0) {
+            if (!thumbprint || strcmp(thumbprint, target) != 0) {
                 continue;
                 continue;
             }
             }
-
-            if (jwk_valid_for_deriving_keys(jwk)) {
-                return json_incref(jwk);
-            } else if (jwk_valid_for_signing(jwk)) {
-                json_auto_t* sign = json_deep_copy(tki->m_sign);
-                if (json_array_append(sign, jwk) == -1) {
-                    return NULL;
-                }
-                json_auto_t* jws = jwk_sign(tki->m_payload, sign);
-                if (!jws) {
-                    return NULL;
-                }
-                return json_incref(jws);
-            }
+            return json_incref(jwk);
         }
         }
     }
     }
     return NULL;
     return NULL;
@@ -337,6 +325,12 @@ create_new_keys(const char* jwkdir)
             fprintf(stderr, "Error saving JWK to file (%s)\n", path);
             fprintf(stderr, "Error saving JWK to file (%s)\n", path);
             return 0;
             return 0;
         }
         }
+
+        /* Set 0440 permission for the new key. */
+        if (chmod(path, S_IRUSR | S_IRGRP) == -1) {
+            fprintf(stderr, "Unable to set permissions for JWK file (%s)\n", path);
+            return 0;
+        }
     }
     }
     return 1;
     return 1;
 }
 }
@@ -438,7 +432,21 @@ find_jws(struct tang_keys_info* tki, const char* thp)
         }
         }
         return json_incref(jws);
         return json_incref(jws);
     }
     }
-    return find_by_thp(tki, thp);
+
+    json_auto_t* jwk = find_by_thp(tki, thp);
+    if (!jwk_valid_for_signing(jwk)) {
+        return NULL;
+    }
+
+    json_auto_t* sign = json_deep_copy(tki->m_sign);
+    if (json_array_append(sign, jwk) == -1) {
+        return NULL;
+    }
+    json_auto_t* jws = jwk_sign(tki->m_payload, sign);
+    if (!jws) {
+        return NULL;
+    }
+    return json_incref(jws);
 }
 }
 
 
 json_t*
 json_t*

+ 16 - 2
src/meson.build

@@ -7,8 +7,22 @@ tangd = executable('tangd',
   install_dir: libexecdir
   install_dir: libexecdir
 )
 )
 
 
+tangd_keygen = configure_file(
+  input: 'tangd-keygen.in',
+  output: 'tangd-keygen',
+  configuration: data,
+  install: true,
+  install_dir: libexecdir
+)
+
+tangd_rotate_keys = configure_file(
+  input: 'tangd-rotate-keys.in',
+  output: 'tangd-rotate-keys',
+  configuration: data,
+  install: true,
+  install_dir: libexecdir
+)
+
 bins += join_paths(meson.current_source_dir(), 'tang-show-keys')
 bins += join_paths(meson.current_source_dir(), 'tang-show-keys')
-libexecbins += join_paths(meson.current_source_dir(), 'tangd-keygen')
-libexecbins += join_paths(meson.current_source_dir(), 'tangd-rotate-keys')
 
 
 # vim:set ts=2 sw=2 et:
 # vim:set ts=2 sw=2 et:

+ 2 - 3
src/tang-show-keys

@@ -27,10 +27,9 @@ fi
 
 
 port=${1-80}
 port=${1-80}
 
 
-adv=$(curl -sSf localhost:$port/adv)
+adv=$(curl -sSf "localhost:$port/adv")
 
 
 THP_DEFAULT_HASH=S256    # SHA-256.
 THP_DEFAULT_HASH=S256    # SHA-256.
-echo $adv \
-    | jose fmt -j- -g payload -y -o- \
+jose fmt --json "${adv}" -g payload -y -o- \
     | jose jwk use -i- -r -u verify -o- \
     | jose jwk use -i- -r -u verify -o- \
     | jose jwk thp -i- -a "${THP_DEFAULT_HASH}"
     | jose jwk thp -i- -a "${THP_DEFAULT_HASH}"

+ 20 - 7
src/tangd-keygen

@@ -18,20 +18,33 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 #
 
 
-trap 'exit' ERR
+set -e
 
 
-if [ $# -ne 1 -a $# -ne 3 ] || [ ! -d "$1" ]; then
+usage() {
     echo "Usage: $0 <jwkdir> [<sig> <exc>]" >&2
     echo "Usage: $0 <jwkdir> [<sig> <exc>]" >&2
     exit 1
     exit 1
-fi
+}
+
+set_perms() {
+    chmod -- 0440 "${1}"
+    if ! chown -- @user@:@group@ "${1}" 2>/dev/null; then
+        echo "Unable to change owner/group for ${1} to @user@:@group@" >&2
+    fi
+}
+
+[ $# -ne 1 ] && [ $# -ne 3 ] && usage
+[ -d "$1" ] || usage
 
 
 [ $# -eq 3 ] && sig=$2 && exc=$3
 [ $# -eq 3 ] && sig=$2 && exc=$3
 
 
 THP_DEFAULT_HASH=S256     # SHA-256.
 THP_DEFAULT_HASH=S256     # SHA-256.
-jwe=`jose jwk gen -i '{"alg":"ES512"}'`
+jwe=$(jose jwk gen -i '{"alg":"ES512"}')
 [ -z "$sig" ] && sig=$(echo "$jwe" | jose jwk thp -i- -a "${THP_DEFAULT_HASH}")
 [ -z "$sig" ] && sig=$(echo "$jwe" | jose jwk thp -i- -a "${THP_DEFAULT_HASH}")
-echo "$jwe" > $1/$sig.jwk
+echo "$jwe" > "$1/$sig.jwk"
+set_perms "$1/$sig.jwk"
+
 
 
-jwe=`jose jwk gen -i '{"alg":"ECMR"}'`
+jwe=$(jose jwk gen -i '{"alg":"ECMR"}')
 [ -z "$exc" ] && exc=$(echo "$jwe" | jose jwk thp -i- -a "${THP_DEFAULT_HASH}")
 [ -z "$exc" ] && exc=$(echo "$jwe" | jose jwk thp -i- -a "${THP_DEFAULT_HASH}")
-echo "$jwe" > $1/$exc.jwk
+echo "$jwe" > "$1/$exc.jwk"
+set_perms "$1/$exc.jwk"

+ 11 - 3
src/tangd-rotate-keys

@@ -21,7 +21,7 @@
 SUMMARY="Perform rotation of tang keys"
 SUMMARY="Perform rotation of tang keys"
 
 
 usage() {
 usage() {
-    local _ret="${1:-1}"
+    _ret="${1:-1}"
     exec >&2
     exec >&2
     echo "Usage: ${0} [-h] [-v] -d <KEYDIR>"
     echo "Usage: ${0} [-h] [-v] -d <KEYDIR>"
     echo
     echo
@@ -37,8 +37,8 @@ usage() {
 }
 }
 
 
 log() {
 log() {
-    local _msg="${1}"
-    local _verbose="${2:-}"
+    _msg="${1}"
+    _verbose="${2:-}"
     [ -z "${_verbose}" ] && return 0
     [ -z "${_verbose}" ] && return 0
     echo "${_msg}" >&2
     echo "${_msg}" >&2
 }
 }
@@ -48,6 +48,13 @@ error() {
     usage 1
     usage 1
 }
 }
 
 
+set_perms() {
+    chmod -- 0440 "${1}"
+    if ! chown -- @user@:@group@ "${1}" 2>/dev/null; then
+        echo "Unable to change owner/group for ${1} to @user@:@group@" >&2
+    fi
+}
+
 JWKDIR=
 JWKDIR=
 VERBOSE=
 VERBOSE=
 while getopts "hvd:" o; do
 while getopts "hvd:" o; do
@@ -78,6 +85,7 @@ cd "${JWKDIR}" || error "Unable to change to keys directory '${JWKDIR}'"
         thp="$(printf '%s' "${jwe}" | jose jwk thp --input=- \
         thp="$(printf '%s' "${jwe}" | jose jwk thp --input=- \
                                            -a "${DEFAULT_THP_HASH}")"
                                            -a "${DEFAULT_THP_HASH}")"
         echo "${jwe}" > "${thp}.jwk"
         echo "${jwe}" > "${thp}.jwk"
+        set_perms "${thp}.jwk"
         log "Created new key ${thp}.jwk" "${VERBOSE}"
         log "Created new key ${thp}.jwk" "${VERBOSE}"
     done
     done
 cd - >/dev/null
 cd - >/dev/null

+ 39 - 8
tests/adv

@@ -27,6 +27,10 @@ export TMP=`mktemp -d`
 mkdir -p $TMP/db
 mkdir -p $TMP/db
 
 
 tangd-keygen $TMP/db sig exc
 tangd-keygen $TMP/db sig exc
+# Make sure keys generated by tangd-keygen have proper permissions.
+valid_key_perm "${TMP}/db/sig.jwk"
+valid_key_perm "${TMP}/db/exc.jwk"
+
 jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.sig.jwk
 jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.sig.jwk
 jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.oth.jwk
 jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.oth.jwk
 
 
@@ -36,11 +40,11 @@ export PID=$!
 sleep 0.5
 sleep 0.5
 
 
 # Make sure requests on the root fail
 # Make sure requests on the root fail
-! fetch /
+fetch / && expected_fail
 
 
 # The request should fail (404) for non-signature key IDs
 # The request should fail (404) for non-signature key IDs
-! fetch /adv/`jose jwk thp -i $TMP/db/exc.jwk`
-! fetch /adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk`
+fetch /adv/`jose jwk thp -i $TMP/db/exc.jwk` && expected_fail
+fetch /adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk` && expected_fail
 
 
 # The default advertisement fetch should succeed and pass verification
 # The default advertisement fetch should succeed and pass verification
 fetch /adv
 fetch /adv
@@ -52,17 +56,17 @@ fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
 fetch /adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
 fetch /adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
 
 
 # Requesting an adv by an advertised key ID should't be signed by hidden keys
 # Requesting an adv by an advertised key ID should't be signed by hidden keys
-! fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk
-! fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk
+fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk && expected_fail
+fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
 
 
 # Verify that the default advertisement is not signed with hidden signature keys
 # Verify that the default advertisement is not signed with hidden signature keys
-! fetch /adv/ | ver $TMP/db/.oth.jwk
-! fetch /adv/ | ver $TMP/db/.sig.jwk
+fetch /adv/ | ver $TMP/db/.oth.jwk && expected_fail
+fetch /adv/ | ver $TMP/db/.sig.jwk && expected_fail
 
 
 # A private key advertisement is signed by all advertised keys and the requested private key
 # A private key advertisement is signed by all advertised keys and the requested private key
 fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk
 fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk
 fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk
 fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk
-! fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk
+fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
 
 
 # Verify that the advertisements contain the cty parameter
 # Verify that the advertisements contain the cty parameter
 fetch /adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E
 fetch /adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E
@@ -93,6 +97,13 @@ fetch /adv
 # Lets's now test with multiple pairs of keys.
 # Lets's now test with multiple pairs of keys.
 for i in 1 2 3 4 5 6 7 8 9; do
 for i in 1 2 3 4 5 6 7 8 9; do
     tangd-keygen "${TMP}"/db other-sig-${i} other-exc-${i}
     tangd-keygen "${TMP}"/db other-sig-${i} other-exc-${i}
+    # Make sure the requested keys exist and are valid.
+    validate_sig "${TMP}/db/other-sig-${i}.jwk"
+    validate_exc "${TMP}/db/other-exc-${i}.jwk"
+
+    # Make sure keys generated by tangd-keygen have proper permissions.
+    valid_key_perm "${TMP}/db/other-sig-${i}.jwk"
+    valid_key_perm "${TMP}/db/other-exc-${i}.jwk"
 done
 done
 
 
 # Verify the advertisement is correct.
 # Verify the advertisement is correct.
@@ -104,3 +115,23 @@ for jwk in "${TMP}"/db/other-sig-*.jwk; do
         fetch /adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}"
         fetch /adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}"
     done
     done
 done
 done
+
+# Now let's test keys rotation.
+tangd-rotate-keys -d "${TMP}/db"
+for i in 1 2 3 4 5 6 7 8 9; do
+    # Make sure keys were excluded from advertisement.
+    validate_sig "${TMP}/db/.other-sig-${i}.jwk"
+    validate_exc "${TMP}/db/.other-exc-${i}.jwk"
+done
+
+# And test also that we have valid keys after rotation.
+thp=
+for jwk in "${TMP}"/db/*.jwk; do
+    validate_sig "${jwk}" && thp="$(jose jwk thp -a "${THP_DEFAULT_HASH}" \
+                                    -i "${jwk}")"
+
+    # Make sure keys generated by tangd-rotate-keys have proper permissions.
+    valid_key_perm "${jwk}"
+done
+[ -z "${thp}" ] && die "There should be valid keys after rotation"
+test "$(tang-show-keys $PORT)" = "${thp}"

+ 29 - 0
tests/helpers

@@ -56,7 +56,36 @@ validate() {
     fi
     fi
 }
 }
 
 
+validate_sig() {
+    jose fmt --json "${1}" --output=- | jose jwk use --input=- --required \
+        --use verify 2>/dev/null
+}
+
+validate_exc() {
+    jose fmt --json "${1}" --output=- | jose jwk use --input=- --required \
+        --use deriveKey 2>/dev/null
+}
+
 sanity_check() {
 sanity_check() {
     # Skip test if socat is not available.
     # Skip test if socat is not available.
     [ -n "${SOCAT}" ] || exit 77
     [ -n "${SOCAT}" ] || exit 77
 }
 }
+
+die() {
+    echo "${1}" >&2
+    exit 1
+}
+
+valid_key_perm() {
+    if [ -n "${TANG_BSD}" ]; then
+        _perm="$(stat -f %Lp "${1}")"
+    else
+        _perm="$(stat -c %a "${1}")"
+    fi
+    [ "${_perm}" = "440" ]
+}
+
+expected_fail () {
+    echo "Test was expected to fail" >&2
+    exit 1
+}

+ 1 - 1
tests/meson.build

@@ -32,7 +32,7 @@ env.prepend('PATH',
   separator: ':'
   separator: ':'
 )
 )
 
 
-if build_machine.system() == 'freebsd'
+if host_machine.system() == 'freebsd'
   env.set('TANG_BSD', '1')
   env.set('TANG_BSD', '1')
 endif
 endif
 
 

+ 5 - 2
tests/rec

@@ -28,6 +28,9 @@ mkdir -p $TMP/db
 
 
 # Generate the server keys
 # Generate the server keys
 tangd-keygen $TMP/db sig exc
 tangd-keygen $TMP/db sig exc
+# Make sure keys generated by tangd-keygen have proper permissions.
+valid_key_perm "${TMP}/db/sig.jwk"
+valid_key_perm "${TMP}/db/exc.jwk"
 
 
 # Generate the client keys
 # Generate the client keys
 exc_kid=`jose jwk thp -i $TMP/db/exc.jwk`
 exc_kid=`jose jwk thp -i $TMP/db/exc.jwk`
@@ -42,8 +45,8 @@ export PID=$!
 sleep 0.5
 sleep 0.5
 
 
 # Make sure that GET fails
 # Make sure that GET fails
-! curl -sf http://127.0.0.1:$PORT/rec
-! curl -sf http://127.0.0.1:$PORT/rec/
+curl -sf http://127.0.0.1:$PORT/rec && expected_fail
+curl -sf http://127.0.0.1:$PORT/rec/ && expected_fail
 
 
 # Make a recovery request (NOTE: this is insecure! Don't do this in real code!)
 # Make a recovery request (NOTE: this is insecure! Don't do this in real code!)
 good=`jose jwk exc -i '{"alg":"ECMR","key_ops":["deriveKey"]}' -l $TMP/exc.jwk -r $TMP/db/exc.jwk`
 good=`jose jwk exc -i '{"alg":"ECMR","key_ops":["deriveKey"]}' -l $TMP/exc.jwk -r $TMP/db/exc.jwk`

+ 59 - 0
tests/test-keys.c.in

@@ -33,6 +33,56 @@ struct test_result_int {
 };
 };
 
 
 static void
 static void
+verify_keys_permissions(const char* targetdir)
+{
+    struct stat st;
+    struct dirent* d;
+    DIR* dir = opendir(targetdir);
+    ASSERT(dir);
+    char filepath[PATH_MAX];
+    const char* pattern = ".jwk";
+    while ((d = readdir(dir)) != NULL) {
+        if (strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0) {
+            continue;
+        }
+
+        char* dot = strrchr(d->d_name, '.');
+        if (!dot) {
+            continue;
+        }
+
+        if (strcmp(dot, pattern) == 0) {
+            /* Found a file with .jwk extension. */
+            if (snprintf(filepath, PATH_MAX, "%s/%s", targetdir, d->d_name) < 0) {
+                fprintf(stderr, "Unable to prepare variable with file full path (%s); skipping\n", d->d_name);
+                continue;
+            }
+            filepath[sizeof(filepath) - 1] = '\0';
+            ASSERT(stat(filepath, &st) == 0);
+
+            ASSERT_WITH_MSG(st.st_mode & (S_IRUSR | S_IRGRP), "key = %s, missing perm (0%o)", filepath, (S_IRUSR | S_IRGRP));
+            int unexpected_perms[] = {
+                S_ISUID, /* 04000 set-user-ID */
+                S_ISGID, /* 02000 set-group-ID */
+                S_IWUSR, /* 00200 write by owner */
+                S_IXUSR, /* 00100 execute/search by owner */
+                S_IWGRP, /* 00020 write by group */
+                S_IXGRP, /* 00010 execute/search by group */
+                S_IROTH, /* 00004 read by others */
+                S_IWOTH, /* 00002 write by others */
+                S_IXOTH, /* 00001 execute/search by others */
+                0
+            };
+            for (int i = 0; unexpected_perms[i] != 0; i++) {
+                ASSERT_WITH_MSG((st.st_mode & unexpected_perms[i]) == 0, "key = %s, i = %d, unexpected perm (0%o)", filepath, i, unexpected_perms[i]);
+            }
+
+        }
+    }
+    closedir(dir);
+}
+
+static void
 test_create_new_keys(void)
 test_create_new_keys(void)
 {
 {
    __attribute__((cleanup(cleanup_str))) char* newdir = create_tempdir();
    __attribute__((cleanup(cleanup_str))) char* newdir = create_tempdir();
@@ -40,6 +90,10 @@ test_create_new_keys(void)
     __attribute__((cleanup(cleanup_tang_keys_info))) struct tang_keys_info* tki = read_keys(newdir);
     __attribute__((cleanup(cleanup_tang_keys_info))) struct tang_keys_info* tki = read_keys(newdir);
     ASSERT(tki);
     ASSERT(tki);
     ASSERT(tki->m_keys_count == 2);
     ASSERT(tki->m_keys_count == 2);
+
+    /* Make sure keys have proper permissions. */
+    verify_keys_permissions(newdir);
+
     remove_tempdir(newdir);
     remove_tempdir(newdir);
 }
 }
 
 
@@ -104,6 +158,11 @@ test_find_jws(void)
         {"ugJ4Ula-YABQIiJ-0g3B_jpFpF2nl3W-DNpfLdXArhTusV0QCcd1vtgDeGHEPzpm7jEsyC7VYYSSOkZicK22mw", 1},
         {"ugJ4Ula-YABQIiJ-0g3B_jpFpF2nl3W-DNpfLdXArhTusV0QCcd1vtgDeGHEPzpm7jEsyC7VYYSSOkZicK22mw", 1},
         {"up0Z4fRhpd4O5QwBaMCXDTlrvxCmZacU0MD8kw", 1},
         {"up0Z4fRhpd4O5QwBaMCXDTlrvxCmZacU0MD8kw", 1},
         {"vllHS-M0aQFCo2yUCcAahMU4TAtXACyeuRf-zbmmTPBg7V0Pb-RRFGo5C6MnpzdirK8B3ORLOsN8RyXClvtjxA", 1},
         {"vllHS-M0aQFCo2yUCcAahMU4TAtXACyeuRf-zbmmTPBg7V0Pb-RRFGo5C6MnpzdirK8B3ORLOsN8RyXClvtjxA", 1},
+        {"-bWkGaJi0Zdvxaj4DCp28umLcRA", 0},
+        {"WEpfFyeoNKkE2-TosN_bP-gd9UgRvQCZpVasZQ", 0},
+        {"L4xg2tZXTEVbsK39bzOZM1jGWn3HtOxF5gh6F9YVf5Q", 0},
+        {"9U8qgy_YjyY6Isuq6QuiKEiYZgNJShcGgJx5FJzCu6m3N6zFaIPy_HDkxkVqAZ9E", 0},
+        {"Cy73glFjs6B6RU7wy6vWxAc-2bJy5VJOT9LyK80eKgZ8k27wXZ-3rjsuNU5tua_yHWtluyoSYtjoKXfI0E8ESw", 0},
         {NULL, 1},
         {NULL, 1},
         {"a", 0},
         {"a", 0},
         {"foo", 0},
         {"foo", 0},

+ 1 - 1
units/meson.build

@@ -3,7 +3,7 @@ tangd_service = configure_file(
   output: 'tangd@.service',
   output: 'tangd@.service',
   configuration: data
   configuration: data
 )
 )
-if build_machine.system() == 'freebsd'
+if host_machine.system() == 'freebsd'
   tangd_rc = configure_file(
   tangd_rc = configure_file(
     input: 'tangd.rc.in',
     input: 'tangd.rc.in',
     output: 'tangd',
     output: 'tangd',

+ 1 - 0
units/tangd@.service.in

@@ -6,3 +6,4 @@ StandardInput=socket
 StandardOutput=socket
 StandardOutput=socket
 StandardError=journal
 StandardError=journal
 ExecStart=@libexecdir@/tangd @jwkdir@
 ExecStart=@libexecdir@/tangd @jwkdir@
+User=@user@