gsstool: Add GSS-based kinit-like acquire_cred cmd

This has most of the features needed to act as a kinit that uses GSS
APIs, specifically gss_acquire_cred_from() and gss_store_cred_into2().

It's missing some functionality, such as being able to drive prompts
from AS responses (if we add minor status codes for representing KDC
pre-auth proposals, then we do drive prompts, but we would have to
encode a lot of mechanism-specific knowledge into gsstool).

The point of this commit is to explore:

 - GSS functionality for kinit-like actions

 - credential store key/value pairs supported by the mechanisms

 - document the credential store key/value pairs (in gsstool.1)

that might lead to further enhancements.  But gsstool acquire-cred
is quite functional at this point!
This commit is contained in:
Nicolas Williams
2022-10-09 01:44:47 -05:00
parent b804b22446
commit ae7d6746d1
5 changed files with 1269 additions and 2 deletions

View File

@@ -51,11 +51,86 @@ command = {
argument = "mechanism"
}
}
command = {
name = "acquire-cred"
help = "Acquire a credential"
option = {
long = "initiator"
type = "flag"
}
option = {
long = "acceptor"
type = "flag"
}
option = {
long = "mech"
type = "strings"
argument = "mechanism"
}
option = {
long = "name-type"
type = "string"
argument = "name-type for desired name"
}
option = {
long = "name"
type = "string"
argument = "desired name"
}
option = {
long = "time-req"
type = "integer"
argument = "desired credential lifetime"
}
option = {
long = "from"
type = "strings"
argument = "key=value pair"
}
option = {
long = "from-prompt"
type = "strings"
argument = "key=prompt pair"
}
option = {
long = "from-file"
type = "strings"
argument = "key=filename pair"
}
option = {
long = "into"
type = "strings"
argument = "key=value pair"
}
option = {
long = "into-prompt"
type = "strings"
argument = "key=prompt pair"
}
option = {
long = "into-file"
type = "strings"
argument = "key=filename pair"
}
option = {
long = "verbose"
short = "v"
type = "flag"
help = "Verbose"
}
option = {
long = "shell"
short = "s"
type = "flag"
help = "Verbose"
}
argument = "[cmd args]"
}
command = {
name = "help"
name = "?"
argument = "[command]"
min_args = "0"
max_args = "1"
help = "Help! I need somebody."
help = "gsstool mechanisms | attributes | acquire-cred"
}

362
lib/gssapi/gsstool.1 Normal file
View File

