CLI key management tool.

This is OpenPAM's oathkey tool ported to libcryb-oath (which is itself derived from OpenPAM's liboath).  Most of its functionality will eventually move into libcryb-otp.
This commit is contained in:
Dag-Erling Smørgrav 2018-04-26 01:34:32 +02:00
parent e4cde94086
commit 284b50eda4
2 changed files with 610 additions and 15 deletions

View file

@ -1,5 +1,6 @@
.\"-
.\" Copyright (c) 2017 Dag-Erling Smørgrav
.\" Copyright (c) 2013-2014 The University of Oslo
.\" Copyright (c) 2016-2018 Dag-Erling Smørgrav
.\" All rights reserved.
.\"
.\" Redistribution and use in source and binary forms, with or without
@ -26,20 +27,112 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.Dd March 3, 2017
.Dd April 26, 2018
.Dt OTPKEY 1
.Os
.Sh NAME
.Nm otpkey
.Nd One-time password key management utility
.Nd OTP key management tool
.Sh SYNOPSIS
.Cm Nm
.Nm
.Op Fl hnrvw
.Op Fl u Ar user
.Op Fl k Ar keyfile
.Ar command
.Op Ar args
.Sh DESCRIPTION
TBW
The
.Nm
utility creates and manages OTP keys, and can be used to compute or
validate an OTP response.
.Pp
The following options are available:
.Bl -tag -width Fl
.It Fl h
Print a usage message and exit.
.It Fl k Ar keyfile
Specify the location of the keyfile on which to operate.
The default is
.Pa /var/oath/ Ns Ar user Ns Pa .otpauth .
.It Fl n
When printing codes with the
.Cm calc
command, print the counter or timestamp along with each code.
.It Fl r
Disable writeback mode.
.It Fl u Ar user
Specify the user on which to operate.
The default is the current user.
Only root may operate on other users.
.It Fl v
Enable verbose mode.
.It Fl w
Enable writeback mode (see below).
This is the default.
.El
.Pp
The commands are:
.Bl -tag -width 6n
.It Cm calc Op Ar count
Compute and display the current code for the given key.
If a count is specified, compute and display
.Ar count
codes in total, starting with the current code, up to a maximum of
1,000 codes.
If writeback mode is enabled, the user's keyfile is updated to prevent
reuse.
.It Cm genkey Ar hotp | totp
Generate a new key for the specified OTP mode.
If writeback mode is enabled, the user's key is set; otherwise, it is
printed to standard output.
.It Cm getkey
Print the user's key.
.It Cm geturi
Print the user's key in otpauth URI form.
.It Cm resync Ar code1 Ar code2 Op Ar code3
Resynchronize an event-mode token that has moved too far ahead of the
validation server.
The codes provided must be consecutive codes within the
resynchronization window.
The resynchronization window is 100 if two codes are provided and 1000
if three codes are provided.
.It Cm setkey Ar uri
Set the user's key to the given otpauth URI.
.It Cm uri
Deprecated synonym for
.Cm geturi .
.It Cm verify Ar code
Verify that the given code is the correct current response for the
user's key.
If writeback mode is enabled and the response matched, the user's
keyfile is updated to prevent reuse.
.El
.Sh EXIT STATUS
The
.Cm verify
command exits 0 if the code is valid, 1 if it is invalid and >1 if an
error occurred.
.Pp
The
.Cm resync
command exits 0 if resynchronization was successful, 1 if it failed or
the specified key does not support resynchronization, and >1 if an
error occurred.
.Pp
All other commands exit 0 if successful and >1 if an error occurred.
.Sh SEE ALSO
.Xr otpverify 8
.Xr oath_hotp 3 ,
.Xr oath_key 3 ,
.Xr oath_totp 3
.Sh AUTHORS
The
.Nm
utility and this manual page were written by
.An Dag-Erling Sm\(/orgrav Aq Mt des@des.no .
.An Dag-Erling Sm\(/orgrav Aq Mt des@des.no
for the University of Oslo.
.Sh BUGS
For TOTP keys, the
.Cm calc Ar count
command will only work correctly for a
.Ar count
of 1.

View file

@ -1,5 +1,6 @@
/*-
* Copyright (c) 2017 Dag-Erling Smørgrav
* Copyright (c) 2013-2016 The University of Oslo
* Copyright (c) 2016-2018 Dag-Erling Smørgrav
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -29,27 +30,442 @@
#include "cryb/impl.h"
#include <sys/types.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <cryb/otp.h>
#include <cryb/ctype.h>
#include <cryb/oath.h>
#include <cryb/strlcmp.h>
#define MAX_KEYURI_SIZE 4096
/* XXX hardcoded windows */
#define HOTP_WINDOW 9
#define TOTP_WINDOW 2
enum { RET_SUCCESS, RET_FAILURE, RET_ERROR, RET_USAGE, RET_UNAUTH };
static char *user;
static char *keyfile;
static int verbose;
static int readonly;
static int numbered;
static int isroot; /* running as root */
static int issameuser; /* real user same as target user */
/*
* Print key in hexadecimal form
*/
static int
oathkey_print_hex(oath_key *key)
{
unsigned int i;
for (i = 0; i < key->keylen; ++i)
printf("%02x", key->key[i]);
printf("\n");
return (RET_SUCCESS);
}
/*
* Print key in otpauth URI form
*/
static int
oathkey_print_uri(oath_key *key)
{
char keyuri[MAX_KEYURI_SIZE];
size_t len;
len = sizeof keyuri;
if (oath_key_to_uri(key, keyuri, &len) != 0) {
warnx("failed to convert key to otpauth URI");
return (RET_ERROR);
}
printf("%s\n", keyuri);
return (RET_SUCCESS);
}
/*
* Load key from file
*/
static int
oathkey_load(oath_key *key)
{
char keyuri[MAX_KEYURI_SIZE];
ssize_t rlen;
size_t len;
int fd;
if (verbose)
warnx("loading key from %s", keyfile);
/* read from file */
if ((fd = open(keyfile, O_RDONLY)) < 0)
return (-1);
if ((rlen = read(fd, keyuri, sizeof keyuri - 1)) < 0) {
warn("%s", keyfile);
close(fd);
return (-1);
}
close(fd);
/* cut off at the first newline character */
for (len = 0; len < (size_t)rlen; len++)
if (keyuri[len] == '\n')
break;
while (len > 0 && is_ws(keyuri[len - 1]))
len--;
keyuri[len] = '\0';
/* check and parse */
if (strlcmp("otpauth://", keyuri, 10) != 0) {
warnx("%s: unrecognized key file format", keyfile);
return (RET_ERROR);
}
if (oath_key_from_uri(key, keyuri) != 0) {
warnx("%s: invalid key URI", keyfile);
if (errno == EACCES || errno == EPERM)
return (RET_UNAUTH);
return (RET_ERROR);
}
return (RET_SUCCESS);
}
/*
* Save key to file
* XXX liboath should take care of this for us
*/
static int
oathkey_save(oath_key *key)
{
char keyuri[MAX_KEYURI_SIZE];
ssize_t wlen;
size_t len;
int fd;
if (verbose)
warnx("saving key to %s", keyfile);
len = sizeof keyuri;
if (oath_key_to_uri(key, keyuri, &len) != 0) {
warnx("failed to convert key to otpauth URI");
return (-1);
}
keyuri[len - 1] = '\n';
if ((fd = open(keyfile, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
warn("%s", keyfile);
return (-1);
}
if ((wlen = write(fd, keyuri, len)) < 0 || (size_t)wlen < len) {
warn("%s", keyfile);
close(fd);
return (-1);
}
close(fd);
return (0);
}
/*
* Generate a new key
*/
static int
oathkey_genkey(int argc, char *argv[])
{
oath_key key;
oath_mode mode;
int ret;
if (argc != 1)
return (RET_USAGE);
if ((mode = oath_mode_value(argv[0])) == om_undef)
return (RET_USAGE);
if (!isroot && !issameuser)
return (RET_UNAUTH);
if (oath_key_create(&key, mode, oh_undef, 0, "", user, NULL, 0) != 0)
return (RET_ERROR);
ret = readonly ? oathkey_print_uri(&key) : oathkey_save(&key);
oath_key_destroy(&key);
return (ret);
}
/*
* Set a user's key
*/
static int
oathkey_setkey(int argc, char *argv[])
{
oath_key key;
int ret;
/* XXX add parameters later */
if (argc != 1)
return (RET_USAGE);
(void)argv;
if (!isroot && !issameuser)
return (RET_UNAUTH);
if (oath_key_from_uri(&key, argv[0]) != 0)
return (RET_ERROR);
ret = oathkey_save(&key);
oath_key_destroy(&key);
return (ret);
}
/*
* Print raw key in hexadecimal
*/
static int
oathkey_getkey(int argc, char *argv[])
{
oath_key key;
int ret;
if (argc != 0)
return (RET_USAGE);
(void)argv;
if (!isroot && !issameuser)
return (RET_UNAUTH);
if ((ret = oathkey_load(&key)) != RET_SUCCESS)
return (ret);
ret = oathkey_print_hex(&key);
oath_key_destroy(&key);
return (ret);
}
/*
* Print the otpauth URI for a key
*/
static int
oathkey_geturi(int argc, char *argv[])
{
oath_key key;
int ret;
if (argc != 0)
return (RET_USAGE);
(void)argv;
if (!isroot && !issameuser)
return (RET_UNAUTH);
if ((ret = oathkey_load(&key)) != RET_SUCCESS)
return (ret);
ret = oathkey_print_uri(&key);
oath_key_destroy(&key);
return (ret);
}
/*
* Check whether a given response is correct for the given keyfile.
*/
static int
oathkey_verify(int argc, char *argv[])
{
oath_key key;
unsigned long counter;
unsigned int response;
char *end;
int match, ret;
if (argc < 1)
return (RET_USAGE);
if ((ret = oathkey_load(&key)) != RET_SUCCESS)
return (ret);
response = strtoul(*argv, &end, 10);
if (end == *argv || *end != '\0')
response = UINT_MAX; /* never valid */
switch (key.mode) {
case om_hotp:
counter = key.counter;
match = oath_hotp_match(&key, response, HOTP_WINDOW);
if (verbose && match > 0 && key.counter > counter + 1)
warnx("skipped %lu codes", key.counter - counter - 1);
break;
case om_totp:
match = oath_totp_match(&key, response, TOTP_WINDOW);
break;
default:
match = -1;
}
/* oath_*_match() return -1 on error, 0 on failure, 1 on success */
if (match < 0) {
warnx("OATH error");
match = 0;
}
if (verbose)
warnx("response: %u %s", response,
match ? "matched" : "did not match");
ret = match ? readonly ? RET_SUCCESS : oathkey_save(&key) : RET_FAILURE;
oath_key_destroy(&key);
return (ret);
}
/*
* Compute the current code
*/
static int
oathkey_calc(int argc, char *argv[])
{
oath_key key;
unsigned int current;
unsigned long i, n;
uintmax_t count;
char *end;
int ret;
if (argc > 1)
return (RET_USAGE);
if (argc > 0) {
n = strtoul(argv[0], &end, 10);
if (end == argv[0] || *end != '\0' || n < 1 || n > 1000)
return (RET_USAGE);
} else {
n = 1;
}
if ((ret = oathkey_load(&key)) != RET_SUCCESS)
return (ret);
for (i = 0; i < n; ++i) {
switch (key.mode) {
case om_hotp:
current = oath_hotp_current(&key);
count = key.counter;
break;
case om_totp:
current = oath_totp_current(&key);
count = key.lastused * key.timestep;
break;
default:
current = UINT_MAX;
count = 0;
}
if (current == UINT_MAX) {
warnx("OATH error");
ret = RET_ERROR;
break;
}
if (numbered)
printf("%6ju ", count);
printf("%.*d\n", (int)key.digits, current);
}
if (ret == RET_SUCCESS && !readonly)
ret = oathkey_save(&key);
oath_key_destroy(&key);
return (ret);
}
/*
* Resynchronize
*/
static int
oathkey_resync(int argc, char *argv[])
{
oath_key key;
unsigned long counter;
unsigned int response[3];
char *end;
int i, match, n, ret, w;
if (argc < 2 || argc > 3)
return (RET_USAGE);
n = argc;
for (i = 0, w = 1; i < n; ++i) {
response[i] = strtoul(argv[i], &end, 10);
if (end == argv[i] || *end != '\0')
response[i] = UINT_MAX; /* never valid */
w = w * (HOTP_WINDOW + 1);
}
w -= n;
if ((ret = oathkey_load(&key)) != RET_SUCCESS)
return (ret);
switch (key.mode) {
case om_hotp:
/* this should be a library function */
counter = key.counter;
match = 0;
while (key.counter < counter + w && match == 0) {
match = oath_hotp_match(&key, response[0],
counter + w - key.counter - 1);
if (match <= 0)
break;
for (i = 1; i < n && match > 0; ++i)
match = oath_hotp_match(&key, response[i], 0);
}
if (verbose && match > 0)
warnx("skipped %lu codes", key.counter - counter);
break;
default:
match = -1;
}
if (match < 0) {
warnx("OATH error");
match = 0;
}
if (verbose)
warnx("resynchronization %s", match ? "succeeded" : "failed");
ret = match ? readonly ? RET_SUCCESS : oathkey_save(&key) : RET_FAILURE;
oath_key_destroy(&key);
return (ret);
}
/*
* Print usage string and exit.
*/
static void
usage(void)
{
fprintf(stderr, "usage: otpkey\n");
fprintf(stderr,
"usage: otpkey [-hnrvw] [-u user] [-k keyfile] command\n"
"\n"
"Commands:\n"
" calc [count]\n"
" Print the next code(s)\n"
" genkey hotp | totp\n"
" Generate a new key\n"
" getkey Print the key in hexadecimal form\n"
" geturi Print the key in otpauth URI form\n"
" resync code1 code2 [code3]\n"
" Resynchronize an HOTP token\n"
" setkey Generate a new key\n"
" verify code\n"
" Verify an HOTP or TOTP code\n");
exit(1);
}
int
main(int argc, char *argv[])
{
int opt;
struct passwd *pw;
int opt, ret;
char *cmd;
while ((opt = getopt(argc, argv, "")) != -1)
/*
* Parse command-line options
*/
while ((opt = getopt(argc, argv, "hk:nru:vw")) != -1)
switch (opt) {
case 'k':
keyfile = optarg;
break;
case 'n':
numbered = 1;
break;
case 'r':
readonly = 1;
break;
case 'u':
user = optarg;
break;
case 'v':
++verbose;
break;
case 'w':
readonly = 0;
break;
case 'h':
default:
usage();
}
@ -57,8 +473,94 @@ main(int argc, char *argv[])
argc -= optind;
argv += optind;
if (argc > 0)
if (argc-- < 1)
usage();
cmd = *argv++;
exit(0);
/*
* Check whether we are (really!) root.
*/
if (getuid() == 0)
isroot = 1;
/*
* If a user was specified on the command line, check whether it
* matches our real UID.
*/
if (user != NULL) {
if ((pw = getpwnam(user)) == NULL)
errx(1, "no such user");
if (getuid() == pw->pw_uid)
issameuser = 1;
}
/*
* If no user was specified on the command line, look up the user
* that corresponds to our real UID.
*/
if (user == NULL) {
if ((pw = getpwuid(getuid())) == NULL)
errx(1, "who are you?");
if (asprintf(&user, "%s", pw->pw_name) < 0)
err(1, "asprintf()");
issameuser = 1;
}
/*
* If no keyfile was specified on the command line, derive it from
* the user name.
*/
if (keyfile == NULL)
/* XXX replace with a function that searches multiple locations? */
if (asprintf(&keyfile, "/var/oath/%s.otpauth", user) < 0)
err(1, "asprintf()");
/*
* Execute the requested command
*/
if (strcmp(cmd, "help") == 0)
ret = RET_USAGE;
else if (strcmp(cmd, "calc") == 0)
ret = oathkey_calc(argc, argv);
else if (strcmp(cmd, "genkey") == 0)
ret = oathkey_genkey(argc, argv);
else if (strcmp(cmd, "getkey") == 0)
ret = oathkey_getkey(argc, argv);
else if (strcmp(cmd, "geturi") == 0 || strcmp(cmd, "uri") == 0)
ret = oathkey_geturi(argc, argv);
else if (strcmp(cmd, "resync") == 0)
ret = oathkey_resync(argc, argv);
else if (strcmp(cmd, "setkey") == 0)
ret = oathkey_setkey(argc, argv);
else if (strcmp(cmd, "verify") == 0)
ret = oathkey_verify(argc, argv);
else
ret = RET_USAGE;
/*
* Check result and act accordingly
*/
switch (ret) {
case RET_UNAUTH:
errno = EPERM;
err(1, "%s", cmd);
break;
case RET_USAGE:
usage();
break;
case RET_SUCCESS:
exit(0);
break;
case RET_FAILURE:
exit(1);
break;
case RET_ERROR:
exit(2);
break;
default:
exit(3);
break;
}
/* not reached */
exit(255);
}