diff --git a/src/internal.c b/src/internal.c index 9759ca5f1..31c57f26e 100644 --- a/src/internal.c +++ b/src/internal.c @@ -9647,6 +9647,9 @@ static int DoExtInfoServerSigAlgs(WOLFSSH* ssh, * ids[0] may be ID_UNKNOWN by design, inert downstream. */ ret = GetNameListRaw(ids, &idsSz, names, namesSz); + if (ret == WS_SUCCESS) + ssh->peerSigAlgsSeen = 1; + if (ret == WS_SUCCESS && idsSz == 0) { /* No usable names, either an empty list or one that opens with an * empty element. Legal, and it advertises nothing. */ @@ -20082,6 +20085,17 @@ static int BuildUserAuthResponseKeyboard(WOLFSSH* ssh, byte* output, word32* idx #endif #ifndef WOLFSSH_NO_RSA +#if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) +/* The rsa-sha2-* variant for an OpenSSH RSA certificate's name and hash. + * With no server-sig-algs, the canned order decides, as for ssh-rsa. */ +static byte ClientOsshRsaCertSigId(WOLFSSH* ssh) +{ + if (ssh->peerSigAlgsSeen) + return OsshRsaCertSigId(ssh->peerSigId, ssh->peerSigIdSz); + return OsshRsaCertSigId(cannedKeyAlgoClient, cannedKeyAlgoClientSz); +} +#endif + static int PrepareUserAuthRequestRsa(WOLFSSH* ssh, word32* payloadSz, const WS_UserAuthData* authData, WS_KeySignature* keySig) { @@ -20170,10 +20184,9 @@ static int BuildUserAuthRequestRsa(WOLFSSH* ssh, #if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) /* An OpenSSH RSA certificate is dispatched by its cert id, but the - * signature itself is rsa-sha2-*. Use the strongest variant the server - * advertised. */ + * signature itself is rsa-sha2-*. */ if (keySig->sigId == ID_OSSH_CERT_RSA) { - effSigId = OsshRsaCertSigId(ssh->peerSigId, ssh->peerSigIdSz); + effSigId = ClientOsshRsaCertSigId(ssh); } #endif @@ -21733,9 +21746,17 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, algoId[algoIdSz++] = keySig->keyId; } - /* Is that in the peerSigId list? */ - matchId = MatchIdLists(WOLFSSH_ENDPOINT_CLIENT, algoId, algoIdSz, - ssh->peerSigId, ssh->peerSigIdSz); + /* Is that in the peerSigId list? With no server-sig-algs, RFC 8308 + * section 2.2, use the canned order, which prefers rsa-sha2-* to + * ssh-rsa; RFC 8332 section 3.3 allows either. */ + if (ssh->peerSigAlgsSeen) { + matchId = MatchIdLists(WOLFSSH_ENDPOINT_CLIENT, algoId, algoIdSz, + ssh->peerSigId, ssh->peerSigIdSz); + } + else { + matchId = MatchIdLists(WOLFSSH_ENDPOINT_CLIENT, algoId, algoIdSz, + cannedKeyAlgoClient, cannedKeyAlgoClientSz); + } if (matchId == ID_UNKNOWN) { ret = WS_MATCH_KEY_ALGO_E; } @@ -21749,8 +21770,7 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, if (keySig->keyId == ID_OSSH_CERT_RSA) { #if !defined(WOLFSSH_NO_RSA_SHA2_256) && \ !defined(WOLFSSH_NO_RSA_SHA2_512) - if (OsshRsaCertSigId(ssh->peerSigId, ssh->peerSigIdSz) - == ID_RSA_SHA2_512) { + if (ClientOsshRsaCertSigId(ssh) == ID_RSA_SHA2_512) { keySig->sigName = cannedKeyAlgoOsshRsaSha2_512CertName; } else { diff --git a/tests/regress.c b/tests/regress.c index 60ed6f710..ce6324a0c 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -12844,6 +12844,223 @@ static void TestClientBuffersIdempotent(void) } #endif +#if !defined(NO_WOLFSSH_CLIENT) && !defined(WOLFSSH_NO_PUBKEY_AUTH) \ + && ((!defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_RSA_SHA2_256)) \ + || !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) +/* When set, the key type the next publickey request claims for the loaded + * key. Only the algorithm choice runs before the key is parsed. */ +static const char* pkCaseKeyType = NULL; +/* When set, a server-sig-algs EXT_INFO sent ahead of the case's own. */ +static const char* pkCasePriorSigAlgs = NULL; + +static int PkCaseUserAuth(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + int ret = ClientUserAuth(authType, authData, ctx); + + if (ret == WOLFSSH_USERAUTH_SUCCESS && pkCaseKeyType != NULL + && authType == WOLFSSH_USERAUTH_PUBLICKEY) { + authData->sf.publicKey.publicKeyType = (const byte*)pkCaseKeyType; + authData->sf.publicKey.publicKeyTypeSz = + (word32)WSTRLEN(pkCaseKeyType); + } + return ret; +} + +/* Send one publickey USERAUTH_REQUEST from a fresh client session with the + * key pair ClientUsePubKey() and ClientSetPrivateKey() loaded. A non-NULL + * extName arrives first in an EXT_INFO with extValue. On success algName + * gets the public key algorithm name the request offered. */ +static int RunUserAuthPkSigAlgsCase(const char* extName, const char* extValue, + char* algName, word32 algNameSz) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[2048]; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + wolfSSH_SetUserAuth(ctx, PkCaseUserAuth); + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_SetUsername(ssh, "gretel"), WS_SUCCESS); + + MemIoInit(&io, NULL, 0, out, (word32)sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + ssh->sessionIdSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->sessionId, 0x33, ssh->sessionIdSz); + + if (pkCasePriorSigAlgs != NULL) { + byte payload[128]; + word32 payloadSz; + word32 idx = 0; + + payloadSz = BuildExtInfoSigAlgs(payload, (word32)sizeof(payload), + pkCasePriorSigAlgs); + AssertIntEQ(wolfSSH_TestDoExtInfo(ssh, payload, payloadSz, &idx), + WS_SUCCESS); + } + + if (extName != NULL) { + byte payload[128]; + word32 payloadSz; + word32 idx = 0; + + payloadSz = AppendUint32(payload, (word32)sizeof(payload), 0, 1); + payloadSz = AppendString(payload, (word32)sizeof(payload), payloadSz, + extName); + payloadSz = AppendString(payload, (word32)sizeof(payload), payloadSz, + extValue); + AssertIntEQ(wolfSSH_TestDoExtInfo(ssh, payload, payloadSz, &idx), + WS_SUCCESS); + } + + algName[0] = '\0'; + ret = SendUserAuthRequest(ssh, WOLFSSH_USERAUTH_PUBLICKEY, 1); + if (ret == WS_SUCCESS) { + static const char osshCertSuffix[] = "-cert-v01@openssh.com"; + const byte* payload; + const byte* field; + const byte* sig; + word32 payloadSz; + word32 fieldSz; + word32 sigSz; + word32 nameSz; + word32 idx = 0; + byte hasSignature = 0; + int i; + + AssertIntEQ(ParseMsgId(io.out, io.outSz), MSGID_USERAUTH_REQUEST); + payloadSz = ParsePayloadLen(io.out, io.outSz) - MSG_ID_SZ; + payload = io.out + UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ; + + /* user name, service name, method name */ + for (i = 0; i < 3; i++) { + AssertIntEQ(GetStringRef(&fieldSz, &field, payload, payloadSz, + &idx), WS_SUCCESS); + } + AssertIntEQ(GetBoolean(&hasSignature, payload, payloadSz, &idx), + WS_SUCCESS); + AssertIntEQ(hasSignature, 1); + AssertIntEQ(GetStringRef(&fieldSz, &field, payload, payloadSz, &idx), + WS_SUCCESS); + AssertTrue(fieldSz < algNameSz); + WMEMCPY(algName, field, fieldSz); + algName[fieldSz] = '\0'; + + /* public key blob, then the signature, which ends the payload */ + AssertIntEQ(GetStringRef(&fieldSz, &field, payload, payloadSz, &idx), + WS_SUCCESS); + AssertIntEQ(GetStringRef(&sigSz, &sig, payload, payloadSz, &idx), + WS_SUCCESS); + AssertIntEQ(idx, payloadSz); + + /* The signature names the offered algorithm, less a cert suffix. */ + idx = 0; + AssertIntEQ(GetStringRef(&fieldSz, &field, sig, sigSz, &idx), + WS_SUCCESS); + nameSz = (word32)WSTRLEN(algName); + if (nameSz > sizeof(osshCertSuffix) - 1 + && WSTRCMP(algName + nameSz - (sizeof(osshCertSuffix) - 1), + osshCertSuffix) == 0) { + nameSz -= (word32)(sizeof(osshCertSuffix) - 1); + } + AssertIntEQ(fieldSz, nameSz); + AssertIntEQ(WMEMCMP(field, algName, nameSz), 0); + } + else { + AssertIntEQ(io.outSz, 0); + } + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + + return ret; +} + +/* With no server-sig-algs, RFC 8308 section 2.2, the client uses its own + * order; with one, only what it lists. */ +static void TestUserAuthPkWithoutServerSigAlgs(void) +{ + char algName[64]; + +#if !defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_RSA_SHA2_256) + #ifndef WOLFSSH_NO_RSA_SHA2_512 + const char* rsaFirst = "rsa-sha2-512"; + #else + const char* rsaFirst = "rsa-sha2-256"; + #endif + + AssertIntEQ(ClientUsePubKey("keys/gretel-key-rsa.pub"), 0); + AssertIntEQ(ClientSetPrivateKey("keys/gretel-key-rsa.pem"), 0); + + AssertIntEQ(RunUserAuthPkSigAlgsCase(NULL, NULL, algName, + sizeof(algName)), WS_SUCCESS); + AssertStrEQ(algName, rsaFirst); + + /* An EXT_INFO without server-sig-algs lists nothing. */ + AssertIntEQ(RunUserAuthPkSigAlgsCase("ping@openssh.com", "0", algName, + sizeof(algName)), WS_SUCCESS); + AssertStrEQ(algName, rsaFirst); + + AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "rsa-sha2-256", + algName, sizeof(algName)), WS_SUCCESS); + AssertStrEQ(algName, "rsa-sha2-256"); + + /* Listed but nothing usable is still a mismatch. */ + AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "bogus", + algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "", + algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + + /* Nor when it replaces a usable one, RFC 8308 section 2.4. */ + pkCasePriorSigAlgs = "rsa-sha2-256"; + AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "bogus", + algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "", + algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + pkCasePriorSigAlgs = NULL; + + #if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_SSH_RSA_SHA1) \ + && !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) + /* Our own choice skips soft-disabled SHA-1. */ + pkCaseKeyType = "x509v3-ssh-rsa"; + AssertIntEQ(RunUserAuthPkSigAlgsCase(NULL, NULL, algName, + sizeof(algName)), WS_MATCH_KEY_ALGO_E); + pkCaseKeyType = NULL; + #endif + + #if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_OSSH_CERT_RSA) + /* An OpenSSH RSA certificate falls back the same way. */ + pkCaseKeyType = "ssh-rsa-cert-v01@openssh.com"; + AssertIntEQ(RunUserAuthPkSigAlgsCase(NULL, NULL, algName, + sizeof(algName)), WS_SUCCESS); + AssertIntEQ(WSTRNCMP(algName, rsaFirst, WSTRLEN(rsaFirst)), 0); + AssertStrEQ(algName + WSTRLEN(rsaFirst), "-cert-v01@openssh.com"); + pkCaseKeyType = NULL; + #endif + + ClientFreeBuffers(); +#endif + +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + AssertIntEQ(ClientUsePubKey("keys/gretel-key-ecc.pub"), 0); + AssertIntEQ(ClientSetPrivateKey("keys/gretel-key-ecc.pem"), 0); + + AssertIntEQ(RunUserAuthPkSigAlgsCase(NULL, NULL, algName, + sizeof(algName)), WS_SUCCESS); + AssertStrEQ(algName, "ecdsa-sha2-nistp256"); + + ClientFreeBuffers(); +#endif +} +#endif + /* Windows has no /dev/null; the null device there is "NUL". Keyed on * _WIN32, like the arpa/inet.h/direct.h include guard above: this is a * libc-availability question, not a wolfSSH API-selection one. */ @@ -15244,6 +15461,8 @@ static void TestExtInfoSigAlgsReplace(void) WS_SUCCESS); AssertIntEQ(ssh->peerSigIdSz, 0); AssertNull(ssh->peerSigId); + /* Still seen, so no fallback to the canned list. */ + AssertIntEQ(ssh->peerSigAlgsSeen, 1); /* Same for an empty list, from a good one again. */ idx = 0; @@ -17401,6 +17620,11 @@ int main(int argc, char** argv) #endif #if !(defined(WOLFSSH_NO_RSA) && defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) TestClientBuffersIdempotent(); +#endif +#if !defined(NO_WOLFSSH_CLIENT) && !defined(WOLFSSH_NO_PUBKEY_AUTH) \ + && ((!defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_RSA_SHA2_256)) \ + || !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) + TestUserAuthPkWithoutServerSigAlgs(); #endif TestPasswordEofNoCrash(); #ifndef NO_WOLFSSH_CLIENT diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 7044912cd..b03714878 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1350,6 +1350,7 @@ struct WOLFSSH { byte userAuthPkDone; byte sendExtInfo; byte extInfoSent; /* track if the ext info has already been sent */ + byte peerSigAlgsSeen; /* peer sent server-sig-algs, even an unusable one */ byte* peerSigId; word32 peerSigIdSz;