@@ -0,0 +1,349 @@
#
# Copyright (c) 2017 Red Hat
#
# Authors: Jakub Jelen <jjelen@redhat.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

tid="pkcs11 tests with soft token"

try_token_libs() {
for _lib in "$@" ; do
if test -f "$_lib" ; then
verbose "Using token library $_lib"
TEST_SSH_PKCS11="$_lib"
return
fi
done
echo "skipped: Unable to find PKCS#11 token library"
exit 0
}

try_token_libs \
/usr/local/lib/softhsm/libsofthsm2.so \
/usr/lib64/pkcs11/libsofthsm2.so \
/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so

TEST_SSH_PIN=1234
TEST_SSH_SOPIN=12345678
if [ "x$TEST_SSH_SSHPKCS11HELPER" != "x" ]; then
SSH_PKCS11_HELPER="${TEST_SSH_SSHPKCS11HELPER}"
export SSH_PKCS11_HELPER
fi

test -f "$TEST_SSH_PKCS11" || fatal "$TEST_SSH_PKCS11 does not exist"

# setup environment for softhsm token
DIR=$OBJ/SOFTHSM
rm -rf $DIR
TOKEN=$DIR/tokendir
mkdir -p $TOKEN
SOFTHSM2_CONF=$DIR/softhsm2.conf
export SOFTHSM2_CONF
cat > $SOFTHSM2_CONF << EOF
# SoftHSM v2 configuration file
directories.tokendir = ${TOKEN}
objectstore.backend = file
# ERROR, WARNING, INFO, DEBUG
log.level = DEBUG
# If CKF_REMOVABLE_DEVICE flag should be set
slots.removable = false
EOF
out=$(softhsm2-util --init-token --free --label token-slot-0 --pin "$TEST_SSH_PIN" --so-pin "$TEST_SSH_SOPIN")
slot=$(echo -- $out | sed 's/.* //')

# prevent ssh-agent from calling ssh-askpass
SSH_ASKPASS=/usr/bin/true
export SSH_ASKPASS
unset DISPLAY
# We need interactive access to test PKCS# since it prompts for PIN
sed -i 's/.*BatchMode.*//g' $OBJ/ssh_proxy

# start command w/o tty, so ssh accepts pin from stdin (from agent-pkcs11.sh)
notty() {
perl -e 'use POSIX; POSIX::setsid();
if (fork) { wait; exit($? >> 8); } else { exec(@ARGV) }' "$@"
}

trace "generating keys"
ID1="02"
ID2="04"
RSA=${DIR}/RSA
EC=${DIR}/EC
openssl genpkey -algorithm rsa > $RSA
openssl pkcs8 -nocrypt -in $RSA |\
softhsm2-util --slot "$slot" --label "SSH RSA Key $ID1" --id $ID1 \
--pin "$TEST_SSH_PIN" --import /dev/stdin
openssl genpkey \
-genparam \
-algorithm ec \
-pkeyopt ec_paramgen_curve:prime256v1 |\
openssl genpkey \
-paramfile /dev/stdin > $EC
openssl pkcs8 -nocrypt -in $EC |\
softhsm2-util --slot "$slot" --label "SSH ECDSA Key $ID2" --id $ID2 \
--pin "$TEST_SSH_PIN" --import /dev/stdin

trace "List the keys in the ssh-keygen with PKCS#11 URIs"
${SSHKEYGEN} -D ${TEST_SSH_PKCS11} > $OBJ/token_keys
if [ $? -ne 0 ]; then
fail "FAIL: keygen fails to enumerate keys on PKCS#11 token"
fi
grep "pkcs11:" $OBJ/token_keys > /dev/null
if [ $? -ne 0 ]; then
fail "FAIL: The keys from ssh-keygen do not contain PKCS#11 URI as a comment"
fi

# Set the ECDSA key to authorized keys
grep "ECDSA" $OBJ/token_keys > $OBJ/authorized_keys_$USER

trace "Simple connect with ssh (without PKCS#11 URI)"
echo ${TEST_SSH_PIN} | notty ${SSH} -I ${TEST_SSH_PKCS11} \
-F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with pkcs11 failed (exit code $r)"
fi

trace "Connect with PKCS#11 URI"
trace " (ECDSA key should succeed)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy \
-i "pkcs11:id=%${ID2}?module-path=${TEST_SSH_PKCS11}" somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI failed (exit code $r)"
fi

