From 72d497728f5963d5eb822dd5cf5bbaa415cbb2ca Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Mon, 5 Oct 2026 11:18:24 +0900 Subject: [PATCH 1/3] internal, ossh: sign RSA user auth through ssh-agent - PrepareUserAuthRequestRsa() checks the agent-backed key blob's type against the request, decodes it with GetOpenSshPublicKey(), and reserves the signature name by sigNameSz. - BuildUserAuthRequestRsa() sizes the agent's reply room from the requested signature algorithm, requests the rsa-sha2-* hash the request names, and refuses a signature made with another algorithm. - GetOpenSshPublicKey() is built under WOLFSSH_AGENT too, decodes an OpenSSH RSA certificate, and is documented in internal.h. - api: run test_GetOpenSshPublicKey_type() in agent builds, with truncated OpenSSH RSA certificate vectors. - regress: share the agent user auth helpers across key types; the mock agent checks the key blob it is asked to sign with. Add tests for an RSA key against OpenSSH's and an rsa-sha2-256-only server, a certificate against a wolfSSH server, a refusing agent, a mismatched blob, and ignored flags and an oversize signature for both the key and the certificate. - openssh-interop.test: part 3 authenticates to sshd with an RSA key held by ssh-agent, once per rsa-sha2 algorithm the client was built with. --- scripts/openssh-interop.test | 109 ++++++- src/internal.c | 59 +++- src/ossh.c | 14 +- tests/api.c | 39 ++- tests/regress.c | 574 ++++++++++++++++++++++++++++------- wolfssh/internal.h | 5 +- 6 files changed, 671 insertions(+), 129 deletions(-) diff --git a/scripts/openssh-interop.test b/scripts/openssh-interop.test index 19ac825e7..51826ea35 100755 --- a/scripts/openssh-interop.test +++ b/scripts/openssh-interop.test @@ -10,6 +10,10 @@ # peer that disagreed about the counters would fail the first packet that # had to authenticate. # +# Part 3 has the wolfSSH client authenticate to sshd with an RSA key held +# by OpenSSH's ssh-agent. The client has only the public half; the agent +# makes the signature. +# # The checks read OpenSSH's own debug output, so they are worded the way # OpenSSH words them. Strict KEX arrived in OpenSSH 9.6; anything older is # skipped. @@ -17,6 +21,7 @@ no_pid=-1 server_pid=$no_pid sshd_pid=$no_pid +agent_pid=$no_pid work_dir="`pwd`/openssh_interop_test$$" ready_file="$work_dir/ready" client_out="$work_dir/client.out" @@ -53,6 +58,11 @@ do_cleanup() { kill -9 $server_pid 2>/dev/null server_pid=$no_pid fi + if [ $agent_pid != $no_pid ] + then + kill $agent_pid 2>/dev/null + agent_pid=$no_pid + fi rm -rf -- "$work_dir" } @@ -300,16 +310,20 @@ ssh-keygen -q -t ed25519 -N '' -f "$work_dir/hostkey-ed25519" < /dev/null \ # sshd in the foreground for one connection. ForceCommand stands in for the # user's login shell: the client exits with the session's exit status, and # whatever the user's dotfiles leave behind is not this test's business. +# Arguments after the port are passed to sshd. start_sshd() { + sshd_listen_port=$1 + shift rm -f "$sshd_log" - "$sshd_bin" -ddd -f /dev/null -p "$1" -h "$work_dir/hostkey" \ + "$sshd_bin" -ddd -f /dev/null -p "$sshd_listen_port" \ + -h "$work_dir/hostkey" \ -h "$work_dir/hostkey-rsa" -h "$work_dir/hostkey-ed25519" \ -o "ListenAddress=127.0.0.1" \ -o "AuthorizedKeysFile=$work_dir/authorized_keys" \ -o "StrictModes=no" \ -o "PasswordAuthentication=no" \ -o "ForceCommand=true" \ - -o "PidFile=none" > "$sshd_log" 2>&1 & + -o "PidFile=none" "$@" > "$sshd_log" 2>&1 & sshd_pid=$! # Its own counter: the loop that calls this one is counting too. A @@ -386,12 +400,12 @@ run_wolfssh_client() { # sshd handles one connection per -d run, so it needs restarting between # attempts. Wait for the old one to go first, or the restart races it for -# the port. +# the port. Any arguments are passed to sshd. restart_sshd() { kill -9 $sshd_pid 2>/dev/null wait $sshd_pid 2>/dev/null sshd_pid=$no_pid - start_sshd $sshd_port || fail "couldn't restart sshd" + start_sshd $sshd_port "$@" || fail "couldn't restart sshd" } run_wolfssh_client "$keys_dir/hansel-key-ecc.der" \ @@ -439,6 +453,91 @@ fi echo "the wolfSSH client negotiated strict KEX with sshd" +# Part 3 needs the client's agent support and OpenSSH's agent tools. +if ! ./examples/client/client '-?' 2>&1 | grep -q 'SSH-AGENT'; then + echo "the example client has no agent support, skipping the agent half" + do_cleanup + exit 0 +fi +if ! command -v ssh-agent > /dev/null 2>&1 || + ! command -v ssh-add > /dev/null 2>&1; then + echo "no ssh-agent or ssh-add, skipping the agent half" + do_cleanup + exit 0 +fi + +# The RSA signature algorithms this client was built with; -E lists them +# without connecting. +agent_rsa_algs= +for alg in rsa-sha2-256 rsa-sha2-512; do + if ./examples/client/client -E -u x 2>&1 | grep -q ": $alg\$"; then + agent_rsa_algs="$agent_rsa_algs $alg" + fi +done +if [ -z "$agent_rsa_algs" ]; then + echo "the client has no rsa-sha2 algorithms, skipping the agent half" + do_cleanup + exit 0 +fi + +echo "Test the wolfSSH client signing through ssh-agent for sshd" + +# Run the client with only the public key $1, leaving the signature to the +# agent. $2 is the authorized_keys file sshd reads. +run_wolfssh_agent_client() { + cat "$2" > "$work_dir/authorized_keys" \ + || fail "couldn't write the authorized_keys file" + chmod 600 "$work_dir/authorized_keys" + + rm -f "$client_out" + HOME="$work_dir" ./examples/client/client \ + -h 127.0.0.1 -p $sshd_port -u "$login_user" -a -j "$1" -x \ + < /dev/null > "$client_out" 2>&1 & + wait_with_limit $! +} + +# Check that the client, which exited with status $1, succeeded and that +# sshd accepted the agent's signature under pkalg $2, an extended regular +# expression. +check_agent_auth() { + counter=0 + while [ "$counter" -lt 50 ]; do + grep -q 'Accepted publickey' "$sshd_log" 2>/dev/null && break + sleep 0.1 + counter=$((counter + 1)) + done + + if [ "$1" -ne 0 ] || ! grep -q 'Accepted publickey' "$sshd_log" || + ! grep -Eq "authenticated 1 pkalg ($2)( |\$)" "$sshd_log"; then + cat "$client_out" + tail -n 20 "$sshd_log" + fail "the client didn't authenticate with $2 signed by ssh-agent" + fi +} + +# The agent picks its own socket path; one under work_dir can outgrow +# sun_path in a deep build directory. +SSH_AGENT_PID= +eval `ssh-agent -s` > /dev/null +[ -n "$SSH_AGENT_PID" ] || fail "couldn't start ssh-agent" +agent_pid=$SSH_AGENT_PID + +# A key of ssh-keygen's default size, which the client doesn't have +# built in, so only the agent can sign for it. +ssh-keygen -q -t rsa -N '' -C wolfssh-agent -f "$work_dir/agent-rsa" \ + < /dev/null || fail "couldn't make an RSA client key" +ssh-add -q "$work_dir/agent-rsa" 2>/dev/null \ + || fail "couldn't add the RSA key to ssh-agent" + +for alg in $agent_rsa_algs; do + restart_sshd -o "PubkeyAcceptedAlgorithms=$alg" + run_wolfssh_agent_client "$work_dir/agent-rsa.pub" \ + "$work_dir/agent-rsa.pub" + check_agent_auth $? "$alg" +done + +echo "ssh-agent signed the wolfSSH client's RSA user auth" + do_cleanup -echo "OpenSSH strict KEX interop tests passed" +echo "OpenSSH interop tests passed" exit 0 diff --git a/src/internal.c b/src/internal.c index e2ae79b14..a2cda8e78 100644 --- a/src/internal.c +++ b/src/internal.c @@ -20272,9 +20272,24 @@ static int PrepareUserAuthRequestRsa(WOLFSSH* ssh, word32* payloadSz, word32 idx = 0; #ifdef WOLFSSH_AGENT if (ssh->agentEnabled) { - ret = wc_RsaPublicKeyDecode(authData->sf.publicKey.publicKey, - &idx, &keySig->ks.rsa.key, - authData->sf.publicKey.publicKeySz); + const byte* blobType = NULL; + word32 blobTypeSz = 0; + + /* Decode only a blob of the key type the request names. */ + ret = GetStringRef(&blobTypeSz, &blobType, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz, &idx); + if (ret == WS_SUCCESS && NameToId((const char*)blobType, + blobTypeSz) != keySig->keyId) { + WLOG(WS_LOG_DEBUG, "SUAR: agent key blob type mismatch"); + ret = WS_INVALID_ALGO_ID; + } + if (ret == WS_SUCCESS) { + idx = 0; + ret = GetOpenSshPublicKey(keySig, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz, &idx); + } } else #endif /* WOLFSSH_AGENT */ @@ -20306,7 +20321,7 @@ static int PrepareUserAuthRequestRsa(WOLFSSH* ssh, word32* payloadSz, if (sigSz >= 0) { *payloadSz += (LENGTH_SZ * 3) + (word32)sigSz + - authData->sf.publicKey.publicKeyTypeSz; + keySig->sigNameSz; keySig->sigSz = sigSz; } else @@ -20383,14 +20398,40 @@ static int BuildUserAuthRequestRsa(WOLFSSH* ssh, #ifdef WOLFSSH_AGENT if (ssh->agentEnabled) { - if (ret == WS_SUCCESS) + /* Size the agent's reply room from the requested signature algorithm */ + word32 agentSigSz = (LENGTH_SZ * 2) + keySig->sigSz + + (word32)WSTRLEN(IdToName(effSigId)); + word32 agentFlags = 0; + const byte* agentSigName = NULL; + word32 agentSigNameSz = 0; + word32 nameIdx = 0; + + /* Ask the agent for the signature algorithm the request names. */ + if (effSigId == ID_RSA_SHA2_256) + agentFlags = AGENT_SIGN_RSA_SHA2_256; + else if (effSigId == ID_RSA_SHA2_512) + agentFlags = AGENT_SIGN_RSA_SHA2_512; + + if (ret == WS_SUCCESS) { + WLOG(WS_LOG_INFO, "Signing with RSA through the agent."); ret = wolfSSH_AGENT_SignRequest(ssh, checkData, checkDataSz, - output + begin + LENGTH_SZ, &keySig->sigSz, + output + begin + LENGTH_SZ, &agentSigSz, authData->sf.publicKey.publicKey, - authData->sf.publicKey.publicKeySz, 0); + authData->sf.publicKey.publicKeySz, agentFlags); + } + /* An agent that ignores the flags signs with another algorithm. */ if (ret == WS_SUCCESS) { - c32toa(keySig->sigSz, output + begin); - begin += LENGTH_SZ + keySig->sigSz; + ret = GetStringRef(&agentSigNameSz, &agentSigName, + output + begin + LENGTH_SZ, agentSigSz, &nameIdx); + } + if (ret == WS_SUCCESS && NameToId((const char*)agentSigName, + agentSigNameSz) != effSigId) { + WLOG(WS_LOG_DEBUG, "SUAR: agent signed with another algorithm"); + ret = WS_INVALID_ALGO_ID; + } + if (ret == WS_SUCCESS) { + c32toa(agentSigSz, output + begin); + begin += LENGTH_SZ + agentSigSz; } } else diff --git a/src/ossh.c b/src/ossh.c index e64591ddc..d102cfad0 100644 --- a/src/ossh.c +++ b/src/ossh.c @@ -399,7 +399,7 @@ static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, } #endif -#ifdef WOLFSSH_TPM +#if defined(WOLFSSH_TPM) || defined(WOLFSSH_AGENT) #ifndef WOLFSSH_NO_ECDSA static int GetOpenSshPublicKeyEcc(ecc_key* key, const byte* buf, word32 len, @@ -501,6 +501,16 @@ int GetOpenSshPublicKey(WS_KeySignature *key, ret = GetOpenSshPublicKeyRsa(&key->ks.rsa.key, buf, len, idx); break; #endif + #if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) + case ID_OSSH_CERT_RSA: + /* The certificate's nonce sits between its type and e. */ + ret = GetSkip(buf, len, idx); + if (ret == WS_SUCCESS) { + ret = GetOpenSshPublicKeyRsa(&key->ks.rsa.key, buf, len, + idx); + } + break; + #endif #ifndef WOLFSSH_NO_ECDSA case ID_ECDSA_SHA2_NISTP256: case ID_ECDSA_SHA2_NISTP384: @@ -537,7 +547,7 @@ int GetOpenSshPublicKey(WS_KeySignature *key, return ret; } -#endif /* WOLFSSH_TPM */ +#endif /* WOLFSSH_TPM || WOLFSSH_AGENT */ /* * Decodes an OpenSSH format key. diff --git a/tests/api.c b/tests/api.c index 8b528290c..5be9c300d 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2357,7 +2357,8 @@ static void test_LoadTpmSshKey_NoTrailingNewline(void) #endif /* WOLFSSH_TPM && FILESYSTEM && !USER_FILESYSTEM */ -#if defined(WOLFSSH_TPM) && defined(WOLFSSH_TEST_INTERNAL) +#if (defined(WOLFSSH_TPM) || defined(WOLFSSH_AGENT)) && \ + defined(WOLFSSH_TEST_INTERNAL) /* The key type is read with GetStringRef(), which sets the length from the * wire but leaves the pointer alone when the name runs past the buffer. */ @@ -2383,6 +2384,25 @@ static void test_GetOpenSshPublicKey_type(void) 0x00, 0x00, 0x00, 0x09, 0x00, 0xC5, 0x1A, 0x37, 0x8B, 0x42, 0x9D, 0xE0, 0x6F }; +#endif +#if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) + /* A nonce whose length runs past the buffer. */ + static const byte rsaCertTruncNonce[] = { + 0x00, 0x00, 0x00, 0x1C, 's', 's', 'h', '-', 'r', 's', 'a', '-', + 'c', 'e', 'r', 't', '-', 'v', '0', '1', '@', 'o', 'p', 'e', 'n', + 's', 's', 'h', '.', 'c', 'o', 'm', + 0x00, 0x00, 0x00, 0x20, 0xA1, 0xB2 + }; + /* An n whose length runs past the buffer. */ + static const byte rsaCertTruncN[] = { + 0x00, 0x00, 0x00, 0x1C, 's', 's', 'h', '-', 'r', 's', 'a', '-', + 'c', 'e', 'r', 't', '-', 'v', '0', '1', '@', 'o', 'p', 'e', 'n', + 's', 's', 'h', '.', 'c', 'o', 'm', + 0x00, 0x00, 0x00, 0x04, 0xA1, 0xB2, 0xC3, 0xD4, + 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x09, + 0x00, 0xC5, 0x1A + }; #endif WS_KeySignature keySig; word32 idx; @@ -2418,9 +2438,21 @@ static void test_GetOpenSshPublicKey_type(void) AssertIntEQ(idx, (word32)sizeof(rsaKey)); AssertIntEQ(wc_FreeRsaKey(&keySig.ks.rsa.key), 0); #endif + +#if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) + idx = 0; + AssertIntEQ(GetOpenSshPublicKey(&keySig, rsaCertTruncNonce, + (word32)sizeof(rsaCertTruncNonce), &idx), WS_BUFFER_E); + + idx = 0; + AssertIntEQ(wc_InitRsaKey(&keySig.ks.rsa.key, NULL), 0); + AssertIntEQ(GetOpenSshPublicKey(&keySig, rsaCertTruncN, + (word32)sizeof(rsaCertTruncN), &idx), WS_BUFFER_E); + AssertIntEQ(wc_FreeRsaKey(&keySig.ks.rsa.key), 0); +#endif } -#endif /* WOLFSSH_TPM && WOLFSSH_TEST_INTERNAL */ +#endif /* (WOLFSSH_TPM || WOLFSSH_AGENT) && WOLFSSH_TEST_INTERNAL */ static void test_wolfSSH_ReadKey_badPad(void) @@ -8704,7 +8736,8 @@ int wolfSSH_ApiTest(int argc, char** argv) !defined(NO_WRITE_TEMP_FILES) && !defined(WOLFSSH_USER_FILESYSTEM) test_LoadTpmSshKey_NoTrailingNewline(); #endif -#if defined(WOLFSSH_TPM) && defined(WOLFSSH_TEST_INTERNAL) +#if (defined(WOLFSSH_TPM) || defined(WOLFSSH_AGENT)) && \ + defined(WOLFSSH_TEST_INTERNAL) test_GetOpenSshPublicKey_type(); #endif test_wolfSSH_ReadKey_shortBuffer(); diff --git a/tests/regress.c b/tests/regress.c index ab626df2c..6eaec6ffa 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -8909,23 +8909,11 @@ static void TestClientRefusesForwardRequestsBeforePolicy(void) #endif /* !NO_WOLFSSH_CLIENT */ -#if defined(WOLFSSH_AGENT) && !defined(WOLFSSH_NO_ED25519) \ - && !defined(NO_WOLFSSH_CLIENT) +#if defined(WOLFSSH_AGENT) && !defined(NO_WOLFSSH_CLIENT) -#define AGENT_ED25519_NAME "ssh-ed25519" - -/* Canned agent state: the sign response handed back on a read, and the - * public key blob the user auth callback offers. */ -typedef struct { - byte response[128]; - word32 responseSz; - byte pubKeyBlob[64]; - word32 pubKeyBlobSz; - byte sigBlob[128]; - word32 sigBlobSz; -} AgentEd25519Ctx; - -static int AgentEd25519Cb(WS_AgentCbAction action, void* agentCbCtx) +/* Shared by the Ed25519 and RSA agent tests; some builds have neither. */ +static WS_MAYBE_UNUSED int AgentSignCb(WS_AgentCbAction action, + void* agentCbCtx) { (void)agentCbCtx; @@ -8937,101 +8925,25 @@ static int AgentEd25519Cb(WS_AgentCbAction action, void* agentCbCtx) return WS_AGENT_INVALID_ACTION; } -static int AgentEd25519IoCb(WS_AgentIoCbAction action, void* buf, - word32 bufSz, void* agentCbCtx) -{ - AgentEd25519Ctx* agentCtx = (AgentEd25519Ctx*)agentCbCtx; - - if (action == WOLFSSH_AGENT_IO_WRITE) - return (int)bufSz; - - if (bufSz < agentCtx->responseSz) - return 0; - WMEMCPY(buf, agentCtx->response, agentCtx->responseSz); - return (int)agentCtx->responseSz; -} - -/* Offer an Ed25519 public key with no private key, as an agent-backed - * client does. */ -static int AgentEd25519UserAuth(byte authType, WS_UserAuthData* authData, - void* ctx) -{ - AgentEd25519Ctx* agentCtx = (AgentEd25519Ctx*)ctx; - - if (authType != WOLFSSH_USERAUTH_PUBLICKEY || authData == NULL) - return WOLFSSH_USERAUTH_INVALID_AUTHTYPE; - - authData->sf.publicKey.publicKeyType = (const byte*)AGENT_ED25519_NAME; - authData->sf.publicKey.publicKeyTypeSz = - (word32)WSTRLEN(AGENT_ED25519_NAME); - authData->sf.publicKey.publicKey = agentCtx->pubKeyBlob; - authData->sf.publicKey.publicKeySz = agentCtx->pubKeyBlobSz; - authData->sf.publicKey.privateKey = NULL; - authData->sf.publicKey.privateKeySz = 0; - - return WOLFSSH_USERAUTH_SUCCESS; -} - -static void InitAgentEd25519Ctx(AgentEd25519Ctx* agentCtx, word32 sigSz) -{ - byte pubKey[ED25519_PUB_KEY_SIZE]; - byte sig[ED25519_SIG_SIZE + 1]; - byte body[128]; - word32 idx; - - AssertTrue(sigSz <= (word32)sizeof(sig)); - - WMEMSET(agentCtx, 0, sizeof(*agentCtx)); - WMEMSET(pubKey, 0x5a, sizeof(pubKey)); - WMEMSET(sig, 0xa5, sizeof(sig)); - - idx = AppendString(agentCtx->pubKeyBlob, sizeof(agentCtx->pubKeyBlob), 0, - AGENT_ED25519_NAME); - idx = AppendUint32(agentCtx->pubKeyBlob, sizeof(agentCtx->pubKeyBlob), idx, - (word32)sizeof(pubKey)); - agentCtx->pubKeyBlobSz = AppendData(agentCtx->pubKeyBlob, - sizeof(agentCtx->pubKeyBlob), idx, pubKey, (word32)sizeof(pubKey)); - - /* The blob an agent returns holds the algorithm name and signature. */ - idx = AppendString(agentCtx->sigBlob, sizeof(agentCtx->sigBlob), 0, - AGENT_ED25519_NAME); - idx = AppendUint32(agentCtx->sigBlob, sizeof(agentCtx->sigBlob), idx, - sigSz); - agentCtx->sigBlobSz = AppendData(agentCtx->sigBlob, - sizeof(agentCtx->sigBlob), idx, sig, sigSz); - - idx = AppendUint32(body, sizeof(body), 0, agentCtx->sigBlobSz); - idx = AppendData(body, sizeof(body), idx, agentCtx->sigBlob, - agentCtx->sigBlobSz); - - agentCtx->responseSz = AppendUint32(agentCtx->response, - sizeof(agentCtx->response), 0, MSG_ID_SZ + idx); - agentCtx->responseSz = AppendByte(agentCtx->response, - sizeof(agentCtx->response), agentCtx->responseSz, - MSGID_AGENT_SIGN_RESPONSE); - agentCtx->responseSz = AppendData(agentCtx->response, - sizeof(agentCtx->response), agentCtx->responseSz, body, idx); -} - /* Record the peer's signature algorithms the way a server would, through * an EXT_INFO carrying server-sig-algs. */ -static void SetPeerSigAlgs(WOLFSSH* ssh, const char* sigAlgs) +static WS_MAYBE_UNUSED void SetPeerSigAlgs(WOLFSSH* ssh, const char* sigAlgs) { - byte payload[128]; + byte payload[512]; word32 payloadSz; word32 idx = 0; payloadSz = BuildExtInfoSigAlgs(payload, sizeof(payload), sigAlgs); AssertIntEQ(wolfSSH_TestDoExtInfo(ssh, payload, payloadSz, &idx), WS_SUCCESS); - AssertIntEQ(ssh->peerSigIdSz, 1); - AssertIntEQ(ssh->peerSigId[0], ID_ED25519); + AssertTrue(ssh->peerSigIdSz > 0); } -/* Walk the fields of the sent USERAUTH_REQUEST and hand back the trailing - * signature. */ -static void ParseUserAuthSignature(const byte* packet, word32 packetSz, - word32* sigSz, const byte** sig) +/* Walk the fields of the sent USERAUTH_REQUEST and hand back its public key + * algorithm name, when algo is not NULL, and the trailing signature. */ +static WS_MAYBE_UNUSED void ParseUserAuthSignature(const byte* packet, + word32 packetSz, word32* algoSz, const byte** algo, word32* sigSz, + const byte** sig) { const byte* payload; const byte* field; @@ -9059,6 +8971,10 @@ static void ParseUserAuthSignature(const byte* packet, word32 packetSz, /* public key algorithm name and public key blob */ AssertIntEQ(GetStringRef(&fieldSz, &field, payload, payloadSz, &idx), WS_SUCCESS); + if (algo != NULL) { + *algoSz = fieldSz; + *algo = field; + } AssertIntEQ(GetStringRef(&fieldSz, &field, payload, payloadSz, &idx), WS_SUCCESS); @@ -9069,19 +8985,49 @@ static void ParseUserAuthSignature(const byte* packet, word32 packetSz, AssertIntEQ(idx, payloadSz); } -/* Stand up a client session that authenticates with an Ed25519 key held by - * the mock agent. */ -static void InitAgentEd25519Session(AgentEd25519Ctx* agentCtx, - WOLFSSH_CTX** ctx, WOLFSSH** ssh, MemIo* io, byte* out, word32 outSz) +/* The public key a mock agent context offers. Each context starts with + * one, so AgentUserAuth() can read it whatever the key type. */ +typedef struct { + const byte* type; + word32 typeSz; + const byte* blob; + word32 blobSz; +} AgentOfferedKey; + +/* Offer the key at the head of the mock agent context with no private key, + * as an agent-backed client does. */ +static WS_MAYBE_UNUSED int AgentUserAuth(byte authType, + WS_UserAuthData* authData, void* ctx) +{ + const AgentOfferedKey* key = (const AgentOfferedKey*)ctx; + + if (authType != WOLFSSH_USERAUTH_PUBLICKEY || authData == NULL) + return WOLFSSH_USERAUTH_INVALID_AUTHTYPE; + + authData->sf.publicKey.publicKeyType = key->type; + authData->sf.publicKey.publicKeyTypeSz = key->typeSz; + authData->sf.publicKey.publicKey = key->blob; + authData->sf.publicKey.publicKeySz = key->blobSz; + authData->sf.publicKey.privateKey = NULL; + authData->sf.publicKey.privateKeySz = 0; + + return WOLFSSH_USERAUTH_SUCCESS; +} + +/* Stand up a client session that signs its user auth through the mock + * agent answering on ioCb, against a server that sent sigAlgs. */ +static WS_MAYBE_UNUSED void InitAgentSession(void* agentCtx, + WS_CallbackAgentIO ioCb, const char* sigAlgs, WOLFSSH_CTX** ctx, + WOLFSSH** ssh, MemIo* io, byte* out, word32 outSz) { *ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(*ctx); wolfSSH_SetIORecv(*ctx, MemRecv); wolfSSH_SetIOSend(*ctx, MemSend); - wolfSSH_SetUserAuth(*ctx, AgentEd25519UserAuth); + wolfSSH_SetUserAuth(*ctx, AgentUserAuth); AssertIntEQ(wolfSSH_CTX_AGENT_enable(*ctx, 1), WS_SUCCESS); - AssertIntEQ(wolfSSH_CTX_set_agent_cb(*ctx, AgentEd25519Cb, - AgentEd25519IoCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_set_agent_cb(*ctx, AgentSignCb, ioCb), + WS_SUCCESS); *ssh = wolfSSH_new(*ctx); AssertNotNull(*ssh); @@ -9097,7 +9043,95 @@ static void InitAgentEd25519Session(AgentEd25519Ctx* agentCtx, (*ssh)->sessionIdSz = WC_SHA256_DIGEST_SIZE; WMEMSET((*ssh)->sessionId, 0x33, (*ssh)->sessionIdSz); - SetPeerSigAlgs(*ssh, AGENT_ED25519_NAME); + SetPeerSigAlgs(*ssh, sigAlgs); +} + +#endif /* WOLFSSH_AGENT && !NO_WOLFSSH_CLIENT */ + + +#if defined(WOLFSSH_AGENT) && !defined(WOLFSSH_NO_ED25519) \ + && !defined(NO_WOLFSSH_CLIENT) + +#define AGENT_ED25519_NAME "ssh-ed25519" + +/* Canned agent state: the key offered, which points into pubKeyBlob, and + * the sign response handed back on a read. */ +typedef struct { + AgentOfferedKey key; + byte response[128]; + word32 responseSz; + byte pubKeyBlob[64]; + byte sigBlob[128]; + word32 sigBlobSz; +} AgentEd25519Ctx; + +static int AgentEd25519IoCb(WS_AgentIoCbAction action, void* buf, + word32 bufSz, void* agentCbCtx) +{ + AgentEd25519Ctx* agentCtx = (AgentEd25519Ctx*)agentCbCtx; + + if (action == WOLFSSH_AGENT_IO_WRITE) + return (int)bufSz; + + if (bufSz < agentCtx->responseSz) + return 0; + WMEMCPY(buf, agentCtx->response, agentCtx->responseSz); + return (int)agentCtx->responseSz; +} + +static void InitAgentEd25519Ctx(AgentEd25519Ctx* agentCtx, word32 sigSz) +{ + byte pubKey[ED25519_PUB_KEY_SIZE]; + byte sig[ED25519_SIG_SIZE + 1]; + byte body[128]; + word32 idx; + + AssertTrue(sigSz <= (word32)sizeof(sig)); + + WMEMSET(agentCtx, 0, sizeof(*agentCtx)); + WMEMSET(pubKey, 0x5a, sizeof(pubKey)); + WMEMSET(sig, 0xa5, sizeof(sig)); + + idx = AppendString(agentCtx->pubKeyBlob, sizeof(agentCtx->pubKeyBlob), 0, + AGENT_ED25519_NAME); + idx = AppendUint32(agentCtx->pubKeyBlob, sizeof(agentCtx->pubKeyBlob), idx, + (word32)sizeof(pubKey)); + agentCtx->key.blobSz = AppendData(agentCtx->pubKeyBlob, + sizeof(agentCtx->pubKeyBlob), idx, pubKey, (word32)sizeof(pubKey)); + agentCtx->key.blob = agentCtx->pubKeyBlob; + agentCtx->key.type = (const byte*)AGENT_ED25519_NAME; + agentCtx->key.typeSz = (word32)WSTRLEN(AGENT_ED25519_NAME); + + /* The blob an agent returns holds the algorithm name and signature. */ + idx = AppendString(agentCtx->sigBlob, sizeof(agentCtx->sigBlob), 0, + AGENT_ED25519_NAME); + idx = AppendUint32(agentCtx->sigBlob, sizeof(agentCtx->sigBlob), idx, + sigSz); + agentCtx->sigBlobSz = AppendData(agentCtx->sigBlob, + sizeof(agentCtx->sigBlob), idx, sig, sigSz); + + idx = AppendUint32(body, sizeof(body), 0, agentCtx->sigBlobSz); + idx = AppendData(body, sizeof(body), idx, agentCtx->sigBlob, + agentCtx->sigBlobSz); + + agentCtx->responseSz = AppendUint32(agentCtx->response, + sizeof(agentCtx->response), 0, MSG_ID_SZ + idx); + agentCtx->responseSz = AppendByte(agentCtx->response, + sizeof(agentCtx->response), agentCtx->responseSz, + MSGID_AGENT_SIGN_RESPONSE); + agentCtx->responseSz = AppendData(agentCtx->response, + sizeof(agentCtx->response), agentCtx->responseSz, body, idx); +} + +/* Stand up a client session that authenticates with an Ed25519 key held by + * the mock agent. */ +static void InitAgentEd25519Session(AgentEd25519Ctx* agentCtx, + WOLFSSH_CTX** ctx, WOLFSSH** ssh, MemIo* io, byte* out, word32 outSz) +{ + InitAgentSession(agentCtx, AgentEd25519IoCb, AGENT_ED25519_NAME, ctx, ssh, + io, out, outSz); + AssertIntEQ((*ssh)->peerSigIdSz, 1); + AssertIntEQ((*ssh)->peerSigId[0], ID_ED25519); } /* An agent-backed Ed25519 publickey request has to carry the signature the @@ -9119,7 +9153,7 @@ static void TestAgentEd25519UserAuthEmitsSignature(void) AssertIntEQ(SendUserAuthRequest(ssh, WOLFSSH_USERAUTH_PUBLICKEY, 1), WS_SUCCESS); - ParseUserAuthSignature(io.out, io.outSz, &sigSz, &sig); + ParseUserAuthSignature(io.out, io.outSz, NULL, NULL, &sigSz, &sig); AssertIntEQ(sigSz, agentCtx.sigBlobSz); AssertIntEQ(WMEMCMP(sig, agentCtx.sigBlob, sigSz), 0); @@ -9175,6 +9209,311 @@ static void TestAgentEd25519UserAuthRejectsOversizeSignature(void) #endif /* WOLFSSH_AGENT && !WOLFSSH_NO_ED25519 && !NO_WOLFSSH_CLIENT */ +#if defined(WOLFSSH_AGENT) && !defined(WOLFSSH_NO_RSA) \ + && !defined(NO_WOLFSSH_CLIENT) \ + && !(defined(WOLFSSH_NO_RSA_SHA2_256) && defined(WOLFSSH_NO_RSA_SHA2_512)) + +/* keys/hansel-key-rsa.pub */ +static const char agentRsaPubKey[] = + "ssh-rsa " + "AAAAB3NzaC1yc2EAAAADAQABAAABAQC9P3ZFowOsONXHD5MwWiCciXytBRZGhoMN" + "iisWSgUs5HdHcACuHYPi2W6Z1PBFmBWT9odOrGRjoZXJfDDoPi+j8SSfDGsc/hsC" + "mc3Gp2yEhUZUEkDhtOXyqjns1ickC9Gh4u80aSVtwHRnJZh9xPhSq5tLOhId4eP6" + "1s+a5pwjTjnEhBaIPUJO2C/M0pFnnbZxKgJlX7t1Doy7h5eXxviymOIvaCZKU+x5" + "OopfzM/wFkey0EPWNmzI5y/+pzU5afsdeEWdiQDIQc80H6Pz8fsoFPvYSG+s4/wz" + "0duu7yeeV1Ypoho65Zr+pEnIf7dO0B8EblgWt+ud+JI8wrAhfE4x" + " hansel"; +/* Both test keys are 2048-bit. */ +#define AGENT_RSA_SIG_SZ 256 + +/* The server-sig-algs OpenSSH 10.3's sshd sends. */ +#define AGENT_RSA_OPENSSH_SIG_ALGS "ssh-ed25519,ecdsa-sha2-nistp256," \ + "ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com," \ + "sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256," \ + "webauthn-sk-ecdsa-sha2-nistp256@openssh.com" +/* The client takes the server's first match. */ +#ifndef WOLFSSH_NO_RSA_SHA2_512 + #define AGENT_RSA_OPENSSH_PICK "rsa-sha2-512" +#else + #define AGENT_RSA_OPENSSH_PICK "rsa-sha2-256" +#endif + +/* How the mock agent answers a sign request. */ +enum { + AGENT_RSA_REPLY_BY_FLAGS, /* as OpenSSH's ssh-agent does */ + AGENT_RSA_REPLY_SSH_RSA, /* as agents before OpenSSH 7.2, which ignore + * the flags */ + AGENT_RSA_REPLY_FAILURE, /* as for a key never added with ssh-add */ + AGENT_RSA_REPLY_OVERSIZE /* by the flags, one byte past the modulus */ +}; + +/* Mock agent state: the key offered, how to answer, and the sign response + * built from the last sign request. */ +typedef struct { + AgentOfferedKey key; + int reply; + byte response[512]; + word32 responseSz; + byte sigBlob[300]; + word32 sigBlobSz; +} AgentRsaCtx; + +/* A signature is modulus sized, as a real agent's is. */ +static int AgentRsaIoCb(WS_AgentIoCbAction action, void* buf, + word32 bufSz, void* agentCbCtx) +{ + AgentRsaCtx* agentCtx = (AgentRsaCtx*)agentCbCtx; + const byte* req = (const byte*)buf; + const byte* reqKey = NULL; + word32 reqKeySz = 0; + const char* name = "ssh-rsa"; + byte sig[AGENT_RSA_SIG_SZ + 1]; + word32 sigSz = AGENT_RSA_SIG_SZ; + word32 flags = 0; + word32 idx; + + if (action == WOLFSSH_AGENT_IO_WRITE) { + /* A real agent finds the key by the blob the request names, so it + * has to be the one the client offered. */ + AssertTrue(bufSz > UINT32_SZ); + AssertIntEQ(req[UINT32_SZ], MSGID_AGENT_SIGN_REQUEST); + idx = UINT32_SZ + MSG_ID_SZ; + AssertIntEQ(GetStringRef(&reqKeySz, &reqKey, req, bufSz, &idx), + WS_SUCCESS); + AssertIntEQ(reqKeySz, agentCtx->key.blobSz); + AssertIntEQ(WMEMCMP(reqKey, agentCtx->key.blob, reqKeySz), 0); + AssertIntEQ(GetSkip(req, bufSz, &idx), WS_SUCCESS); + AssertIntEQ(GetUint32(&flags, req, bufSz, &idx), WS_SUCCESS); + + if (agentCtx->reply == AGENT_RSA_REPLY_FAILURE) { + idx = AppendUint32(agentCtx->response, sizeof(agentCtx->response), + 0, MSG_ID_SZ); + agentCtx->responseSz = AppendByte(agentCtx->response, + sizeof(agentCtx->response), idx, MSGID_AGENT_FAILURE); + return (int)bufSz; + } + + if (agentCtx->reply == AGENT_RSA_REPLY_OVERSIZE) + sigSz++; + if (agentCtx->reply != AGENT_RSA_REPLY_SSH_RSA) { + if (flags & AGENT_SIGN_RSA_SHA2_256) + name = "rsa-sha2-256"; + else if (flags & AGENT_SIGN_RSA_SHA2_512) + name = "rsa-sha2-512"; + } + + WMEMSET(sig, 0xa5, sizeof(sig)); + idx = AppendString(agentCtx->sigBlob, sizeof(agentCtx->sigBlob), 0, + name); + idx = AppendUint32(agentCtx->sigBlob, sizeof(agentCtx->sigBlob), idx, + sigSz); + agentCtx->sigBlobSz = AppendData(agentCtx->sigBlob, + sizeof(agentCtx->sigBlob), idx, sig, sigSz); + + idx = AppendUint32(agentCtx->response, sizeof(agentCtx->response), 0, + MSG_ID_SZ + UINT32_SZ + agentCtx->sigBlobSz); + idx = AppendByte(agentCtx->response, sizeof(agentCtx->response), idx, + MSGID_AGENT_SIGN_RESPONSE); + idx = AppendUint32(agentCtx->response, sizeof(agentCtx->response), + idx, agentCtx->sigBlobSz); + agentCtx->responseSz = AppendData(agentCtx->response, + sizeof(agentCtx->response), idx, agentCtx->sigBlob, + agentCtx->sigBlobSz); + return (int)bufSz; + } + + if (bufSz < agentCtx->responseSz) + return 0; + WMEMCPY(buf, agentCtx->response, agentCtx->responseSz); + return (int)agentCtx->responseSz; +} + +/* Send a publickey request for pubKeyLine, a .pub file's contents, signed by + * the mock agent answering as reply, and check the result against expect*. */ +static void RunAgentRsaUserAuth(const char* pubKeyLine, const char* sigAlgs, + int reply, int expectRet, const char* expectAlgo, + const char* expectSigName) +{ + AgentRsaCtx agentCtx; + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[4096]; + byte* pubKey = NULL; + word32 pubKeySz = 0; + const byte* algo = NULL; + const byte* sig = NULL; + const byte* sigName = NULL; + word32 algoSz = 0; + word32 sigSz = 0; + word32 sigNameSz = 0; + word32 idx = 0; + + WMEMSET(&agentCtx, 0, sizeof(agentCtx)); + agentCtx.reply = reply; + AssertIntEQ(wolfSSH_ReadKey_buffer((const byte*)pubKeyLine, + (word32)WSTRLEN(pubKeyLine), WOLFSSH_FORMAT_SSH, &pubKey, + &pubKeySz, &agentCtx.key.type, &agentCtx.key.typeSz, NULL), + WS_SUCCESS); + agentCtx.key.blob = pubKey; + agentCtx.key.blobSz = pubKeySz; + + InitAgentSession(&agentCtx, AgentRsaIoCb, sigAlgs, &ctx, &ssh, &io, out, + (word32)sizeof(out)); + + AssertIntEQ(SendUserAuthRequest(ssh, WOLFSSH_USERAUTH_PUBLICKEY, 1), + expectRet); + + if (expectRet == WS_SUCCESS) { + ParseUserAuthSignature(io.out, io.outSz, &algoSz, &algo, &sigSz, + &sig); + AssertIntEQ(algoSz, WSTRLEN(expectAlgo)); + AssertIntEQ(WMEMCMP(algo, expectAlgo, algoSz), 0); + AssertIntEQ(sigSz, agentCtx.sigBlobSz); + AssertIntEQ(WMEMCMP(sig, agentCtx.sigBlob, sigSz), 0); + + /* A server rejects a signature that doesn't use the algorithm the + * request names. */ + AssertIntEQ(GetStringRef(&sigNameSz, &sigName, sig, sigSz, &idx), + WS_SUCCESS); + AssertIntEQ(sigNameSz, WSTRLEN(expectSigName)); + AssertIntEQ(WMEMCMP(sigName, expectSigName, sigNameSz), 0); + } + else { + AssertIntEQ(io.outSz, 0); + } + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + WFREE(pubKey, NULL, DYNTYPE_PRIVKEY); +} + +static void TestAgentRsaUserAuthOpenSshServer(void) +{ + RunAgentRsaUserAuth(agentRsaPubKey, AGENT_RSA_OPENSSH_SIG_ALGS, + AGENT_RSA_REPLY_BY_FLAGS, WS_SUCCESS, AGENT_RSA_OPENSSH_PICK, + AGENT_RSA_OPENSSH_PICK); +} + +#ifndef WOLFSSH_NO_RSA_SHA2_256 +/* A server limited to rsa-sha2-256, as sshd is with + * PubkeyAcceptedAlgorithms=rsa-sha2-256. */ +static void TestAgentRsaUserAuthSha256Server(void) +{ + RunAgentRsaUserAuth(agentRsaPubKey, "rsa-sha2-256", + AGENT_RSA_REPLY_BY_FLAGS, WS_SUCCESS, "rsa-sha2-256", + "rsa-sha2-256"); +} +#endif + +/* An agent without the key refuses to sign. The error has to reach the + * caller with nothing sent. */ +static void TestAgentRsaUserAuthAgentWithoutKey(void) +{ + RunAgentRsaUserAuth(agentRsaPubKey, AGENT_RSA_OPENSSH_SIG_ALGS, + AGENT_RSA_REPLY_FAILURE, WS_AGENT_NO_KEY_E, NULL, NULL); +} + +/* An agent that ignores the flags signs with SHA-1 ssh-rsa, which must not + * go out under an rsa-sha2-* request. */ +static void TestAgentRsaUserAuthRefusesIgnoredFlags(void) +{ + RunAgentRsaUserAuth(agentRsaPubKey, AGENT_RSA_OPENSSH_SIG_ALGS, + AGENT_RSA_REPLY_SSH_RSA, WS_INVALID_ALGO_ID, NULL, NULL); +} + +/* An agent signature one byte longer than the modulus is refused. */ +static void TestAgentRsaUserAuthRejectsOversizeSignature(void) +{ + RunAgentRsaUserAuth(agentRsaPubKey, AGENT_RSA_OPENSSH_SIG_ALGS, + AGENT_RSA_REPLY_OVERSIZE, WS_BUFFER_E, NULL, NULL); +} + +/* A .pub line that says ssh-rsa over another key type's blob, here + * keys/hansel-key-ed25519.pub's, is refused before the blob is decoded. */ +static void TestAgentRsaUserAuthRefusesMismatchedBlob(void) +{ + static const char mismatched[] = + "ssh-rsa " + "AAAAC3NzaC1lZDI1NTE5AAAAIHTSoBZIJBO2V0Jb2OWyMWNbkDd6ReDfKxnrAPlb" + "PuCe hansel"; + + RunAgentRsaUserAuth(mismatched, AGENT_RSA_OPENSSH_SIG_ALGS, + AGENT_RSA_REPLY_BY_FLAGS, WS_INVALID_ALGO_ID, NULL, NULL); +} + +#if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) +/* The RSA part of a wolfSSH server's server-sig-algs, which also names the + * certificate algorithms. */ +#define AGENT_RSA_WOLFSSH_SIG_ALGS "rsa-sha2-256,rsa-sha2-512," \ + "ssh-rsa-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com," \ + "rsa-sha2-512-cert-v01@openssh.com" + +/* keys/ossh-user-rsa.pub signed by keys/ossh-ca-rsa: ssh-keygen -s + * ossh-ca-rsa -I wolfssh-agent-test -n hansel -V always:forever */ +static const char agentRsaCert[] = + "ssh-rsa-cert-v01@openssh.com " + "AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAga1B4hnmhn37kMGWS" + "R3lji74KzQmOj4ZQri6pxSBB+E8AAAADAQABAAABAQDMhB3Xi7lqn3w7bYorlUAx" + "BgdjAkmD/j1Sx+nRIqnGqk1YFNFmbl3iQ+JG3S13jwwi/CJyLXrdkVocsBOhHebM" + "yyPHN0b7Cgl1Dvtxg+2Zh1K+dkzC2BhTmby/hj4NySnP04EgmY0w9f8uPCZHY0s0" + "fq6nsMCJOo9/53JR38HJFAFabI344vY2kbUPFqBIuG45OfIQrJK3KeqQGTcGRzhO" + "Ogk5AkkytcIRrLeKbdBCCYYUcCFIEH/RL7fCSkbth54kJ8FWNEdJu5YYbpJY4Yog" + "ITeFlZi4Z8sMbO2I53MBaWleMjjntuUtac0Yry9CXWz1fgImAYjCDB7h4wHF/nQ5" + "AAAAAAAAAAAAAAABAAAAEndvbGZzc2gtYWdlbnQtdGVzdAAAAAoAAAAGaGFuc2Vs" + "AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGlu" + "ZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1w" + "b3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11" + "c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDCSZff5o7U" + "OUQ9sC4YqX++TYFHRZtrsBWPKKokdo7DPzveiOfHpnMps9/sZB9yiz+A7mGBRPJP" + "uCep1G95j1CA57/yb5/4jqvoD6ZSFPUKgHeQ21oIraSFneynw76+lrd87OCrWMkx" + "Dh6ogV2kkbNK3+HgzbnkyUdRo/dq7ZS0/DbA2wMGTzD2j+mMgkk3aazWlBdlnazk" + "NwjZZdUbyW9CI8tdlnRZ/6j0aH4Te6YkoTTDMN9s0xI5VsJzP8vcVt4RZkMaE6M9" + "pJyWfhh2HTsUILui3F232RpwdlxksnBbzptNpSBvaPfYlTeaCtzriuf4w0sYINxj" + "xcaoLMResmLhAAABFAAAAAxyc2Etc2hhMi01MTIAAAEAqyoYPA0HxiSIjEZcFw63" + "Id1DrawFuPFaLuuoaBu/U/hvpO9T6dmLYYeNSggS4/I3zlQS29dmQWDQDRSikOg4" + "q4upCl8LCHtZh7q0mAM4Yc8akdeYeXtoxNdSo04Gor+IlyD78WXlwshH7Nm2qtWD" + "mPrQYeciXPn1IN4cn5KR6DuetU2zf9IihN7Ay9jIzId+JUJykWtzegMKcnGQ0bcS" + "PeCWclWDyYsZYgrHmDwPxs0gw/+HwEj7ZNHpfj6fyFFnBul8TnX5RlvYdR5dYjCs" + "LhC4shipEwdr7CFD6dBzSeEQBF06ao7fThpJ9p/eFQCDwyjF3UOqiqAZNLzi4VAm" + "Pg==" + " wolfssh-ossh-test-user-rsa"; + +/* An OpenSSH RSA certificate held by the agent. The request names the + * certificate algorithm; the signature names the RSA algorithm under it. */ +static void TestAgentOsshRsaCertUserAuthWolfSshServer(void) +{ +#ifndef WOLFSSH_NO_RSA_SHA2_512 + RunAgentRsaUserAuth(agentRsaCert, AGENT_RSA_WOLFSSH_SIG_ALGS, + AGENT_RSA_REPLY_BY_FLAGS, WS_SUCCESS, + "rsa-sha2-512-cert-v01@openssh.com", "rsa-sha2-512"); +#else + RunAgentRsaUserAuth(agentRsaCert, AGENT_RSA_WOLFSSH_SIG_ALGS, + AGENT_RSA_REPLY_BY_FLAGS, WS_SUCCESS, + "rsa-sha2-256-cert-v01@openssh.com", "rsa-sha2-256"); +#endif +} + +/* An agent signature for the certificate one byte longer than the modulus + * is refused. */ +static void TestAgentOsshRsaCertUserAuthRejectsOversizeSignature(void) +{ + RunAgentRsaUserAuth(agentRsaCert, AGENT_RSA_WOLFSSH_SIG_ALGS, + AGENT_RSA_REPLY_OVERSIZE, WS_BUFFER_E, NULL, NULL); +} + +/* Agents before OpenSSH 7.2 hold certificates but ignore the flags. */ +static void TestAgentOsshRsaCertUserAuthRefusesIgnoredFlags(void) +{ + RunAgentRsaUserAuth(agentRsaCert, AGENT_RSA_WOLFSSH_SIG_ALGS, + AGENT_RSA_REPLY_SSH_RSA, WS_INVALID_ALGO_ID, NULL, NULL); +} + +#endif /* WOLFSSH_OSSH_CERTS && !WOLFSSH_NO_OSSH_CERT_RSA */ + +#endif /* WOLFSSH_AGENT && !WOLFSSH_NO_RSA && !NO_WOLFSSH_CLIENT */ + + /* The client-side forwarding tests. They drive a client session, so they * run whether or not this build has a server. */ @@ -18465,6 +18804,23 @@ int main(int argc, char** argv) TestAgentEd25519UserAuthPropagatesAgentError(); TestAgentEd25519UserAuthRejectsOversizeSignature(); #endif +#if defined(WOLFSSH_AGENT) && !defined(WOLFSSH_NO_RSA) \ + && !defined(NO_WOLFSSH_CLIENT) \ + && !(defined(WOLFSSH_NO_RSA_SHA2_256) && defined(WOLFSSH_NO_RSA_SHA2_512)) + TestAgentRsaUserAuthOpenSshServer(); +#ifndef WOLFSSH_NO_RSA_SHA2_256 + TestAgentRsaUserAuthSha256Server(); +#endif + TestAgentRsaUserAuthAgentWithoutKey(); + TestAgentRsaUserAuthRefusesIgnoredFlags(); + TestAgentRsaUserAuthRefusesMismatchedBlob(); + TestAgentRsaUserAuthRejectsOversizeSignature(); +#if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) + TestAgentOsshRsaCertUserAuthWolfSshServer(); + TestAgentOsshRsaCertUserAuthRejectsOversizeSignature(); + TestAgentOsshRsaCertUserAuthRefusesIgnoredFlags(); +#endif +#endif #if defined(WOLFSSH_FWD) && !defined(NO_WOLFSSH_CLIENT) TestForwardedTcpipRegisteredIsAccepted(); TestForwardedTcpipUnregisteredSendsOpenFail(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 2526d180e..f4bb4a055 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1693,7 +1693,10 @@ WOLFSSH_LOCAL void wolfSSH_KEY_clean(WS_KeySignature* key); WOLFSSH_LOCAL int IdentifyOpenSshKey(const byte* in, word32 inSz, void* heap); WOLFSSH_LOCAL int GetOpenSshKey(WS_KeySignature *key, const byte* buf, word32 len, word32* idx); -#ifdef WOLFSSH_TPM +#if defined(WOLFSSH_TPM) || defined(WOLFSSH_AGENT) +/* Decodes an SSH-format public key into the key->ks member its type names; + * for RSA, key->ks.rsa.key must already be initialized. An OpenSSH RSA + * certificate yields its RSA key only, leaving *idx just past n. */ WOLFSSH_LOCAL int GetOpenSshPublicKey(WS_KeySignature *key, const byte* buf, word32 len, word32* idx); #endif From c93031686842bdef979836fcb287a2fe56d6e8ba Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Mon, 5 Oct 2026 14:14:37 +0900 Subject: [PATCH 2/3] tpm-ssh.yml, README: test and document TPM keys with OpenSSH certs - tpm-ssh.yml builds wolfSSH with --enable-ossh-certs, and the RSA raw cells log in with the TPM key offered as an OpenSSH user certificate, and fail with a certificate for another key. - README: a TPM PUBLIC KEY AUTHENTICATION subsection on offering the TPM key as an OpenSSH user certificate. --- .github/workflows/tpm-ssh.yml | 60 ++++++++++++++++++++++++++++++++++- README.md | 30 ++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tpm-ssh.yml b/.github/workflows/tpm-ssh.yml index e13485b77..80a62e2f1 100644 --- a/.github/workflows/tpm-ssh.yml +++ b/.github/workflows/tpm-ssh.yml @@ -110,7 +110,7 @@ jobs: run: | cd wolfssh ./autogen.sh - ./configure --enable-tpm --enable-certs + ./configure --enable-tpm --enable-certs --enable-ossh-certs make sudo make install sudo ldconfig @@ -228,6 +228,60 @@ jobs: timeout 20 ./examples/client/client -i ../wolftpm/keyblob.bin \ -u hansel -K ThisIsMyKeyAuth + # The same TPM key offered as an OpenSSH user certificate. The certificate + # is a file carrying the TPM key's public half; the TPM still signs. + - name: Test TPM client OpenSSH certificate auth + if: matrix.keytype == 'rsa' && matrix.hostkey == 'raw' + run: | + cd wolfssh + ssh-keygen -q -t rsa -N '' -f ossh_ca < /dev/null + cp key.ssh tpmkey.pub + ssh-keygen -q -s ossh_ca -I tpm-cert -n hansel tpmkey.pub + timeout 30 ./examples/echoserver/echoserver -1 -p 22225 -s key.ssh \ + > ossh_cert_server.txt 2>&1 & + SERVER_PID=$! + echo "Echoserver (TPM key behind an OpenSSH certificate) PID: $SERVER_PID" + sleep 2 + timeout 20 ./examples/client/client -i ../wolftpm/keyblob.bin \ + -u hansel -K ThisIsMyKeyAuth -j tpmkey-cert.pub -p 22225 -x \ + < /dev/null > ossh_cert_client.txt 2>&1 || { + cat ossh_cert_client.txt + exit 1 + } + # The one-shot server flushes its output when it exits. + wait $SERVER_PID || true + echo "----- server -----"; cat ossh_cert_server.txt + grep -q "auth = Success" ossh_cert_server.txt + + # Negative test: a certificate for another key. The echoserver accepts + # that key, so only the signature check can refuse the TPM's signature. + - name: Test TPM client OpenSSH certificate for another key fails + if: matrix.keytype == 'rsa' && matrix.hostkey == 'raw' + run: | + cd wolfssh + # Same size as the TPM key, or the client fails before signing. The + # comment is cut because the echoserver appends the user name. + ssh-keygen -q -t rsa -b 2048 -N '' -f other < /dev/null + ssh-keygen -q -s ossh_ca -I other-cert -n hansel other.pub + cut -d' ' -f1,2 other.pub > other.ssh + timeout 30 ./examples/echoserver/echoserver -1 -p 22226 -s other.ssh \ + > ossh_cert_server_neg.txt 2>&1 & + SERVER_PID=$! + echo "Echoserver (another key's certificate) PID: $SERVER_PID" + sleep 2 + if timeout 20 ./examples/client/client -i ../wolftpm/keyblob.bin \ + -u hansel -K ThisIsMyKeyAuth -j other-cert.pub -p 22226 -x \ + < /dev/null > ossh_cert_client_neg.txt 2>&1; then + echo "ERROR: a certificate for another key authenticated" + cat ossh_cert_client_neg.txt + exit 1 + fi + wait $SERVER_PID || true + echo "----- server -----"; cat ossh_cert_server_neg.txt + echo "----- client -----"; cat ossh_cert_client_neg.txt + # The server took the key and then refused the signature. + grep -q "auth = Failure" ossh_cert_server_neg.txt + - name: Archive test artifacts if: always() uses: actions/upload-artifact@v7 @@ -241,4 +295,8 @@ jobs: wolfssh/tpmcert_client.txt wolfssh/tpmcert_server_neg.txt wolfssh/tpmcert_client_neg.txt + wolfssh/ossh_cert_server.txt + wolfssh/ossh_cert_client.txt + wolfssh/ossh_cert_server_neg.txt + wolfssh/ossh_cert_client_neg.txt wolfssh/tests/api.log diff --git a/README.md b/README.md index 5395affb0..4bb67e030 100644 --- a/README.md +++ b/README.md @@ -672,6 +672,36 @@ If you used a custom password for keygen you must specify the password you used: $ ./examples/client/client -i ../wolfTPM/keyblob.bin -u hansel -K +with an OpenSSH user certificate +-------------------------------- + +The TPM key can also be offered as an OpenSSH user certificate +(`ssh-rsa-cert-v01@openssh.com`). The certificate is a file carrying the TPM +key's public half; the private key stays in the TPM, which makes the +signature. Build wolfSSH with OpenSSH certificate support as well: + + wolfSSH + $ ./configure --enable-tpm --enable-ossh-certs + +Create the key and `key.ssh` as above, then have your CA sign the public key. +`ssh-keygen` names the certificate after its input, so this writes +`tpmkey-cert.pub`: + + $ cp key.ssh tpmkey.pub + $ ssh-keygen -s -I -n hansel tpmkey.pub + +Run the echoserver with `key.ssh` as above, and pass the certificate to the +client with `-j`: + + $ ./examples/client/client -i ../wolfTPM/keyblob.bin -u hansel \ + -K ThisIsMyKeyAuth -j tpmkey-cert.pub + +Notes: + +- RSA keys only, as for the plain TPM key. +- The certificate must be issued for the key in the TPM, or the server + rejects the signature. + TPM SERVER HOST KEY (ECDSA / RSA) ================================= From 1fa47d429f984ec76afd83ba2abef4e39d73c4ed Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Mon, 5 Oct 2026 20:13:16 +0900 Subject: [PATCH 3/3] sanitizer.yml, openssh-interop.test: fail instead of skipping in CI - openssh-interop.test fails at each skip point when WOLFSSH_TEST_REQUIRE_OPENSSH=1, and skips through skip_rest() for the sshd and ssh-agent parts. - sanitizer.yml sets WOLFSSH_TEST_REQUIRE_OPENSSH=1 for make check. --- .github/workflows/sanitizer.yml | 3 ++ scripts/openssh-interop.test | 52 +++++++++++++++++---------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/.github/workflows/sanitizer.yml b/.github/workflows/sanitizer.yml index fca6e16f7..35950aad0 100644 --- a/.github/workflows/sanitizer.yml +++ b/.github/workflows/sanitizer.yml @@ -102,6 +102,9 @@ jobs: make -j$(nproc) - name: Run tests + env: + # The runner has OpenSSH, so openssh-interop.test must run in full. + WOLFSSH_TEST_REQUIRE_OPENSSH: 1 run: timeout -k 30 300 make check - name: Show test logs on failure diff --git a/scripts/openssh-interop.test b/scripts/openssh-interop.test index 51826ea35..752a6e460 100755 --- a/scripts/openssh-interop.test +++ b/scripts/openssh-interop.test @@ -112,12 +112,29 @@ fail() { exit 1 } +# Set WOLFSSH_TEST_REQUIRE_OPENSSH=1 where every part must run, as in CI; +# each skip below is then a failure. +require_openssh=${WOLFSSH_TEST_REQUIRE_OPENSSH:-0} + skip() { + if [ "$require_openssh" = "1" ]; then + fail "$1; WOLFSSH_TEST_REQUIRE_OPENSSH=1 does not allow skipping" + fi printf '%s, skipping\n' "$1" rm -rf -- "$work_dir" exit 77 } +# Skip the part named by $2, keeping the parts that already passed. +skip_rest() { + if [ "$require_openssh" = "1" ]; then + fail "$1; WOLFSSH_TEST_REQUIRE_OPENSSH=1 does not allow skipping $2" + fi + echo "$1, skipping $2" + do_cleanup + exit 0 +} + command -v ssh > /dev/null 2>&1 || skip "no ssh in the path" ssh_version=`ssh -V 2>&1` @@ -279,19 +296,13 @@ if [ -z "$sshd_bin" ]; then fi if [ -z "$sshd_bin" ] || [ ! -x "$sshd_bin" ]; then - echo "no sshd on this machine, skipping the wolfSSH client half" - do_cleanup - exit 0 + skip_rest "no sshd on this machine" "the wolfSSH client half" fi if ! command -v ssh-keygen > /dev/null 2>&1; then - echo "no ssh-keygen, skipping the wolfSSH client half" - do_cleanup - exit 0 + skip_rest "no ssh-keygen" "the wolfSSH client half" fi if [ ! -x ./examples/client/client ]; then - echo "the example client wasn't built, skipping the wolfSSH client half" - do_cleanup - exit 0 + skip_rest "the example client wasn't built" "the wolfSSH client half" fi echo "Test the wolfSSH client against OpenSSH's sshd" @@ -358,10 +369,8 @@ while [ "$counter" -lt 5 ]; do done if [ "$sshd_port" -eq 0 ]; then - echo "sshd wouldn't start here, skipping the wolfSSH client half" [ -s "$sshd_log" ] && tail -n 5 "$sshd_log" - do_cleanup - exit 0 + skip_rest "sshd wouldn't start here" "the wolfSSH client half" fi # The version gate above covered the client. This sshd was found on its own @@ -372,10 +381,9 @@ sshd_version=`sed -n 's/.*sshd version \(OpenSSH_[^,]*\).*/\1/p' \ "$sshd_log" | head -n 1` has_strict_kex "$sshd_version" if [ $? -ne 0 ]; then - echo "$sshd_bin is ${sshd_version:-an unreadable version}, no strict KEX," - echo "skipping the wolfSSH client half" - do_cleanup - exit 0 + skip_rest \ + "$sshd_bin is ${sshd_version:-an unreadable version}, no strict KEX" \ + "the wolfSSH client half" fi # sshd authenticates the user running this test, with one of the canned @@ -455,15 +463,11 @@ echo "the wolfSSH client negotiated strict KEX with sshd" # Part 3 needs the client's agent support and OpenSSH's agent tools. if ! ./examples/client/client '-?' 2>&1 | grep -q 'SSH-AGENT'; then - echo "the example client has no agent support, skipping the agent half" - do_cleanup - exit 0 + skip_rest "the example client has no agent support" "the agent half" fi if ! command -v ssh-agent > /dev/null 2>&1 || ! command -v ssh-add > /dev/null 2>&1; then - echo "no ssh-agent or ssh-add, skipping the agent half" - do_cleanup - exit 0 + skip_rest "no ssh-agent or ssh-add" "the agent half" fi # The RSA signature algorithms this client was built with; -E lists them @@ -475,9 +479,7 @@ for alg in rsa-sha2-256 rsa-sha2-512; do fi done if [ -z "$agent_rsa_algs" ]; then - echo "the client has no rsa-sha2 algorithms, skipping the agent half" - do_cleanup - exit 0 + skip_rest "the client has no rsa-sha2 algorithms" "the agent half" fi echo "Test the wolfSSH client signing through ssh-agent for sshd"