From 7faf79568d94f8e123f70c910cdf641c0e7bfcfd Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Mon, 29 Jun 2026 16:59:34 -0600 Subject: [PATCH] openssh ca certificate support --- apps/wolfsshd/auth.c | 201 +++++++++- apps/wolfsshd/auth.h | 10 + apps/wolfsshd/test/test_configuration.c | 494 ++++++++++++++++++++++++ apps/wolfsshd/wolfsshd.c | 72 +++- configure.ac | 10 +- wolfssh/ssh.h | 18 +- 6 files changed, 775 insertions(+), 30 deletions(-) diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index d79d21508..2e61af0e5 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -835,7 +835,6 @@ static int SearchForPubKey(const char* path, const char* authKeysFile, if (f != WBADFILE) { WFCLOSE(NULL, f); } - if (lineBuf != NULL) { WFREE(lineBuf, NULL, DYNTYPE_BUFFER); } @@ -869,6 +868,37 @@ static int CheckUserUnix(const char* name) { return ret; } +#if defined(WOLFSSH_OSSH_CERTS) && !defined(NO_SHA256) +/* Returns 1 if name is found in the SSH wire-format principals list + * (sequence of uint32-prefixed strings, big-endian), 0 otherwise. */ +static int PrincipalInList(const byte* list, word32 listSz, const char* name) +{ + word32 idx = 0; + word32 nameSz; + + if (list == NULL || name == NULL) + return 0; + nameSz = (word32)WSTRLEN(name); + + while (listSz - idx >= 4) { + word32 pSz = ((word32)list[idx ] << 24) | + ((word32)list[idx + 1] << 16) | + ((word32)list[idx + 2] << 8) | + (word32)list[idx + 3]; + idx += 4; + if (pSz > listSz - idx) { + break; + } + if ((pSz == nameSz) && + (WMEMCMP(list + idx, name, nameSz) == 0)) { + return 1; + } + idx += pSz; + } + return 0; +} +#endif /* WOLFSSH_OSSH_CERTS && !NO_SHA256 */ + static int CheckPublicKeyUnix(const char* name, const WS_UserAuthData_PublicKey* pubKeyCtx, const char* usrCaKeysFile, @@ -878,23 +908,45 @@ static int CheckPublicKeyUnix(const char* name, int ret = WSSHD_AUTH_SUCCESS; struct passwd* pwInfo; -#ifdef WOLFSSH_OSSH_CERTS +#if defined(WOLFSSH_OSSH_CERTS) && !defined(NO_SHA256) if (pubKeyCtx->isOsshCert) { int rc; byte* caKey = NULL; - word32 caKeySz; + word32 caKeySz = 0; const byte* caKeyType = NULL; - word32 caKeyTypeSz; + word32 caKeyTypeSz = 0; byte fingerprint[WC_SHA256_DIGEST_SIZE]; - - if (pubKeyCtx->caKey == NULL || - pubKeyCtx->caKeySz != WC_SHA256_DIGEST_SIZE) { + WFILE* f = WBADFILE; + char* lineBuf = NULL; + char* current = NULL; + word32 currentSz = 0; + int foundKey = 0; + + if (pubKeyCtx->caKeyHash == NULL || + pubKeyCtx->caKeyHashSz != WC_SHA256_DIGEST_SIZE) { ret = WS_FATAL_ERROR; } if (ret == WSSHD_AUTH_SUCCESS) { - f = XFOPEN(usrCaKeysFile, "rb"); - if (f == XBADFILE) { + if (usrCaKeysFile == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] TrustedUserCAKeys not " + "configured"); + ret = WS_BAD_ARGUMENT; + } + } + if (ret == WSSHD_AUTH_SUCCESS) { + int strictModes = wolfSSHD_ConfigGetStrictModes( + (authCtx != NULL) ? authCtx->conf : NULL); + if (strictModes) { + if (wolfSSHD_OpenSecureFile(usrCaKeysFile, geteuid(), + 0, NULL, &f) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] CA keys file %s failed StrictModes check", + usrCaKeysFile); + ret = WSSHD_AUTH_FAILURE; + } + } + else if (WFOPEN(NULL, &f, usrCaKeysFile, "rb") != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to open %s", usrCaKeysFile); ret = WS_BAD_FILE_E; @@ -907,8 +959,8 @@ static int CheckPublicKeyUnix(const char* name, } } while (ret == WSSHD_AUTH_SUCCESS && - (current = XFGETS(lineBuf, MAX_LINE_SZ, f)) != NULL) { - currentSz = (word32)XSTRLEN(current); + (current = WFGETS(lineBuf, MAX_LINE_SZ, f)) != NULL) { + currentSz = (word32)WSTRLEN(current); /* remove leading spaces */ while (currentSz > 0 && current[0] == ' ') { @@ -924,22 +976,77 @@ static int CheckPublicKeyUnix(const char* name, continue; /* commented out line */ } + if (caKey != NULL) { + WFREE(caKey, NULL, DYNTYPE_PRIVKEY); + caKey = NULL; + caKeySz = 0; + caKeyType = NULL; + caKeyTypeSz = 0; + } + rc = wolfSSH_ReadKey_buffer((const byte*)current, currentSz, WOLFSSH_FORMAT_SSH, &caKey, &caKeySz, &caKeyType, &caKeyTypeSz, NULL); if (rc == WS_SUCCESS) { + /* fingerprint is fully overwritten by wc_Hash on success + * and discarded on failure, so no pre-clear is needed. */ rc = wc_Hash(WC_HASH_TYPE_SHA256, caKey, caKeySz, fingerprint, WC_SHA256_DIGEST_SIZE); - if (rc == 0 && ConstantCompare(fingerprint, pubKeyCtx->caKey, - WC_SHA256_DIGEST_SIZE) == 0) { - foundKey = 1; + if (rc != 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to hash CA key entry (rc=%d)", rc); + ret = WS_FATAL_ERROR; + break; + } + if (ConstantCompare(fingerprint, pubKeyCtx->caKeyHash, + WC_SHA256_DIGEST_SIZE) == 0) { + /* name=NULL is a test-only caller convention used by + * wolfSSHD_TestCheckOsshCertCa. In the default wolfSSHD + * production path name is always non-NULL (set by + * DoCheckUser before the callback fires). Custom + * CallbackCheckPublicKey implementations that pass NULL + * for name when validPrincipals is non-empty will cause + * the principal check below to unconditionally deny + * access (name == NULL short-circuits to failure), not + * grant it. */ + /* TODO: validPrincipals is not yet populated + * outside the unit-test shim; see + * WS_UserAuthData_PublicKey.validPrincipals in ssh.h. */ + if ((pubKeyCtx->validPrincipals != NULL) && + (pubKeyCtx->validPrincipalsSz > 0) && + ((name == NULL) || + (!PrincipalInList(pubKeyCtx->validPrincipals, + pubKeyCtx->validPrincipalsSz, + name)))) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Certificate principal does not " + "match user %s", name ? name : "NULL"); + ret = WSSHD_AUTH_FAILURE; + } + else { + foundKey = 1; + } break; } } } + + if (caKey != NULL) { + WFREE(caKey, NULL, DYNTYPE_PRIVKEY); + } + if (f != WBADFILE) { + WFCLOSE(NULL, f); + } + if (lineBuf != NULL) { + WFREE(lineBuf, NULL, DYNTYPE_BUFFER); + } + + if (ret == WSSHD_AUTH_SUCCESS && !foundKey) { + ret = WSSHD_AUTH_FAILURE; + } } else - #endif /* WOLFSSH_OSSH_CERTS */ + #endif /* WOLFSSH_OSSH_CERTS && !NO_SHA256 */ { errno = 0; pwInfo = getpwnam((const char*)name); @@ -957,8 +1064,9 @@ static int CheckPublicKeyUnix(const char* name, } } +#if !defined(WOLFSSH_OSSH_CERTS) || defined(NO_SHA256) WOLFSSH_UNUSED(usrCaKeysFile); - WOLFSSH_UNUSED(authCtx); +#endif return ret; } #endif /* !_WIN32*/ @@ -1626,9 +1734,9 @@ static int RequestAuthentication(WS_UserAuthData* authData, * closed: require AuthorizedKeysFile (per-user key/cert mapping) * or a wolfSSL build with FPKI. */ wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Certificate authentication cannot bind the requested " - "user without FPKI or AuthorizedKeysFile; rejecting " - "(user=%s)", usr); + "[SSHD] Certificate authentication cannot bind " + "the requested user without FPKI or " + "AuthorizedKeysFile; rejecting (user=%s)", usr); ret = WOLFSSH_USERAUTH_REJECTED; #endif } @@ -2249,4 +2357,59 @@ WOLFSSHD_CONFIG* wolfSSHD_AuthGetUserConf(const WOLFSSHD_AUTH* auth, } return ret; } +#if defined(WOLFSSHD_UNIT_TEST) && defined(WOLFSSH_OSSH_CERTS) && \ + !defined(NO_SHA256) && !defined(_WIN32) +/* Expose the OSSH CA-fingerprint matching path of CheckPublicKeyUnix for + * unit testing. caKeyHash must be a SHA-256 digest of the raw public key. + * strictModes drives the file-open gate: <0 exercises the no-authCtx + * (fail-safe default) path, 0/1 build a real WOLFSSHD_AUTH/config pair + * with StrictModes forced off/on so the wolfSSHD_OpenSecureFile() branch is + * reachable from tests. + * Returns WSSHD_AUTH_SUCCESS on success, WSSHD_AUTH_FAILURE or error code on + * failure. */ +int wolfSSHD_TestCheckOsshCertCa(const byte* caKeyHash, + word32 caKeyHashSz, + const char* caKeysFile, + const char* name, + const byte* validPrincipals, + word32 validPrincipalsSz, + int strictModes) +{ + WS_UserAuthData_PublicKey pk; + WOLFSSHD_AUTH authCtx; + WOLFSSHD_CONFIG* conf = NULL; + int ret; + + WMEMSET(&pk, 0, sizeof(pk)); + pk.isOsshCert = 1; + pk.caKeyHash = caKeyHash; + pk.caKeyHashSz = caKeyHashSz; + pk.validPrincipals = validPrincipals; + pk.validPrincipalsSz = validPrincipalsSz; + + if (strictModes < 0) { + return CheckPublicKeyUnix(name, &pk, caKeysFile, NULL, NULL); + } + + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) { + return WS_MEMORY_E; + } + { + const char* strictModesStr = strictModes ? "StrictModes yes" : + "StrictModes no"; + if (ParseConfigLine(&conf, strictModesStr, + (int)WSTRLEN(strictModesStr), 0) != WS_SUCCESS) { + wolfSSHD_ConfigFree(conf); + return WS_FATAL_ERROR; + } + } + + WMEMSET(&authCtx, 0, sizeof(authCtx)); + authCtx.conf = conf; + ret = CheckPublicKeyUnix(name, &pk, caKeysFile, NULL, &authCtx); + wolfSSHD_ConfigFree(conf); + return ret; +} +#endif /* WOLFSSHD_UNIT_TEST && WOLFSSH_OSSH_CERTS && !NO_SHA256 && !_WIN32 */ #endif /* WOLFSSH_SSHD */ diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index c487a68ca..e0de1fc38 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -101,5 +101,15 @@ int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, word32 keySz); int CAKeysFileDiffers(const char* a, const char* b); int wolfSSHD_GetUserAuthTypes(const WOLFSSHD_CONFIG* usrConf); +#if defined(WOLFSSHD_UNIT_TEST) && defined(WOLFSSH_OSSH_CERTS) && \ + !defined(NO_SHA256) && !defined(_WIN32) +int wolfSSHD_TestCheckOsshCertCa(const byte* caKeyHash, + word32 caKeyHashSz, + const char* caKeysFile, + const char* name, + const byte* validPrincipals, + word32 validPrincipalsSz, + int strictModes); +#endif #endif #endif /* WOLFAUTH_H */ diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index aa5c410de..b6466400f 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -23,6 +23,7 @@ #endif #include +#include #include #include #include @@ -1900,6 +1901,493 @@ static int test_OpenSecureFile(void) } #endif /* !_WIN32 */ +#if defined(WOLFSSH_OSSH_CERTS) && !defined(NO_SHA256) && !defined(_WIN32) +#include +static int test_CheckOsshCertCa(void) +{ + int ret = WS_SUCCESS; + int rc; + /* ECC-P256 public key in OpenSSH format used as a stand-in CA key. */ + static const char caKeyStr[] = + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA" + "BBBNkI5JTP6D0lF42tbxX19cE87hztUS6FSDoGvPfiU0CgeNSbI+aFdKIzTP5CQEJSvm25" + "qUzgDtH7oyaQROUnNvk= hansel"; + static const char caKeyFile[] = "./test_ossh_ca_keys.txt"; + byte fingerprint[WC_SHA256_DIGEST_SIZE]; + byte wrongHash[WC_SHA256_DIGEST_SIZE]; + byte* caKeyRaw = NULL; + word32 caKeyRawSz = 0; + const byte* caKeyType = NULL; + word32 caKeyTypeSz = 0; + WFILE* f = WBADFILE; + + rc = wolfSSH_ReadKey_buffer((const byte*)caKeyStr, + (word32)WSTRLEN(caKeyStr), + WOLFSSH_FORMAT_SSH, &caKeyRaw, &caKeyRawSz, + &caKeyType, &caKeyTypeSz, NULL); + if (rc != WS_SUCCESS) { + Log(" Failed to parse CA key: %d\n", rc); + return WS_FATAL_ERROR; + } + rc = wc_Sha256Hash(caKeyRaw, caKeyRawSz, fingerprint); + WFREE(caKeyRaw, NULL, DYNTYPE_PRIVKEY); + if (rc != 0) { + Log(" Failed to hash CA key: %d\n", rc); + return WS_FATAL_ERROR; + } + + /* Write a one-line CA keys file. */ + if (WFOPEN(NULL, &f, caKeyFile, "w") != 0) { + Log(" Failed to create CA keys file\n"); + return WS_FATAL_ERROR; + } + if (WFWRITE(NULL, caKeyStr, sizeof(char), WSTRLEN(caKeyStr), f) + != WSTRLEN(caKeyStr) || + WFWRITE(NULL, "\n", sizeof(char), 1, f) != 1) { + WFCLOSE(NULL, f); + Log(" Failed to write CA keys file\n"); + WREMOVE(0, caKeyFile); + return WS_FATAL_ERROR; + } + WFCLOSE(NULL, f); + + Log(" Testing: matching CA fingerprint."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + + if (ret == WS_SUCCESS) { + WMEMSET(wrongHash, 0xBB, WC_SHA256_DIGEST_SIZE); + Log(" Testing: non-matching CA fingerprint."); + rc = wolfSSHD_TestCheckOsshCertCa(wrongHash, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* Empty file: no matching key fails with WSSHD_AUTH_FAILURE */ + if (ret == WS_SUCCESS) { + if (WFOPEN(NULL, &f, caKeyFile, "w") != 0) { + Log(" Failed to recreate CA keys file (empty)\n"); + ret = WS_FATAL_ERROR; + } + else { + WFCLOSE(NULL, f); + Log(" Testing: empty CA keys file."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, + WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + } + + /* Comment-only file: no matching key fails with WSSHD_AUTH_FAILURE */ + if (ret == WS_SUCCESS) { + static const char comment[] = "# trusted CAs\n"; + if (WFOPEN(NULL, &f, caKeyFile, "w") != 0) { + Log(" Failed to recreate CA keys file (comment-only)\n"); + ret = WS_FATAL_ERROR; + } + else { + if (WFWRITE(NULL, comment, sizeof(char), WSTRLEN(comment), f) + != WSTRLEN(comment)) { + WFCLOSE(NULL, f); + Log(" Failed to write comment CA keys file\n"); + ret = WS_FATAL_ERROR; + } + else { + WFCLOSE(NULL, f); + Log(" Testing: comment-only CA keys file."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, + WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + } + } + + WREMOVE(0, caKeyFile); + return ret; +} +static int test_CheckOsshCertCa_Malformed(void) +{ + int ret = WS_SUCCESS; + int rc; + static const char caKeyStr[] = + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA" + "BBBNkI5JTP6D0lF42tbxX19cE87hztUS6FSDoGvPfiU0CgeNSbI+aFdKIzTP5CQEJSvm25" + "qUzgDtH7oyaQROUnNvk= hansel"; + static const char caKeyFile[] = "./test_ossh_ca_keys_malformed.txt"; + static const char badLine[] = "not-a-valid-key\n"; + byte fingerprint[WC_SHA256_DIGEST_SIZE]; + byte* caKeyRaw = NULL; + word32 caKeyRawSz = 0; + const byte* caKeyType = NULL; + word32 caKeyTypeSz = 0; + WFILE* f = WBADFILE; + + rc = wolfSSH_ReadKey_buffer((const byte*)caKeyStr, + (word32)WSTRLEN(caKeyStr), + WOLFSSH_FORMAT_SSH, &caKeyRaw, &caKeyRawSz, + &caKeyType, &caKeyTypeSz, NULL); + if (rc != WS_SUCCESS) { return WS_FATAL_ERROR; } + rc = wc_Sha256Hash(caKeyRaw, caKeyRawSz, fingerprint); + WFREE(caKeyRaw, NULL, DYNTYPE_PRIVKEY); + if (rc != 0) { return WS_FATAL_ERROR; } + + /* write malformed line */ + if (WFOPEN(NULL, &f, caKeyFile, "w") != 0) { + return WS_FATAL_ERROR; + } + if (WFWRITE(NULL, badLine, sizeof(char), WSTRLEN(badLine), f) + != WSTRLEN(badLine)) { + WFCLOSE(NULL, f); + return WS_FATAL_ERROR; + } + WFCLOSE(NULL, f); + + Log(" Testing: malformed key line CA keys file."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + + WREMOVE(0, caKeyFile); + return ret; +} + +/* Covers the usrCaKeysFile == NULL guard and the StrictModes-gated + * wolfSSHD_OpenSecureFile() branch in CheckPublicKeyUnix, neither of which + * is reachable through the strictModes=0 calls above. + * Returns WS_SUCCESS on success, WS_FATAL_ERROR on failure. */ +static int test_CheckOsshCertCa_StrictModes(void) +{ + int ret = WS_SUCCESS; + int rc; + static const char caKeyStr[] = + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA" + "BBBNkI5JTP6D0lF42tbxX19cE87hztUS6FSDoGvPfiU0CgeNSbI+aFdKIzTP5CQEJSvm25" + "qUzgDtH7oyaQROUnNvk= hansel"; + /* StrictModes also gates parent-directory permissions, so the CA file + * lives in its own private temp dir rather than the (group/world + * writable) test working directory used by the strictModes=0 tests. */ + char base[] = "/tmp/wolfsshd_ossh_caXXXXXX"; + char caKeyFile[96] = ""; + byte fingerprint[WC_SHA256_DIGEST_SIZE] = {0}; + byte* caKeyRaw = NULL; + word32 caKeyRawSz = 0; + const byte* caKeyType = NULL; + word32 caKeyTypeSz = 0; + WFILE* f = WBADFILE; + + Log(" Testing: NULL usrCaKeysFile rejected."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + NULL, NULL, NULL, 0, 0); + if (rc != WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + + if (mkdtemp(base) == NULL) { + Log(" mkdtemp failed.\n"); + return WS_FATAL_ERROR; + } + snprintf(caKeyFile, sizeof(caKeyFile), "%s/ca_keys.txt", base); + if (smChmod(base, 0700) != WS_SUCCESS) { + ret = WS_FATAL_ERROR; + goto exit; + } + + rc = wolfSSH_ReadKey_buffer((const byte*)caKeyStr, + (word32)WSTRLEN(caKeyStr), + WOLFSSH_FORMAT_SSH, &caKeyRaw, &caKeyRawSz, + &caKeyType, &caKeyTypeSz, NULL); + if (rc != WS_SUCCESS) { + Log(" Failed to parse CA key: %d\n", rc); + ret = WS_FATAL_ERROR; + goto exit; + } + rc = wc_Sha256Hash(caKeyRaw, caKeyRawSz, fingerprint); + WFREE(caKeyRaw, NULL, DYNTYPE_PRIVKEY); + if (rc != 0) { + Log(" Failed to hash CA key: %d\n", rc); + ret = WS_FATAL_ERROR; + goto exit; + } + + if (ret == WS_SUCCESS) { + if (WFOPEN(NULL, &f, caKeyFile, "w") != 0) { + Log(" Failed to create CA keys file\n"); + ret = WS_FATAL_ERROR; + } + else { + if (WFWRITE(NULL, caKeyStr, sizeof(char), WSTRLEN(caKeyStr), f) + != WSTRLEN(caKeyStr) || + WFWRITE(NULL, "\n", sizeof(char), 1, f) != 1) { + WFCLOSE(NULL, f); + Log(" Failed to write CA keys file\n"); + ret = WS_FATAL_ERROR; + } + else { + WFCLOSE(NULL, f); + } + } + } + + if (ret == WS_SUCCESS) + ret = smChmod(caKeyFile, 0600); + if (ret == WS_SUCCESS) { + Log(" Testing: StrictModes on, securely-permissioned CA file."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 1); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* strictModes=-1 drives the test shim's authCtx==NULL path, exercising + * CheckPublicKeyUnix's fail-safe default (StrictModes on when authCtx + * is NULL). A securely-permissioned file must still be accepted. */ + if (ret == WS_SUCCESS) { + Log(" Testing: NULL authCtx fail-safe default accepts good perms."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, -1); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) + ret = smChmod(caKeyFile, 0606); + if (ret == WS_SUCCESS) { + Log(" Testing: StrictModes on, world-writable CA file rejected."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 1); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* The same insecure permissions are accepted when StrictModes is off, + * confirming the rejection above is StrictModes-driven. */ + if (ret == WS_SUCCESS) { + Log(" Testing: StrictModes off, same file accepted."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, NULL, 0, 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + +exit: + WREMOVE(0, caKeyFile); + rmdir(base); + return ret; +} +/* Verify checking of OpenSSH principal restrictions. + * Returns WS_SUCCESS on success, WS_FATAL_ERROR on failure. */ +static int test_CheckOsshCertCa_Principals(void) +{ + int ret = WS_SUCCESS; + int rc; + static const char caKeyStr[] = + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAA" + "BBBNkI5JTP6D0lF42tbxX19cE87hztUS6FSDoGvPfiU0CgeNSbI+aFdKIzTP5CQEJSvm25" + "qUzgDtH7oyaQROUnNvk= hansel"; + static const char caKeyFile[] = "./test_ossh_ca_principals.txt"; + /* SSH wire-format principal lists: uint32 len (big-endian) + bytes */ + static const byte princsMatch[] = { + 0x00, 0x00, 0x00, 0x06, 'h', 'a', 'n', 's', 'e', 'l' + }; + static const byte princsNoMatch[] = { + 0x00, 0x00, 0x00, 0x06, 'g', 'r', 'e', 't', 'e', 'l' + }; + /* "gretel" followed by "hansel": match is at a non-first position, + * exercising the idx += pSz skip-and-advance path. */ + static const byte princsMatchSecond[] = { + 0x00, 0x00, 0x00, 0x06, 'g', 'r', 'e', 't', 'e', 'l', + 0x00, 0x00, 0x00, 0x06, 'h', 'a', 'n', 's', 'e', 'l' + }; + /* Declared length (8) exceeds remaining bytes (4): exercises the + * truncation-guard break in PrincipalInList. */ + static const byte princsTruncated[] = { + 0x00, 0x00, 0x00, 0x08, 'h', 'a', 'n', 's' + }; + byte fingerprint[WC_SHA256_DIGEST_SIZE]; + byte* caKeyRaw = NULL; + word32 caKeyRawSz = 0; + const byte* caKeyType = NULL; + word32 caKeyTypeSz = 0; + WFILE* f = WBADFILE; + + rc = wolfSSH_ReadKey_buffer((const byte*)caKeyStr, + (word32)WSTRLEN(caKeyStr), + WOLFSSH_FORMAT_SSH, &caKeyRaw, &caKeyRawSz, + &caKeyType, &caKeyTypeSz, NULL); + if (rc != WS_SUCCESS) { + Log(" Failed to parse CA key: %d\n", rc); + return WS_FATAL_ERROR; + } + rc = wc_Sha256Hash(caKeyRaw, caKeyRawSz, fingerprint); + WFREE(caKeyRaw, NULL, DYNTYPE_PRIVKEY); + if (rc != 0) { + Log(" Failed to hash CA key: %d\n", rc); + return WS_FATAL_ERROR; + } + + if (WFOPEN(NULL, &f, caKeyFile, "w") != 0) { + Log(" Failed to create CA keys file\n"); + return WS_FATAL_ERROR; + } + { + word32 wr, sz; + wr = (word32)WFWRITE(NULL, caKeyStr, sizeof(char), + WSTRLEN(caKeyStr), f); + sz = (word32)WSTRLEN(caKeyStr); + if (sz != wr || (word32)WFWRITE(NULL, "\n", sizeof(char), 1, f) != 1) { + WFCLOSE(NULL, f); + Log(" Failed to write CA keys file\n"); + WREMOVE(0, caKeyFile); + return WS_FATAL_ERROR; + } + } + WFCLOSE(NULL, f); + + /* Empty principals list: any user should be accepted. */ + Log(" Testing: empty principals (any user)."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, "hansel", NULL, 0, 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + + /* Principal list contains the connecting user. */ + if (ret == WS_SUCCESS) { + Log(" Testing: matching principal."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, "hansel", princsMatch, sizeof(princsMatch), 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* Principal list does not contain the connecting user. */ + if (ret == WS_SUCCESS) { + Log(" Testing: non-matching principal."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, "hansel", princsNoMatch, sizeof(princsNoMatch), 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* name=NULL is rejected if the list is non-empty. */ + if (ret == WS_SUCCESS) { + Log(" Testing: name=NULL fails principal check."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, NULL, princsNoMatch, sizeof(princsNoMatch), 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* Multi-entry list: user is at the second position, exercises skip path. */ + if (ret == WS_SUCCESS) { + Log(" Testing: matching principal at non-first position."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, "hansel", princsMatchSecond, + sizeof(princsMatchSecond), 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* Truncated entry: declared pSz exceeds remaining bytes, should reject. */ + if (ret == WS_SUCCESS) { + Log(" Testing: truncated principal list rejected."); + rc = wolfSSHD_TestCheckOsshCertCa(fingerprint, WC_SHA256_DIGEST_SIZE, + caKeyFile, "hansel", princsTruncated, + sizeof(princsTruncated), 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + WREMOVE(0, caKeyFile); + return ret; +} +#endif /* WOLFSSH_OSSH_CERTS && !NO_SHA256 && !_WIN32 */ + const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_ParseConfigLine), @@ -1933,6 +2421,12 @@ const TEST_CASE testCases[] = { #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) TEST_DECL(test_CheckPasswordHashUnix), #endif +#if defined(WOLFSSH_OSSH_CERTS) && !defined(NO_SHA256) && !defined(_WIN32) + TEST_DECL(test_CheckOsshCertCa), + TEST_DECL(test_CheckOsshCertCa_Malformed), + TEST_DECL(test_CheckOsshCertCa_StrictModes), + TEST_DECL(test_CheckOsshCertCa_Principals), +#endif }; int main(int argc, char** argv) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index b25a3f2fb..cb3774db3 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -446,28 +446,82 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } if (ret == WS_SUCCESS) { - #ifdef WOLFSSH_OPENSSH_CERTS + int certUsed = 0; + #ifdef WOLFSSH_OSSH_CERTS + /* TODO: Host OpenSSH certificates are not yet supported by + * wolfSSH (wolfSSH_CTX_UseOsshCert_buffer is pending + * implementation in the library). Once implemented, the + * block below can be enabled. */ + #if 0 if (wolfSSH_CTX_UseOsshCert_buffer(*ctx, data, dataSz) < 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to use host certificate."); ret = WS_BAD_ARGUMENT; } + else { + certUsed = 1; + } + #else + { + /* OpenSSH cert key types are always of the form + * "-cert-v01@openssh.com" - check the + * suffix of the first (whitespace-delimited) field + * so a plain OpenSSH public key (e.g. "ssh-rsa ...") + * is not misidentified as a certificate. */ + static const char certSuffix[] = "-cert-v01@openssh.com"; + const word32 certSuffixLen = + (word32)WSTRLEN(certSuffix); + word32 typeLen = 0; + + while ((typeLen < dataSz) && + (data[typeLen] != ' ') && + (data[typeLen] != '\t') && + (data[typeLen] != '\r') && + (data[typeLen] != '\n')) { + typeLen++; + } + + if ((typeLen >= certSuffixLen) && + (WMEMCMP(data + (typeLen - certSuffixLen), + certSuffix, certSuffixLen) == 0)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Host OpenSSH certificates are not " + "yet supported by wolfSSH."); + ret = WS_BAD_ARGUMENT; + } + } + #endif #endif #ifdef WOLFSSH_CERTS - if (ret == WS_SUCCESS || ret == WS_BAD_ARGUMENT) { - ret = wolfSSH_CTX_UseCert_buffer(*ctx, data, dataSz, + if (((ret == WS_SUCCESS) || + (ret == WS_BAD_ARGUMENT)) && !certUsed) { + int useRet = wolfSSH_CTX_UseCert_buffer(*ctx, data, dataSz, WOLFSSH_FORMAT_PEM); - if (ret != WS_SUCCESS) { - ret = wolfSSH_CTX_UseCert_buffer(*ctx, data, dataSz, + if (useRet != WS_SUCCESS) { + useRet = wolfSSH_CTX_UseCert_buffer(*ctx, data, dataSz, WOLFSSH_FORMAT_ASN1); } - if (ret != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to load in host certificate."); + if (useRet != WS_SUCCESS) { + if (ret == WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to load in host certificate."); + ret = useRet; + } + } + else { + ret = WS_SUCCESS; + certUsed = 1; } } #endif + if (ret == WS_SUCCESS && !certUsed) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Host certificate configured but " + "cannot be applied."); + ret = NOT_COMPILED_IN; + } + freeBufferFromFile(data, heap); } } @@ -501,7 +555,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, WOLFSSH_FORMAT_ASN1); } if (ret != WS_SUCCESS) { - #ifdef WOLFSSH_OPENSSH_CERTS + #ifdef WOLFSSH_OSSH_CERTS wolfSSH_Log(WS_LOG_INFO, "[SSHD] Continuing on in case CA is openssh " "style."); diff --git a/configure.ac b/configure.ac index 07731f0b3..70d9235f9 100644 --- a/configure.ac +++ b/configure.ac @@ -215,6 +215,11 @@ AC_ARG_ENABLE([certs], [AS_HELP_STRING([--enable-certs],[Enable X.509 cert support (default: disabled)])], [ENABLED_CERTS=$enableval],[ENABLED_CERTS=no]) +# OpenSSH certs +AC_ARG_ENABLE([openssh-certs], + [AS_HELP_STRING([--enable-openssh-certs],[Enable OpenSSH cert support (default: disabled)])], + [ENABLED_OSSH_CERTS=$enableval],[ENABLED_OSSH_CERTS=no]) + # TPM 2.0 Support AC_ARG_ENABLE([tpm], [AS_HELP_STRING([--enable-tpm],[Enable TPM 2.0 support (default: disabled)])], @@ -245,7 +250,7 @@ AC_ARG_ENABLE([distro], AS_IF([test "x$ENABLED_DISTRO" = "xyes"], [ENABLED_ALL=yes; enable_shared=yes; enable_static=yes]) AS_IF([test "x$ENABLED_ALL" = "xyes"], - [ENABLED_KEYGEN=yes; ENABLED_SCP=yes; ENABLED_SFTP=yes; ENABLED_FWD=yes; ENABLED_SHELL=yes; ENABLED_AGENT=yes; ENABLED_SSHD=yes; ENABLED_SSHCLIENT=yes; ENABLED_CERTS=yes; ENABLED_KEYBOARD_INTERACTIVE=yes]) + [ENABLED_KEYGEN=yes; ENABLED_SCP=yes; ENABLED_SFTP=yes; ENABLED_FWD=yes; ENABLED_SHELL=yes; ENABLED_AGENT=yes; ENABLED_SSHD=yes; ENABLED_SSHCLIENT=yes; ENABLED_CERTS=yes; ENABLED_OSSH_CERTS=yes; ENABLED_KEYBOARD_INTERACTIVE=yes]) AS_IF([test "x$ENABLED_SSHD" = "xyes"], [ENABLED_SHELL=yes]) @@ -276,6 +281,8 @@ AS_IF([test "x$ENABLED_AGENT" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_AGENT"]) AS_IF([test "x$ENABLED_CERTS" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_CERTS"]) +AS_IF([test "x$ENABLED_OSSH_CERTS" = "xyes"], + [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_OSSH_CERTS"]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) AS_IF([test "x$ENABLED_SSHCLIENT" = "xyes"], @@ -380,4 +387,5 @@ AS_ECHO([" * agent: $ENABLED_AGENT"]) AS_ECHO([" * TPM 2.0 support: $ENABLED_TPM"]) AS_ECHO([" * TCP/IP Forwarding: $ENABLED_FWD"]) AS_ECHO([" * X.509 Certs: $ENABLED_CERTS"]) +AS_ECHO([" * OpenSSH Certs: $ENABLED_OSSH_CERTS"]) AS_ECHO([" * Examples: $ENABLED_EXAMPLES"]) diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 6ce19aa9d..fe6c4f8cb 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -384,6 +384,23 @@ typedef struct WS_UserAuthData_PublicKey { const byte* signature; word32 signatureSz; byte isCert:1; +#ifdef WOLFSSH_OSSH_CERTS + byte isOsshCert:1; + const byte* caKeyHash; + word32 caKeyHashSz; + /* Wire-format sequence of SSH strings (uint32 len + bytes each). + * Non-empty: connecting username must appear in this list. + * Empty/NULL: cert is valid for any user. + * + * TODO: currently only populated by the unit-test shim + * (wolfSSHD_TestCheckOsshCertCa). A companion change in + * src/internal.c is required to extract the valid_principals list + * from the client's presented OpenSSH certificate and write it here + * before the user-auth callback fires, so that CheckPublicKeyUnix + * enforces principal matching in production. */ + const byte* validPrincipals; + word32 validPrincipalsSz; +#endif } WS_UserAuthData_PublicKey; typedef struct WS_UserAuthData { @@ -580,4 +597,3 @@ WOLFSSH_API void wolfSSH_ShowSizes(void); #endif #endif /* _WOLFSSH_SSH_H_ */ -