trace " (RSA key should fail)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy \
-i "pkcs11:id=%${ID1}?module-path=${TEST_SSH_PKCS11}" somehost exit 5
r=$?
if [ $r -eq 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI succeeded (should fail)"
fi

trace "Connect with PKCS#11 URI including PIN should not prompt"
trace " (ECDSA key should succeed)"
${SSH} -F $OBJ/ssh_proxy -i \
"pkcs11:id=%${ID2}?module-path=${TEST_SSH_PKCS11}&pin-value=${TEST_SSH_PIN}" somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI failed (exit code $r)"
fi

trace " (RSA key should fail)"
${SSH} -F $OBJ/ssh_proxy -i \
"pkcs11:id=%${ID1}?module-path=${TEST_SSH_PKCS11}&pin-value=${TEST_SSH_PIN}" somehost exit 5
r=$?
if [ $r -eq 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI succeeded (should fail)"
fi

trace "Connect with various filtering options in PKCS#11 URI"
trace " (by object label, ECDSA should succeed)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy \
-i "pkcs11:object=SSH%20ECDSA%20Key%2004?module-path=${TEST_SSH_PKCS11}" somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI failed (exit code $r)"
fi

trace " (by object label, RSA key should fail)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy \
-i "pkcs11:object=SSH%20RSA%20Key%2002?module-path=${TEST_SSH_PKCS11}" somehost exit 5
r=$?
if [ $r -eq 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI succeeded (should fail)"
fi

trace " (by token label, ECDSA key should succeed)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy \
-i "pkcs11:id=%${ID2};token=token-slot-0?module-path=${TEST_SSH_PKCS11}" somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI failed (exit code $r)"
fi

trace " (by wrong token label, should fail)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy \
-i "pkcs11:token=token-slot-99?module-path=${TEST_SSH_PKCS11}" somehost exit 5
r=$?
if [ $r -eq 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI succeeded (should fail)"
fi




trace "Test PKCS#11 URI specification in configuration files"
echo "IdentityFile \"pkcs11:id=%${ID2}?module-path=${TEST_SSH_PKCS11}\"" \
>> $OBJ/ssh_proxy
trace " (ECDSA key should succeed)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI in config failed (exit code $r)"
fi

# Set the RSA key as authorized
grep "RSA" $OBJ/token_keys > $OBJ/authorized_keys_$USER

trace " (RSA key should fail)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -eq 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI in config succeeded (should fail)"
fi
sed -i -e "/IdentityFile/d" $OBJ/ssh_proxy

trace "Test PKCS#11 URI specification in configuration files with bogus spaces"
echo "IdentityFile \" pkcs11:?module-path=${TEST_SSH_PKCS11} \"" \
>> $OBJ/ssh_proxy
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI with bogus spaces in config failed" \
"(exit code $r)"
fi
sed -i -e "/IdentityFile/d" $OBJ/ssh_proxy


trace "Combination of PKCS11Provider and PKCS11URI on commandline"
trace " (RSA key should succeed)"
echo ${TEST_SSH_PIN} | notty ${SSH} -F $OBJ/ssh_proxy \
-i "pkcs11:id=%${ID1}" -I ${TEST_SSH_PKCS11} somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect with PKCS#11 URI and provider combination" \
"failed (exit code $r)"
fi

trace "Regress: Missing provider in PKCS11URI option"
${SSH} -F $OBJ/ssh_proxy \
-o IdentityFile=\"pkcs11:token=segfault\" somehost exit 5
r=$?
if [ $r -eq 139 ]; then
fail "FAIL: ssh connect with missing provider_id from configuration option" \
"crashed (exit code $r)"
fi


trace "SSH Agent can work with PKCS#11 URI"
trace "start the agent"
eval `${SSHAGENT} -s` > /dev/null

r=$?
if [ $r -ne 0 ]; then
fail "could not start ssh-agent: exit code $r"
else
trace "add whole provider to agent"
echo ${TEST_SSH_PIN} | notty ${SSHADD} \
"pkcs11:?module-path=${TEST_SSH_PKCS11}" #> /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "FAIL: ssh-add failed with whole provider: exit code $r"
fi

trace " pkcs11 list via agent (all keys)"
${SSHADD} -l > /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "FAIL: ssh-add -l failed with whole provider: exit code $r"
fi

trace " pkcs11 connect via agent (all keys)"
${SSH} -F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect failed with whole provider (exit code $r)"
fi

trace " remove pkcs11 keys (all keys)"
${SSHADD} -d "pkcs11:?module-path=${TEST_SSH_PKCS11}" > /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "FAIL: ssh-add -d failed with whole provider: exit code $r"
fi

trace "add only RSA key to the agent"
echo ${TEST_SSH_PIN} | notty ${SSHADD} \
"pkcs11:id=%${ID1}?module-path=${TEST_SSH_PKCS11}" > /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "FAIL ssh-add failed with RSA key: exit code $r"
fi

trace " pkcs11 connect via agent (RSA key)"
${SSH} -F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "FAIL: ssh connect failed with RSA key (exit code $r)"
fi

trace " remove RSA pkcs11 key"
${SSHADD} -d "pkcs11:id=%${ID1}?module-path=${TEST_SSH_PKCS11}" \
> /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "FAIL: ssh-add -d failed with RSA key: exit code $r"
fi

trace "add only ECDSA key to the agent"
echo ${TEST_SSH_PIN} | notty ${SSHADD} \
"pkcs11:id=%${ID2}?module-path=${TEST_SSH_PKCS11}" > /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "FAIL: ssh-add failed with second key: exit code $r"
fi

trace " pkcs11 connect via agent (ECDSA key should fail)"
${SSH} -F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -eq 5 ]; then
fail "FAIL: ssh connect passed with ECDSA key (should fail)"
fi

trace "add also the RSA key to the agent"
echo ${TEST_SSH_PIN} | notty ${SSHADD} \
"pkcs11:id=%${ID1}?module-path=${TEST_SSH_PKCS11}" > /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "FAIL: ssh-add failed with first key: exit code $r"
fi

trace " remove ECDSA pkcs11 key"
${SSHADD} -d "pkcs11:id=%${ID2}?module-path=${TEST_SSH_PKCS11}" \
> /dev/null 2>&1
r=$?
if [ $r -ne 0 ]; then
fail "ssh-add -d failed with ECDSA key: exit code $r"
fi

trace " remove already-removed pkcs11 key should fail"
${SSHADD} -d "pkcs11:id=%${ID2}?module-path=${TEST_SSH_PKCS11}" \
> /dev/null 2>&1
r=$?
if [ $r -eq 0 ]; then
fail "FAIL: ssh-add -d passed with non-existing key (should fail)"
fi

trace " pkcs11 connect via agent (the RSA key should be still usable)"
${SSH} -F $OBJ/ssh_proxy somehost exit 5
r=$?
if [ $r -ne 5 ]; then
fail "ssh connect failed with RSA key (after removing ECDSA): exit code $r"
fi

trace "kill agent"
${SSHAGENT} -k > /dev/null
fi
@@ -2,6 +2,6 @@

REGRESS_FAIL_EARLY?= yes
SUBDIR= test_helper sshbuf sshkey bitmap kex hostkeys utf8 match conversion
SUBDIR+=authopt misc
SUBDIR+=authopt misc pkcs11

.include <bsd.subdir.mk>
@@ -0,0 +1,337 @@
/*
* Copyright (c) 2017 Red Hat
*
* Authors: Jakub Jelen <jjelen@redhat.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include "includes.h"

#include <locale.h>
#include <string.h>

#include "../test_helper/test_helper.h"

#include "sshbuf.h"
#include "ssh-pkcs11-uri.h"

#define EMPTY_URI compose_uri(NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL)

/* prototypes are not public -- specify them here internally for tests */
struct sshbuf *percent_encode(const char *, size_t, char *);
int percent_decode(char *, char **);

void
compare_uri(struct pkcs11_uri *a, struct pkcs11_uri *b)
{
ASSERT_PTR_NE(a, NULL);
ASSERT_PTR_NE(b, NULL);
ASSERT_SIZE_T_EQ(a->id_len, b->id_len);
ASSERT_MEM_EQ(a->id, b->id, a->id_len);
if (b->object != NULL)
ASSERT_STRING_EQ(a->object, b->object);
else /* both should be null */
ASSERT_PTR_EQ(a->object, b->object);
if (b->module_path != NULL)
ASSERT_STRING_EQ(a->module_path, b->module_path);
else /* both should be null */
ASSERT_PTR_EQ(a->module_path, b->module_path);
if (b->token != NULL)
ASSERT_STRING_EQ(a->token, b->token);
else /* both should be null */
ASSERT_PTR_EQ(a->token, b->token);
if (b->manuf != NULL)
ASSERT_STRING_EQ(a->manuf, b->manuf);
else /* both should be null */
ASSERT_PTR_EQ(a->manuf, b->manuf);
if (b->lib_manuf != NULL)
ASSERT_STRING_EQ(a->lib_manuf, b->lib_manuf);
else /* both should be null */
ASSERT_PTR_EQ(a->lib_manuf, b->lib_manuf);
}

void
check_parse_rv(char *uri, struct pkcs11_uri *expect, int expect_rv)
{
char *buf = NULL, *str;
struct pkcs11_uri *pkcs11uri = NULL;
int rv;

if (expect_rv == 0)
str = "Valid";
else
str = "Invalid";
asprintf(&buf, "%s PKCS#11 URI parsing: %s", str, uri);
TEST_START(buf);
free(buf);
pkcs11uri = pkcs11_uri_init();
rv = pkcs11_uri_parse(uri, pkcs11uri);
ASSERT_INT_EQ(rv, expect_rv);
if (rv == 0) /* in case of failure result is undefined */
compare_uri(pkcs11uri, expect);
pkcs11_uri_cleanup(pkcs11uri);
free(expect);
TEST_DONE();
}

void
check_parse(char *uri, struct pkcs11_uri *expect)
{
check_parse_rv(uri, expect, 0);
}

struct pkcs11_uri *
compose_uri(unsigned char *id, size_t id_len, char *token, char *lib_manuf,
char *manuf, char *module_path, char *object, char *pin)
{
struct pkcs11_uri *uri = pkcs11_uri_init();
if (id_len > 0) {
uri->id_len = id_len;
uri->id = id;
}
uri->module_path = module_path;
uri->token = token;
uri->lib_manuf = lib_manuf;
uri->manuf = manuf;
uri->object = object;
uri->pin = pin;
return uri;
}

static void
test_parse_valid(void)
{
/* path arguments */
check_parse("pkcs11:id=%01",
compose_uri("\x01", 1, NULL, NULL, NULL, NULL, NULL, NULL));
check_parse("pkcs11:id=%00%01",
compose_uri("\x00\x01", 2, NULL, NULL, NULL, NULL, NULL, NULL));
check_parse("pkcs11:token=SSH%20Keys",
compose_uri(NULL, 0, "SSH Keys", NULL, NULL, NULL, NULL, NULL));
check_parse("pkcs11:library-manufacturer=OpenSC",
compose_uri(NULL, 0, NULL, "OpenSC", NULL, NULL, NULL, NULL));
check_parse("pkcs11:manufacturer=piv_II",
compose_uri(NULL, 0, NULL, NULL, "piv_II", NULL, NULL, NULL));
check_parse("pkcs11:object=SIGN%20Key",
compose_uri(NULL, 0, NULL, NULL, NULL, NULL, "SIGN Key", NULL));
/* query arguments */
check_parse("pkcs11:?module-path=/usr/lib64/p11-kit-proxy.so",
compose_uri(NULL, 0, NULL, NULL, NULL, "/usr/lib64/p11-kit-proxy.so", NULL, NULL));
check_parse("pkcs11:?pin-value=123456",
compose_uri(NULL, 0, NULL, NULL, NULL, NULL, NULL, "123456"));

/* combinations */
/* ID SHOULD be percent encoded */
check_parse("pkcs11:token=SSH%20Key;id=0",
compose_uri("0", 1, "SSH Key", NULL, NULL, NULL, NULL, NULL));
check_parse(
"pkcs11:manufacturer=CAC?module-path=/usr/lib64/p11-kit-proxy.so",
compose_uri(NULL, 0, NULL, NULL, "CAC",
"/usr/lib64/p11-kit-proxy.so", NULL, NULL));
check_parse(
"pkcs11:object=RSA%20Key?module-path=/usr/lib64/pkcs11/opencryptoki.so",
compose_uri(NULL, 0, NULL, NULL, NULL,
"/usr/lib64/pkcs11/opencryptoki.so", "RSA Key", NULL));
check_parse("pkcs11:?module-path=/usr/lib64/p11-kit-proxy.so&pin-value=123456",
compose_uri(NULL, 0, NULL, NULL, NULL, "/usr/lib64/p11-kit-proxy.so", NULL, "123456"));

/* empty path component matches everything */
check_parse("pkcs11:", EMPTY_URI);

/* empty string is a valid to match against (and different from NULL) */
check_parse("pkcs11:token=",
compose_uri(NULL, 0, "", NULL, NULL, NULL, NULL, NULL));
/* Percent character needs to be percent-encoded */
check_parse("pkcs11:token=%25",
compose_uri(NULL, 0, "%", NULL, NULL, NULL, NULL, NULL));
}

static void
test_parse_invalid(void)
{
/* Invalid percent encoding */
check_parse_rv("pkcs11:id=%0", EMPTY_URI, -1);
/* Invalid percent encoding */
check_parse_rv("pkcs11:id=%ZZ", EMPTY_URI, -1);
/* Space MUST be percent encoded -- XXX not enforced yet */
check_parse("pkcs11:token=SSH Keys",
compose_uri(NULL, 0, "SSH Keys", NULL, NULL, NULL, NULL, NULL));
/* MUST NOT contain duplicate attributes of the same name */
check_parse_rv("pkcs11:id=%01;id=%02", EMPTY_URI, -1);
/* MUST NOT contain duplicate attributes of the same name */
check_parse_rv("pkcs11:?pin-value=111111&pin-value=123456", EMPTY_URI, -1);
/* Unrecognized attribute in path are ignored with log message */
check_parse("pkcs11:key_name=SSH", EMPTY_URI);
/* Unrecognized attribute in query SHOULD be ignored */
check_parse("pkcs11:?key_name=SSH", EMPTY_URI);
}

void
check_gen(char *expect, struct pkcs11_uri *uri)
{
char *buf = NULL, *uri_str;

asprintf(&buf, "Valid PKCS#11 URI generation: %s", expect);
TEST_START(buf);
free(buf);
uri_str = pkcs11_uri_get(uri);
ASSERT_PTR_NE(uri_str, NULL);
ASSERT_STRING_EQ(uri_str, expect);
free(uri_str);
TEST_DONE();
}

static void
test_generate_valid(void)
{
/* path arguments */
check_gen("pkcs11:id=%01",
compose_uri("\x01", 1, NULL, NULL, NULL, NULL, NULL, NULL));
check_gen("pkcs11:id=%00%01",
compose_uri("\x00\x01", 2, NULL, NULL, NULL, NULL, NULL, NULL));
check_gen("pkcs11:token=SSH%20Keys", /* space must be percent encoded */
compose_uri(NULL, 0, "SSH Keys", NULL, NULL, NULL, NULL, NULL));
/* library-manufacturer is not implmented now */
/*check_gen("pkcs11:library-manufacturer=OpenSC",
compose_uri(NULL, 0, NULL, "OpenSC", NULL, NULL, NULL, NULL));*/
check_gen("pkcs11:manufacturer=piv_II",
compose_uri(NULL, 0, NULL, NULL, "piv_II", NULL, NULL, NULL));
check_gen("pkcs11:object=RSA%20Key",
compose_uri(NULL, 0, NULL, NULL, NULL, NULL, "RSA Key", NULL));
/* query arguments */
check_gen("pkcs11:?module-path=/usr/lib64/p11-kit-proxy.so",
compose_uri(NULL, 0, NULL, NULL, NULL, "/usr/lib64/p11-kit-proxy.so", NULL, NULL));

/* combinations */
check_gen("pkcs11:id=%02;token=SSH%20Keys",
compose_uri("\x02", 1, "SSH Keys", NULL, NULL, NULL, NULL, NULL));
check_gen("pkcs11:id=%EE%02?module-path=/usr/lib64/p11-kit-proxy.so",
compose_uri("\xEE\x02", 2, NULL, NULL, NULL, "/usr/lib64/p11-kit-proxy.so", NULL, NULL));
check_gen("pkcs11:object=Encryption%20Key;manufacturer=piv_II",
compose_uri(NULL, 0, NULL, NULL, "piv_II", NULL, "Encryption Key", NULL));

/* empty path component matches everything */
check_gen("pkcs11:", EMPTY_URI);

}

void
check_encode(char *source, size_t len, char *whitelist, char *expect)
{
char *buf = NULL;
struct sshbuf *b;

asprintf(&buf, "percent_encode: expected %s", expect);
TEST_START(buf);
free(buf);

b = percent_encode(source, len, whitelist);
ASSERT_STRING_EQ(sshbuf_ptr(b), expect);
sshbuf_free(b);
TEST_DONE();
}

static void
test_percent_encode_multibyte(void)
{
/* SHOULD be encoded as octets according to the UTF-8 character encoding */

/* multi-byte characters are "for free" */
check_encode("$", 1, "", "%24");
check_encode("¢", 2, "", "%C2%A2");
check_encode("", 3, "", "%E2%82%AC");
check_encode("𐍈", 4, "", "%F0%90%8D%88");

/* CK_UTF8CHAR is unsigned char (1 byte) */
/* labels SHOULD be normalized to NFC [UAX15] */

}

static void
test_percent_encode(void)
{
/* Without whitelist encodes everything (for CKA_ID) */
check_encode("A*", 2, "", "%41%2A");
check_encode("\x00", 1, "", "%00");
check_encode("\x7F", 1, "", "%7F");
check_encode("\x80", 1, "", "%80");
check_encode("\xff", 1, "", "%FF");

/* Default whitelist encodes anything but safe letters */
check_encode("test" "\x00" "0alpha", 11, PKCS11_URI_WHITELIST,
"test%000alpha");
check_encode(" ", 1, PKCS11_URI_WHITELIST,
"%20"); /* Space MUST be percent encoded */
check_encode("/", 1, PKCS11_URI_WHITELIST,
"%2F"); /* '/' delimiter MUST be percent encoded (in the path) */
check_encode("?", 1, PKCS11_URI_WHITELIST,
"%3F"); /* delimiter '?' MUST be percent encoded (in the path) */
check_encode("#", 1, PKCS11_URI_WHITELIST,
"%23"); /* '#' MUST be always percent encoded */
check_encode("key=value;separator?query&amp;#anch", 35, PKCS11_URI_WHITELIST,
"key%3Dvalue%3Bseparator%3Fquery%26amp%3B%23anch");

/* Components in query can have '/' unencoded (useful for paths) */
check_encode("/path/to.file", 13, PKCS11_URI_WHITELIST "/",
"/path/to.file");
}

void
check_decode(char *source, char *expect, int expect_len)
{
char *buf = NULL, *out = NULL;
int rv;

asprintf(&buf, "percent_decode: %s", source);
TEST_START(buf);
free(buf);

rv = percent_decode(source, &out);
ASSERT_INT_EQ(rv, expect_len);
if (rv >= 0)
ASSERT_MEM_EQ(out, expect, expect_len);
free(out);
TEST_DONE();
}

static void
test_percent_decode(void)
{
/* simple valid cases */
check_decode("%00", "\x00", 1);
check_decode("%FF", "\xFF", 1);

/* normal strings shold be kept intact */
check_decode("strings are left", "strings are left", 16);
check_decode("10%25 of trees", "10% of trees", 12);

/* make sure no more than 2 bytes are parsed */
check_decode("%222", "\x22" "2", 2);

/* invalid expects failure */
check_decode("%0", "", -1);
check_decode("%Z", "", -1);
check_decode("%FG", "", -1);
}

void
tests(void)
{
test_percent_encode();
test_percent_encode_multibyte();
test_percent_decode();
test_parse_valid();
test_parse_invalid();
test_generate_valid();
}
@@ -67,6 +67,7 @@
#include "ssherr.h"
#include "digest.h"
#include "ssh-sk.h"
#include "ssh-pkcs11-uri.h"

/* argv0 */
extern char *__progname;
@@ -193,6 +194,32 @@ delete_all(int agent_fd, int qflag)
return ret;
}

#ifdef ENABLE_PKCS11
static int update_card(int, int, const char *, int, char *);

int
update_pkcs11_uri(int agent_fd, int adding, const char *pkcs11_uri, int qflag)
{
char *pin = NULL;
struct pkcs11_uri *uri;

/* dry-run parse to make sure the URI is valid and to report errors */
uri = pkcs11_uri_init();
if (pkcs11_uri_parse((char *) pkcs11_uri, uri) != 0)
fatal("Failed to parse PKCS#11 URI");
if (uri->pin != NULL) {
pin = strdup(uri->pin);
if (pin == NULL) {
fatal("Failed to dupplicate string");
}
/* pin is freed in the update_card() */
}
pkcs11_uri_cleanup(uri);

return update_card(agent_fd, adding, pkcs11_uri, qflag, pin);
}
#endif

static int
add_file(int agent_fd, const char *filename, int key_only, int qflag,
const char *skprovider)
@@ -402,12 +429,11 @@ add_file(int agent_fd, const char *filename, int key_only, int qflag,
}

static int
update_card(int agent_fd, int add, const char *id, int qflag)
update_card(int agent_fd, int add, const char *id, int qflag, char *pin)
{
char *pin = NULL;
int r, ret = -1;

if (add) {
if (add && pin == NULL) {
if ((pin = read_passphrase("Enter passphrase for PKCS#11: ",
RP_ALLOW_STDIN)) == NULL)
return -1;
@@ -591,6 +617,13 @@ static int
do_file(int agent_fd, int deleting, int key_only, char *file, int qflag,
const char *skprovider)
{
#ifdef ENABLE_PKCS11
if (strlen(file) >= strlen(PKCS11_URI_SCHEME) &&
strncmp(file, PKCS11_URI_SCHEME,
strlen(PKCS11_URI_SCHEME)) == 0) {
return update_pkcs11_uri(agent_fd, !deleting, file, qflag);
}
#endif
if (deleting) {
if (delete_file(agent_fd, file, key_only, qflag) == -1)
return -1;
@@ -773,7 +806,7 @@ main(int argc, char **argv)
}
if (pkcs11provider != NULL) {
if (update_card(agent_fd, !deleting, pkcs11provider,
qflag) == -1)
qflag, NULL) == -1)
ret = 1;
goto done;
}
@@ -641,10 +641,72 @@ no_identities(SocketEntry *e)
}

#ifdef ENABLE_PKCS11
static char *
sanitize_pkcs11_provider(const char *provider)
{
struct pkcs11_uri *uri = NULL;
char *sane_uri, *module_path = NULL; /* default path */
char canonical_provider[PATH_MAX];

if (provider == NULL)
return NULL;

if (strlen(provider) >= strlen(PKCS11_URI_SCHEME) &&
strncmp(provider, PKCS11_URI_SCHEME,
strlen(PKCS11_URI_SCHEME)) == 0) {
/* PKCS#11 URI */
uri = pkcs11_uri_init();
if (uri == NULL) {
error("Failed to init PKCS#11 URI");
return NULL;
}

if (pkcs11_uri_parse(provider, uri) != 0) {
error("Failed to parse PKCS#11 URI");
return NULL;
}
/* validate also provider from URI */
if (uri->module_path)
module_path = strdup(uri->module_path);
} else
module_path = strdup(provider); /* simple path */

if (module_path != NULL) { /* do not validate default NULL path in URI */
if (realpath(module_path, canonical_provider) == NULL) {
verbose("failed PKCS#11 provider \"%.100s\": realpath: %s",
module_path, strerror(errno));
free(module_path);
pkcs11_uri_cleanup(uri);
return NULL;
}
free(module_path);
if (match_pattern_list(canonical_provider, provider_whitelist, 0) != 1) {
verbose("refusing PKCS#11 provider \"%.100s\": "
"not whitelisted", canonical_provider);
pkcs11_uri_cleanup(uri);
return NULL;
}

/* copy verified and sanitized provider path back to the uri */
if (uri) {
free(uri->module_path);
uri->module_path = xstrdup(canonical_provider);
}
}

if (uri) {
sane_uri = pkcs11_uri_get(uri);
pkcs11_uri_cleanup(uri);
return sane_uri;
} else {
return xstrdup(canonical_provider); /* simple path */
}
}

static void
process_add_smartcard_key(SocketEntry *e)
{
char *provider = NULL, *pin = NULL, canonical_provider[PATH_MAX];
char *provider = NULL, *pin = NULL, *sane_uri = NULL;
char **comments = NULL;
int r, i, count = 0, success = 0, confirm = 0;
u_int seconds;
@@ -681,33 +743,28 @@ process_add_smartcard_key(SocketEntry *e)
goto send;
}
}
if (realpath(provider, canonical_provider) == NULL) {
verbose("failed PKCS#11 add of \"%.100s\": realpath: %s",
provider, strerror(errno));
goto send;
}
if (match_pattern_list(canonical_provider, provider_whitelist, 0) != 1) {
verbose("refusing PKCS#11 add of \"%.100s\": "
"provider not whitelisted", canonical_provider);

sane_uri = sanitize_pkcs11_provider(provider);
if (sane_uri == NULL)
goto send;
}
debug("%s: add %.100s", __func__, canonical_provider);

if (lifetime && !death)
death = monotime() + lifetime;

count = pkcs11_add_provider(canonical_provider, pin, &keys, &comments);
debug("%s: add %.100s", __func__, sane_uri);
count = pkcs11_add_provider(sane_uri, pin, &keys, &comments);
for (i = 0; i < count; i++) {
k = keys[i];
if (lookup_identity(k) == NULL) {
id = xcalloc(1, sizeof(Identity));
id->key = k;
keys[i] = NULL; /* transferred */
id->provider = xstrdup(canonical_provider);
id->provider = xstrdup(sane_uri);
if (*comments[i] != '\0') {
id->comment = comments[i];
comments[i] = NULL; /* transferred */
} else {
id->comment = xstrdup(canonical_provider);
id->comment = xstrdup(sane_uri);
}
id->death = death;
id->confirm = confirm;
@@ -721,6 +778,7 @@ process_add_smartcard_key(SocketEntry *e)
send:
free(pin);
free(provider);
free(sane_uri);
free(keys);
free(comments);
send_status(e, success);
@@ -729,7 +787,7 @@ process_add_smartcard_key(SocketEntry *e)
static void
process_remove_smartcard_key(SocketEntry *e)
{
char *provider = NULL, *pin = NULL, canonical_provider[PATH_MAX];
char *provider = NULL, *pin = NULL, *sane_uri = NULL;
int r, success = 0;
Identity *id, *nxt;

@@ -740,30 +798,29 @@ process_remove_smartcard_key(SocketEntry *e)
}
free(pin);

if (realpath(provider, canonical_provider) == NULL) {
verbose("failed PKCS#11 add of \"%.100s\": realpath: %s",
provider, strerror(errno));
sane_uri = sanitize_pkcs11_provider(provider);
if (sane_uri == NULL)
goto send;
}

debug("%s: remove %.100s", __func__, canonical_provider);
debug("%s: remove %.100s", __func__, sane_uri);
for (id = TAILQ_FIRST(&idtab->idlist); id; id = nxt) {
nxt = TAILQ_NEXT(id, next);
/* Skip file--based keys */
if (id->provider == NULL)
continue;
if (!strcmp(canonical_provider, id->provider)) {
if (!strcmp(sane_uri, id->provider)) {
TAILQ_REMOVE(&idtab->idlist, id, next);
free_identity(id);
idtab->nentries--;
}
}
if (pkcs11_del_provider(canonical_provider) == 0)
if (pkcs11_del_provider(sane_uri) == 0)
success = 1;
else
error("%s: pkcs11_del_provider failed", __func__);
send:
free(provider);
free(sane_uri);
send_status(e, success);
}
#endif /* ENABLE_PKCS11 */
@@ -855,8 +855,11 @@ do_download(struct passwd *pw)
free(fp);
} else {
(void) sshkey_write(keys[i], stdout); /* XXX check */
fprintf(stdout, "%s%s\n",
*(comments[i]) == '\0' ? "" : " ", comments[i]);
if (*(comments[i]) != '\0') {
fprintf(stdout, " %s", comments[i]);
}
(void) pkcs11_uri_write(keys[i], stdout);
fprintf(stdout, "\n");
}
free(comments[i]);
sshkey_free(keys[i]);
@@ -323,6 +323,8 @@ pkcs11_add_provider(char *name, char *pin, struct sshkey ***keysp,
u_int nkeys, i;
struct sshbuf *msg;

debug("%s: called, name = %s", __func__, name);

if (fd < 0 && pkcs11_start_helper() < 0)
return (-1);

@@ -342,6 +344,7 @@ pkcs11_add_provider(char *name, char *pin, struct sshkey ***keysp,
*keysp = xcalloc(nkeys, sizeof(struct sshkey *));
if (labelsp)
*labelsp = xcalloc(nkeys, sizeof(char *));
debug("%s: nkeys = %u", __func__, nkeys);
for (i = 0; i < nkeys; i++) {
/* XXX clean up properly instead of fatal() */
if ((r = sshbuf_get_string(msg, &blob, &blen)) != 0 ||

Large diffs are not rendered by default.

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2017 Red Hat
*
* Authors: Jakub Jelen <jjelen@redhat.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#define PKCS11_URI_SCHEME "pkcs11:"
#define PKCS11_URI_WHITELIST "abcdefghijklmnopqrstuvwxyz" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"0123456789_-.()"

struct pkcs11_uri {
/* path */
char *id;
size_t id_len;
char *token;
char *object;
char *lib_manuf;
char *manuf;
/* query */
char *module_path;
char *pin; /* Only parsed, but not printed */
};

struct pkcs11_uri *pkcs11_uri_init();
void pkcs11_uri_cleanup(struct pkcs11_uri *);
int pkcs11_uri_parse(const char *, struct pkcs11_uri *);
struct pkcs11_uri *pkcs11_uri_init();
char *pkcs11_uri_get(struct pkcs11_uri *uri);

Large diffs are not rendered by default.

@@ -22,10 +22,14 @@
#define SSH_PKCS11_ERR_PIN_REQUIRED 4
#define SSH_PKCS11_ERR_PIN_LOCKED 5

#include "ssh-pkcs11-uri.h"

int pkcs11_init(int);
void pkcs11_terminate(void);
int pkcs11_add_provider(char *, char *, struct sshkey ***, char ***);
int pkcs11_add_provider_by_uri(struct pkcs11_uri *, char *, struct sshkey ***, char ***);
int pkcs11_del_provider(char *);
int pkcs11_uri_write(const struct sshkey *, FILE *);
#ifdef WITH_PKCS11_KEYGEN
struct sshkey *
pkcs11_gakp(char *, char *, unsigned int, char *, unsigned int,
104 ssh.c
@@ -795,6 +795,14 @@ main(int ac, char **av)
options.gss_deleg_creds = 1;
break;
case 'i':
#ifdef ENABLE_PKCS11
if (strlen(optarg) >= strlen(PKCS11_URI_SCHEME) &&
strncmp(optarg, PKCS11_URI_SCHEME,
strlen(PKCS11_URI_SCHEME)) == 0) {
add_identity_file(&options, NULL, optarg, 1);
break;
}
#endif
p = tilde_expand_filename(optarg, getuid());
if (stat(p, &st) == -1)
fprintf(stderr, "Warning: Identity file %s "
@@ -1603,6 +1611,7 @@ main(int ac, char **av)
free(options.certificate_files[i]);
options.certificate_files[i] = NULL;
}
pkcs11_terminate();

skip_connect:
exit_status = ssh_session2(ssh, pw);
@@ -2076,6 +2085,45 @@ ssh_session2(struct ssh *ssh, struct passwd *pw)
options.escape_char : SSH_ESCAPECHAR_NONE, id);
}

#ifdef ENABLE_PKCS11
static void
load_pkcs11_identity(char *pkcs11_uri, char *identity_files[],
struct sshkey *identity_keys[], int *n_ids)
{
int nkeys, i;
struct sshkey **keys;
struct pkcs11_uri *uri;

debug("identity file '%s' from pkcs#11", pkcs11_uri);
uri = pkcs11_uri_init();
if (uri == NULL)
fatal("Failed to init PKCS#11 URI");

if (pkcs11_uri_parse(pkcs11_uri, uri) != 0)
fatal("Failed to parse PKCS#11 URI %s", pkcs11_uri);

/* we need to merge URI and provider together */
if (options.pkcs11_provider != NULL && uri->module_path == NULL)
uri->module_path = strdup(options.pkcs11_provider);

if (options.num_identity_files < SSH_MAX_IDENTITY_FILES &&
(nkeys = pkcs11_add_provider_by_uri(uri, NULL, &keys, NULL)) > 0) {
for (i = 0; i < nkeys; i++) {
if (*n_ids >= SSH_MAX_IDENTITY_FILES) {
sshkey_free(keys[i]);
continue;
}
identity_keys[*n_ids] = keys[i];
identity_files[*n_ids] = pkcs11_uri_get(uri);
(*n_ids)++;
}
free(keys);
}

pkcs11_uri_cleanup(uri);
}
#endif /* ENABLE_PKCS11 */

/* Loads all IdentityFile and CertificateFile keys */
static void
load_public_identity_files(struct passwd *pw)
@@ -2090,11 +2138,6 @@ load_public_identity_files(struct passwd *pw)
char *certificate_files[SSH_MAX_CERTIFICATE_FILES];
struct sshkey *certificates[SSH_MAX_CERTIFICATE_FILES];
int certificate_file_userprovided[SSH_MAX_CERTIFICATE_FILES];
#ifdef ENABLE_PKCS11
struct sshkey **keys = NULL;
char **comments = NULL;
int nkeys;
#endif /* PKCS11 */

n_ids = n_certs = 0;
memset(identity_files, 0, sizeof(identity_files));
@@ -2107,33 +2150,46 @@ load_public_identity_files(struct passwd *pw)
sizeof(certificate_file_userprovided));

#ifdef ENABLE_PKCS11
if (options.pkcs11_provider != NULL &&
options.num_identity_files < SSH_MAX_IDENTITY_FILES &&
(pkcs11_init(!options.batch_mode) == 0) &&
(nkeys = pkcs11_add_provider(options.pkcs11_provider, NULL,
&keys, &comments)) > 0) {
for (i = 0; i < nkeys; i++) {
if (n_ids >= SSH_MAX_IDENTITY_FILES) {
sshkey_free(keys[i]);
free(comments[i]);
continue;
}
identity_keys[n_ids] = keys[i];
identity_files[n_ids] = comments[i]; /* transferred */
n_ids++;
}
free(keys);
free(comments);
/* handle fallback from PKCS11Provider option */
pkcs11_init(!options.batch_mode);

if (options.pkcs11_provider != NULL) {
struct pkcs11_uri *uri;

uri = pkcs11_uri_init();
if (uri == NULL)
fatal("Failed to init PKCS#11 URI");

/* Construct simple PKCS#11 URI to simplify access */
uri->module_path = strdup(options.pkcs11_provider);

/* Add it as any other IdentityFile */
cp = pkcs11_uri_get(uri);
add_identity_file(&options, NULL, cp, 1);
free(cp);

pkcs11_uri_cleanup(uri);
}
#endif /* ENABLE_PKCS11 */
for (i = 0; i < options.num_identity_files; i++) {
char *name = options.identity_files[i];
if (n_ids >= SSH_MAX_IDENTITY_FILES ||
strcasecmp(options.identity_files[i], "none") == 0) {
strcasecmp(name, "none") == 0) {
free(options.identity_files[i]);
options.identity_files[i] = NULL;
continue;
}
cp = tilde_expand_filename(options.identity_files[i], getuid());
#ifdef ENABLE_PKCS11
if (strlen(name) >= strlen(PKCS11_URI_SCHEME) &&
strncmp(name, PKCS11_URI_SCHEME,
strlen(PKCS11_URI_SCHEME)) == 0) {
load_pkcs11_identity(name, identity_files,
identity_keys, &n_ids);
free(options.identity_files[i]);
continue;
}
#endif /* ENABLE_PKCS11 */
cp = tilde_expand_filename(name, getuid());
filename = percent_expand(cp, "d", pw->pw_dir,
"u", pw->pw_name, "l", thishost, "h", host,
"r", options.user, (char *)NULL);
@@ -986,6 +986,21 @@ may also be used in conjunction with
.Cm CertificateFile
in order to provide any certificate also needed for authentication with
the identity.
.Pp
The authentication identity can be also specified in a form of PKCS#11 URI
starting with a string
.Cm pkcs11: .
There is supported a subset of the PKCS#11 URI as defined
in RFC 7512 (implemented path arguments
.Cm id ,
.Cm manufacturer ,
.Cm object ,
.Cm token
and query arguments
.Cm module-path
and
.Cm pin-value
). The URI can not be in quotes.
.It Cm IgnoreUnknown
Specifies a pattern-list of unknown options to be ignored if they are
encountered in configuration parsing.