@@ -0,0 +1,362 @@
.\" Copyright (c) 2022 Kungliga Tekniska Högskolan
.\" (Royal Institute of Technology, Stockholm, Sweden).
.\" All rights reserved.
.\"
.\" Redistribution and use in source and binary forms, with or without
.\" modification, are permitted provided that the following conditions
.\" are met:
.\"
.\" 1. Redistributions of source code must retain the above copyright
.\" notice, this list of conditions and the following disclaimer.
.\"
.\" 2. Redistributions in binary form must reproduce the above copyright
.\" notice, this list of conditions and the following disclaimer in the
.\" documentation and/or other materials provided with the distribution.
.\"
.\" 3. Neither the name of the Institute nor the names of its contributors
.\" may be used to endorse or promote products derived from this software
.\" without specific prior written permission.
.\"
.\" THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
.\" ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.\" $Id$
.\"
.Dd October 9, 2022
.Dt GSSTOOL 1
.Os HEIMDAL
.Sh NAME
.Nm gsstool
.Nd command-line interface to GSS-API
.Sh SYNOPSIS
.Nm
.Ar command
.Op Ar args
.Sh DESCRIPTION
.Nm
is a program for using the GSS-API from a shell.
.Pp
.Ar command
can be one of the following:
.Bl -tag -width srvconvert
.It Nm mechanisms
Lists available GSS-API mechanisms.
.It Nm attributes Oo Fl Fl all Oc
Lists all the mechanism attributes known to
.Nm .
.It Nm attributes Oo Fl Fl mech=MECH Oc
Lists the attributes of the given
.Ar MECH .
.It Nm acquire-cred \
Oo Fl Fl verbose Oc \
Oo Fl s \*(Ba Xo Fl Fl shell Xc Oc \
Oo Fl Fl initiator Oc \
Oo Fl Fl acceptor Oc \
Oo Fl Fl mech= Ns Ar MECH Oc \
Oo Fl Fl name-type= Ns Ar NAME-TYPE Oc \
Oo Fl Fl name= Ns Ar NAME Oc \
Oo Fl Fl time-req= Ns Ar SECONDS Oc \
Oo Fl Fl from= Ns Ar KEY=VALUE Oc \
Oo Fl Fl from-file=echo-on: Ns Ar KEY=FILE Oc \
Oo Fl Fl from-prompt= Ns Ar KEY=PROMPT Oc \
Oo Fl Fl from-prompt= Ns Ar KEY=PROMPT Oc \
Oo Fl Fl into= Ns Ar KEY=VALUE Oc \
Oo Fl Fl into-file=echo-on: Ns Ar KEY=FILE Oc \
Oo Fl Fl into-prompt=echo-on: Ns Ar KEY=PROMPT Oc \
Oo Fl Fl into-prompt=echo-off: Ns Ar KEY=PROMPT Oc \
Oo Ar cmd Oo arguments... Oc Oc
.Pp
Acquires a credential for each of the
.Fl Fl mech= Ns Ar MECH
mechanisms given for the given principal
.Ar NAME ,
or the default principal for the invoking user, as specified by
the
.Fl Fl from= Ns Ar KEY=VALUE
options and stores it into the given credential store specified
by the
.Fl Fl into= Ns Ar KEY=VALUE
options, if any are given.
If no
.Fl Fl mech= Ns Ar MECH
is given, then the Kerberos mechanism will be used.
If no
.Fl Fl into= Ns Ar KEY=VALUE
options are given, the credential acquired will not be stored
anywhere, but the exit status code of
.Nm
can be used to test if a credential could be acquired.
.Pp
In other words,
.Nm
.Nm acquire-cred
provides the same kind of functionality as
.Xr kinit 1
but using pure GSS-API functions rather than
.Dq krb5
APIs, though with some limitations, chiefly that the GSS-API
functions used do not support interaction with the user,
therefore the user must know a priori all the
.Ar KEY=VALUE
pairs needed to successfully acquire a credential.
Because
.Nm
uses pure GSS-API functions, it can work with any mechanism that
provides functionality similar to the Kerberos mechanism's
initial credential acquisition functionality.
.Pp
If a
.Ar cmd Oo arguments.. Oc
is given, then the command will be run, and for as long as it
runs the credentials will be kept fresh by renewing or
re-acquiring them.
Alternatively, if a
.Ar cmd Oo arguments.. Oc
is not given, and the
.Fl Fl shell \*(Ba Xo Fl s Xc
option is given, then environment variable settings may be
output.
.Pp
Mechanisms may, and the Kerberos GSS-API mechanism does try all
the strategies that are possible for the given
.Fl Fl from= Ns Ar KEY=VALUE
options, and in a reasonable order (such as: from a credentials
cache, using a keytab, using PKINIT, or using a password).
.Pp
As well,
.Nm
.Nm acquire-cred
can prompt for
.Ar VALUEs
with echo on and echo off, or even read them from files, so that
the user experience can be quite like that of
.Xr kinit 1 .
.Pp
Note that the
.Ar KEY=VALUE
pairs are specific to the GSS-API mechanisms.
See
.Sx CREDENTIAL STORE SPECIFICATION
for more details.
.Pp
By default
.Nm
acquires initiator credentials, unless
.Fl Fl acceptor
is given.
If both,
.Fl Fl initiator
and
.Fl Fl acceptor
are given, then
.Nm
acquires credentials for both.
Note though that currently there is no support for storing
acquired acceptor credentials.
.Pp
.Ar MECH
values currently accepted:
.Bl -tag -width Ds -offset indent
.It Ar all
(try acquiring credentials for all mechanisms)
.It Ar krb5
.It Ar ntlm
.It Ar spnego
.It Ar sanon_x25519
.It any mechanism OID, such as 1.3.6.1.5.5.2.
.El
.Pp
.Ar NAME-TYPE
values currently accepted:
.Bl -tag -width Ds -offset indent
.It Ar anonymous
.It Ar hostbased-service
.It Ar machine-uid
.It Ar string-uid
.It Ar user
.It any name-type OID, such as 1.3.6.1.5.5.2.
.El
.El
.Sh CREDENTIAL STORE SPECIFICATION
.Pp
Some of the
.Ar KEYs
for use in the
.Fl Fl from= Ns Ar KEY=VALUE
options for the Kerberos GSS-API mechanism are:
.Bl -tag -width Ds -offset indent
.It Nm appname= Ns Ar APP
Use the given
.Ar APP
name for any appdefaults.
.It Nm only=Ns Ar SOURCE
Only uses the given
.Ar SOURCE
for credentials acquisition.
Valid
.Ar SOURCEs
are:
.Bl -tag -width Ds -offset indent
.It Ar cache
Only look for cached credentials.
.It Ar renew
Only look for cached credentials, and renew them.
Requires the
.Ar renew KEY
to be set.
.It Ar keytab
Only acquire credentials with a keytab.
.It Ar pkinit
Only acquire credentials with PKINIT.
.It Ar password
Only acquire credentials with a password.
.El
.It Nm ccache= Ns Ar CREDENTIALS-CACHE
.It Nm renew
Attempts to renew the credential found in the
.Ar ccache .
But note that the renewed credential will not be written to the
cache.
Use the
.Fl Fl into= Ns Ar KEY=VALUE
command-line options to cause fresh/renewed credentials to be
stored.
.It Nm fresh
Acquire a fresh credential / do not use a cached credential
without renewing it.
If
.Ar fresh
is given without
.Ar renew
then no cached credential will be used.
.It Nm initial
Like
.Ar fresh ,
no cached credential will be acquired, but also no credential
will be renewed even if
.Ar renew
is set.
.It Nm service=PRINCIPAL
Instead of getting the usual TGT for the initiator, get a TGT for
the given
.Ar PRINCIPAL .
.It Nm keytab= Ns Ar KEYTAB
.It Nm client_keytab= Ns Ar KEYTAB
.It Nm password= Ns Ar PASSWORD
.It Nm kdc= Ns Ar HOSTNAME
Use the given KDC
.Ar HOSTNAME .
.It Nm sitename= Ns Ar SITENAME
Use the given site name
.Ar SITENAME
when searching for KDCs.
.It Nm pkinit_cert= Ns Ar CERT-STORE
Location of PKINIT client certificate and private key.
See
.Xr hxtool 1 .
.It Nm pkinit_pool= Ns Ar CERT-STORE
Optional store of certificates used to construct the client
certificate's chain.
.It Nm pkinit_anchors= Ns Ar CERT-STORE
Trust anchors for validating the KDCs' PKINIT certificates.
.It Nm pkinit_crl= Ns Ar CRL
CRL for KDC certificate validation.
.It Nm pkinit_password= Ns Ar PASSWORD
Optional password for the
.Nm pkinit_cert= Ns Ar CERT-STORE .
.It Nm pkinit_use_enckey
.It Nm pkinit_no_anchors
.It Nm pkinit_btmm
.It Nm fast_armor_cache= Ns Ar CREDENTIALS-CACHE
Use FAST armor, with the credentials in the given
.Ar CREDENTIALS-CACHE
.It Nm fast_anon_pkinit
Use FAST armored with an armor ticket obtained via anonymous
PKINIT.
.It Nm optimistic_fast_anon_pkinit
Try FAST armored with an armor ticket obtained via anonymous
PKINIT.
.It Nm renewable
.It Nm forwardable
.It Nm validate
.It Nm request_pac
.It Nm addressless
.El
.Pp
Some of the
.Ar KEYs
for use in the
.Fl Fl into= Ns Ar KEY=VALUE
options for the Kerberos mechanism are:
.Bl -tag -width Ds -offset indent
.It Nm unique_ccache_type=TYPE
Create a new, unique credentials cache of the given
.Ar TYPE .
E.g.,
.Dq Ar FILE .
.It Nm ccache= Ns Ar CREDENTIALS-CACHE
The specific credentials cache to store into.
E.g.,
.Dq Ar FILE:/tmp/some-ccache .
.It Nm username=USER-NAME
This is used for selecting the best credentials cache to use.
.It Nm appname=APP-NAME
This is used for resolving appdefaults.
.El
.Sh EXAMPLES
Test if there is an unexpired Kerberos credential:
.Bd -literal -offset indent
gsstool acquire-cred --from=only=cache
.Ed
.Pp
Test if there is an unexpired Kerberos credential for a specific
principal in a principal-specific cache in the default cache
collection:
.Bd -literal -offset indent
gsstool acquire-cred --name=some-principal-here \\
--from=only=cache
.Ed
.Pp
Test if there is an unexpired Kerberos credential in a specific
cache:
.Bd -literal -offset indent
gsstool acquire-cred --from=only=cache \\
--from=ccache=FILE:/tmp/some-ccache
.Ed
.Pp
Test what mechanisms the user has cached credentials for:
.Bd -literal -offset indent
gsstool acquire-cred --mech=all --from=only=cache
.Ed
.Pp
Renew a credential:
.Bd -literal -offset indent
gsstool acquire-cred --mech=krb5 --from=renew
.Ed
.Pp
Acquire a fresh Kerberos credential with a password, prompting
for it, then store it in a particular cache:
.Bd -literal -offset indent
gsstool acquire-cred --into=ccache=FILE:/tmp/some-ccache \\
--from-prompt=password="Password: "
.Ed
.Pp
Acquire a fresh Kerberos credential with a password read from a
file:
.Bd -literal -offset indent
gsstool acquire-cred --into=ccache=FILE:/tmp/some-ccache \\
--from-file=password=/tmp/password
.Ed
.Sh SEE ALSO
.Xr gss-token 1 ,
.Xr hxtool 1 ,
.Xr kinit 1 .

View File

@@ -37,6 +37,7 @@
#include <roken.h>
#include <stdio.h>
#include <string.h>
#include <gssapi.h>
#include <gssapi_krb5.h>
#include <gssapi_spnego.h>
@@ -58,10 +59,122 @@ static void
usage (int ret)
{
arg_printusage (args, sizeof(args)/sizeof(*args),
NULL, "service@host");
NULL, "help | mechanisms | attributes | acquire-cred");
exit (ret);
}
/* XXX Move the gss_display_status() wrappers into common code */
static char *
gss_fmt_errors(OM_uint32 code, int code_type, gss_OID mech, char *acc)
{
OM_uint32 maj, min;
OM_uint32 more = 0;
gss_buffer_desc buf;
do {
char *tmp = NULL;
char *s = NULL;
maj = gss_display_status(&min, code, code_type, mech, &more, &buf);
switch (maj) {
case GSS_S_COMPLETE:
s = strndup(buf.value, buf.length);
break;
case GSS_S_BAD_MECH:
s = strdup("<bad mechanism>");
more = 0;
break;
case GSS_S_BAD_STATUS:
if (asprintf(&s, "<unrecognized status code %u (%d)>", code, code_type) == -1)
s = NULL;
more = 0;
break;
default:
errx(1, "Could not display status code %u (%d)", code, code_type);
}
gss_release_buffer(&min, &buf);
if (s == NULL)
err(1, "Out of memory");
if (acc == NULL) {
acc = s;
s = NULL;
} else if (asprintf(&tmp, "%s; %s", acc, s) == -1 || tmp == NULL) {
err(1, "Out of memory formatting \"%s; %s\"", acc, s);
} else {
free(acc);
acc = tmp;
}
} while (more != 0);
return acc;
}
static void
gss_vwarn(OM_uint32 maj,
OM_uint32 min,
gss_OID mech,
const char *fmt,
va_list ap)
{
char *acc = NULL;
char *msg = NULL;
acc = gss_fmt_errors(maj, GSS_C_GSS_CODE, GSS_C_NO_OID, acc);
acc = gss_fmt_errors(min, GSS_C_MECH_CODE, mech, acc);
if (vasprintf(&msg, fmt, ap) == -1 || msg == NULL)
errx(1, "Out of memory formatting error message \"%s\"", fmt);
warnx("%s: %s", msg, acc);
}
static void
gss_warn(OM_uint32 maj,
OM_uint32 min,
gss_OID mech,
const char *fmt,
...)
{
va_list ap;
va_start(ap, fmt);
gss_vwarn(maj, min, mech, fmt, ap);
va_end(ap);
}
static void
gss_verr(int code,
OM_uint32 maj,
OM_uint32 min,
gss_OID mech,
const char *fmt,
va_list ap)
{
char *acc = NULL;
char *msg = NULL;
acc = gss_fmt_errors(maj, GSS_C_GSS_CODE, GSS_C_NO_OID, acc);
acc = gss_fmt_errors(min, GSS_C_MECH_CODE, mech, acc);
if (vasprintf(&msg, fmt, ap) == -1 || msg == NULL)
errx(1, "Out of memory formatting error message \"%s\"", fmt);
errx(code, "%s: %s", msg, acc);
}
static void
gss_err(int code,
OM_uint32 maj,
OM_uint32 min,
gss_OID mech,
const char *fmt,
...)
{
va_list ap;
va_start(ap, fmt);
gss_verr(code, maj, min, mech, fmt, ap);
va_end(ap);
}
#define COL_OID "OID"
#define COL_NAME "Name"
#define COL_DESC "Description"
@@ -213,6 +326,529 @@ attributes(struct attributes_options *opt, int argc, char **argv)
return 0;
}
static void
do_file(const char *arg,
gss_key_value_element_desc *store,
char **freeme,
size_t *k)
{
char *key, *fn;
void *contents;
size_t n;
if ((key = strdup(arg)) == NULL)
err(1, "Out of memory");
freeme[(*k)++] = key;
n = strcspn(key, "=");
key[n] = '\0';
fn = key + n + 1;
if ((errno = rk_undumpdata(fn, &contents, &n)))
err(1, "Could not read file %s", fn);
freeme[(*k)++] = contents;
store->key = key;
store->value = contents;
}
static sig_atomic_t intr_flag;
static void
intr(int sig)
{
intr_flag++;
}
#ifdef HAVE_CONIO_H
/*
* Windows does console slightly different then then unix case.
*/
static int
read_string(const char *preprompt, const char *prompt,
char *buf, size_t len, int echo)
{
int of = 0;
int c;
char *p;
void (*oldsigintr)(int);
printf("%s%s", preprompt, prompt);
fflush(stdout);
oldsigintr = signal(SIGINT, intr);
p = buf;
while(intr_flag == 0){
c = ((echo)? _getche(): _getch());
if(c == '\n' || c == '\r')
break;
if(of == 0)
*p++ = c;
of = (p == buf + len);
}
if(of)
p--;
*p = 0;
if(echo == 0){
printf("\n");
}
signal(SIGINT, oldsigintr);
if(intr_flag)
return -2;
if(of)
return -1;
return 0;
}
#else /* !HAVE_CONIO_H */
#ifndef NSIG
#define NSIG 47
#endif
static int
read_string(const char *preprompt, const char *prompt,
char *buf, size_t len, int echo)
{
struct sigaction sigs[NSIG];
int oksigs[NSIG];
struct sigaction sa;
FILE *tty;
int ret = 0;
int of = 0;
int i;
int c;
char *p;
struct termios t_new, t_old;
memset(&oksigs, 0, sizeof(oksigs));
memset(&sa, 0, sizeof(sa));
sa.sa_handler = intr;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
for(i = 1; i < sizeof(sigs) / sizeof(sigs[0]); i++)
if (i != SIGALRM)
if (sigaction(i, &sa, &sigs[i]) == 0)
oksigs[i] = 1;
if((tty = fopen("/dev/tty", "r")) != NULL)
rk_cloexec_file(tty);
else
tty = stdin;
fprintf(stderr, "%s%s", preprompt, prompt);
fflush(stderr);
if(echo == 0){
tcgetattr(fileno(tty), &t_old);
memcpy(&t_new, &t_old, sizeof(t_new));
t_new.c_lflag &= ~ECHO;
tcsetattr(fileno(tty), TCSANOW, &t_new);
}
intr_flag = 0;
p = buf;
while(intr_flag == 0){
c = getc(tty);
if(c == EOF){
if(!ferror(tty))
ret = 1;
break;
}
if(c == '\n')
break;
if(of == 0)
*p++ = c;
of = (p == buf + len);
}
if(of)
p--;
*p = 0;
if(echo == 0){
fprintf(stderr, "\n");
tcsetattr(fileno(tty), TCSANOW, &t_old);
}
if(tty != stdin)
fclose(tty);
for(i = 1; i < sizeof(sigs) / sizeof(sigs[0]); i++)
if (oksigs[i])
sigaction(i, &sigs[i], NULL);
if(ret)
return -3;
if(intr_flag)
return -2;
if(of)
return -1;
return 0;
}
#endif /* HAVE_CONIO_H */
static int
gsstool_UI_UTIL_read_pw_string(char *buf, int length, const char *prompt)
{
return read_string("", prompt, buf, length, 0);
}
static void
prompt(const char *arg,
gss_key_value_element_desc *store,
char **freeme,
size_t *k)
{
char *key, *val, *prompt;
char buf[1024];
size_t n;
int echo_on = 0;
memset(buf, 0, sizeof(buf));
if (strncmp(arg, "echo-on:", sizeof("echo-on:") - 1) == 0) {
arg += sizeof("echo-on:") - 1;
echo_on = 1;
} else if (strncmp(arg, "echo-off:", sizeof("echo-off:") - 1) == 0) {
arg += sizeof("echo-off:") - 1;
} else {
errx(1, "Invalid prompt specification");
}
if ((key = strdup(arg)) == NULL)
err(1, "Out of memory");
freeme[(*k)++] = key;
n = strcspn(key, "=");
prompt = key + n + 1;
key[n] = '\0';
if (echo_on) {
printf("%s", prompt);
if (fgets(buf, sizeof(buf) - 1, stdin) == NULL)
errx(1, "Could not read input");
} else if (gsstool_UI_UTIL_read_pw_string(buf, sizeof(buf) - 1, prompt)) {
memset(buf, 0, sizeof(buf));
errx(1, "Could not read input");
}
if ((val = strdup(buf)) == NULL)
err(1, "Out of memory");
freeme[(*k)++] = val;
store->key = key;
store->value = val;
}
static void
fill_in(const char *arg,
gss_key_value_element_desc *store,
char **freeme,
size_t *k)
{
size_t n;
char *s;
if ((s = strdup(arg)) == NULL)
err(1, "Out of memory");
freeme[(*k)++] = s;
n = strcspn(s, "=");
s[n] = '\0';
store->key = s;
store->value = s + n + 1;
}
static void
do_acquire(struct acquire_cred_options *opt,
gss_name_t name,
OM_uint32 time_req,
gss_OID_set mechs,
gss_cred_usage_t cred_usage,
gss_key_value_set_t from,
gss_key_value_set_t into,
int argc,
int renew,
OM_uint32 *time_rec)
{
gss_buffer_set_t env = GSS_C_NO_BUFFER_SET;
gss_cred_id_t cred = GSS_C_NO_CREDENTIAL;
gss_OID_set actual_mechs = GSS_C_NO_OID_SET;
OM_uint32 min, maj;
OM_uint32 flags = GSS_C_STORE_CRED_OVERWRITE;
maj = gss_acquire_cred_from(&min, name, time_req, mechs, cred_usage,
from, &cred, &actual_mechs, time_rec);
if (maj != GSS_S_COMPLETE) {
if (renew) {
gss_warn(maj, min, GSS_C_NO_OID, "Could not acquire credential");
return;
}
gss_err(1, maj, min, GSS_C_NO_OID, "Could not acquire credential");
}
if (opt->verbose_flag) {
size_t i;
for (i = 0; i < actual_mechs->count; i++) {
gss_buffer_desc str;
maj = gss_oid_to_str(&min, &actual_mechs->elements[i], &str);
if (maj != GSS_S_COMPLETE) {
gss_warn(maj, min, GSS_C_NO_OID,
"Could display mechanism OID");
continue;
}
fprintf(stderr, "Acquired credentials for mechanism %.*s "
"with %us lifetime\n", (int)str.length, (char *)str.value,
*time_rec);
gss_release_buffer(&min, &str);
}
}
gss_release_oid_set(&min, &actual_mechs);
if (into->count == 0 && argc == 0) {
gss_release_cred(&min, &cred);
warnx("Not storing acquired credentials; use --into option");
return;
}
if (argc)
flags |= GSS_C_STORE_CRED_SET_PROCESS;
maj = gss_store_cred_into2(&min, cred, cred_usage, GSS_C_NO_OID,
flags, into, NULL, NULL, &env);
gss_release_cred(&min, &cred);
if (maj != GSS_S_COMPLETE) {
if (renew) {
gss_warn(maj, min, GSS_C_NO_OID, "Could not store credential");
return;
}
gss_err(1, maj, min, GSS_C_NO_OID, "Could not store credential");
}
if (opt->verbose_flag)
fprintf(stderr, "Stored credentials\n");
if (env != GSS_C_NO_BUFFER_SET) {
size_t i;
for (i = 0; i < env->count; i++) {
if (argc) {
char *envvar;
if (renew)
continue;
if ((envvar = strndup((char *)env->elements[i].value, env->elements[i].length)) == NULL)
err(1, "Out of memory");
putenv(envvar);
continue;
}
if (opt->verbose_flag)
fprintf(stderr, "Environment variable: %.*s\n", (int)env->elements[i].length,
(char *)env->elements[i].value);
if (opt->shell_flag)
printf("%.*s\n", (int)env->elements[i].length,
(char *)env->elements[i].value);
}
}
}
struct renew_ctx {
struct acquire_cred_options *opt;
gss_name_t name;
OM_uint32 time_req;
gss_OID_set mechs;
gss_cred_usage_t cred_usage;
gss_key_value_set_t from;
gss_key_value_set_t into;
};
static time_t
renew_func(void *ptr)
{
struct renew_ctx *c = ptr;
OM_uint32 time_rec = 0;
do_acquire(c->opt, c->name, c->time_req, c->mechs, c->cred_usage, c->from,
c->into, 1, 1, &time_rec);
if (time_rec == 0)
time_rec = c->time_req;
if (time_rec > INT32_MAX)
return INT32_MAX;
return time_rec;
}
int
acquire_cred(struct acquire_cred_options *opt, int argc, char **argv)
{
gss_name_t name = GSS_C_NO_NAME;
gss_key_value_element_desc *from = NULL;
gss_key_value_element_desc *into = NULL;
gss_key_value_set_desc from_store, into_store;
gss_cred_usage_t cred_usage = 0;
gss_OID_set mechs = GSS_C_NO_OID_SET;
gss_OID name_type = GSS_C_NO_OID;
OM_uint32 min, maj, time_req, time_rec;
size_t k = 0;
size_t i, idx;
size_t num_from, num_into;
char **freeme = NULL;
int ret = 0;
if (opt->initiator_flag && opt->acceptor_flag)
cred_usage = GSS_C_BOTH;
else if (opt->acceptor_flag)
cred_usage = GSS_C_ACCEPT;
else
cred_usage = GSS_C_INITIATE;
if (opt->time_req_integer < 0)
time_req = GSS_C_INDEFINITE;
else
time_req = opt->time_req_integer;
if (opt->name_type_string) {
if (strcmp(opt->name_type_string, "user") == 0)
name_type = GSS_C_NT_USER_NAME;
else if (strcmp(opt->name_type_string, "machine-uid") == 0)
name_type = GSS_C_NT_MACHINE_UID_NAME;
else if (strcmp(opt->name_type_string, "string-uid") == 0)
name_type = GSS_C_NT_STRING_UID_NAME;
else if (strcmp(opt->name_type_string, "hostbased-service") == 0)
name_type = GSS_C_NT_HOSTBASED_SERVICE;
else if (strcmp(opt->name_type_string, "anonymous") == 0)
name_type = GSS_C_NT_ANONYMOUS;
else
name_type = gss_name_to_oid(opt->name_type_string);
if (name_type == GSS_C_NO_OID)
errx(1, "Could not parse the given name-type");
}
if (opt->name_string) {
gss_buffer_desc b;
b.length = strlen(opt->name_string);
b.value = opt->name_string;
maj = gss_import_name(&min, &b, name_type, &name);
if (maj != GSS_S_COMPLETE)
gss_err(1, maj, min, GSS_C_NO_OID, "Failed to import name");
}
num_from =
opt->from_strings.num_strings +
opt->from_prompt_strings.num_strings +
opt->from_file_strings.num_strings;
num_into =
opt->into_strings.num_strings +
opt->into_prompt_strings.num_strings +
opt->into_file_strings.num_strings;
from = calloc(num_from + 1, sizeof(*from));
into = calloc(num_into + 1, sizeof(*into));
freeme = calloc(2 * (num_from + num_into) + 1, sizeof(*freeme));
if (from == NULL || into == NULL || freeme == NULL)
err(1, "Out of memory");
/* Set up the cred store we're acquiring from */
from_store.count = num_from;
from_store.elements = from;
for (i = idx = 0; i < opt->from_strings.num_strings; i++, idx++)
fill_in(opt->from_strings.strings[i], &from[idx], freeme, &k);
for (i = 0; i < opt->from_prompt_strings.num_strings; i++, idx++)
prompt(opt->from_prompt_strings.strings[i], &from[idx], freeme, &k);
for (i = 0; i < opt->from_file_strings.num_strings; i++, idx++)
do_file(opt->from_file_strings.strings[i], &from[idx], freeme, &k);
/* Set up the cred store we're storing into */
into_store.count = num_into;
into_store.elements = into;
for (i = idx = 0; i < opt->into_strings.num_strings; i++, idx++)
fill_in(opt->into_strings.strings[i], &into[idx], freeme, &k);
for (i = k = 0; i < opt->into_prompt_strings.num_strings; i++, idx++)
prompt(opt->into_prompt_strings.strings[i], &into[idx], freeme, &k);
for (i = k = 0; i < opt->into_file_strings.num_strings; i++, idx++)
do_file(opt->into_file_strings.strings[i], &into[idx], freeme, &k);
if (opt->mech_strings.num_strings) {
maj = gss_create_empty_oid_set(&min, &mechs);
for (i = 0;
maj == GSS_S_COMPLETE && i < opt->mech_strings.num_strings;
i++) {
if (strcmp(opt->mech_strings.strings[i], "all") == 0) {
maj = gss_release_oid_set(&min, &mechs);
} else if (strcmp(opt->mech_strings.strings[i], "krb5") == 0) {
maj = gss_add_oid_set_member(&min, GSS_KRB5_MECHANISM, &mechs);
} else if (strcmp(opt->mech_strings.strings[i], "ntlm") == 0) {
maj = gss_add_oid_set_member(&min, GSS_NTLM_MECHANISM, &mechs);
} else if (strcmp(opt->mech_strings.strings[i], "spnego") == 0) {
maj = gss_add_oid_set_member(&min, GSS_SPNEGO_MECHANISM, &mechs);
} else if (strcmp(opt->mech_strings.strings[i], "sanon_x25519") == 0) {
maj = gss_add_oid_set_member(&min, GSS_SANON_X25519_MECHANISM, &mechs);
} else {
gss_OID mech = gss_name_to_oid(opt->mech_strings.strings[i]);
if (mech == GSS_C_NO_OID)
errx(1, "Could not parse the given name-type");
maj = gss_add_oid_set_member(&min, mech, &mechs);
}
}
if (maj != GSS_S_COMPLETE)
gss_err(1, min, maj, GSS_C_NO_OID,
"Could not make a set of mechanism OIDs");
} else {
maj = gss_create_empty_oid_set(&min, &mechs);
if (maj == GSS_S_COMPLETE)
maj = gss_add_oid_set_member(&min, GSS_KRB5_MECHANISM, &mechs);
if (maj != GSS_S_COMPLETE)
gss_err(1, min, maj, GSS_C_NO_OID,
"Could not make a set of the Kerberos mechanism OID");
}
do_acquire(opt, name, time_req, mechs, cred_usage, &from_store,
&into_store, argc, 0, &time_rec);
if (argc) {
struct renew_ctx ctx;
/*
* We have room for one more cred store item in `from'. We'll say we
* want to renew if possible. If renewing doesn't work, we hope that
* gss_acquire_cred_from() will then try to get fresh credentials (ours
* will), though that can fail (e.g., if passwords get changed).
*/
from[from_store.count].key = "renew";
from[from_store.count++].value = "";
ctx.opt = opt;
ctx.name = name;
ctx.time_req = time_req;
ctx.mechs = mechs;
ctx.cred_usage = cred_usage;
ctx.from = &from_store;
ctx.into = &into_store;
ret = simple_execvp_timed(argv[0], argv, renew_func, &ctx,
/* Timeout at 75% of credential lifetime */
(time_rec - (time_rec >> 2)));
}
gss_release_name(&min, &name);
for (i = 0; freeme[i] != NULL; i++)
free(freeme[i]);
free(freeme);
free(from);
free(into);
return ret;
}
/*
*

View File

@@ -60,6 +60,7 @@ kadmin="${kadmin} -l -A -r $R"
kdc="${kdc} --addresses=localhost -P $port"
acquire_cred="${TESTS_ENVIRONMENT} ../../lib/gssapi/test_acquire_cred"
acquire_cred2="${TESTS_ENVIRONMENT} $gsstool acquire-cred"
test_kcred="${TESTS_ENVIRONMENT} ../../lib/gssapi/test_kcred"
test_add_store_cred="${TESTS_ENVIRONMENT} ../../lib/gssapi/test_add_store_cred"
@@ -215,6 +216,13 @@ test_run ${acquire_cred} \
--ccache=${cache} \
--acquire-name=host@host.test.h5l.se
echo "test gsstool acquire-cred"
${acquire_cred2} \
--mech=krb5 \
--name=user \
--from=password=upw \
--into=unique_ccache_type=MEMORY || exit 1
trap "" EXIT
echo "killing kdc (${kdcpid})"

View File

@@ -583,6 +583,192 @@ test_run ${context} \
--max-loops=5 \
--name-type=hostbased-service host@lucid.test.h5l.se
# PKINIT credential store tests using gsstool
# These test the new pkinit_* credential store keys
gsstool="${TESTS_ENVIRONMENT} ../../lib/gssapi/gsstool"
hxtool="${TESTS_ENVIRONMENT} ../../lib/hx509/hxtool"
hx509_data="${top_srcdir}/lib/hx509/data"
keyfile="${hx509_data}/key.der"
keyfile2="${hx509_data}/key2.der"
# Check if PKINIT is available
pkinit_available=no
if ${kinit} --help 2>&1 | grep "CA certificates" > /dev/null; then
pkinit_available=yes
fi
# Check if we have RSA support
if ${hxtool} info 2>/dev/null | grep 'rsa: hx509 null RSA' > /dev/null; then
pkinit_available=no
fi
if ${hxtool} info 2>/dev/null | grep 'rand: not available' > /dev/null; then
pkinit_available=no
fi
if test "$pkinit_available" = yes; then
test_section "PKINIT credential store tests"
# Create certificates for PKINIT testing
echo "Setting up PKINIT certificates"
${hxtool} request-create \
--subject="CN=user1,DC=test,DC=h5l,DC=se" \
--key=FILE:${keyfile2} \
${objdir}/req-pkinit-user1.der || exit 1
# Issue self-signed CA cert
${hxtool} issue-certificate \
--self-signed \
--issue-ca \
--ca-private-key=FILE:${keyfile} \
--subject="CN=CA,DC=test,DC=h5l,DC=se" \
--certificate="FILE:${objdir}/pkinit-ca.crt" || exit 1
# Issue KDC certificate
${hxtool} request-create \
--subject="CN=kdc,DC=test,DC=h5l,DC=se" \
--key=FILE:${keyfile2} \
${objdir}/req-kdc.der || exit 1
${hxtool} issue-certificate \
--ca-certificate=FILE:${objdir}/pkinit-ca.crt,${keyfile} \
--type="pkinit-kdc" \
--pk-init-principal="krbtgt/${R}@${R}" \
--req="PKCS10:${objdir}/req-kdc.der" \
--certificate="FILE:${objdir}/pkinit-kdc.crt" || exit 1
# Issue user certificate with PKINIT SAN
${hxtool} issue-certificate \
--ca-certificate=FILE:${objdir}/pkinit-ca.crt,${keyfile} \
--type="pkinit-client" \
--pk-init-principal="user1@${R}" \
--req="PKCS10:${objdir}/req-pkinit-user1.der" \
--lifetime=7d \
--certificate="FILE:${objdir}/pkinit-user1.crt" || exit 1
# Restart KDC with PKINIT configuration
echo "Restarting KDC with PKINIT support"
kill ${kdcpid} 2>/dev/null
sleep 1
# Create PKINIT-enabled krb5.conf
cat > ${objdir}/krb5-pkinit-gss.conf <<EOF
[libdefaults]
default_realm = ${R}
no-addresses = TRUE
allow_weak_crypto = TRUE
[realms]
${R} = {
kdc = localhost:${port}
}
[kdc]
database = {
dbname = ${objdir}/current-db
realm = ${R}
mkey_file = ${objdir}/mkey.file
}
pkinit_identity = FILE:${objdir}/pkinit-kdc.crt,${keyfile2}
pkinit_anchors = FILE:${objdir}/pkinit-ca.crt
pkinit_mappings_file = ${objdir}/pki-mapping
enable-pkinit = true
pkinit_allow_proxy_certificate = true
[logging]
kdc = 0-/FILE:${objdir}/messages.log
default = 0-/FILE:${objdir}/messages.log
EOF
# Create PKI mapping file
cat > ${objdir}/pki-mapping <<EOF
user1@${R} CN=user1,DC=test,DC=h5l,DC=se
EOF
KRB5_CONFIG="${objdir}/krb5-pkinit-gss.conf"
export KRB5_CONFIG
${kdc} --detach --testing || { echo "PKINIT KDC failed to start"; cat messages.log; exit 1; }
kdcpid=`getpid kdc`
trap "kill ${kdcpid} 2>/dev/null; exit 1" EXIT INT TERM
test_section "gsstool acquire-cred with PKINIT credential store"
# Test basic PKINIT with pkinit_client_certs and pkinit_trust_anchors
test_run ${gsstool} acquire-cred \
--name=user1@${R} \
--mech=krb5 \
"--from=pkinit_client_certs=FILE:${objdir}/pkinit-user1.crt,${keyfile2}" \
"--from=pkinit_trust_anchors=FILE:${objdir}/pkinit-ca.crt" \
"--into=ccache=FILE:${objdir}/pkinit-gss-cache" \
--verbose
# Verify the credential was acquired
test_run ${klist} -c FILE:${objdir}/pkinit-gss-cache
# Test with multiple pkinit_intermediates (even if empty, tests the parsing)
test_run ${gsstool} acquire-cred \
--name=user1@${R} \
--mech=krb5 \
"--from=pkinit_client_certs=FILE:${objdir}/pkinit-user1.crt,${keyfile2}" \
"--from=pkinit_trust_anchors=FILE:${objdir}/pkinit-ca.crt" \
"--from=pkinit_intermediates=FILE:${objdir}/pkinit-ca.crt" \
"--into=ccache=FILE:${objdir}/pkinit-gss-cache2" \
--verbose
test_run ${klist} -c FILE:${objdir}/pkinit-gss-cache2
# Note: Anonymous PKINIT via GSS_C_NT_ANONYMOUS name type is not yet fully
# supported. Anonymous PKINIT requires either:
# 1. A credential store key to explicitly request anonymous mode
# 2. Or proper handling of GSS_C_NT_ANONYMOUS name import
# Skipping anonymous PKINIT test for now.
# Test fast_anon_pkinit credential store key (informational - may fail if
# KDC doesn't support FAST or anonymous PKINIT)
test_section "gsstool acquire-cred with FAST anonymous PKINIT (informational)"
if ${gsstool} acquire-cred \
--name=user1@${R} \
--mech=krb5 \
"--from=pkinit_client_certs=FILE:${objdir}/pkinit-user1.crt,${keyfile2}" \
"--from=pkinit_trust_anchors=FILE:${objdir}/pkinit-ca.crt" \
"--from=fast_anon_pkinit=" \
"--into=ccache=FILE:${objdir}/pkinit-fast-cache" \
--verbose 2>&1; then
echo "FAST anonymous PKINIT succeeded"
test_run ${klist} -c FILE:${objdir}/pkinit-fast-cache
else
echo "FAST anonymous PKINIT not available (expected in some configurations)"
fi
# Test fast_anon_pkinit_optimistic credential store key (informational)
if ${gsstool} acquire-cred \
--name=user1@${R} \
--mech=krb5 \
"--from=pkinit_client_certs=FILE:${objdir}/pkinit-user1.crt,${keyfile2}" \
"--from=pkinit_trust_anchors=FILE:${objdir}/pkinit-ca.crt" \
"--from=fast_anon_pkinit_optimistic=" \
"--into=ccache=FILE:${objdir}/pkinit-fast-opt-cache" \
--verbose 2>&1; then
echo "FAST optimistic PKINIT succeeded"
test_run ${klist} -c FILE:${objdir}/pkinit-fast-opt-cache
else
echo "FAST optimistic PKINIT not available (expected in some configurations)"
fi
# Clean up PKINIT test files
rm -f ${objdir}/req-pkinit-user1.der ${objdir}/req-kdc.der
rm -f ${objdir}/pkinit-ca.crt ${objdir}/pkinit-kdc.crt ${objdir}/pkinit-user1.crt
rm -f ${objdir}/pkinit-gss-cache ${objdir}/pkinit-gss-cache2
rm -f ${objdir}/pkinit-anon-cache ${objdir}/pkinit-fast-cache ${objdir}/pkinit-fast-opt-cache
rm -f ${objdir}/krb5-pkinit-gss.conf ${objdir}/pki-mapping
else
echo "Skipping PKINIT credential store tests (PKINIT not available)"
fi
trap "" EXIT
echo "killing kdc (${kdcpid})"