From c00d11157df2a1c1b8018f53c57f73cf45907f28 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Thu, 18 Jun 2026 16:27:01 +0900 Subject: [PATCH] Add OpenSSH certificate user authentication --- .gitignore | 3 + apps/wolfsshd/auth.c | 472 ++++++-- apps/wolfsshd/auth.h | 13 + apps/wolfsshd/test/run_all_sshd_tests.sh | 8 + apps/wolfsshd/test/sshd_ossh_cert_test.sh | 282 +++++ apps/wolfsshd/test/test_configuration.c | 238 ++++ apps/wolfsshd/wolfsshd.c | 124 ++- configure.ac | 10 +- ide/winvs/wolfssh/wolfssh.vcxproj | 1 + keys/fred-ossh-badca-cert.pub | 1 + keys/fred-ossh-cert.pub | 1 + keys/fred-ossh-ecdsaca-cert.pub | 1 + keys/fred-ossh-ecdsauser-cert.pub | 1 + keys/fred-ossh-expired-cert.pub | 1 + keys/fred-ossh-forcecmd-cert.pub | 1 + keys/fred-ossh-internalsftp-cert.pub | 1 + keys/fred-ossh-noprincipal-cert.pub | 1 + keys/fred-ossh-rsaca-cert.pub | 1 + keys/fred-ossh-rsauser-cert.pub | 1 + keys/fred-ossh-srcbad-cert.pub | 1 + keys/fred-ossh-srcok-cert.pub | 1 + keys/fred-ossh-unkcrit-cert.pub | 1 + keys/fred-ossh-wrongprincipal-cert.pub | 1 + keys/include.am | 18 +- keys/ossh-bad-ca | 7 + keys/ossh-bad-ca.pub | 1 + keys/ossh-ca | 7 + keys/ossh-ca-ecdsa | 10 + keys/ossh-ca-ecdsa.pub | 1 + keys/ossh-ca-rsa | 27 + keys/ossh-ca-rsa.pub | 1 + keys/ossh-ca.pub | 1 + keys/ossh-user | 7 + keys/ossh-user-ecdsa | 9 + keys/ossh-user-ecdsa.pub | 1 + keys/ossh-user-rsa | 27 + keys/ossh-user-rsa.pub | 1 + keys/ossh-user.pub | 1 + keys/renew-ossh-certs.sh | 116 ++ src/include.am | 5 + src/internal.c | 858 +++++++------- src/ossh.c | 1234 +++++++++++++++++++++ src/ssh.c | 20 + tests/api.c | 392 +++++++ tests/unit.c | 22 +- wolfssh/include.am | 1 + wolfssh/internal.h | 17 + wolfssh/ossh.h | 113 ++ wolfssh/ssh.h | 27 + 49 files changed, 3529 insertions(+), 560 deletions(-) create mode 100755 apps/wolfsshd/test/sshd_ossh_cert_test.sh create mode 100644 keys/fred-ossh-badca-cert.pub create mode 100644 keys/fred-ossh-cert.pub create mode 100644 keys/fred-ossh-ecdsaca-cert.pub create mode 100644 keys/fred-ossh-ecdsauser-cert.pub create mode 100644 keys/fred-ossh-expired-cert.pub create mode 100644 keys/fred-ossh-forcecmd-cert.pub create mode 100644 keys/fred-ossh-internalsftp-cert.pub create mode 100644 keys/fred-ossh-noprincipal-cert.pub create mode 100644 keys/fred-ossh-rsaca-cert.pub create mode 100644 keys/fred-ossh-rsauser-cert.pub create mode 100644 keys/fred-ossh-srcbad-cert.pub create mode 100644 keys/fred-ossh-srcok-cert.pub create mode 100644 keys/fred-ossh-unkcrit-cert.pub create mode 100644 keys/fred-ossh-wrongprincipal-cert.pub create mode 100644 keys/ossh-bad-ca create mode 100644 keys/ossh-bad-ca.pub create mode 100644 keys/ossh-ca create mode 100644 keys/ossh-ca-ecdsa create mode 100644 keys/ossh-ca-ecdsa.pub create mode 100644 keys/ossh-ca-rsa create mode 100644 keys/ossh-ca-rsa.pub create mode 100644 keys/ossh-ca.pub create mode 100644 keys/ossh-user create mode 100644 keys/ossh-user-ecdsa create mode 100644 keys/ossh-user-ecdsa.pub create mode 100644 keys/ossh-user-rsa create mode 100644 keys/ossh-user-rsa.pub create mode 100644 keys/ossh-user.pub create mode 100755 keys/renew-ossh-certs.sh create mode 100644 src/ossh.c create mode 100644 wolfssh/ossh.h diff --git a/.gitignore b/.gitignore index ff72dc839..438392314 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,9 @@ apps/wolfsshd/test/stdout.txt keys/root-* keys/*.csr keys/renewcerts-*.cnf +# Per-user OpenSSH certificates issued at test time; keep the fred baseline. +keys/*-ossh-*cert.pub +!keys/fred-ossh-*cert.pub random-test.txt random-test-result.txt test.dat diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 50225637a..aece5e15e 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -72,6 +72,10 @@ #include #include #include +#ifdef WOLFSSH_OSSH_CERTS +#include /* in6_addr, AF_INET6 */ +#include /* inet_pton for source-address matching */ +#endif #endif #if !defined(_WIN32) && !(defined(__OSX__) || defined(__APPLE__)) @@ -84,6 +88,13 @@ int (*wsshd_setregid_cb)(WGID_T, WGID_T) = setregid; int (*wsshd_setreuid_cb)(WUID_T, WUID_T) = setreuid; #endif +#ifdef WOLFSSH_OSSH_CERTS +/* Peer IP string buffer; sized above INET6_ADDRSTRLEN (46) with room to spare. */ +#define WOLFSSHD_PEER_IP_SZ 64 +/* One source-address CIDR entry ("addr/len"); an IPv6 entry is well under this. */ +#define WOLFSSHD_CIDR_ENTRY_SZ 80 +#endif + struct WOLFSSHD_AUTH { CallbackCheckUser checkUserCb; CallbackCheckPassword checkPasswordCb; @@ -98,6 +109,12 @@ struct WOLFSSHD_AUTH { int sUid; /* saved uid */ int attempts; void* heap; +#ifdef WOLFSSH_OSSH_CERTS + /* Per-connection cert state, written only on the forked Unix path (the + * writes are compiled out on Windows); see HandleConnection. */ + char peerIp[WOLFSSHD_PEER_IP_SZ]; /* connection peer IP, for source-address */ + char* certForcedCmd; /* force-command from an authenticated OpenSSH cert */ +#endif }; #ifndef WOLFSSHD_MAX_PASSWORD_ATTEMPTS @@ -526,32 +543,28 @@ static int ResolveAuthKeysPath(const char* homeDir, const char* pattern, return ret; } -static int SearchForPubKey(const char* path, const char* authKeysFile, - const WS_UserAuthData_PublicKey* pubKeyCtx) +/* Scan an already-resolved keys file (authorized_keys or TrustedUserCAKeys), + * skipping blank and commented lines, and report whether (key, keySz) matches + * one of the listed keys via CheckAuthKeysLine(). Fails closed: returns + * WSSHD_AUTH_FAILURE when no line matches, or a negative error. */ +static int SearchKeysFile(const char* keysFilePath, const byte* key, + word32 keySz) { int ret = WSSHD_AUTH_SUCCESS; - char authKeysPath[MAX_PATH_SZ]; - WFILE *f = XBADFILE; + WFILE *f = WBADFILE; char* lineBuf = NULL; char* current; word32 currentSz; int foundKey = 0; int rc = 0; - WMEMSET(authKeysPath, 0, sizeof(authKeysPath)); - rc = ResolveAuthKeysPath(path, authKeysFile, authKeysPath); - if (rc != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to resolve authorized keys" - " file path."); - ret = rc; + if (keysFilePath == NULL || key == NULL || keySz == 0) { + return WS_BAD_ARGUMENT; } - if (ret == WSSHD_AUTH_SUCCESS) { - if (WFOPEN(NULL, &f, authKeysPath, "rb") != 0) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to open %s", - authKeysPath); - ret = WS_BAD_FILE_E; - } + if (WFOPEN(NULL, &f, keysFilePath, "rb") != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to open %s", keysFilePath); + ret = WS_BAD_FILE_E; } if (ret == WSSHD_AUTH_SUCCESS) { lineBuf = (char*)WMALLOC(MAX_LINE_SZ, NULL, DYNTYPE_BUFFER); @@ -577,8 +590,7 @@ static int SearchForPubKey(const char* path, const char* authKeysFile, continue; /* commented out line */ } - rc = CheckAuthKeysLine(current, currentSz, pubKeyCtx->publicKey, - pubKeyCtx->publicKeySz); + rc = CheckAuthKeysLine(current, currentSz, key, keySz); if (rc == WSSHD_AUTH_SUCCESS) { foundKey = 1; break; @@ -589,6 +601,9 @@ static int SearchForPubKey(const char* path, const char* authKeysFile, } } + if (lineBuf != NULL) { + WFREE(lineBuf, NULL, DYNTYPE_BUFFER); + } if (f != WBADFILE) { WFCLOSE(NULL, f); } @@ -600,6 +615,30 @@ static int SearchForPubKey(const char* path, const char* authKeysFile, return ret; } + +static int SearchForPubKey(const char* path, const char* authKeysFile, + const WS_UserAuthData_PublicKey* pubKeyCtx) +{ + int ret = WSSHD_AUTH_SUCCESS; + char authKeysPath[MAX_PATH_SZ]; + int rc = 0; + + WMEMSET(authKeysPath, 0, sizeof(authKeysPath)); + rc = ResolveAuthKeysPath(path, authKeysFile, authKeysPath); + if (rc != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to resolve authorized keys" + " file path."); + ret = rc; + } + + if (ret == WSSHD_AUTH_SUCCESS) { + ret = SearchKeysFile(authKeysPath, pubKeyCtx->publicKey, + pubKeyCtx->publicKeySz); + } + + return ret; +} + #ifndef _WIN32 static int CheckUserUnix(const char* name) { int ret = WSSHD_AUTH_FAILURE; @@ -622,6 +661,244 @@ static int CheckUserUnix(const char* name) { return ret; } +#ifdef WOLFSSH_OSSH_CERTS +/* Return WSSHD_AUTH_SUCCESS when name appears in the certificate's principal + * list. A principal-less certificate is rejected, matching OpenSSH sshd + * (sshkey_cert_check_authority rejects a user cert with no principals). */ +#ifdef WOLFSSHD_UNIT_TEST +int OsshCertCheckPrincipal(const WS_UserAuthData_PublicKey* pubKeyCtx, + const char* name) +#else +static int OsshCertCheckPrincipal(const WS_UserAuthData_PublicKey* pubKeyCtx, + const char* name) +#endif +{ + const byte* p = pubKeyCtx->principals; + word32 sz = pubKeyCtx->principalsSz; + word32 idx = 0; + word32 nameSz; + word32 entSz; + + /* OpenSSH sshd rejects a principal-less user certificate; the empty-list + * "any principal" rule in PROTOCOL.certkeys is wire format, not policy. + * Fail closed so such a cert cannot authenticate as an arbitrary user. */ + if (p == NULL || sz == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] OpenSSH certificate lacks a principal list"); + return WSSHD_AUTH_FAILURE; + } + + nameSz = (word32)WSTRLEN(name); + while (idx + UINT32_SZ <= sz) { + ato32(p + idx, &entSz); + idx += UINT32_SZ; + if (entSz > sz - idx) { + break; /* malformed principals region */ + } + if (entSz == nameSz && XMEMCMP(p + idx, name, nameSz) == 0) { + return WSSHD_AUTH_SUCCESS; + } + idx += entSz; + } + + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] User %s is not a listed certificate principal", name); + return WSSHD_AUTH_FAILURE; +} + + +/* Return WSSHD_AUTH_SUCCESS when the current time is within the certificate's + * validity window. */ +#ifdef WOLFSSHD_UNIT_TEST +int OsshCertCheckValidity(const WS_UserAuthData_PublicKey* pubKeyCtx) +#else +static int OsshCertCheckValidity(const WS_UserAuthData_PublicKey* pubKeyCtx) +#endif +{ + word64 now = (word64)WTIME(NULL); + + if (now < pubKeyCtx->validAfter || now >= pubKeyCtx->validBefore) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] OpenSSH certificate is outside its validity window"); + return WSSHD_AUTH_FAILURE; + } + + return WSSHD_AUTH_SUCCESS; +} + + +/* Return 1 if the first 'bits' bits of a and b match, over 'len' bytes. */ +#ifdef WOLFSSHD_UNIT_TEST +int OsshPrefixMatch(const byte* a, const byte* b, int bits, int len) +#else +static int OsshPrefixMatch(const byte* a, const byte* b, int bits, int len) +#endif +{ + int fullBytes = bits / 8; + int remBits = bits % 8; + byte mask; + + if (bits < 0 || bits > len * 8) { + return 0; + } + if (fullBytes > 0 && XMEMCMP(a, b, fullBytes) != 0) { + return 0; + } + if (remBits != 0) { + mask = (byte)(0xFFu << (8 - remBits)); + if ((a[fullBytes] & mask) != (b[fullBytes] & mask)) { + return 0; + } + } + + return 1; +} + + +/* Return 1 if peerIp falls within a comma-separated CIDR entry in (list, + * listSz). IPv4/IPv6 only; negated ("!") entries fail closed (no match). + * Unix-only: uses POSIX inet_pton (Windows path does not enforce this yet). */ +#ifdef WOLFSSHD_UNIT_TEST +int OsshSourceAddrMatch(const byte* list, word32 listSz, + const char* peerIp) +#else +static int OsshSourceAddrMatch(const byte* list, word32 listSz, + const char* peerIp) +#endif +{ + byte peer[16]; + byte addr[16]; + int peerLen = 0; + int addrLen = 0; + int prefix; + word32 i = 0; + word32 entLen; + char ent[WOLFSSHD_CIDR_ENTRY_SZ]; + char* slash; + char* pp; + struct in_addr v4; + struct in6_addr v6; + + if (inet_pton(AF_INET, peerIp, &v4) == 1) { + XMEMCPY(peer, &v4, 4); + peerLen = 4; + } + else if (inet_pton(AF_INET6, peerIp, &v6) == 1) { + XMEMCPY(peer, &v6, 16); + peerLen = 16; + } + else { + return 0; + } + + while (i < listSz) { + entLen = 0; + while (i < listSz && list[i] != ',') { + if (entLen < (word32)sizeof(ent) - 1) { + ent[entLen] = (char)list[i]; + } + entLen++; /* count full length, even past the buffer */ + i++; + } + if (i < listSz) { + i++; /* skip comma */ + } + if (entLen == 0) { + continue; + } + if (entLen >= (word32)sizeof(ent)) { + continue; /* over-length entry, would not fit: skip (fail closed) */ + } + ent[entLen] = '\0'; + if (ent[0] == '!') { + /* OpenSSH allows negated entries; wolfSSHd does not yet, so deny + * the whole list (fail closed). Log the entry that caused it. */ + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Certificate source-address negation not supported: %s", + ent); + return 0; + } + + slash = WSTRCHR(ent, '/'); + if (slash == NULL) { + prefix = -1; /* no prefix: full-length match */ + } + else { + *slash = '\0'; + pp = slash + 1; + if (*pp == '\0') { + continue; /* empty prefix: malformed, skip (fail closed) */ + } + prefix = 0; + while (*pp >= '0' && *pp <= '9') { + prefix = prefix * 10 + (*pp - '0'); + if (prefix > 128) { + break; /* oversize; rejected by range check below */ + } + pp++; + } + if (*pp != '\0') { + continue; /* non-numeric prefix: malformed, skip */ + } + } + + if (inet_pton(AF_INET, ent, &v4) == 1) { + XMEMCPY(addr, &v4, 4); + addrLen = 4; + } + else if (inet_pton(AF_INET6, ent, &v6) == 1) { + XMEMCPY(addr, &v6, 16); + addrLen = 16; + } + else { + continue; /* malformed entry */ + } + + if (addrLen != peerLen) { + continue; /* address family mismatch */ + } + if (prefix < 0) { + prefix = addrLen * 8; /* no prefix given: full-length */ + } + else if (prefix > addrLen * 8) { + continue; /* prefix too large for this family: malformed, skip */ + } + if (OsshPrefixMatch(peer, addr, prefix, addrLen)) { + return 1; + } + } + + return 0; +} + + +/* Enforce the certificate's source-address restriction (if any) against the + * connection peer IP. Returns WSSHD_AUTH_SUCCESS when allowed. */ +static int OsshCertCheckSourceAddress( + const WS_UserAuthData_PublicKey* pubKeyCtx, const char* peerIp) +{ + if (pubKeyCtx->sourceAddress == NULL || pubKeyCtx->sourceAddressSz == 0) { + return WSSHD_AUTH_SUCCESS; /* no restriction */ + } + + if (peerIp == NULL || peerIp[0] == '\0') { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Cannot enforce certificate source-address: no peer IP"); + return WSSHD_AUTH_FAILURE; + } + + if (OsshSourceAddrMatch(pubKeyCtx->sourceAddress, + pubKeyCtx->sourceAddressSz, peerIp)) { + return WSSHD_AUTH_SUCCESS; + } + + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Peer %s not permitted by certificate source-address", peerIp); + return WSSHD_AUTH_FAILURE; +} +#endif /* WOLFSSH_OSSH_CERTS */ + + static int CheckPublicKeyUnix(const char* name, const WS_UserAuthData_PublicKey* pubKeyCtx, const char* usrCaKeysFile, @@ -633,63 +910,59 @@ static int CheckPublicKeyUnix(const char* name, #ifdef WOLFSSH_OSSH_CERTS if (pubKeyCtx->isOsshCert) { - int rc; - byte* caKey = NULL; - word32 caKeySz; - const byte* caKeyType = NULL; - word32 caKeyTypeSz; - byte fingerprint[WC_SHA256_DIGEST_SIZE]; - - if (pubKeyCtx->caKey == NULL || - pubKeyCtx->caKeySz != WC_SHA256_DIGEST_SIZE) { + /* caKey is the raw signing-CA blob from the certificate, identical to + * a decoded TrustedUserCAKeys line, so the trust check reuses the same + * scanner as authorized_keys. */ + if (pubKeyCtx->caKey == NULL || pubKeyCtx->caKeySz == 0) { ret = WS_FATAL_ERROR; } - if (ret == WSSHD_AUTH_SUCCESS) { - f = XFOPEN(usrCaKeysFile, "rb"); - if (f == XBADFILE) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to open %s", - usrCaKeysFile); - ret = WS_BAD_FILE_E; - } + if (ret == WSSHD_AUTH_SUCCESS && usrCaKeysFile == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] No TrustedUserCAKeys configured for certificate auth"); + ret = WSSHD_AUTH_FAILURE; } + + /* The requested user must be a real local account. */ if (ret == WSSHD_AUTH_SUCCESS) { - lineBuf = (char*)WMALLOC(MAX_LINE_SZ, NULL, DYNTYPE_BUFFER); - if (lineBuf == NULL) { - ret = WS_MEMORY_E; + errno = 0; + pwInfo = getpwnam((const char*)name); + if (pwInfo == NULL) { + if (errno != 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error calling getpwnam for user %s.", name); + } + ret = WS_FATAL_ERROR; } } - while (ret == WSSHD_AUTH_SUCCESS && - (current = XFGETS(lineBuf, MAX_LINE_SZ, f)) != NULL) { - currentSz = (word32)XSTRLEN(current); - /* remove leading spaces */ - while (currentSz > 0 && current[0] == ' ') { - currentSz = currentSz - 1; - current = current + 1; - } + /* Trust: the signing CA must be listed in TrustedUserCAKeys. */ + if (ret == WSSHD_AUTH_SUCCESS) { + ret = SearchKeysFile(usrCaKeysFile, pubKeyCtx->caKey, + pubKeyCtx->caKeySz); + } - if (currentSz <= 1) { - continue; /* empty line */ - } + /* Bind the certificate to the requested user via its principals. */ + if (ret == WSSHD_AUTH_SUCCESS) { + ret = OsshCertCheckPrincipal(pubKeyCtx, name); + } - if (current[0] == '#') { - continue; /* commented out line */ - } + /* Reject outside the certificate validity window. */ + if (ret == WSSHD_AUTH_SUCCESS) { + ret = OsshCertCheckValidity(pubKeyCtx); + } - rc = wolfSSH_ReadKey_buffer((const byte*)current, currentSz, - WOLFSSH_FORMAT_SSH, &caKey, &caKeySz, - &caKeyType, &caKeyTypeSz, NULL); - if (rc == WS_SUCCESS) { - 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; - break; - } - } + /* Enforce the source-address critical option against the peer IP. A + * NULL authCtx leaves the peer IP unknown; OsshCertCheckSourceAddress + * then fails closed if the certificate restricts the source address. */ + if (ret == WSSHD_AUTH_SUCCESS) { + ret = OsshCertCheckSourceAddress(pubKeyCtx, + (authCtx != NULL) ? authCtx->peerIp : NULL); } + + /* The force-command is stashed in UserAuthResult (after the signature + * verifies), not here: this callback runs before verification, so the + * command binds to the credential that actually authenticated. */ } else #endif /* WOLFSSH_OSSH_CERTS */ @@ -1032,6 +1305,18 @@ static int CheckPublicKeyWIN(const char* usr, wolfSSH_Log(WS_LOG_INFO, "[SSHD] Windows check public key"); +#ifdef WOLFSSH_OSSH_CERTS + /* OpenSSH certificate enforcement (CA trust, principal binding, validity + * window, source-address and force-command) lives only in the Unix path + * (CheckPublicKeyUnix). Until it is ported, fail closed so a certificate is + * never accepted on Windows on the strength of its reconstructed key. */ + if (pubKeyCtx->isOsshCert) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] OpenSSH certificate auth is not supported on Windows"); + return WSSHD_AUTH_FAILURE; + } +#endif + ret = SetupUserTokenWin(usr, pubKeyCtx,usrCaKeysFile, authCtx); /* after successful logon check the public key sent */ @@ -1603,6 +1888,9 @@ WOLFSSHD_AUTH* wolfSSHD_AuthCreateUser(void* heap, const WOLFSSHD_CONFIG* conf) if (auth != NULL) { int ret; + /* Zero first so optional members (e.g. certForcedCmd, peerIp) start in a + * known state; the fields below are then set explicitly. */ + WMEMSET(auth, 0, sizeof(WOLFSSHD_AUTH)); auth->heap = heap; auth->conf = conf; auth->attempts = WOLFSSHD_MAX_PASSWORD_ATTEMPTS; @@ -1656,12 +1944,70 @@ WOLFSSHD_AUTH* wolfSSHD_AuthCreateUser(void* heap, const WOLFSSHD_CONFIG* conf) int wolfSSHD_AuthFreeUser(WOLFSSHD_AUTH* auth) { if (auth != NULL) { + #ifdef WOLFSSH_OSSH_CERTS + if (auth->certForcedCmd != NULL) { + WFREE(auth->certForcedCmd, auth->heap, DYNTYPE_STRING); + auth->certForcedCmd = NULL; + } + #endif WFREE(auth, auth->heap, DYNTYPE_SSHD); } return WS_SUCCESS; } +#ifdef WOLFSSH_OSSH_CERTS +/* Record the connection peer IP for certificate source-address enforcement. */ +void wolfSSHD_AuthSetPeerIp(WOLFSSHD_AUTH* auth, const char* ip) +{ + if (auth != NULL && ip != NULL) { + WSTRNCPY(auth->peerIp, ip, sizeof(auth->peerIp) - 1); + auth->peerIp[sizeof(auth->peerIp) - 1] = '\0'; + } +} + + +/* Return the force-command from an authenticated OpenSSH certificate, or NULL + * when none was present. */ +const char* wolfSSHD_AuthGetForcedCmd(const WOLFSSHD_AUTH* auth) +{ + if (auth != NULL) { + return auth->certForcedCmd; + } + return NULL; +} + + +/* Store a copy of an authenticated OpenSSH certificate's force-command on the + * auth context, replacing any previous value. Called only after the user + * signature has verified, so the command is bound to the credential that + * authenticated. */ +int wolfSSHD_AuthSetCertForcedCmd(WOLFSSHD_AUTH* auth, const byte* cmd, + word32 cmdSz) +{ + char* copy = NULL; + + if (auth == NULL || cmd == NULL || cmdSz == 0) { + return WS_BAD_ARGUMENT; + } + + copy = (char*)WMALLOC(cmdSz + 1, auth->heap, DYNTYPE_STRING); + if (copy == NULL) { + return WS_MEMORY_E; + } + WMEMCPY(copy, cmd, cmdSz); + copy[cmdSz] = '\0'; + + if (auth->certForcedCmd != NULL) { + WFREE(auth->certForcedCmd, auth->heap, DYNTYPE_STRING); + } + auth->certForcedCmd = copy; + + return WS_SUCCESS; +} +#endif /* WOLFSSH_OSSH_CERTS */ + + /* return WS_SUCCESS on success */ int wolfSSHD_AuthRaisePermissions(WOLFSSHD_AUTH* auth) { diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 298ea4690..aac808aca 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -71,6 +71,12 @@ int wolfSSHD_AuthReducePermissionsUser(WOLFSSHD_AUTH* auth, WUID_T uid, int wolfSSHD_AuthSetGroups(const WOLFSSHD_AUTH* auth, const char* usr, WGID_T gid); long wolfSSHD_AuthGetGraceTime(const WOLFSSHD_AUTH* auth); +#ifdef WOLFSSH_OSSH_CERTS +void wolfSSHD_AuthSetPeerIp(WOLFSSHD_AUTH* auth, const char* ip); +const char* wolfSSHD_AuthGetForcedCmd(const WOLFSSHD_AUTH* auth); +int wolfSSHD_AuthSetCertForcedCmd(WOLFSSHD_AUTH* auth, const byte* cmd, + word32 cmdSz); +#endif WOLFSSHD_CONFIG* wolfSSHD_AuthGetUserConf(const WOLFSSHD_AUTH* auth, const char* usr, const char* host, const char* localAdr, word16* localPort, const char* RDomain, @@ -95,5 +101,12 @@ 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(WOLFSSH_OSSH_CERTS) && !defined(_WIN32) +int OsshPrefixMatch(const byte* a, const byte* b, int bits, int len); +int OsshSourceAddrMatch(const byte* list, word32 listSz, const char* peerIp); +int OsshCertCheckPrincipal(const WS_UserAuthData_PublicKey* pubKeyCtx, + const char* name); +int OsshCertCheckValidity(const WS_UserAuthData_PublicKey* pubKeyCtx); +#endif #endif #endif /* WOLFAUTH_H */ diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index de9aba64f..f6c09674f 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -176,6 +176,14 @@ else printf "Shutting down test wolfSSHd\n" stop_wolfsshd fi + + # OpenSSH certificate user-auth test (self-contained: starts its own + # wolfSSHd; skips when not built with --enable-ossh-certs). Runs the suite + # against the wolfSSH example client and, for interop, the system OpenSSH + # client when present. + if [ "$USING_LOCAL_HOST" == 1 ]; then + run_test "sshd_ossh_cert_test.sh" + fi fi printf "All tests ran, $TOTAL passed, $SKIPPED skipped\n" diff --git a/apps/wolfsshd/test/sshd_ossh_cert_test.sh b/apps/wolfsshd/test/sshd_ossh_cert_test.sh new file mode 100755 index 000000000..e86a01551 --- /dev/null +++ b/apps/wolfsshd/test/sshd_ossh_cert_test.sh @@ -0,0 +1,282 @@ +#!/bin/bash + +# OpenSSH certificate (user auth) test for wolfSSHd. +# +# Regenerates the OpenSSH user certificates for the login user via +# keys/renew-ossh-certs.sh (the certificate principal must match the login user, +# like the X.509 test which regenerates per-user via renewcerts.sh), starts +# wolfSSHd with TrustedUserCAKeys set to the signing CAs, and runs the same +# suite against two drivers: +# * the wolfSSH example client (self-contained, always run when built), and +# * the system OpenSSH "ssh" client (interop, run when ssh is present). +# Confirms a valid certificate is accepted and that an untrusted CA, a +# non-matching principal, an unknown critical option and a source-address +# mismatch are each rejected, and that force-command overrides the command. +# +# Requires: ssh-keygen and the wolfSSH client. Skips cleanly (77) when either is +# missing or wolfSSHd was not built with --enable-ossh-certs. The OpenSSH-client +# interop pass is skipped when "ssh" is unavailable. +# +# This is the gate for the CheckPublicKeyUnix OSSH orchestration (CA-trust -> +# principal -> validity -> source-address ordering); a deterministic unit-level +# test of that ordering is a deferred follow-up. +# +# On Windows, OpenSSH certificate auth is intentionally rejected outright +# (CheckPublicKeyWIN fails closed). That guard is compile-verified only (no +# Windows unit harness) and is a known untested edge; the cases here are Unix. + +set +m # quiet job-control "Terminated" notices when stopping the daemon + +PWD0=$(pwd) +cd ../../.. +ROOT=$(pwd) + +skip() { echo "$1"; cd "$PWD0"; exit 77; } + +# Only meaningful when wolfSSHd was built with OpenSSH certificate support. +grep -q "WOLFSSH_OSSH_CERTS" config.log 2>/dev/null || \ + skip "wolfSSHd not built with --enable-ossh-certs, skipping" + +WOLFSSHD="$ROOT/apps/wolfsshd/wolfsshd" +CLIENT="$ROOT/examples/client/client" +HOSTKEY="$ROOT/keys/server-key.pem" +PORT=22226 +LOGINUSER=${SUDO_USER:-$(whoami)} + +[ -x "$WOLFSSHD" ] || skip "wolfsshd not built, skipping OpenSSH cert test" +[ -x "$CLIENT" ] || skip "wolfSSH client not built, skipping OpenSSH cert test" +command -v ssh-keygen >/dev/null 2>&1 || \ + skip "ssh-keygen not found, skipping OpenSSH cert test" + +WORK=$(mktemp -d) +trap 'pkill -f "wolfsshd .*sshd_config_ossh" 2>/dev/null; rm -rf "$WORK"' EXIT + +# The suite may run under sudo (CI): this script runs as root while the daemon +# session runs as the login user. Let that user traverse $WORK and write the +# force-command marker via a world-writable marker dir. +chmod 711 "$WORK" +MARKERDIR="$WORK/markers" +mkdir -p "$MARKERDIR" +chmod 777 "$MARKERDIR" + +# Issue certificates bound to the login user (and the negatives). The +# force-command marker is placed under the per-run work dir, not a fixed, +# world-readable /tmp path. +( cd "$ROOT/keys" && OSSH_FORCED_MARKER="$MARKERDIR/forced_marker" \ + ./renew-ossh-certs.sh "$LOGINUSER" ) + +# Trust all three signing CAs (Ed25519, RSA, ECDSA) but not ossh-bad-ca. +cat "$ROOT/keys/ossh-ca.pub" "$ROOT/keys/ossh-ca-rsa.pub" \ + "$ROOT/keys/ossh-ca-ecdsa.pub" > "$WORK/trusted-cas.pub" + +cat > "$WORK/sshd_config_ossh" </dev/null 2>&1 +} + +# Connect with the system OpenSSH client. +connect_ssh() { # user-key cert remote-command + ssh -p $PORT -i "$1" -o CertificateFile="$2" \ + -o IdentitiesOnly=yes -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=publickey \ + -o BatchMode=yes -o ConnectTimeout=5 \ + "$LOGINUSER@127.0.0.1" "$3" >/dev/null 2>&1 +} + +# (re)start the daemon, drive the selected client, return its exit code. +attempt() { # user-key cert [remote-command] + pkill -f "wolfsshd .*sshd_config_ossh" 2>/dev/null + sleep 1 + "$WOLFSSHD" -D -f "$WORK/sshd_config_ossh" -E "$WORK/sshd.log" & + local wp=$! + disown "$wp" 2>/dev/null + sleep 1 + "connect_$DRIVER" "$1" "$2" "${3:-true}" + local rc=$? + kill $wp 2>/dev/null + return $rc +} + +check() { # label user-key cert expect(0=accept,1=reject) + attempt "$2" "$3" + local rc=$? + local got=1; [ $rc -eq 0 ] && got=0 + if [ $got -eq $4 ]; then + printf " %-20s %s\n" "$1" "PASS" + else + printf " %-20s %s (rc=%d)\n" "$1" "*** FAIL" "$rc" + FAIL=1 + fi +} + +force_command_check() { # user-key cert + local forced="$MARKERDIR/forced_marker" + local requested="$MARKERDIR/requested_marker" + rm -f "$forced" "$requested" + attempt "$1" "$2" "touch $requested" + local rc=$? + sleep 1 + if [ $rc -eq 0 ] && [ -f "$forced" ] && [ ! -f "$requested" ]; then + printf " %-20s %s\n" "force-command" "PASS" + else + printf " %-20s %s (rc=%d forced=%s requested=%s)\n" "force-command" \ + "*** FAIL" "$rc" \ + "$([ -f "$forced" ] && echo yes || echo no)" \ + "$([ -f "$requested" ] && echo yes || echo no)" + FAIL=1 + fi + rm -f "$forced" "$requested" +} + +ED="$ROOT/keys/ossh-user" +RSA="$ROOT/keys/ossh-user-rsa" +ECC="$ROOT/keys/ossh-user-ecdsa" +SFTP="$ROOT/examples/sftpclient/wolfsftp" +SCP="$ROOT/examples/scpclient/wolfscp" +SCPSRC="$WORK/scp_src.dat" +SCPDST="$WORK/scp_dst.dat" +echo "scp payload" > "$SCPSRC" + +# (re)start the daemon, leaving its PID in DPID. +start_daemon() { + pkill -f "wolfsshd .*sshd_config_ossh" 2>/dev/null + sleep 1 + "$WOLFSSHD" -D -f "$WORK/sshd_config_ossh" -E "$WORK/sshd.log" & + DPID=$! + disown "$DPID" 2>/dev/null + sleep 1 +} + +# Drive an SFTP session with the wolfSSH and system clients (echo a quit +# command so a granted session exits cleanly with no transfer). The clients +# report a non-zero exit code when the subsystem request is denied. +sftp_client() { # user-key cert + echo "exit" | "$SFTP" -u "$LOGINUSER" -i "$1" -j "$2" \ + -h 127.0.0.1 -p $PORT >/dev/null 2>&1 +} +sftp_ssh() { # user-key cert + echo "bye" | sftp -P $PORT -i "$1" -o CertificateFile="$2" \ + -o IdentitiesOnly=yes -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null -o BatchMode=yes \ + -o PreferredAuthentications=publickey -o ConnectTimeout=5 \ + "$LOGINUSER@127.0.0.1" >/dev/null 2>&1 +} + +sftp_available() { + if [ "$DRIVER" = client ]; then + [ -x "$SFTP" ] + else + command -v sftp >/dev/null 2>&1 + fi +} + +# Like check(), but drives an SFTP subsystem instead of a shell command. +sftp_check() { # label user-key cert expect(0=accept,1=reject) + start_daemon + "sftp_$DRIVER" "$2" "$3" + local rc=$? + kill $DPID 2>/dev/null + local got=1; [ $rc -eq 0 ] && got=0 + if [ $got -eq $4 ]; then + printf " %-20s %s\n" "$1" "PASS" + else + printf " %-20s %s (rc=%d)\n" "$1" "*** FAIL" "$rc" + FAIL=1 + fi +} + +# Drive a native SCP upload with the wolfSSH client. wolfscp masks its exit +# code and the destination path resolves differently across platforms, so +# verify the server's enforcement decision from its log, not from a file. +scp_check() { # label user-key cert expect(0=allowed,1=denied) + [ -x "$SCP" ] || { echo " ($1: wolfscp unavailable, skipping)"; return; } + : > "$WORK/sshd.log" + start_daemon + "$SCP" -u "$LOGINUSER" -i "$2" -j "$3" \ + -S"$SCPSRC:$SCPDST" -H 127.0.0.1 -p $PORT >/dev/null 2>&1 + kill $DPID 2>/dev/null + rm -f "$SCPDST" + local got=0 + grep -q "denying SCP" "$WORK/sshd.log" 2>/dev/null && got=1 + if [ $got -eq $4 ]; then + printf " %-20s %s\n" "$1" "PASS" + else + printf " %-20s %s\n" "$1" "*** FAIL" + FAIL=1 + fi +} + +run_suite() { # driver + DRIVER=$1 + echo "OpenSSH cert test via $DRIVER client (user=$LOGINUSER, port=$PORT):" + check "valid cert" "$ED" "$ROOT/keys/$LOGINUSER-ossh-cert.pub" 0 + check "RSA CA" "$ED" "$ROOT/keys/$LOGINUSER-ossh-rsaca-cert.pub" 0 + check "ECDSA CA" "$ED" "$ROOT/keys/$LOGINUSER-ossh-ecdsaca-cert.pub" 0 + check "RSA user key" "$RSA" "$ROOT/keys/$LOGINUSER-ossh-rsauser-cert.pub" 0 + check "ECDSA user key" "$ECC" "$ROOT/keys/$LOGINUSER-ossh-ecdsauser-cert.pub" 0 + check "untrusted CA" "$ED" "$ROOT/keys/$LOGINUSER-ossh-badca-cert.pub" 1 + check "wrong principal" "$ED" "$ROOT/keys/$LOGINUSER-ossh-wrongprincipal-cert.pub" 1 + check "empty principal" "$ED" "$ROOT/keys/$LOGINUSER-ossh-noprincipal-cert.pub" 1 + check "unknown crit opt" "$ED" "$ROOT/keys/$LOGINUSER-ossh-unkcrit-cert.pub" 1 + check "source-addr match" "$ED" "$ROOT/keys/$LOGINUSER-ossh-srcok-cert.pub" 0 + check "source-addr deny" "$ED" "$ROOT/keys/$LOGINUSER-ossh-srcbad-cert.pub" 1 + check "expired cert" "$ED" "$ROOT/keys/$LOGINUSER-ossh-expired-cert.pub" 1 + force_command_check "$ED" "$ROOT/keys/$LOGINUSER-ossh-forcecmd-cert.pub" + + # A force-command must not be bypassed by requesting the SFTP subsystem. + # "internal-sftp" still permits SFTP; any other force-command denies it. + if sftp_available; then + sftp_check "valid cert sftp" "$ED" \ + "$ROOT/keys/$LOGINUSER-ossh-cert.pub" 0 + sftp_check "forcecmd sftp deny" "$ED" \ + "$ROOT/keys/$LOGINUSER-ossh-forcecmd-cert.pub" 1 + sftp_check "internal-sftp sftp" "$ED" \ + "$ROOT/keys/$LOGINUSER-ossh-internalsftp-cert.pub" 0 + else + echo " (sftp $DRIVER client unavailable, skipping sftp cases)" + fi + + # Native SCP (an exec, not the SFTP subsystem) is denied under any + # force-command, including "internal-sftp". Driven by the wolfSSH client + # only; the system "scp" uses the SFTP protocol and is covered above. + if [ "$DRIVER" = client ]; then + scp_check "valid cert scp" "$ED" \ + "$ROOT/keys/$LOGINUSER-ossh-cert.pub" 0 + scp_check "forcecmd scp deny" "$ED" \ + "$ROOT/keys/$LOGINUSER-ossh-forcecmd-cert.pub" 1 + scp_check "internal-sftp scp" "$ED" \ + "$ROOT/keys/$LOGINUSER-ossh-internalsftp-cert.pub" 1 + fi +} + +# Primary, self-contained pass with the wolfSSH client. +run_suite client + +# Interop pass with the OpenSSH client, when available. +if command -v ssh >/dev/null 2>&1; then + run_suite ssh +else + echo "ssh not found, skipping OpenSSH-client interop pass" +fi + +cd "$PWD0" +if [ $FAIL -ne 0 ]; then + echo "OpenSSH certificate test FAILED" + exit 1 +fi +echo "OpenSSH certificate test passed" +exit 0 diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index ab51cc1e4..948516002 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -1604,6 +1604,238 @@ static int test_GetUserAuthTypes(void) return ret; } +#if defined(WOLFSSH_OSSH_CERTS) && !defined(_WIN32) +/* Direct coverage for the bit-level prefix matcher used by the certificate + * source-address check: byte boundaries, partial-byte masks, the zero-prefix + * match-all case, and the out-of-range guard. */ +static int test_OsshPrefixMatch(void) +{ + int ret = WS_SUCCESS; + int i; + static const byte a[4] = { 0xAB, 0xCD, 0xEF, 0x12 }; + static const byte b[4] = { 0xAB, 0xCD, 0x00, 0x00 }; + static const byte c[4] = { 0xAB, 0xCD, 0xE0, 0xFF }; + static const struct { + const char* desc; + const byte* other; + int bits; + int expected; + } vectors[] = { + { "zero prefix matches anything", b, 0, 1 }, + { "one full byte matches", b, 8, 1 }, + { "two full bytes match", b, 16, 1 }, + { "third full byte differs", b, 24, 0 }, + { "partial byte differs at bit 17", b, 17, 0 }, + { "full-length compare differs", b, 32, 0 }, + { "prefix beyond length rejected", b, 33, 0 }, + { "negative prefix rejected", b, -1, 0 }, + { "partial-byte mask matches", c, 20, 1 }, + { "partial-byte boundary differs", c, 24, 0 }, + }; + + for (i = 0; i < (int)(sizeof(vectors) / sizeof(vectors[0])); i++) { + Log(" Testing OsshPrefixMatch: %s.", vectors[i].desc); + if (OsshPrefixMatch(a, vectors[i].other, vectors[i].bits, 4) + != vectors[i].expected) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + break; + } + Log(" PASSED.\n"); + } + + return ret; +} + +/* Direct coverage for the comma-separated CIDR matcher: IPv4/IPv6 prefixes, + * exact (prefix-less) entries, the match-all /0 case, family mismatch, oversize + * and empty prefixes, negation (fail closed), and over-length entries. */ +static int test_OsshSourceAddrMatch(void) +{ + int ret = WS_SUCCESS; + int i; + word32 n; + char buf[160]; + /* 90-character entry, longer than the matcher's internal buffer: must be + * skipped rather than truncated-and-parsed. */ + static const char longEnt[] = + "123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890"; + static const struct { + const char* desc; + const char* list; + const char* peer; + int expected; + } vectors[] = { + { "ipv4 inside /24", "192.168.1.0/24", "192.168.1.50", 1 }, + { "ipv4 outside /24", "192.168.1.0/24", "192.168.2.50", 0 }, + { "ipv4 exact no-prefix match", "10.0.0.5", "10.0.0.5", 1 }, + { "ipv4 exact no-prefix miss", "10.0.0.5", "10.0.0.6", 0 }, + { "ipv4 /32 exact", "192.168.1.5/32", "192.168.1.5", 1 }, + { "ipv4 /0 matches all", "0.0.0.0/0", "8.8.8.8", 1 }, + { "second list entry matches", + "192.168.1.0/24,10.0.0.0/8", "10.1.2.3", 1 }, + { "ipv6 inside /32", "2001:db8::/32", "2001:db8::1", 1 }, + { "ipv6 outside /32", "2001:db8::/32", "2001:db9::1", 0 }, + { "family mismatch v4 vs v6", "192.168.1.0/24", "2001:db8::1", 0 }, + { "oversize prefix skipped", "192.168.1.0/33", "192.168.1.1", 0 }, + { "empty prefix skipped", "192.168.1.0/", "192.168.1.1", 0 }, + { "negation fails closed", "!192.168.1.0/24", "192.168.1.1", 0 }, + { "malformed entry skipped", "not-an-ip", "1.2.3.4", 0 }, + }; + + for (i = 0; i < (int)(sizeof(vectors) / sizeof(vectors[0])); i++) { + Log(" Testing OsshSourceAddrMatch: %s.", vectors[i].desc); + if (OsshSourceAddrMatch((const byte*)vectors[i].list, + (word32)WSTRLEN(vectors[i].list), vectors[i].peer) + != vectors[i].expected) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + break; + } + Log(" PASSED.\n"); + } + + if (ret == WS_SUCCESS) { + /* An over-length entry is skipped; a following valid entry still + * matches, proving parsing continues past the skip. */ + n = (word32)WSTRLEN(longEnt); + WMEMCPY(buf, longEnt, n); + WMEMCPY(buf + n, ",10.0.0.0/8", WSTRLEN(",10.0.0.0/8") + 1); + Log(" Testing OsshSourceAddrMatch: over-length entry skipped."); + if (OsshSourceAddrMatch((const byte*)buf, (word32)WSTRLEN(buf), + "10.1.1.1") != 1) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + return ret; +} + +/* Direct coverage for principal binding: empty list rejected (matching OpenSSH + * sshd), exact and multi-entry matches/misses, and a malformed length-prefixed + * blob. */ +static int test_OsshCertCheckPrincipal(void) +{ + int ret = WS_SUCCESS; + WS_UserAuthData_PublicKey pub; + /* SSH string list: uint32 length prefix + bytes, repeated. */ + static const byte one[] = { 0,0,0,4, 'f','r','e','d' }; + static const byte two[] = { 0,0,0,5, 'a','l','i','c','e', + 0,0,0,4, 'f','r','e','d' }; + static const byte bad[] = { 0,0,0,9, 'f','r','e','d' }; /* len > data */ + + WMEMSET(&pub, 0, sizeof(pub)); + pub.principals = NULL; + pub.principalsSz = 0; + Log(" Testing OsshCertCheckPrincipal: empty list rejected."); + if (OsshCertCheckPrincipal(&pub, "anyone") != WSSHD_AUTH_FAILURE) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + + if (ret == WS_SUCCESS) { + pub.principals = one; + pub.principalsSz = (word32)sizeof(one); + Log(" Testing OsshCertCheckPrincipal: exact match / miss."); + if (OsshCertCheckPrincipal(&pub, "fred") != WSSHD_AUTH_SUCCESS || + OsshCertCheckPrincipal(&pub, "bob") != WSSHD_AUTH_FAILURE) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + if (ret == WS_SUCCESS) { + pub.principals = two; + pub.principalsSz = (word32)sizeof(two); + Log(" Testing OsshCertCheckPrincipal: multi-entry match / miss."); + if (OsshCertCheckPrincipal(&pub, "fred") != WSSHD_AUTH_SUCCESS || + OsshCertCheckPrincipal(&pub, "alice") != WSSHD_AUTH_SUCCESS || + OsshCertCheckPrincipal(&pub, "carol") != WSSHD_AUTH_FAILURE) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + if (ret == WS_SUCCESS) { + pub.principals = bad; + pub.principalsSz = (word32)sizeof(bad); + Log(" Testing OsshCertCheckPrincipal: malformed blob rejected."); + if (OsshCertCheckPrincipal(&pub, "fred") != WSSHD_AUTH_FAILURE) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + return ret; +} + +/* Direct coverage for the validity window: inclusive lower bound, exclusive + * upper bound, expired, and not-yet-valid, relative to the current time. */ +static int test_OsshCertCheckValidity(void) +{ + int ret = WS_SUCCESS; + WS_UserAuthData_PublicKey pub; + word64 now = (word64)WTIME(NULL); + + WMEMSET(&pub, 0, sizeof(pub)); + pub.validAfter = now; /* inclusive lower bound */ + pub.validBefore = now + 1000; + Log(" Testing OsshCertCheckValidity: inside window."); + if (OsshCertCheckValidity(&pub) != WSSHD_AUTH_SUCCESS) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + + if (ret == WS_SUCCESS) { + pub.validAfter = 0; + pub.validBefore = now; /* exclusive upper bound */ + Log(" Testing OsshCertCheckValidity: expired at upper bound."); + if (OsshCertCheckValidity(&pub) != WSSHD_AUTH_FAILURE) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + if (ret == WS_SUCCESS) { + pub.validAfter = now + 1000; + pub.validBefore = now + 2000; + Log(" Testing OsshCertCheckValidity: not yet valid."); + if (OsshCertCheckValidity(&pub) != WSSHD_AUTH_FAILURE) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + return ret; +} +#endif /* WOLFSSH_OSSH_CERTS && !_WIN32 */ + const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_ParseConfigLine), @@ -1633,6 +1865,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(_WIN32) + TEST_DECL(test_OsshPrefixMatch), + TEST_DECL(test_OsshSourceAddrMatch), + TEST_DECL(test_OsshCertCheckPrincipal), + TEST_DECL(test_OsshCertCheckValidity), +#endif }; int main(int argc, char** argv) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 4389e2774..b7ce3f08f 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -402,11 +402,21 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } if (ret == WS_SUCCESS) { - #ifdef WOLFSSH_OPENSSH_CERTS + #ifdef WOLFSSH_OSSH_CERTS + /* OpenSSH host-certificate loading is not implemented yet. With + * X.509 host certs also built, log at debug and let that loader + * take over; otherwise fail so the operator can diagnose it. */ if (wolfSSH_CTX_UseOsshCert_buffer(*ctx, data, dataSz) < 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to use host certificate."); + #ifdef WOLFSSH_CERTS + wolfSSH_Log(WS_LOG_DEBUG, + "[SSHD] OpenSSH host certificate not loaded."); ret = WS_BAD_ARGUMENT; + #else + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostCertFile is set but OpenSSH host " + "certificates are not supported in this build."); + ret = WS_UNIMPLEMENTED_E; + #endif } #endif #ifdef WOLFSSH_CERTS @@ -455,10 +465,13 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, WOLFSSH_FORMAT_ASN1); } if (ret != WS_SUCCESS) { - #ifdef WOLFSSH_OPENSSH_CERTS + #ifdef WOLFSSH_OSSH_CERTS + /* The file is not X.509; when OpenSSH certificates are also + * built, fall through and let the OpenSSH user-auth path + * consume TrustedUserCAKeys instead of failing startup. */ wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Continuing on in case CA is openssh " - "style."); + "[SSHD] CA keys file is not X.509; using it as an " + "OpenSSH-style CA."); ret = WS_SUCCESS; #else wolfSSH_Log(WS_LOG_ERROR, @@ -516,6 +529,29 @@ static int SetupChroot(WOLFSSHD_CONFIG* usrConf) } #endif +/* Resolve the force-command in effect for this session. A force-command from + * an authenticated OpenSSH certificate takes precedence over a configured + * ForceCommand, per OpenSSH. Returns NULL when none is set. */ +static const char* GetEffectiveForcedCmd(WOLFSSHD_CONNECTION* conn, + WOLFSSHD_CONFIG* usrConf) +{ + const char* forcedCmd; +#ifdef WOLFSSH_OSSH_CERTS + const char* certCmd; +#endif + + forcedCmd = wolfSSHD_ConfigGetForcedCmd(usrConf); +#ifdef WOLFSSH_OSSH_CERTS + certCmd = wolfSSHD_AuthGetForcedCmd(conn->auth); + if (certCmd != NULL) { + forcedCmd = certCmd; + } +#else + (void)conn; +#endif + return forcedCmd; +} + #ifdef WOLFSSH_SCP static int SCP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, WPASSWD* pPasswd, WOLFSSHD_CONFIG* usrConf) @@ -1278,7 +1314,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, #endif byte shellBuffer[WOLFSSHD_SHELL_BUFFER_SZ]; byte channelBuffer[WOLFSSHD_SHELL_BUFFER_SZ]; - char* forcedCmd; + const char* forcedCmd; int windowFull = 0; /* Contains size of bytes from shellBuffer that did * not get passed on to wolfSSH yet. This happens * with window full errors or when rekeying. */ @@ -1299,7 +1335,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, stdinPipe[0] = -1; stdinPipe[1] = -1; - forcedCmd = wolfSSHD_ConfigGetForcedCmd(usrConf); + forcedCmd = GetEffectiveForcedCmd(conn, usrConf); ptyReq = SHELL_IsPty(ssh); if (ptyReq < 0) { @@ -1310,7 +1346,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, /* do not overwrite a forced command with 'exec' sub shell. Only set the * 'exec' command when no forced command is set */ if (forcedCmd == NULL) { - forcedCmd = (char*)subCmd; + forcedCmd = subCmd; } if (forcedCmd != NULL && WSTRCMP(forcedCmd, "internal-sftp") == 0) { @@ -1914,17 +1950,36 @@ static void alarmCatch(int signum) static int UserAuthResult(byte result, WS_UserAuthData* authData, void* userAuthResultCtx) { + int ret = WS_SUCCESS; + +#if !defined(WOLFSSH_OSSH_CERTS) || defined(_WIN32) (void)authData; (void)userAuthResultCtx; +#endif if (result == WOLFSSH_USERAUTH_SUCCESS) { #ifndef WIN32 /* @TODO alarm catch on windows */ alarm(0); #endif + #if defined(WOLFSSH_OSSH_CERTS) && !defined(_WIN32) + /* Auth (incl. the user signature) has verified, so stash the cert's + * force-command now, bound to the authenticated credential. Fail closed + * if the copy fails so the session cannot run unrestricted. */ + if (authData != NULL && + authData->type == WOLFSSH_USERAUTH_PUBLICKEY && + authData->sf.publicKey.isOsshCert && + authData->sf.publicKey.forceCommand != NULL && + authData->sf.publicKey.forceCommandSz > 0) { + ret = wolfSSHD_AuthSetCertForcedCmd( + (WOLFSSHD_AUTH*)userAuthResultCtx, + authData->sf.publicKey.forceCommand, + authData->sf.publicKey.forceCommandSz); + } + #endif } - return WS_SUCCESS; + return ret; } /* handle wolfSSH accept and directing to correct subsystem */ @@ -1960,6 +2015,13 @@ static void* HandleConnection(void* arg) wolfSSH_set_fd(ssh, conn->fd); wolfSSH_SetUserAuthCtx(ssh, conn->auth); + #if defined(WOLFSSH_OSSH_CERTS) && !defined(_WIN32) + /* Unix-only: each connection is a forked child with its own copy of the + * auth struct, so these per-connection cert writes never race. The + * Windows threaded path shares one struct and does not enforce certs. */ + wolfSSHD_AuthSetPeerIp(conn->auth, conn->ip); + wolfSSH_SetUserAuthResultCtx(ssh, conn->auth); + #endif /* set alarm for login grace time */ graceTime = wolfSSHD_AuthGetGraceTime(conn->auth); @@ -2022,6 +2084,7 @@ static void* HandleConnection(void* arg) WPASSWD* pPasswd = NULL; WOLFSSHD_CONFIG* usrConf; char* usr; + const char* forcedCmd = NULL; /* get configuration for user */ usr = wolfSSH_GetUsername(ssh); @@ -2045,7 +2108,6 @@ static void* HandleConnection(void* arg) #endif if (ret != WS_FATAL_ERROR) { - /* check for any forced command set for the user */ switch (wolfSSH_GetSessionType(ssh)) { case WOLFSSH_SESSION_SHELL: #ifdef WOLFSSH_SHELL @@ -2061,6 +2123,19 @@ static void* HandleConnection(void* arg) break; case WOLFSSH_SESSION_SUBSYSTEM: + /* A force-command overrides the requested subsystem. + * "internal-sftp" still permits SFTP; any other command + * denies file transfer (fail closed). */ + forcedCmd = GetEffectiveForcedCmd(conn, usrConf); + if (forcedCmd != NULL && + WSTRCMP(forcedCmd, "internal-sftp") != 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Force-command set for user %s; denying " + "subsystem request", wolfSSH_GetUsername(ssh)); + ret = WS_FATAL_ERROR; + break; + } + /* test for known subsystems */ switch (ret) { case WS_SFTP_COMPLETE: @@ -2074,7 +2149,18 @@ static void* HandleConnection(void* arg) case WS_SCP_INIT: #ifdef WOLFSSH_SCP - ret = SCP_Subsystem(conn, ssh, pPasswd, usrConf); + /* internal-sftp restricts the session to SFTP, so + * SCP is denied under any force-command. */ + if (forcedCmd != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Force-command set for user %s; " + "denying SCP", wolfSSH_GetUsername(ssh)); + ret = WS_FATAL_ERROR; + } + else { + ret = SCP_Subsystem(conn, ssh, pPasswd, + usrConf); + } #else err_sys("SCP not compiled in. Please use " "--enable-scp"); @@ -2105,7 +2191,18 @@ static void* HandleConnection(void* arg) /* SCP can be an exec type */ if (ret == WS_SCP_INIT) { #ifdef WOLFSSH_SCP - ret = SCP_Subsystem(conn, ssh, pPasswd, usrConf); + /* A force-command overrides the SCP request + * (fail closed). */ + forcedCmd = GetEffectiveForcedCmd(conn, usrConf); + if (forcedCmd != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Force-command set for user %s; denying " + "SCP", wolfSSH_GetUsername(ssh)); + ret = WS_FATAL_ERROR; + } + else { + ret = SCP_Subsystem(conn, ssh, pPasswd, usrConf); + } #else err_sys("SCP not compiled in. Please use " "--enable-scp"); @@ -2686,6 +2783,7 @@ static int StartSSHD(int argc, char** argv) "[SSHD] Failed to malloc memory for connection"); break; } + WMEMSET(conn, 0, sizeof(WOLFSSHD_CONNECTION)); conn->auth = auth; conn->listenFd = (int)listenFd; diff --git a/configure.ac b/configure.ac index 4aab10756..d32a23787 100644 --- a/configure.ac +++ b/configure.ac @@ -186,6 +186,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([ossh-certs], + [AS_HELP_STRING([--enable-ossh-certs],[Enable OpenSSH certificate user auth (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)])], @@ -216,7 +221,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_KEYBOARD_INTERACTIVE=yes; ENABLED_OSSH_CERTS=yes]) AS_IF([test "x$ENABLED_SSHD" = "xyes"], [ENABLED_SHELL=yes]) @@ -245,6 +250,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"], @@ -348,4 +355,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/ide/winvs/wolfssh/wolfssh.vcxproj b/ide/winvs/wolfssh/wolfssh.vcxproj index 8bff98e6d..0808a12e2 100644 --- a/ide/winvs/wolfssh/wolfssh.vcxproj +++ b/ide/winvs/wolfssh/wolfssh.vcxproj @@ -72,6 +72,7 @@ + diff --git a/keys/fred-ossh-badca-cert.pub b/keys/fred-ossh-badca-cert.pub new file mode 100644 index 000000000..6e28845eb --- /dev/null +++ b/keys/fred-ossh-badca-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIG8vUO/ZUk66DCTtAdRBuCOJBqQZpekTPSSviZQ5DHyRAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAACm9zc2gtYmFkY2EAAAAIAAAABGZyZWQAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAguUTbeNF5GPnyNmtpUiLcpWeBNI+i0G5JCP+CJ0XKwCYAAABTAAAAC3NzaC1lZDI1NTE5AAAAQGq2rjf9FupWO/NYXNH19rlh1vrIGlwfy8dqpQTeitdenfAMRdCeFMz0hvgiNHSCgtt4aNK4tUCZNe816IUWHQ0= wolfssh-ossh-test-user diff --git a/keys/fred-ossh-cert.pub b/keys/fred-ossh-cert.pub new file mode 100644 index 000000000..aec6e124c --- /dev/null +++ b/keys/fred-ossh-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGbRKcwIm8568QLY+JLywiMitG4qjVLHrXTCr+ffaYrwAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAACW9zc2gtZnJlZAAAAAgAAAAEZnJlZAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCNVm8OjNvnQeO8wYBPkRrAmFwOOL8R0OhReiTstCji8QAAAFMAAAALc3NoLWVkMjU1MTkAAABAQyv564o5MTDBw/phbPLUMAD7fso66n9EUsA7pXk/bgOA3XiMinWKkxa7Yf34Cw6E74jETJ7w80PheCmEDx6FAA== wolfssh-ossh-test-user diff --git a/keys/fred-ossh-ecdsaca-cert.pub b/keys/fred-ossh-ecdsaca-cert.pub new file mode 100644 index 000000000..dbfcd0fb3 --- /dev/null +++ b/keys/fred-ossh-ecdsaca-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICPiPFyw1oHBpkQFog5YfjLIH2WrS82sdO5lCa4M4vGHAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAADG9zc2gtZWNkc2FjYQAAAAgAAAAEZnJlZAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQQks+LVAhBWkbbztnp09gBfgKoOiCPHViSVz9/FF8xpH8Iz6AngzyJErGSsUbi75MVEh8R9go9BPcA4NzfYl75VAOZ28xyWj2zMPqwxD8uhNvv0fSLh3jsnlVHsO2VVjuUAAACDAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAABoAAAAMEGQ5JNxzmv2BTkM/qtNeaPlOelI/6zbqhlOSrJ8E/csTOkjjo9JuNsywBTLyORMUAAAADBRO/GK/bBiCMjQ73g1j05X1oMSCqqaOxsAFZ9EV0FDBuhPsl9im/KhYR2m8pSA9GY= wolfssh-ossh-test-user diff --git a/keys/fred-ossh-ecdsauser-cert.pub b/keys/fred-ossh-ecdsauser-cert.pub new file mode 100644 index 000000000..ba77b8706 --- /dev/null +++ b/keys/fred-ossh-ecdsauser-cert.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgXA+GAt/npCHzE7phWs7KAS9EEU3an4IYtJ5+jA6Yvf4AAAAIbmlzdHAyNTYAAABBBIFpLWrTCMCYLpf7cdn5k+ofTKgOKc3NClhlfwHXl0TUtXULQiqwVfzuDzLO8CmXb0nbvorA2dhONNsUUUR97toAAAAAAAAAAAAAAAEAAAAOb3NzaC1lY2RzYXVzZXIAAAAIAAAABGZyZWQAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgjVZvDozb50HjvMGAT5EawJhcDji/EdDoUXok7LQo4vEAAABTAAAAC3NzaC1lZDI1NTE5AAAAQMJPw7dBZw3Ao5jxXSjpzbRoJVC2nxoNMk9b42x+9ghWdhQ/VMm3Pv6SXbCTH9+HJzrQX32vLiPxijwvMlKgEg0= wolfssh-ossh-test-user-ecdsa diff --git a/keys/fred-ossh-expired-cert.pub b/keys/fred-ossh-expired-cert.pub new file mode 100644 index 000000000..980996c7d --- /dev/null +++ b/keys/fred-ossh-expired-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIJ212TzgVce7tPszIMHDRL+6ttJSLz50WJfH1Od21BdsAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAADG9zc2gtZXhwaXJlZAAAAAgAAAAEZnJlZAAAAABeC2JwAAAAAF4Ms/AAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCNVm8OjNvnQeO8wYBPkRrAmFwOOL8R0OhReiTstCji8QAAAFMAAAALc3NoLWVkMjU1MTkAAABAh3sN77c03Xha7TK4j6w3FKWtHXbSvjUoiTqZni3mFY+tSaHUAVFeafaq0Dr2fqmpHmHJ6ONmuK1vDBMgRXiUAw== wolfssh-ossh-test-user diff --git a/keys/fred-ossh-forcecmd-cert.pub b/keys/fred-ossh-forcecmd-cert.pub new file mode 100644 index 000000000..e57e30293 --- /dev/null +++ b/keys/fred-ossh-forcecmd-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIJjRMyGuqIuhsoJSma1wdUOJkWSx7OHnryYJ493j9Y1NAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAADW9zc2gtZm9yY2VjbWQAAAAIAAAABGZyZWQAAAAAAAAAAP//////////AAAAPwAAAA1mb3JjZS1jb21tYW5kAAAAKgAAACZ0b3VjaCAvdG1wL3dvbGZzc2hkX29zc2hfZm9yY2VkX21hcmtlcgAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgjVZvDozb50HjvMGAT5EawJhcDji/EdDoUXok7LQo4vEAAABTAAAAC3NzaC1lZDI1NTE5AAAAQNGmiYezz82E7iHh9XsYygW+K4lQzSIYLlb+kh9Ek3dP8IEPVxiUln3nunmvMAzPbUFYrmsGYTPQgLdelNGbzQE= wolfssh-ossh-test-user diff --git a/keys/fred-ossh-internalsftp-cert.pub b/keys/fred-ossh-internalsftp-cert.pub new file mode 100644 index 000000000..38e7a140d --- /dev/null +++ b/keys/fred-ossh-internalsftp-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICSTgWilZgCLPr6V4d+6NdtFYAPt3175XJg4z/Iexdn4AAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAAEW9zc2gtaW50ZXJuYWxzZnRwAAAACAAAAARmcmVkAAAAAAAAAAD//////////wAAACYAAAANZm9yY2UtY29tbWFuZAAAABEAAAANaW50ZXJuYWwtc2Z0cAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgjVZvDozb50HjvMGAT5EawJhcDji/EdDoUXok7LQo4vEAAABTAAAAC3NzaC1lZDI1NTE5AAAAQBkmhfVEvISuE8hwVFAjn3RKtIAfvf8bQN4gtLN09GL5sVTqwBrbWCO9R2JhHHPBq7Q1QuYWzBEiiUlql45XgQU= wolfssh-ossh-test-user diff --git a/keys/fred-ossh-noprincipal-cert.pub b/keys/fred-ossh-noprincipal-cert.pub new file mode 100644 index 000000000..728d264f3 --- /dev/null +++ b/keys/fred-ossh-noprincipal-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIBpSkGhjEOrUTa1+2xykJN+BrdPlPH3aLLpJwwMXPBXpAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAAEG9zc2gtbm9wcmluY2lwYWwAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAII1Wbw6M2+dB47zBgE+RGsCYXA44vxHQ6FF6JOy0KOLxAAAAUwAAAAtzc2gtZWQyNTUxOQAAAECRf1PVP0FdwJMWmfGch7oNEnxBoU3pgJfEdoVMz/VYfAai2bjMXezoNNfqFOTmYdaGY5Ma5XGbmGecNYynvoUO wolfssh-ossh-test-user diff --git a/keys/fred-ossh-rsaca-cert.pub b/keys/fred-ossh-rsaca-cert.pub new file mode 100644 index 000000000..6c96a4e9d --- /dev/null +++ b/keys/fred-ossh-rsaca-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIiKD+B67GgjPRWPzaXkVKUGN7jVUpPBn/3uHrsGrBYNAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAACm9zc2gtcnNhY2EAAAAIAAAABGZyZWQAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBAMJJl9/mjtQ5RD2wLhipf75NgUdFm2uwFY8oqiR2jsM/O96I58emcymz3+xkH3KLP4DuYYFE8k+4J6nUb3mPUIDnv/Jvn/iOq+gPplIU9QqAd5DbWgitpIWd7KfDvr6Wt3zs4KtYyTEOHqiBXaSRs0rf4eDNueTJR1Gj92rtlLT8NsDbAwZPMPaP6YyCSTdprNaUF2WdrOQ3CNll1RvJb0Ijy12WdFn/qPRofhN7piShNMMw32zTEjlWwnM/y9xW3hFmQxoToz2knJZ+GHYdOxQgu6LcXbfZGnB2XGSycFvOm02lIG9o99iVN5oK3OuK5/jDSxgg3GPFxqgsxF6yYuEAAAEUAAAADHJzYS1zaGEyLTUxMgAAAQBJIz2UFueYnWpSxqVwZfGUs6hpAAHUL9sltSEPzVhfts0OA5YzVHB0As62MUHGdrOVWxRan88Oh92sfQZft60vaBzDYj2oqzsD5MPc34966lRtIM4RMpyXA7BvrddlfpZXPe0JWhk0TwGswH70mz84E3PHO4YE030VJ8Y1udSOfM1UR70DZjzUNDuDodWvsljhH+XO1WI8t1O/m4CBBYV7075a+bXbhjAjmy1EmzXbGLUylYiGu4QwDvLCkp88/1gmzJyOXBfAATnbtyNiZ31tlaMItwoPpK+buEG8w0vJjduT6o4uq1tKO+ySchke2uc3TUzcy0UkOUQ6TtZl7zZU wolfssh-ossh-test-user diff --git a/keys/fred-ossh-rsauser-cert.pub b/keys/fred-ossh-rsauser-cert.pub new file mode 100644 index 000000000..04e1207f0 --- /dev/null +++ b/keys/fred-ossh-rsauser-cert.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAglXnitG2MY0k78yKoDW9vi0Gaj53I3aG8GNU66W92mgMAAAADAQABAAABAQDMhB3Xi7lqn3w7bYorlUAxBgdjAkmD/j1Sx+nRIqnGqk1YFNFmbl3iQ+JG3S13jwwi/CJyLXrdkVocsBOhHebMyyPHN0b7Cgl1Dvtxg+2Zh1K+dkzC2BhTmby/hj4NySnP04EgmY0w9f8uPCZHY0s0fq6nsMCJOo9/53JR38HJFAFabI344vY2kbUPFqBIuG45OfIQrJK3KeqQGTcGRzhOOgk5AkkytcIRrLeKbdBCCYYUcCFIEH/RL7fCSkbth54kJ8FWNEdJu5YYbpJY4YogITeFlZi4Z8sMbO2I53MBaWleMjjntuUtac0Yry9CXWz1fgImAYjCDB7h4wHF/nQ5AAAAAAAAAAAAAAABAAAADG9zc2gtcnNhdXNlcgAAAAgAAAAEZnJlZAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCNVm8OjNvnQeO8wYBPkRrAmFwOOL8R0OhReiTstCji8QAAAFMAAAALc3NoLWVkMjU1MTkAAABAlI5jGfjiE67+F1vaeZokqkTEFKuTP20qIWWK+/hQ6tdrGp0RWDiltcDmZ95mpFi5+w9zgriEBO/x7ojgjZnYAw== wolfssh-ossh-test-user-rsa diff --git a/keys/fred-ossh-srcbad-cert.pub b/keys/fred-ossh-srcbad-cert.pub new file mode 100644 index 000000000..5b4ae3b86 --- /dev/null +++ b/keys/fred-ossh-srcbad-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAID7yzOpLmjGaVRPqpoiqlA+bfGnTODZFBz3INGu9gi78AAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAAC29zc2gtc3JjYmFkAAAACAAAAARmcmVkAAAAAAAAAAD//////////wAAACQAAAAOc291cmNlLWFkZHJlc3MAAAAOAAAACjEwLjAuMC4wLzgAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAII1Wbw6M2+dB47zBgE+RGsCYXA44vxHQ6FF6JOy0KOLxAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDDT9/BfCKuet67zYs7ThnjHiJilcHsH6jqngNqP58AGfU1UWAlYpflOkrF/97ictI7Jx0feD10bLLU9txmggIB wolfssh-ossh-test-user diff --git a/keys/fred-ossh-srcok-cert.pub b/keys/fred-ossh-srcok-cert.pub new file mode 100644 index 000000000..f5e61848f --- /dev/null +++ b/keys/fred-ossh-srcok-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIBxskY6vA6I3shOacZQoc/CUI3Su5ZVgIN6kxQnlAhUHAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAACm9zc2gtc3Jjb2sAAAAIAAAABGZyZWQAAAAAAAAAAP//////////AAAALQAAAA5zb3VyY2UtYWRkcmVzcwAAABcAAAATMTI3LjAuMC4wLzgsOjoxLzEyOAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgjVZvDozb50HjvMGAT5EawJhcDji/EdDoUXok7LQo4vEAAABTAAAAC3NzaC1lZDI1NTE5AAAAQBGmAQFNFxSYAfbpUwgZP6grxeiAmCQGdBYzHgHXnbikDLIuaR4yFrnKhZap/yA7cGiR+sm09is+nFv1Gk2p/w8= wolfssh-ossh-test-user diff --git a/keys/fred-ossh-unkcrit-cert.pub b/keys/fred-ossh-unkcrit-cert.pub new file mode 100644 index 000000000..eeed2b258 --- /dev/null +++ b/keys/fred-ossh-unkcrit-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIG7Gw92ORYwYObtSKK5JLE5hv4HiN4br1tEUACD6gCIlAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAADG9zc2gtdW5rY3JpdAAAAAgAAAAEZnJlZAAAAAAAAAAA//////////8AAAAbAAAADm1hZGUtdXAtb3B0aW9uAAAABQAAAAF4AAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCNVm8OjNvnQeO8wYBPkRrAmFwOOL8R0OhReiTstCji8QAAAFMAAAALc3NoLWVkMjU1MTkAAABAWnMLgd+bTLTDadfmHZaV9Wqel6B6Fa5uksC9hdFyUTrr5xGLgWMHSlXFvLSdzRResXnn4Xu3eUzfuZWeuoIADw== wolfssh-ossh-test-user diff --git a/keys/fred-ossh-wrongprincipal-cert.pub b/keys/fred-ossh-wrongprincipal-cert.pub new file mode 100644 index 000000000..0fd9ba620 --- /dev/null +++ b/keys/fred-ossh-wrongprincipal-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIJwmuinIdoH//oj7ZexA7xBP1bMHS4m8ceUL/HoYcb7rAAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlYAAAAAAAAAAAAAAABAAAAE29zc2gtd3JvbmdwcmluY2lwYWwAAAAOAAAACm90aGVyLWZyZWQAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgjVZvDozb50HjvMGAT5EawJhcDji/EdDoUXok7LQo4vEAAABTAAAAC3NzaC1lZDI1NTE5AAAAQCX1AgKC7nZpZrn77voyKv2XaZHlZsFRoo4cE9NgE6JtihHyyCspMR9GvLy+HuopXAdWYcStx5uJcJyk8C6xfQM= wolfssh-ossh-test-user diff --git a/keys/include.am b/keys/include.am index fd82f10c5..403651179 100644 --- a/keys/include.am +++ b/keys/include.am @@ -24,5 +24,21 @@ EXTRA_DIST+= \ keys/server-key.pem keys/fred-key.der keys/fred-key.pem \ keys/id_ecdsa keys/id_ecdsa.pub keys/id_rsa keys/id_rsa.pub \ keys/renewcerts.sh keys/renewcerts.cnf \ - keys/server-key-ed25519.der keys/server-key-ed25519.pem + keys/server-key-ed25519.der keys/server-key-ed25519.pem \ + keys/renew-ossh-certs.sh \ + keys/ossh-ca keys/ossh-ca.pub \ + keys/ossh-ca-rsa keys/ossh-ca-rsa.pub \ + keys/ossh-ca-ecdsa keys/ossh-ca-ecdsa.pub \ + keys/ossh-bad-ca keys/ossh-bad-ca.pub \ + keys/ossh-user keys/ossh-user.pub \ + keys/ossh-user-rsa keys/ossh-user-rsa.pub \ + keys/ossh-user-ecdsa keys/ossh-user-ecdsa.pub \ + keys/fred-ossh-cert.pub keys/fred-ossh-badca-cert.pub \ + keys/fred-ossh-wrongprincipal-cert.pub keys/fred-ossh-unkcrit-cert.pub \ + keys/fred-ossh-forcecmd-cert.pub \ + keys/fred-ossh-srcok-cert.pub keys/fred-ossh-srcbad-cert.pub \ + keys/fred-ossh-rsaca-cert.pub keys/fred-ossh-ecdsaca-cert.pub \ + keys/fred-ossh-rsauser-cert.pub keys/fred-ossh-ecdsauser-cert.pub \ + keys/fred-ossh-expired-cert.pub keys/fred-ossh-internalsftp-cert.pub \ + keys/fred-ossh-noprincipal-cert.pub diff --git a/keys/ossh-bad-ca b/keys/ossh-bad-ca new file mode 100644 index 000000000..ef51593c3 --- /dev/null +++ b/keys/ossh-bad-ca @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACC5RNt40XkY+fI2a2lSItylZ4E0j6LQbkkI/4InRcrAJgAAAKC7N8UCuzfF +AgAAAAtzc2gtZWQyNTUxOQAAACC5RNt40XkY+fI2a2lSItylZ4E0j6LQbkkI/4InRcrAJg +AAAECIGOc0ssN0dLOUGYIHL3Gq0SWlA1/f7NFIvmRTrWs8ublE23jReRj58jZraVIi3KVn +gTSPotBuSQj/gidFysAmAAAAGXdvbGZzc2gtb3NzaC11bnRydXN0ZWQtY2EBAgME +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/ossh-bad-ca.pub b/keys/ossh-bad-ca.pub new file mode 100644 index 000000000..c3a531c09 --- /dev/null +++ b/keys/ossh-bad-ca.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILlE23jReRj58jZraVIi3KVngTSPotBuSQj/gidFysAm wolfssh-ossh-untrusted-ca diff --git a/keys/ossh-ca b/keys/ossh-ca new file mode 100644 index 000000000..fca9cfff5 --- /dev/null +++ b/keys/ossh-ca @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCNVm8OjNvnQeO8wYBPkRrAmFwOOL8R0OhReiTstCji8QAAAJjrhmfh64Zn +4QAAAAtzc2gtZWQyNTUxOQAAACCNVm8OjNvnQeO8wYBPkRrAmFwOOL8R0OhReiTstCji8Q +AAAEBEDvqEoIpI9s88eLS2yjDPypfsH6WRBVQi0KVXAjLJJI1Wbw6M2+dB47zBgE+RGsCY +XA44vxHQ6FF6JOy0KOLxAAAAFHdvbGZzc2gtb3NzaC10ZXN0LWNhAQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/ossh-ca-ecdsa b/keys/ossh-ca-ecdsa new file mode 100644 index 000000000..f0511ccbd --- /dev/null +++ b/keys/ossh-ca-ecdsa @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQQks+LVAhBWkbbztnp09gBfgKoOiCPH +ViSVz9/FF8xpH8Iz6AngzyJErGSsUbi75MVEh8R9go9BPcA4NzfYl75VAOZ28xyWj2zMPq +wxD8uhNvv0fSLh3jsnlVHsO2VVjuUAAADoX43CM1+NwjMAAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEEJLPi1QIQVpG287Z6dPYAX4CqDogjx1Yklc/fxRfMaR +/CM+gJ4M8iRKxkrFG4u+TFRIfEfYKPQT3AODc32Je+VQDmdvMclo9szD6sMQ/LoTb79H0i +4d47J5VR7DtlVY7lAAAAMHpQmKQbKHSZoQwN7VOIpXuqvvEJiXj4AdXzz9+QYFWwI57lYW +88ZIfmG2idvHM4rAAAABp3b2xmc3NoLW9zc2gtdGVzdC1jYS1lY2RzYQECAwQFBg== +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/ossh-ca-ecdsa.pub b/keys/ossh-ca-ecdsa.pub new file mode 100644 index 000000000..ec661ab4e --- /dev/null +++ b/keys/ossh-ca-ecdsa.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBCSz4tUCEFaRtvO2enT2AF+Aqg6II8dWJJXP38UXzGkfwjPoCeDPIkSsZKxRuLvkxUSHxH2Cj0E9wDg3N9iXvlUA5nbzHJaPbMw+rDEPy6E2+/R9IuHeOyeVUew7ZVWO5Q== wolfssh-ossh-test-ca-ecdsa diff --git a/keys/ossh-ca-rsa b/keys/ossh-ca-rsa new file mode 100644 index 000000000..1e719cb78 --- /dev/null +++ b/keys/ossh-ca-rsa @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAwkmX3+aO1DlEPbAuGKl/vk2BR0Wba7AVjyiqJHaOwz873ojnx6Zz +KbPf7GQfcos/gO5hgUTyT7gnqdRveY9QgOe/8m+f+I6r6A+mUhT1CoB3kNtaCK2khZ3sp8 +O+vpa3fOzgq1jJMQ4eqIFdpJGzSt/h4M255MlHUaP3au2UtPw2wNsDBk8w9o/pjIJJN2ms +1pQXZZ2s5DcI2WXVG8lvQiPLXZZ0Wf+o9Gh+E3umJKE0wzDfbNMSOVbCcz/L3FbeEWZDGh +OjPaScln4Ydh07FCC7otxdt9kacHZcZLJwW86bTaUgb2j32JU3mgrc64rn+MNLGCDcY8XG +qCzEXrJi4QAAA9BSpoHeUqaB3gAAAAdzc2gtcnNhAAABAQDCSZff5o7UOUQ9sC4YqX++TY +FHRZtrsBWPKKokdo7DPzveiOfHpnMps9/sZB9yiz+A7mGBRPJPuCep1G95j1CA57/yb5/4 +jqvoD6ZSFPUKgHeQ21oIraSFneynw76+lrd87OCrWMkxDh6ogV2kkbNK3+HgzbnkyUdRo/ +dq7ZS0/DbA2wMGTzD2j+mMgkk3aazWlBdlnazkNwjZZdUbyW9CI8tdlnRZ/6j0aH4Te6Yk +oTTDMN9s0xI5VsJzP8vcVt4RZkMaE6M9pJyWfhh2HTsUILui3F232RpwdlxksnBbzptNpS +BvaPfYlTeaCtzriuf4w0sYINxjxcaoLMResmLhAAAAAwEAAQAAAQEAmlul0jl3GJ023lvv +A6EG4MbrAxkGVhRbzJVeOAIJgo3mnvLeUvynWmaSbVlOss528ZRy2yVP7o481Oz2c/ms1/ +1HvvF4gx227nQgi+4ikOloFSpw6Zwrrgy+TLtz/C/6L8Jy7S9pCRRSv0WohGtNqxscdmJ8 +YoyXoQFIPSfskay9jee38rLO8z1LvzrxXAw+zFNuljIVtJSwBz0H+hy9vkfUaG4BQzwjK9 +oCHtzdkS7z3exbyM8C67m2laOO/2oXHdcBNb8XA26/nlgZReo5DS0q4JtcTSa1i01Mi7JS +gs7ML4tjRJuYxI1QbQTL0m9/zPLV+CKsXBUaqNTNGQs90QAAAIA+cev32GRYTgKqfFIqTZ +HIZPjbBcREMthzQ2GFc1Ar/hKLbBSCXawGR6tPkVi7uJvc4ruE/op7nbHKbpfAfAveKl5M +DNli4JmEYM7ypVmjj1j5r6DrcRUT1Wj2Lo6t9EvFbp0GX6HeubADsiMUWD5wAV04c7V2Ov +i9mPvoTzOq1gAAAIEA6OvJxKCL3oxExSIooGfHvTrWXwtLPfbiNQ17+6UrW1pxXtokeA6T +SQan9Vtvr0Dhg5/RaEc37wSNaef/NtKpzY1QOEJdxgC9iAtp/MZeGQ+4fbr5jng9Y4btDL +dyxfXiVmycEj8AU7L5qv2SoTw5GA09oIoxXNM9BEHlOyzccwsAAACBANWJ1h3qhMEgx1Jz +y/BnBKDrZkSlcfCOXoolQOrVSfbm0utmqUKOqBIWohSaZP4yDwCMLNDIDtZgArOzqBWVa+ +WLeGZO6Ht2f1f5ggHGlP4R1BzgevcrTCtoQP94tIN99/7Lo+V1qQ/rSTNQPGc6/rrBrada +lTnpYXO5QWP07TVDAAAAGHdvbGZzc2gtb3NzaC10ZXN0LWNhLXJzYQEC +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/ossh-ca-rsa.pub b/keys/ossh-ca-rsa.pub new file mode 100644 index 000000000..ef3e02bfe --- /dev/null +++ b/keys/ossh-ca-rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCSZff5o7UOUQ9sC4YqX++TYFHRZtrsBWPKKokdo7DPzveiOfHpnMps9/sZB9yiz+A7mGBRPJPuCep1G95j1CA57/yb5/4jqvoD6ZSFPUKgHeQ21oIraSFneynw76+lrd87OCrWMkxDh6ogV2kkbNK3+HgzbnkyUdRo/dq7ZS0/DbA2wMGTzD2j+mMgkk3aazWlBdlnazkNwjZZdUbyW9CI8tdlnRZ/6j0aH4Te6YkoTTDMN9s0xI5VsJzP8vcVt4RZkMaE6M9pJyWfhh2HTsUILui3F232RpwdlxksnBbzptNpSBvaPfYlTeaCtzriuf4w0sYINxjxcaoLMResmLh wolfssh-ossh-test-ca-rsa diff --git a/keys/ossh-ca.pub b/keys/ossh-ca.pub new file mode 100644 index 000000000..dc22ed3f2 --- /dev/null +++ b/keys/ossh-ca.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII1Wbw6M2+dB47zBgE+RGsCYXA44vxHQ6FF6JOy0KOLx wolfssh-ossh-test-ca diff --git a/keys/ossh-user b/keys/ossh-user new file mode 100644 index 000000000..e878ba2a5 --- /dev/null +++ b/keys/ossh-user @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDpws6wkSlpFW9Dsha738Pk1c5lR5SjikG0H+Og+TMZWAAAAKAPwTBPD8Ew +TwAAAAtzc2gtZWQyNTUxOQAAACDpws6wkSlpFW9Dsha738Pk1c5lR5SjikG0H+Og+TMZWA +AAAEB8y09bUTojcgNv0U99wDG7D2EjgOuiak5j9S/lbmgYi+nCzrCRKWkVb0OyFrvfw+TV +zmVHlKOKQbQf46D5MxlYAAAAFndvbGZzc2gtb3NzaC10ZXN0LXVzZXIBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/ossh-user-ecdsa b/keys/ossh-user-ecdsa new file mode 100644 index 000000000..157e13a63 --- /dev/null +++ b/keys/ossh-user-ecdsa @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSBaS1q0wjAmC6X+3HZ+ZPqH0yoDinN +zQpYZX8B15dE1LV1C0IqsFX87g8yzvApl29J276KwNnYTjTbFFFEfe7aAAAAuAo7P4cKOz ++HAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIFpLWrTCMCYLpf7 +cdn5k+ofTKgOKc3NClhlfwHXl0TUtXULQiqwVfzuDzLO8CmXb0nbvorA2dhONNsUUUR97t +oAAAAhALgOpG82Hg3/ITVApXuc+lANx9hCTopR0FzO7giA00blAAAAHHdvbGZzc2gtb3Nz +aC10ZXN0LXVzZXItZWNkc2EBAgM= +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/ossh-user-ecdsa.pub b/keys/ossh-user-ecdsa.pub new file mode 100644 index 000000000..8399d9f8b --- /dev/null +++ b/keys/ossh-user-ecdsa.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIFpLWrTCMCYLpf7cdn5k+ofTKgOKc3NClhlfwHXl0TUtXULQiqwVfzuDzLO8CmXb0nbvorA2dhONNsUUUR97to= wolfssh-ossh-test-user-ecdsa diff --git a/keys/ossh-user-rsa b/keys/ossh-user-rsa new file mode 100644 index 000000000..32f4afa65 --- /dev/null +++ b/keys/ossh-user-rsa @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAzIQd14u5ap98O22KK5VAMQYHYwJJg/49Usfp0SKpxqpNWBTRZm5d +4kPiRt0td48MIvwici163ZFaHLAToR3mzMsjxzdG+woJdQ77cYPtmYdSvnZMwtgYU5m8v4 +Y+Dckpz9OBIJmNMPX/LjwmR2NLNH6up7DAiTqPf+dyUd/ByRQBWmyN+OL2NpG1DxagSLhu +OTnyEKyStynqkBk3Bkc4TjoJOQJJMrXCEay3im3QQgmGFHAhSBB/0S+3wkpG7YeeJCfBVj +RHSbuWGG6SWOGKICE3hZWYuGfLDGztiOdzAWlpXjI457blLWnNGK8vQl1s9X4CJgGIwgwe +4eMBxf50OQAAA9Cums6/rprOvwAAAAdzc2gtcnNhAAABAQDMhB3Xi7lqn3w7bYorlUAxBg +djAkmD/j1Sx+nRIqnGqk1YFNFmbl3iQ+JG3S13jwwi/CJyLXrdkVocsBOhHebMyyPHN0b7 +Cgl1Dvtxg+2Zh1K+dkzC2BhTmby/hj4NySnP04EgmY0w9f8uPCZHY0s0fq6nsMCJOo9/53 +JR38HJFAFabI344vY2kbUPFqBIuG45OfIQrJK3KeqQGTcGRzhOOgk5AkkytcIRrLeKbdBC +CYYUcCFIEH/RL7fCSkbth54kJ8FWNEdJu5YYbpJY4YogITeFlZi4Z8sMbO2I53MBaWleMj +jntuUtac0Yry9CXWz1fgImAYjCDB7h4wHF/nQ5AAAAAwEAAQAAAQBXrUwd3AjhbP4VfCCA +Drw2SB9ikthxfc1Mb+gNgI7IXLpLyKD9CNO27ONU/f1ABFNvrCgYSuchle7L3bCMogUQRw +ZPoaMMfIERbhrdz3FNIHaYsJ636WyEaqRAd4yi3FrQfhwdnbaqBswfRioi8K6NEsJNobjp +G/HpI5AfCY5KZUQpl+azgcnfFjFbT6ttynEA+53O3RcZlcDbaTZETAvXuumil5O2mDsk6p +htoI0DAu6twjTjRqXf+UCi9fdST/iVMGqV850EegsGbEF7DaUYohpA/oe6yiE98L0s1Llt +IG+LaDWeMBTQsM5XnsTMRnKMXrfmVFzBSnbvZjRQH8wBAAAAgQDfqZnInu7mLsTkm5dWUq +HcWRN6oQwdpmcVJ1be+SKD7uzHiuCvtoIqp/dluuAl7gi0xXizd3n7r4ZkzQbAQrhjurMk +E0g+JAIZX00pA2Gf6cWhSzuFdulvHJZYmdXKeXivbhuodUYNoTOqVYdcRk2dyiHxTaPZaa +GhmPwEQLJDdAAAAIEA+t9dxjDQzVOFt/xthPAYERBnBjKbgoP1W+pdgdhseF3WBT2KU6+e +4mhUrnqTEf851QbW4tXos1RS0Ku51hOFhCA9QenAG31dVRCDbXEJnG6mFuoevAX3ur5U0Q +DDDn65aNKp+eVBjYYSVXuZM2UUUHFPNhFh4gEA7U+L6L4qGt0AAACBANCyM162NSwWEkDZ +gM76P91Y0Xl5z/0Kj0ljdGc01MVxyXhXmB7JP9Lbu8byLZky69AZZjHbvr/jy7HD1+2Roc +ieqJrF+5RAyWlDeoaD/PTs5WEENE3jQlK1utpd/iPDw5FZKlnGolzlGomdQgs3kgPaVKmh +DKvOGUKJ6SQyzIMNAAAAGndvbGZzc2gtb3NzaC10ZXN0LXVzZXItcnNh +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/ossh-user-rsa.pub b/keys/ossh-user-rsa.pub new file mode 100644 index 000000000..691d31cd3 --- /dev/null +++ b/keys/ossh-user-rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMhB3Xi7lqn3w7bYorlUAxBgdjAkmD/j1Sx+nRIqnGqk1YFNFmbl3iQ+JG3S13jwwi/CJyLXrdkVocsBOhHebMyyPHN0b7Cgl1Dvtxg+2Zh1K+dkzC2BhTmby/hj4NySnP04EgmY0w9f8uPCZHY0s0fq6nsMCJOo9/53JR38HJFAFabI344vY2kbUPFqBIuG45OfIQrJK3KeqQGTcGRzhOOgk5AkkytcIRrLeKbdBCCYYUcCFIEH/RL7fCSkbth54kJ8FWNEdJu5YYbpJY4YogITeFlZi4Z8sMbO2I53MBaWleMjjntuUtac0Yry9CXWz1fgImAYjCDB7h4wHF/nQ5 wolfssh-ossh-test-user-rsa diff --git a/keys/ossh-user.pub b/keys/ossh-user.pub new file mode 100644 index 000000000..f6dc68da0 --- /dev/null +++ b/keys/ossh-user.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOnCzrCRKWkVb0OyFrvfw+TVzmVHlKOKQbQf46D5MxlY wolfssh-ossh-test-user diff --git a/keys/renew-ossh-certs.sh b/keys/renew-ossh-certs.sh new file mode 100755 index 000000000..1744f7809 --- /dev/null +++ b/keys/renew-ossh-certs.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +# Regenerate the OpenSSH ("*-cert-v01@openssh.com") user certificates used by +# the wolfSSHd OpenSSH certificate test. Mirrors renewcerts.sh: the signing CA, +# an untrusted CA, and the user key are committed and reused; only the +# certificates are (re)issued, bound to the requested principal (default fred). +# +# The certificate principal must match the login user at test time, so the test +# harness calls this with the current user. Certificates are issued +# non-expiring (-V always:forever) so committed baselines do not go stale. +# +# Usage: ./renew-ossh-certs.sh [user] + +set -e + +USER_NAME=${1:-fred} + +# Path the force-command certificate's command writes to. Overridable so the +# test can point it at a per-run temp dir instead of a fixed, shared location; +# the committed baseline uses a stable default. +OSSH_FORCED_MARKER="${OSSH_FORCED_MARKER:-/tmp/wolfsshd_ossh_forced_marker}" + +if ! command -v ssh-keygen >/dev/null 2>&1; then + echo "ssh-keygen not found, cannot renew OpenSSH certificates" + exit 1 +fi + +# Committed key material (created on first run, reused afterwards). A CA is +# generated per algorithm so every verification path (Ed25519/RSA/ECDSA) is +# exercised; ossh-bad-ca is an untrusted CA for the negative test. +[ -f ossh-ca ] || ssh-keygen -q -t ed25519 -f ossh-ca -N "" \ + -C "wolfssh-ossh-test-ca" +[ -f ossh-ca-rsa ] || ssh-keygen -q -t rsa -b 2048 -f ossh-ca-rsa -N "" \ + -C "wolfssh-ossh-test-ca-rsa" +[ -f ossh-ca-ecdsa ] || ssh-keygen -q -t ecdsa -b 384 -f ossh-ca-ecdsa -N "" \ + -C "wolfssh-ossh-test-ca-ecdsa" +[ -f ossh-bad-ca ] || ssh-keygen -q -t ed25519 -f ossh-bad-ca -N "" \ + -C "wolfssh-ossh-untrusted-ca" +[ -f ossh-user ] || ssh-keygen -q -t ed25519 -f ossh-user -N "" \ + -C "wolfssh-ossh-test-user" +[ -f ossh-user-rsa ] || ssh-keygen -q -t rsa -b 2048 -f ossh-user-rsa -N "" \ + -C "wolfssh-ossh-test-user-rsa" +[ -f ossh-user-ecdsa ] || ssh-keygen -q -t ecdsa -b 256 -f ossh-user-ecdsa \ + -N "" -C "wolfssh-ossh-test-user-ecdsa" + +# ssh-keygen (signing below) and the OpenSSH client (-i at test time) both +# refuse private keys with group/world-readable permissions. git does not track +# the rw bits, so committed keys can check out as 0644; tighten them. +chmod 600 ossh-ca ossh-ca-rsa ossh-ca-ecdsa ossh-bad-ca \ + ossh-user ossh-user-rsa ossh-user-ecdsa + +# gen_cert_u [opts...] +# ssh-keygen writes -cert.pub from a copy of the user public key. +gen_cert_u() { + out_base=$1; user_pub=$2; ca=$3; key_id=$4; principal=$5 + shift 5 + cp "$user_pub" "$out_base.pub" + ssh-keygen -q -s "$ca" -I "$key_id" -n "$principal" -V always:forever \ + "$@" "$out_base.pub" + rm -f "$out_base.pub" +} + +# gen_cert [opts...] (Ed25519 user key) +gen_cert() { + out_base=$1; ca=$2; key_id=$3; principal=$4 + shift 4 + gen_cert_u "$out_base" ossh-user.pub "$ca" "$key_id" "$principal" "$@" +} + +gen_cert "$USER_NAME-ossh" ossh-ca "ossh-$USER_NAME" \ + "$USER_NAME" +gen_cert "$USER_NAME-ossh-badca" ossh-bad-ca "ossh-badca" \ + "$USER_NAME" +gen_cert "$USER_NAME-ossh-wrongprincipal" ossh-ca "ossh-wrongprincipal" \ + "other-$USER_NAME" +# A certificate with no principals (signed without -n). OpenSSH sshd, and now +# wolfSSHd, reject a principal-less user certificate, so this must not log in. +cp ossh-user.pub "$USER_NAME-ossh-noprincipal.pub" +ssh-keygen -q -s ossh-ca -I "ossh-noprincipal" -V always:forever \ + "$USER_NAME-ossh-noprincipal.pub" +rm -f "$USER_NAME-ossh-noprincipal.pub" +# Certificates signed by an RSA and an ECDSA (P-384) CA exercise the RSA and +# ECDSA CA-signature verification paths. The user key stays Ed25519, so the +# ECDSA case also covers selecting the digest from the CA curve, not the user +# key type. +gen_cert "$USER_NAME-ossh-rsaca" ossh-ca-rsa "ossh-rsaca" \ + "$USER_NAME" +gen_cert "$USER_NAME-ossh-ecdsaca" ossh-ca-ecdsa "ossh-ecdsaca" \ + "$USER_NAME" +# Critical options: an unrecognized one must be rejected; force-command and +# source-address are recognized and enforced. +gen_cert "$USER_NAME-ossh-unkcrit" ossh-ca "ossh-unkcrit" \ + "$USER_NAME" -O critical:made-up-option=x +gen_cert "$USER_NAME-ossh-forcecmd" ossh-ca "ossh-forcecmd" \ + "$USER_NAME" -O force-command="touch $OSSH_FORCED_MARKER" +# force-command "internal-sftp" restricts the session to SFTP: shell/exec/SCP +# are denied, but an SFTP subsystem request is still served. +gen_cert "$USER_NAME-ossh-internalsftp" ossh-ca "ossh-internalsftp" \ + "$USER_NAME" -O force-command="internal-sftp" +gen_cert "$USER_NAME-ossh-srcok" ossh-ca "ossh-srcok" \ + "$USER_NAME" -O source-address="127.0.0.0/8,::1/128" +gen_cert "$USER_NAME-ossh-srcbad" ossh-ca "ossh-srcbad" \ + "$USER_NAME" -O source-address="10.0.0.0/8" +# A fixed validity window entirely in the past is permanently expired (so the +# committed baseline does not go stale), exercising the daemon's validity check +# end-to-end through parse -> reconstruct -> CheckPublicKeyUnix. +gen_cert "$USER_NAME-ossh-expired" ossh-ca "ossh-expired" \ + "$USER_NAME" -V 20200101:20200102 +# Certificates over RSA and ECDSA user keys, to exercise a client offering +# those user-key types (the client signs with the certified RSA/ECDSA key). +gen_cert_u "$USER_NAME-ossh-rsauser" ossh-user-rsa.pub ossh-ca \ + "ossh-rsauser" "$USER_NAME" +gen_cert_u "$USER_NAME-ossh-ecdsauser" ossh-user-ecdsa.pub ossh-ca \ + "ossh-ecdsauser" "$USER_NAME" + +exit 0 diff --git a/src/include.am b/src/include.am index 88e535610..b08131136 100644 --- a/src/include.am +++ b/src/include.am @@ -50,3 +50,8 @@ if BUILD_CERTS src_libwolfssh_la_SOURCES += src/certman.c src_libwolfssh_test_la_SOURCES += src/certman.c endif + +# ossh.c holds the OpenSSH binary key-format decoders (always built) and, gated +# by WOLFSSH_OSSH_CERTS, the OpenSSH certificate code. +src_libwolfssh_la_SOURCES += src/ossh.c +src_libwolfssh_test_la_SOURCES += src/ossh.c diff --git a/src/internal.c b/src/internal.c index dfb92d728..f673cd684 100644 --- a/src/internal.c +++ b/src/internal.c @@ -34,6 +34,9 @@ #include #include #include +#ifdef WOLFSSH_OSSH_CERTS + #include +#endif #include #include #ifndef WOLFSSH_NO_DH @@ -972,6 +975,16 @@ static const char cannedKexAlgoNames[] = #ifndef WOLFSSH_NO_ED25519 static const char cannedKeyAlgoEd25519Name[] = "ssh-ed25519"; #endif +#ifdef WOLFSSH_OSSH_CERTS +#ifndef WOLFSSH_NO_RSA_SHA2_256 + static const char cannedKeyAlgoOsshRsaSha2_256CertName[] = + "rsa-sha2-256-cert-v01@openssh.com"; +#endif +#ifndef WOLFSSH_NO_RSA_SHA2_512 + static const char cannedKeyAlgoOsshRsaSha2_512CertName[] = + "rsa-sha2-512-cert-v01@openssh.com"; +#endif +#endif /* WOLFSSH_OSSH_CERTS */ static const char cannedKeyAlgoNames[] = #ifndef WOLFSSH_NO_ED25519 @@ -1000,11 +1013,73 @@ static const char cannedKeyAlgoNames[] = "x509v3-ssh-rsa," #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ #endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_OSSH_CERTS + #ifndef WOLFSSH_NO_ED25519 + "ssh-ed25519-cert-v01@openssh.com," + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + "ecdsa-sha2-nistp256-cert-v01@openssh.com," + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + "ecdsa-sha2-nistp384-cert-v01@openssh.com," + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + "ecdsa-sha2-nistp521-cert-v01@openssh.com," + #endif + #ifndef WOLFSSH_NO_RSA_SHA2_256 + "ssh-rsa-cert-v01@openssh.com," + "rsa-sha2-256-cert-v01@openssh.com," + #endif + #ifndef WOLFSSH_NO_RSA_SHA2_512 + "rsa-sha2-512-cert-v01@openssh.com," + #endif +#endif /* WOLFSSH_OSSH_CERTS */ #ifdef WOLFSSH_NO_SHA1_SOFT_DISABLE "ssh-rsa," #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ ""; +#ifdef WOLFSSH_OSSH_CERTS +/* Like cannedKeyAlgoNames but without the OpenSSH certificate + * ("*-cert-v01@openssh.com") names: host-cert verification is unimplemented, so + * a client must not advertise them as host keys. Keep plain/X.509 in sync. */ +static const char cannedKeyAlgoNamesHostKey[] = +#ifndef WOLFSSH_NO_ED25519 + "ssh-ed25519," +#endif /* WOLFSSH_NO_ED25519 */ +#ifndef WOLFSSH_NO_RSA_SHA2_256 + "rsa-sha2-256," +#endif/* WOLFSSH_NO_RSA_SHA2_256 */ +#ifndef WOLFSSH_NO_RSA_SHA2_512 + "rsa-sha2-512," +#endif /* WOLFSSH_NO_RSA_SHA2_512 */ +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + "ecdsa-sha2-nistp256," +#endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + "ecdsa-sha2-nistp384," +#endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP384 */ +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + "ecdsa-sha2-nistp521," +#endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP521 */ +#ifdef WOLFSSH_CERTS + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + "x509v3-ecdsa-sha2-nistp256," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ + #ifdef WOLFSSH_NO_SHA1_SOFT_DISABLE + "x509v3-ssh-rsa," + #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ +#endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_NO_SHA1_SOFT_DISABLE + "ssh-rsa," +#endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ + ""; +#else +/* No OpenSSH certificate names are present, so the host-key list is identical + * to cannedKeyAlgoNames. */ +#define cannedKeyAlgoNamesHostKey cannedKeyAlgoNames +#endif /* WOLFSSH_OSSH_CERTS */ + static const char cannedEncAlgoNames[] = #if !defined(WOLFSSH_NO_AES_GCM) "aes256-gcm@openssh.com," @@ -1081,7 +1156,7 @@ WOLFSSH_CTX* CtxInit(WOLFSSH_CTX* ctx, byte side, void* heap) ctx->sshProtoIdStr = sshProtoIdStr; ctx->algoListKex = cannedKexAlgoNames; if (side == WOLFSSH_ENDPOINT_CLIENT) { - ctx->algoListKey = cannedKeyAlgoNames; + ctx->algoListKey = cannedKeyAlgoNamesHostKey; } ctx->algoListCipher = cannedEncAlgoNames; ctx->algoListMac = cannedMacAlgoNames; @@ -1612,474 +1687,8 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, } -#ifndef WOLFSSH_NO_RSA - -#if (LIBWOLFSSL_VERSION_HEX > WOLFSSL_V5_7_0) && !defined(HAVE_FIPS) -/* - * The function wc_RsaPrivateKeyDecodeRaw() is available - * from wolfSSL after v5.7.0. - */ - -/* - * Utility for GetOpenSshKey() to read in RSA keys. - */ -static int GetOpenSshKeyRsa(RsaKey* key, - const byte* buf, word32 len, word32* idx) -{ - const byte *n, *e, *d, *u, *p, *q; - word32 nSz, eSz, dSz, uSz, pSz, qSz; - int ret; - - ret = wc_InitRsaKey(key, NULL); - if (ret == WS_SUCCESS) - ret = GetMpint(&nSz, &n, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpint(&eSz, &e, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpint(&dSz, &d, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpint(&uSz, &u, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpint(&pSz, &p, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpint(&qSz, &q, buf, len, idx); - if (ret == WS_SUCCESS) - ret = wc_RsaPrivateKeyDecodeRaw(n, nSz, e, eSz, d, dSz, - u, uSz, p, pSz, q, qSz, NULL, 0, NULL, 0, key); - - if (ret != WS_SUCCESS) - ret = WS_RSA_E; - - return ret; -} - -#else /* LIBWOLFSSL_VERSION_HEX > WOLFSSL_V5_7_0 */ - -#include - -/* - * Utility function to read an Mpint from the stream directly into a mp_int. - * The RsaKey members u, dP, and dQ do not exist when wolfCrypt is built - * with RSA_LOW_MEM. (That mode of wolfCrypt isn't using the extra values - * for the Chinese Remainder Theorem.) - */ -static int GetMpintToMp(mp_int* mp, - const byte* buf, word32 len, word32* idx) -{ - const byte* val = NULL; - word32 valSz = 0; - int ret; - - ret = GetMpint(&valSz, &val, buf, len, idx); - if (ret == WS_SUCCESS) - ret = mp_read_unsigned_bin(mp, val, valSz); - - return ret; -} - - -#ifndef RSA_LOW_MEM -/* - * For the given RSA key, calculate d mod(p-1) and d mod(q-1). - * wolfCrypt's RSA code expects them, but the OpenSSH format key - * doesn't store them. - */ -static int CalcRsaDX(RsaKey* key) -{ - mp_int m; - int ret; - - ret = mp_init(&m); - if (ret == MP_OKAY) { - ret = mp_sub_d(&key->p, 1, &m); - if (ret == MP_OKAY) - ret = mp_mod(&key->d, &m, &key->dP); - if (ret == MP_OKAY) - ret = mp_sub_d(&key->q, 1, &m); - if (ret == MP_OKAY) - ret = mp_mod(&key->d, &m, &key->dQ); - mp_forcezero(&m); - } - - return ret; -} -#endif - -/* - * Utility for GetOpenSshKey() to read in RSA keys. - */ -static int GetOpenSshKeyRsa(RsaKey* key, - const byte* buf, word32 len, word32* idx) -{ - int ret; - - ret = wc_InitRsaKey(key, NULL); - if (ret == WS_SUCCESS) - ret = GetMpintToMp(&key->n, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpintToMp(&key->e, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpintToMp(&key->d, buf, len, idx); -#ifndef RSA_LOW_MEM - if (ret == WS_SUCCESS) - ret = GetMpintToMp(&key->u, buf, len, idx); -#else - /* Skipping the u value in the key. */ - if (ret == WS_SUCCESS) - ret = GetSkip(buf, len, idx); -#endif - if (ret == WS_SUCCESS) - ret = GetMpintToMp(&key->p, buf, len, idx); - if (ret == WS_SUCCESS) - ret = GetMpintToMp(&key->q, buf, len, idx); - -#ifndef RSA_LOW_MEM - /* Calculate dP and dQ for wolfCrypt. */ - if (ret == WS_SUCCESS) - ret = CalcRsaDX(key); -#endif - - if (ret != WS_SUCCESS) - ret = WS_RSA_E; - - return ret; -} - -#endif /* LIBWOLFSSL_VERSION_HEX > WOLFSSL_V5_7_0 */ - -#endif /* WOLFSSH_NO_RSA */ - - -#ifndef WOLFSSH_NO_ECDSA -/* - * Utility for GetOpenSshKey() to read in ECDSA keys. - */ -static int GetOpenSshKeyEcc(ecc_key* key, - const byte* buf, word32 len, word32* idx) -{ - const byte *name = NULL, *priv = NULL, *pub = NULL; - word32 nameSz = 0, privSz = 0, pubSz = 0; - int ret; - - ret = wc_ecc_init(key); - if (ret == WS_SUCCESS) - ret = GetStringRef(&nameSz, &name, buf, len, idx); /* curve name */ - if (ret == WS_SUCCESS) - ret = GetStringRef(&pubSz, &pub, buf, len, idx); /* Q */ - if (ret == WS_SUCCESS) - ret = GetMpint(&privSz, &priv, buf, len, idx); /* d */ - - if (ret == WS_SUCCESS) - ret = wc_ecc_import_private_key_ex(priv, privSz, pub, pubSz, - key, ECC_CURVE_DEF); - - if (ret != WS_SUCCESS) - ret = WS_ECC_E; - - return ret; -} -#endif - -#ifndef WOLFSSH_NO_ED25519 -/* - * Utility for GetOpenSshKey() to read in Ed25519 keys. - */ -static int GetOpenSshKeyEd25519(ed25519_key* key, - const byte* buf, word32 len, word32* idx) -{ - const byte *priv = NULL, *pub = NULL; - word32 privSz = 0, pubSz = 0; - int ret; - - ret = wc_ed25519_init_ex(key, key->heap, INVALID_DEVID); - - /* OpenSSH key formatting stores the public key, ENC(A), and the - * private key (k) concatenated with the public key, k || ENC(A). */ - if (ret == WS_SUCCESS) - ret = GetStringRef(&pubSz, &pub, buf, len, idx); /* ENC(A) */ - if (ret == WS_SUCCESS) - ret = GetStringRef(&privSz, &priv, buf, len, idx); /* k || ENC(A) */ - if (ret == WS_SUCCESS) - if (privSz < pubSz) - ret = WS_KEY_FORMAT_E; - - if (ret == WS_SUCCESS) - ret = wc_ed25519_import_private_key(priv, privSz - pubSz, - pub, pubSz, key); - - if (ret != WS_SUCCESS) - ret = WS_ECC_E; - - return ret; -} -#endif - -#ifdef WOLFSSH_TPM - -#ifndef WOLFSSH_NO_ECDSA -static int GetOpenSshPublicKeyEcc(ecc_key* key, const byte* buf, word32 len, - word32* idx) -{ - int ret = WS_CRYPTO_FAILED; - (void)key; - (void)buf; - (void)len; - (void)idx; - /* TODO: Add ECC public key: See DoUserAuthRequestEcc and wc_ecc_import_x963 */ - return ret; -} -#endif -#ifndef WOLFSSH_NO_ED25519 -static int GetOpenSshKeyPublicEd25519(ed25519_key* key, const byte* buf, - word32 len, word32* idx) -{ - int ret = WS_CRYPTO_FAILED; - (void)key; - (void)buf; - (void)len; - (void)idx; - /* TODO: Add ECC public key: See DoUserAuthRequestEd25519 and wc_ed25519_import_public */ - return ret; -} -#endif -#ifndef WOLFSSH_NO_RSA -static int GetOpenSshPublicKeyRsa(RsaKey* key, const byte* buf, word32 len, - word32* idx) -{ - int ret; - const byte *n = NULL, *e = NULL; - word32 nSz = 0, eSz = 0; - - ret = GetMpint(&eSz, &e, buf, len, idx); - if (ret == WS_SUCCESS) { - ret = GetMpint(&nSz, &n, buf, len, idx); - } - if (ret == WS_SUCCESS) { - ret = wc_RsaPublicKeyDecodeRaw(n, nSz, e, eSz, key); - if (ret != 0) { - WLOG(WS_LOG_DEBUG, "Could not decode RSA public key"); - ret = WS_CRYPTO_FAILED; - } - } - return ret; -} -#endif - -static int GetOpenSshPublicKey(WS_KeySignature *key, - const byte* buf, word32 len, word32* idx) -{ - int ret = WS_SUCCESS; - const byte* publicKeyType; - word32 publicKeyTypeSz = 0; - byte keyId; - - ret = GetStringRef(&publicKeyTypeSz, &publicKeyType, buf, len, idx); - keyId = NameToId((const char*)publicKeyType, publicKeyTypeSz); - - switch (keyId) { - #ifndef WOLFSSH_NO_RSA - case ID_SSH_RSA: - 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: - case ID_ECDSA_SHA2_NISTP521: - ret = GetOpenSshPublicKeyEcc(&key->ks.ecc.key, buf, len, idx); - break; - #endif - #ifndef WOLFSSH_NO_ED25519 - case ID_ED25519: - ret = GetOpenSshKeyPublicEd25519(&key->ks.ed25519.key, buf, len, idx); - break; - #endif - default: - ret = WS_UNIMPLEMENTED_E; - break; - } - return ret; -} - -#endif /* WOLFSSH_TPM */ - -/* - * Decodes an OpenSSH format key. - */ -static int GetOpenSshKey(WS_KeySignature *key, - const byte* buf, word32 len, word32* idx) -{ - const char AuthMagic[] = "openssh-key-v1"; - const byte* str = NULL; - word32 keyCount = 0, strSz, i; - int ret = WS_SUCCESS; - - if (WSTRCMP(AuthMagic, (const char*)buf) != 0) { - ret = WS_KEY_AUTH_MAGIC_E; - } - - if (ret == WS_SUCCESS) { - *idx += (word32)WSTRLEN(AuthMagic) + 1; - ret = GetSkip(buf, len, idx); /* ciphername */ - } - - if (ret == WS_SUCCESS) - ret = GetSkip(buf, len, idx); /* kdfname */ - - if (ret == WS_SUCCESS) - ret = GetSkip(buf, len, idx); /* kdfoptions */ - - if (ret == WS_SUCCESS) - ret = GetUint32(&keyCount, buf, len, idx); /* key count */ - - if (ret == WS_SUCCESS) { - if (keyCount != WOLFSSH_KEY_QUANTITY_REQ) { - ret = WS_KEY_FORMAT_E; - } - } - - if (ret == WS_SUCCESS) { - strSz = 0; - ret = GetStringRef(&strSz, &str, buf, len, idx); - /* public buf */ - } - - if (ret == WS_SUCCESS) { - strSz = 0; - ret = GetStringRef(&strSz, &str, buf, len, idx); - /* list of private keys */ - - /* If there isn't a private key, the key file is bad. */ - if (ret == WS_SUCCESS && strSz == 0) { - ret = WS_KEY_FORMAT_E; - } - - if (ret == WS_SUCCESS) { - const byte* subStr = NULL; - word32 subStrSz = 0, subIdx = 0, check1 = 0, check2 = ~0; - byte keyId; - - ret = GetUint32(&check1, str, strSz, &subIdx); /* checkint 1 */ - if (ret == WS_SUCCESS) - ret = GetUint32(&check2, str, strSz, &subIdx); /* checkint 2 */ - if (ret == WS_SUCCESS) { - if (check1 != check2) { - ret = WS_KEY_CHECK_VAL_E; - } - } - if (ret == WS_SUCCESS) { - for (i = 0; i < keyCount; i++) { - ret = GetStringRef(&subStrSz, &subStr, - str, strSz, &subIdx); - if (ret == WS_SUCCESS) { - keyId = NameToId((const char*)subStr, subStrSz); - key->keyId = keyId; - } - if (ret == WS_SUCCESS) { - switch (keyId) { - #ifndef WOLFSSH_NO_RSA - case ID_SSH_RSA: - ret = GetOpenSshKeyRsa(&key->ks.rsa.key, - str, strSz, &subIdx); - break; - #endif - #ifndef WOLFSSH_NO_ECDSA - case ID_ECDSA_SHA2_NISTP256: - case ID_ECDSA_SHA2_NISTP384: - case ID_ECDSA_SHA2_NISTP521: - ret = GetOpenSshKeyEcc(&key->ks.ecc.key, - str, strSz, &subIdx); - break; - #endif - #ifndef WOLFSSH_NO_ED25519 - case ID_ED25519: - ret = GetOpenSshKeyEd25519(&key->ks.ed25519.key, - str, strSz, &subIdx); - break; - #endif - default: - ret = WS_UNIMPLEMENTED_E; - break; - } - if (ret == WS_SUCCESS) - ret = GetSkip(str, strSz, &subIdx); - /* key comment */ - } - } - /* Padding: Add increasing digits to pad to the nearest - * block size. Default block size is 8, but depends on - * the encryption algo. The private key chunk's length, - * and the length of the comment delimit the end of the - * encrypted blob. No added padding required. */ - if (ret == WS_SUCCESS) { - if (strSz % MIN_BLOCK_SZ == 0) { - if (strSz > subIdx) { - /* The padding starts at 1. */ - check2 = strSz - subIdx; - for (check1 = 1; - check1 <= check2; - check1++, subIdx++) { - if (check1 != str[subIdx]) { - /* Bad pad value. */ - ret = WS_KEY_FORMAT_E; - break; - } - } - } - } - } - } - } - } - - return ret; -} - - -/* - * Identifies the flavor of an OpenSSH key, RSA or ECDSA, and returns the - * key type ID. The process is to decode the key extracting the identifiers, - * and try to decode the key as the type indicated type. For RSA keys, the - * key format is described as "ssh-rsa". - * - * @param in key to identify - * @param inSz size of key - * @param heap heap to use for memory allocation - * @return keyId as int, WS_MEMORY_E, WS_UNIMPLEMENTED_E, - * WS_INVALID_ALGO_ID - */ -int IdentifyOpenSshKey(const byte* in, word32 inSz, void* heap) -{ - WS_KeySignature *key = NULL; - word32 idx = 0; - int ret; - - key = (WS_KeySignature*)WMALLOC(sizeof(WS_KeySignature), - heap, DYNTYPE_PRIVKEY); - - if (key == NULL) { - ret = WS_MEMORY_E; - } - else { - WMEMSET(key, 0, sizeof(*key)); - key->heap = heap; - key->keyId = ID_NONE; - - ret = GetOpenSshKey(key, in, inSz, &idx); - - if (ret == WS_SUCCESS) { - ret = key->keyId; - } - else if (key->keyId == ID_UNKNOWN) { - ret = WS_UNIMPLEMENTED_E; - } - - wolfSSH_KEY_clean(key); - WFREE(key, heap, DYNTYPE_PRIVKEY); - } - - return ret; -} +/* The OpenSSH binary key-format decoders (GetOpenSshKey, + * IdentifyOpenSshKey, and their helpers) live in src/ossh.c. */ #ifdef WOLFSSH_CERTS @@ -2974,6 +2583,33 @@ static const NameIdPair NameIdMap[] = { { ID_X509V3_ECDSA_SHA2_NISTP521, TYPE_KEY, "x509v3-ecdsa-sha2-nistp521" }, #endif #endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_OSSH_CERTS +#ifndef WOLFSSH_NO_RSA_SHA2_256 + { ID_OSSH_CERT_RSA, TYPE_KEY, "ssh-rsa-cert-v01@openssh.com" }, + /* Modern OpenSSH offers an RSA certificate under an rsa-sha2-* algorithm + * name (RFC 8332). The certificate format is identical to ssh-rsa-cert, so + * these map to the same id; the signature algorithm name carries the hash. */ + { ID_OSSH_CERT_RSA, TYPE_KEY, "rsa-sha2-256-cert-v01@openssh.com" }, +#endif +#ifndef WOLFSSH_NO_RSA_SHA2_512 + { ID_OSSH_CERT_RSA, TYPE_KEY, "rsa-sha2-512-cert-v01@openssh.com" }, +#endif +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + { ID_OSSH_CERT_ECDSA_SHA2_NISTP256, TYPE_KEY, + "ecdsa-sha2-nistp256-cert-v01@openssh.com" }, +#endif +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + { ID_OSSH_CERT_ECDSA_SHA2_NISTP384, TYPE_KEY, + "ecdsa-sha2-nistp384-cert-v01@openssh.com" }, +#endif +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + { ID_OSSH_CERT_ECDSA_SHA2_NISTP521, TYPE_KEY, + "ecdsa-sha2-nistp521-cert-v01@openssh.com" }, +#endif +#ifndef WOLFSSH_NO_ED25519 + { ID_OSSH_CERT_ED25519, TYPE_KEY, "ssh-ed25519-cert-v01@openssh.com" }, +#endif +#endif /* WOLFSSH_OSSH_CERTS */ /* Service IDs */ { ID_SERVICE_USERAUTH, TYPE_OTHER, "ssh-userauth" }, @@ -3728,6 +3364,23 @@ int GetUint32(word32* v, const byte* buf, word32 len, word32* idx) } +#ifdef WOLFSSH_OSSH_CERTS +int GetUint64(word64* v, const byte* buf, word32 len, word32* idx) +{ + int result; + word32 hi = 0, lo = 0; + + result = GetUint32(&hi, buf, len, idx); + if (result == WS_SUCCESS) + result = GetUint32(&lo, buf, len, idx); + if (result == WS_SUCCESS) + *v = ((word64)hi << 32) | (word64)lo; + + return result; +} +#endif /* WOLFSSH_OSSH_CERTS */ + + int GetSize(word32* v, const byte* buf, word32 len, word32* idx) { int result; @@ -4016,6 +3669,23 @@ static const byte cannedKeyAlgoClient[] = { #endif /* WOLFSSH_NO_SSH_RSA_SHA1 */ #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ #endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_OSSH_CERTS + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + ID_OSSH_CERT_ECDSA_SHA2_NISTP521, + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + ID_OSSH_CERT_ECDSA_SHA2_NISTP384, + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + ID_OSSH_CERT_ECDSA_SHA2_NISTP256, + #endif + #ifndef WOLFSSH_NO_ED25519 + ID_OSSH_CERT_ED25519, + #endif + #ifndef WOLFSSH_NO_RSA_SHA2_256 + ID_OSSH_CERT_RSA, + #endif +#endif /* WOLFSSH_OSSH_CERTS */ #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 ID_ECDSA_SHA2_NISTP521, #endif @@ -4216,6 +3886,9 @@ enum wc_HashType HashForId(byte id) case ID_ECDSA_SHA2_NISTP256: #ifdef WOLFSSH_CERTS case ID_X509V3_ECDSA_SHA2_NISTP256: + #endif + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP256: #endif return WC_HASH_TYPE_SHA256; #endif @@ -4239,6 +3912,9 @@ enum wc_HashType HashForId(byte id) #ifndef WOLFSSH_NO_ED25519 case ID_ED25519: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ED25519: + #endif return WC_HASH_TYPE_SHA512; #endif /* SHA2-384 */ @@ -4254,6 +3930,9 @@ enum wc_HashType HashForId(byte id) case ID_ECDSA_SHA2_NISTP384: #ifdef WOLFSSH_CERTS case ID_X509V3_ECDSA_SHA2_NISTP384: + #endif + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP384: #endif return WC_HASH_TYPE_SHA384; #endif @@ -4267,6 +3946,9 @@ enum wc_HashType HashForId(byte id) case ID_ECDSA_SHA2_NISTP521: #ifdef WOLFSSH_CERTS case ID_X509V3_ECDSA_SHA2_NISTP521: + #endif + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP521: #endif return WC_HASH_TYPE_SHA512; #endif @@ -5201,7 +4883,8 @@ static int ParseECCPubKey(WOLFSSH *ssh, if (ret == WS_SUCCESS) { algoName = IdToName(ssh->handshake->pubKeyId); - if (keyAlgoNameSz != (word32)WSTRLEN(algoName) + if (algoName == NULL || keyAlgoName == NULL + || keyAlgoNameSz != (word32)WSTRLEN(algoName) || WMEMCMP(keyAlgoName, algoName, keyAlgoNameSz) != 0) { ret = WS_INVALID_ALGO_ID; } @@ -8233,15 +7916,21 @@ static int DoUserAuthRequestEd25519(WOLFSSH* ssh, ret = wc_ed25519_verify_msg_update(temp, MSG_ID_SZ, key_ptr); } - /* The rest of the fields in the signature are already - * in the buffer. Just need to account for the sizes. */ + /* The rest of the fields in the signature are already in the buffer. + * Prefer the on-the-wire signed length (robust when the public key field + * was replaced, e.g. an OpenSSH certificate); fall back to summing the + * field sizes. */ if (ret == WS_SUCCESS) { - ret = wc_ed25519_verify_msg_update(pk->dataToSign, - authData->usernameSz + - authData->serviceNameSz + - authData->authNameSz + BOOLEAN_SZ + - pk->publicKeyTypeSz + pk->publicKeySz + - (UINT32_SZ * 5), key_ptr); + word32 dataToSignSz = pk->dataToSignSz; + if (dataToSignSz == 0) { + dataToSignSz = authData->usernameSz + + authData->serviceNameSz + + authData->authNameSz + BOOLEAN_SZ + + pk->publicKeyTypeSz + pk->publicKeySz + + (UINT32_SZ * 5); + } + ret = wc_ed25519_verify_msg_update(pk->dataToSign, dataToSignSz, + key_ptr); } if (ret == WS_SUCCESS) { @@ -8292,6 +7981,11 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, int partialSuccess = 0; byte hasSig = 0; byte pkTypeId = ID_NONE; +#ifdef WOLFSSH_OSSH_CERTS + byte* osshUserKey = NULL; + byte sigId; + int sigOk; +#endif WLOG(WS_LOG_DEBUG, "Entering DoUserAuthRequestPublicKey()"); @@ -8329,6 +8023,10 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, authData->sf.publicKey.publicKeySz = pubKeyBlobSz; authData->sf.publicKey.signature = sig; authData->sf.publicKey.signatureSz = sigSz; + /* Length of the signed request region. Robust against the public key + * field being replaced (e.g. an OpenSSH certificate reconstructed into + * its base key), since it is derived from the on-the-wire message. */ + authData->sf.publicKey.dataToSignSz = len - sigSz - LENGTH_SZ; } /* Parse the public key format, signature algo, and signature blob. */ @@ -8402,6 +8100,136 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, } #endif /* WOLFSSH_CERTS */ + #ifdef WOLFSSH_OSSH_CERTS + if (ret == WS_SUCCESS && !authFailure) { + if (pkTypeId == ID_OSSH_CERT_RSA + || pkTypeId == ID_OSSH_CERT_ECDSA_SHA2_NISTP256 + || pkTypeId == ID_OSSH_CERT_ECDSA_SHA2_NISTP384 + || pkTypeId == ID_OSSH_CERT_ECDSA_SHA2_NISTP521 + || pkTypeId == ID_OSSH_CERT_ED25519) { + WS_OsshCert cert; + int rc; + + rc = OsshCertParse(&cert, pkTypeId, (byte*)pubKeyBlob, + pubKeyBlobSz); + if (rc == WS_SUCCESS) { + rc = OsshCertCheckType(&cert); + } + if (rc == WS_SUCCESS) { + rc = OsshCertVerifySignature(&cert, ssh->ctx->heap); + } + if (rc == WS_SUCCESS) { + /* Audit trail: record the certificate identity (key id and + * serial) whose CA signature just verified. */ + WLOG(WS_LOG_DEBUG, + "DUARPK: OpenSSH certificate verified, key ID \"%.*s\", " + "serial %llu", + (int)cert.keyIdSz, cert.keyId, + (unsigned long long)cert.serial); + } + /* Validate critical options + extensions. Unknown critical + * options are rejected; force-command and source-address are + * extracted for the application to enforce. */ + if (rc == WS_SUCCESS) { + rc = OsshCertCheckOptions(&cert); + } + if (rc == WS_SUCCESS) { + /* Reconstruct the base user public key as [type][params] so + * the standard per-algorithm verifier can check the user's + * signature against the certified key. */ + const char* baseName = IdToName(cert.baseTypeId); + word32 baseNameSz = (word32)WSTRLEN(baseName); + word32 ukSz = LENGTH_SZ + baseNameSz + cert.userKeyParmsSz; + + osshUserKey = (byte*)WMALLOC(ukSz, ssh->ctx->heap, + DYNTYPE_PUBKEY); + if (osshUserKey == NULL) { + rc = WS_MEMORY_E; + } + else { + c32toa(baseNameSz, osshUserKey); + WMEMCPY(osshUserKey + LENGTH_SZ, baseName, baseNameSz); + WMEMCPY(osshUserKey + LENGTH_SZ + baseNameSz, + cert.userKeyParms, cert.userKeyParmsSz); + + authData->sf.publicKey.publicKey = osshUserKey; + authData->sf.publicKey.publicKeySz = ukSz; + authData->sf.publicKey.publicKeyType = + osshUserKey + LENGTH_SZ; + authData->sf.publicKey.publicKeyTypeSz = baseNameSz; + authData->sf.publicKey.isOsshCert = 1; + authData->sf.publicKey.caKey = cert.caKey; + authData->sf.publicKey.caKeySz = cert.caKeySz; + authData->sf.publicKey.principals = cert.principals; + authData->sf.publicKey.principalsSz = cert.principalsSz; + authData->sf.publicKey.validAfter = cert.validAfter; + authData->sf.publicKey.validBefore = cert.validBefore; + authData->sf.publicKey.forceCommand = cert.forceCommand; + authData->sf.publicKey.forceCommandSz = cert.forceCommandSz; + authData->sf.publicKey.sourceAddress = cert.sourceAddress; + authData->sf.publicKey.sourceAddressSz = + cert.sourceAddressSz; + + /* Verify the user signature by its own algorithm (it + * carries the negotiated hash); the no-signature probe uses + * the base key type, bound to the cert key family. */ + if (hasSig) { + sigId = NameToId((const char*)sigAlgo, sigAlgoSz); + + switch (cert.baseTypeId) { + #ifndef WOLFSSH_NO_RSA + case ID_SSH_RSA: + sigOk = (sigId == ID_SSH_RSA + || sigId == ID_RSA_SHA2_256 + || sigId == ID_RSA_SHA2_512); + break; + #endif + #ifndef WOLFSSH_NO_ECDSA + case ID_ECDSA_SHA2_NISTP256: + case ID_ECDSA_SHA2_NISTP384: + case ID_ECDSA_SHA2_NISTP521: + sigOk = (sigId == cert.baseTypeId); + break; + #endif + #ifndef WOLFSSH_NO_ED25519 + case ID_ED25519: + sigOk = (sigId == ID_ED25519); + break; + #endif + default: + sigOk = 0; + } + + if (sigOk) { + pkTypeId = sigId; + /* The per-algorithm verifier matches the signature + * algorithm against publicKeyType; for RSA the + * signature name (rsa-sha2-*) differs from the base + * key type, so use the offered signature name. */ + authData->sf.publicKey.publicKeyType = sigAlgo; + authData->sf.publicKey.publicKeyTypeSz = sigAlgoSz; + } + else { + WLOG(WS_LOG_DEBUG, "DUARPK: OSSH cert signature " + "algorithm does not match certificate key " + "type"); + rc = WS_INVALID_ALGO_ID; + } + } + else { + pkTypeId = cert.baseTypeId; + } + } + } + if (rc != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "DUARPK: OSSH cert rejected (%d)", rc); + authFailure = 1; + ret = WS_SUCCESS; + } + } + } + #endif /* WOLFSSH_OSSH_CERTS */ + if (ret == WS_SUCCESS && !authFailure) { if (ssh->ctx->userAuthCb != NULL) { WLOG(WS_LOG_DEBUG, "DUARPK: Calling the userauth callback"); @@ -8621,6 +8449,12 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, ret = SendUserAuthFailure(ssh, 1); } +#ifdef WOLFSSH_OSSH_CERTS + if (osshUserKey != NULL) { + WFREE(osshUserKey, ssh->ctx->heap, DYNTYPE_PUBKEY); + } +#endif + WLOG(WS_LOG_DEBUG, "Leaving DoUserAuthRequestPublicKey(), ret = %d", ret); return ret; } @@ -12302,7 +12136,9 @@ int wolfSSH_RsaVerify(const byte *sig, word32 sigSz, } WMEMSET(checkSig, 0, offset); - WMEMCPY(checkSig + offset, sig, sigSz); + if (sig != NULL) { + WMEMCPY(checkSig + offset, sig, sigSz); + } } if (ret == WS_SUCCESS) { @@ -14914,6 +14750,7 @@ static int BuildUserAuthRequestRsa(WOLFSSH* ssh, int ret = WS_SUCCESS; byte* checkData = NULL; word32 checkDataSz = 0; + byte effSigId; if (ssh == NULL || output == NULL || idx == NULL || authData == NULL || sigStart == NULL || keySig == NULL) { @@ -14922,9 +14759,19 @@ static int BuildUserAuthRequestRsa(WOLFSSH* ssh, } begin = *idx; + effSigId = keySig->sigId; + +#ifdef WOLFSSH_OSSH_CERTS + /* An OpenSSH RSA certificate is dispatched by its cert id, but the + * signature itself is rsa-sha2-*. Use the strongest variant the server + * advertised. */ + if (keySig->sigId == ID_OSSH_CERT_RSA) { + effSigId = OsshRsaCertSigId(ssh->peerSigId, ssh->peerSigIdSz); + } +#endif if (ret == WS_SUCCESS) { - hashId = HashForId(keySig->sigId); + hashId = HashForId(effSigId); if (hashId == WC_HASH_TYPE_NONE) ret = WS_INVALID_ALGO_ID; } @@ -14984,7 +14831,7 @@ static int BuildUserAuthRequestRsa(WOLFSSH* ssh, byte encDigest[MAX_ENCODED_SIG_SZ]; int encDigestSz; - switch (keySig->sigId) { + switch (effSigId) { #ifndef WOLFSSH_NO_SSH_RSA_SHA1 case ID_SSH_RSA: names = cannedKeyAlgoSshRsaNames; @@ -15462,16 +15309,25 @@ static int BuildUserAuthRequestEcc(WOLFSSH* ssh, switch (keySig->sigId) { #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 case ID_ECDSA_SHA2_NISTP256: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP256: + #endif names = cannedKeyAlgoEcc256Names; break; #endif #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 case ID_ECDSA_SHA2_NISTP384: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP384: + #endif names = cannedKeyAlgoEcc384Names; break; #endif #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 case ID_ECDSA_SHA2_NISTP521: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP521: + #endif names = cannedKeyAlgoEcc521Names; break; #endif @@ -16002,6 +15858,24 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, keySig->sigId = matchId; keySig->sigName = IdToName(matchId); keySig->sigNameSz = (word32)WSTRLEN(keySig->sigName); + #ifdef WOLFSSH_OSSH_CERTS + /* Offer an OpenSSH RSA certificate under the rsa-sha2-* certificate + * algorithm name matching the signature we will produce (RFC 8332), + * rather than the legacy ssh-rsa-cert name. */ + if (keySig->keyId == ID_OSSH_CERT_RSA) { + #ifndef WOLFSSH_NO_RSA_SHA2_512 + if (OsshRsaCertSigId(ssh->peerSigId, ssh->peerSigIdSz) + == ID_RSA_SHA2_512) { + keySig->sigName = cannedKeyAlgoOsshRsaSha2_512CertName; + } + else + #endif + { + keySig->sigName = cannedKeyAlgoOsshRsaSha2_256CertName; + } + keySig->sigNameSz = (word32)WSTRLEN(keySig->sigName); + } + #endif /* WOLFSSH_OSSH_CERTS */ } if (ret == WS_SUCCESS) { @@ -16014,6 +15888,9 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, switch (keySig->keyId) { #ifndef WOLFSSH_NO_RSA case ID_SSH_RSA: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_RSA: + #endif ret = PrepareUserAuthRequestRsa(ssh, payloadSz, authData, keySig); break; @@ -16028,6 +15905,11 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, case ID_ECDSA_SHA2_NISTP256: case ID_ECDSA_SHA2_NISTP384: case ID_ECDSA_SHA2_NISTP521: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP256: + case ID_OSSH_CERT_ECDSA_SHA2_NISTP384: + case ID_OSSH_CERT_ECDSA_SHA2_NISTP521: + #endif ret = PrepareUserAuthRequestEcc(ssh, payloadSz, authData, keySig); break; @@ -16042,6 +15924,9 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, #endif #ifndef WOLFSSH_NO_ED25519 case ID_ED25519: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ED25519: + #endif ret = PrepareUserAuthRequestEd25519(ssh, payloadSz, authData, keySig); break; @@ -16086,6 +15971,9 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, case ID_SSH_RSA: case ID_RSA_SHA2_256: case ID_RSA_SHA2_512: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_RSA: + #endif c32toa(keySig->sigNameSz, output + begin); begin += LENGTH_SZ; WMEMCPY(output + begin, keySig->sigName, keySig->sigNameSz); @@ -16120,6 +16008,11 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, case ID_ECDSA_SHA2_NISTP256: case ID_ECDSA_SHA2_NISTP384: case ID_ECDSA_SHA2_NISTP521: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ECDSA_SHA2_NISTP256: + case ID_OSSH_CERT_ECDSA_SHA2_NISTP384: + case ID_OSSH_CERT_ECDSA_SHA2_NISTP521: + #endif c32toa(pk->publicKeyTypeSz, output + begin); begin += LENGTH_SZ; WMEMCPY(output + begin, @@ -16156,6 +16049,9 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, #endif #ifndef WOLFSSH_NO_ED25519 case ID_ED25519: + #ifdef WOLFSSH_OSSH_CERTS + case ID_OSSH_CERT_ED25519: + #endif c32toa(pk->publicKeyTypeSz, output + begin); begin += LENGTH_SZ; WMEMCPY(output + begin, diff --git a/src/ossh.c b/src/ossh.c new file mode 100644 index 000000000..7532125ea --- /dev/null +++ b/src/ossh.c @@ -0,0 +1,1234 @@ +/* ossh.c + * + * Copyright (C) 2014-2026 wolfSSL Inc. + * + * This file is part of wolfSSH. + * + * wolfSSH is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfSSH. If not, see . + */ + +/* + * The ossh module parses and verifies OpenSSH ("*-cert-v01@openssh.com") + * user certificates. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#ifdef WOLFSSL_USER_SETTINGS + #include +#else + #include +#endif + +#include + +#include +#include +#include + +#include +#include +#include +#include +#ifndef WOLFSSH_NO_RSA + #include +#endif +#ifndef WOLFSSH_NO_ECDSA + #include +#endif +#ifndef WOLFSSH_NO_ED25519 + #include +#endif + + + +/* OpenSSH binary key-format decoders (moved from internal.c); + * compiled regardless of WOLFSSH_OSSH_CERTS. */ + +#ifndef WOLFSSH_NO_RSA + +#if (LIBWOLFSSL_VERSION_HEX > WOLFSSL_V5_7_0) && !defined(HAVE_FIPS) +/* + * The function wc_RsaPrivateKeyDecodeRaw() is available + * from wolfSSL after v5.7.0. + */ + +/* + * Utility for GetOpenSshKey() to read in RSA keys. + */ +static int GetOpenSshKeyRsa(RsaKey* key, + const byte* buf, word32 len, word32* idx) +{ + const byte *n, *e, *d, *u, *p, *q; + word32 nSz, eSz, dSz, uSz, pSz, qSz; + int ret; + + ret = wc_InitRsaKey(key, NULL); + if (ret == WS_SUCCESS) + ret = GetMpint(&nSz, &n, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpint(&eSz, &e, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpint(&dSz, &d, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpint(&uSz, &u, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpint(&pSz, &p, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpint(&qSz, &q, buf, len, idx); + if (ret == WS_SUCCESS) + ret = wc_RsaPrivateKeyDecodeRaw(n, nSz, e, eSz, d, dSz, + u, uSz, p, pSz, q, qSz, NULL, 0, NULL, 0, key); + + if (ret != WS_SUCCESS) + ret = WS_RSA_E; + + return ret; +} + +#else /* LIBWOLFSSL_VERSION_HEX > WOLFSSL_V5_7_0 */ + +#include + +/* + * Utility function to read an Mpint from the stream directly into a mp_int. + * The RsaKey members u, dP, and dQ do not exist when wolfCrypt is built + * with RSA_LOW_MEM. (That mode of wolfCrypt isn't using the extra values + * for the Chinese Remainder Theorem.) + */ +static int GetMpintToMp(mp_int* mp, + const byte* buf, word32 len, word32* idx) +{ + const byte* val = NULL; + word32 valSz = 0; + int ret; + + ret = GetMpint(&valSz, &val, buf, len, idx); + if (ret == WS_SUCCESS) + ret = mp_read_unsigned_bin(mp, val, valSz); + + return ret; +} + + +#ifndef RSA_LOW_MEM +/* + * For the given RSA key, calculate d mod(p-1) and d mod(q-1). + * wolfCrypt's RSA code expects them, but the OpenSSH format key + * doesn't store them. + */ +static int CalcRsaDX(RsaKey* key) +{ + mp_int m; + int ret; + + ret = mp_init(&m); + if (ret == MP_OKAY) { + ret = mp_sub_d(&key->p, 1, &m); + if (ret == MP_OKAY) + ret = mp_mod(&key->d, &m, &key->dP); + if (ret == MP_OKAY) + ret = mp_sub_d(&key->q, 1, &m); + if (ret == MP_OKAY) + ret = mp_mod(&key->d, &m, &key->dQ); + mp_forcezero(&m); + } + + return ret; +} +#endif + +/* + * Utility for GetOpenSshKey() to read in RSA keys. + */ +static int GetOpenSshKeyRsa(RsaKey* key, + const byte* buf, word32 len, word32* idx) +{ + int ret; + + ret = wc_InitRsaKey(key, NULL); + if (ret == WS_SUCCESS) + ret = GetMpintToMp(&key->n, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpintToMp(&key->e, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpintToMp(&key->d, buf, len, idx); +#ifndef RSA_LOW_MEM + if (ret == WS_SUCCESS) + ret = GetMpintToMp(&key->u, buf, len, idx); +#else + /* Skipping the u value in the key. */ + if (ret == WS_SUCCESS) + ret = GetSkip(buf, len, idx); +#endif + if (ret == WS_SUCCESS) + ret = GetMpintToMp(&key->p, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetMpintToMp(&key->q, buf, len, idx); + +#ifndef RSA_LOW_MEM + /* Calculate dP and dQ for wolfCrypt. */ + if (ret == WS_SUCCESS) + ret = CalcRsaDX(key); +#endif + + if (ret != WS_SUCCESS) + ret = WS_RSA_E; + + return ret; +} + +#endif /* LIBWOLFSSL_VERSION_HEX > WOLFSSL_V5_7_0 */ + +#endif /* WOLFSSH_NO_RSA */ + + +#ifndef WOLFSSH_NO_ECDSA +/* + * Utility for GetOpenSshKey() to read in ECDSA keys. + */ +static int GetOpenSshKeyEcc(ecc_key* key, + const byte* buf, word32 len, word32* idx) +{ + const byte *name = NULL, *priv = NULL, *pub = NULL; + word32 nameSz = 0, privSz = 0, pubSz = 0; + int ret; + + ret = wc_ecc_init(key); + if (ret == WS_SUCCESS) + ret = GetStringRef(&nameSz, &name, buf, len, idx); /* curve name */ + if (ret == WS_SUCCESS) + ret = GetStringRef(&pubSz, &pub, buf, len, idx); /* Q */ + if (ret == WS_SUCCESS) + ret = GetMpint(&privSz, &priv, buf, len, idx); /* d */ + + if (ret == WS_SUCCESS) + ret = wc_ecc_import_private_key_ex(priv, privSz, pub, pubSz, + key, ECC_CURVE_DEF); + + if (ret != WS_SUCCESS) + ret = WS_ECC_E; + + return ret; +} +#endif + +#ifndef WOLFSSH_NO_ED25519 +/* + * Utility for GetOpenSshKey() to read in Ed25519 keys. + */ +static int GetOpenSshKeyEd25519(ed25519_key* key, + const byte* buf, word32 len, word32* idx) +{ + const byte *priv = NULL, *pub = NULL; + word32 privSz = 0, pubSz = 0; + int ret; + + ret = wc_ed25519_init_ex(key, key->heap, INVALID_DEVID); + + /* OpenSSH key formatting stores the public key, ENC(A), and the + * private key (k) concatenated with the public key, k || ENC(A). */ + if (ret == WS_SUCCESS) + ret = GetStringRef(&pubSz, &pub, buf, len, idx); /* ENC(A) */ + if (ret == WS_SUCCESS) + ret = GetStringRef(&privSz, &priv, buf, len, idx); /* k || ENC(A) */ + if (ret == WS_SUCCESS) + if (privSz < pubSz) + ret = WS_KEY_FORMAT_E; + + if (ret == WS_SUCCESS) + ret = wc_ed25519_import_private_key(priv, privSz - pubSz, + pub, pubSz, key); + + if (ret != WS_SUCCESS) + ret = WS_ECC_E; + + return ret; +} +#endif + +#ifdef WOLFSSH_TPM + +#ifndef WOLFSSH_NO_ECDSA +static int GetOpenSshPublicKeyEcc(ecc_key* key, const byte* buf, word32 len, + word32* idx) +{ + int ret = WS_CRYPTO_FAILED; + (void)key; + (void)buf; + (void)len; + (void)idx; + /* TODO: Add ECC public key: See DoUserAuthRequestEcc and wc_ecc_import_x963 */ + return ret; +} +#endif +#ifndef WOLFSSH_NO_ED25519 +static int GetOpenSshKeyPublicEd25519(ed25519_key* key, const byte* buf, + word32 len, word32* idx) +{ + int ret = WS_CRYPTO_FAILED; + (void)key; + (void)buf; + (void)len; + (void)idx; + /* TODO: Add ECC public key: See DoUserAuthRequestEd25519 and wc_ed25519_import_public */ + return ret; +} +#endif +#ifndef WOLFSSH_NO_RSA +static int GetOpenSshPublicKeyRsa(RsaKey* key, const byte* buf, word32 len, + word32* idx) +{ + int ret; + const byte *n = NULL, *e = NULL; + word32 nSz = 0, eSz = 0; + + ret = GetMpint(&eSz, &e, buf, len, idx); + if (ret == WS_SUCCESS) { + ret = GetMpint(&nSz, &n, buf, len, idx); + } + if (ret == WS_SUCCESS) { + ret = wc_RsaPublicKeyDecodeRaw(n, nSz, e, eSz, key); + if (ret != 0) { + WLOG(WS_LOG_DEBUG, "Could not decode RSA public key"); + ret = WS_CRYPTO_FAILED; + } + } + return ret; +} +#endif + +int GetOpenSshPublicKey(WS_KeySignature *key, + const byte* buf, word32 len, word32* idx) +{ + int ret = WS_SUCCESS; + const byte* publicKeyType; + word32 publicKeyTypeSz = 0; + byte keyId; + + ret = GetStringRef(&publicKeyTypeSz, &publicKeyType, buf, len, idx); + keyId = NameToId((const char*)publicKeyType, publicKeyTypeSz); + + switch (keyId) { + #ifndef WOLFSSH_NO_RSA + case ID_SSH_RSA: + 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: + case ID_ECDSA_SHA2_NISTP521: + ret = GetOpenSshPublicKeyEcc(&key->ks.ecc.key, buf, len, idx); + break; + #endif + #ifndef WOLFSSH_NO_ED25519 + case ID_ED25519: + ret = GetOpenSshKeyPublicEd25519(&key->ks.ed25519.key, buf, len, idx); + break; + #endif + default: + ret = WS_UNIMPLEMENTED_E; + break; + } + return ret; +} + +#endif /* WOLFSSH_TPM */ + +/* + * Decodes an OpenSSH format key. + */ +int GetOpenSshKey(WS_KeySignature *key, + const byte* buf, word32 len, word32* idx) +{ + const char AuthMagic[] = "openssh-key-v1"; + const byte* str = NULL; + word32 keyCount = 0, strSz, i; + int ret = WS_SUCCESS; + + if (WSTRCMP(AuthMagic, (const char*)buf) != 0) { + ret = WS_KEY_AUTH_MAGIC_E; + } + + if (ret == WS_SUCCESS) { + *idx += (word32)WSTRLEN(AuthMagic) + 1; + ret = GetSkip(buf, len, idx); /* ciphername */ + } + + if (ret == WS_SUCCESS) + ret = GetSkip(buf, len, idx); /* kdfname */ + + if (ret == WS_SUCCESS) + ret = GetSkip(buf, len, idx); /* kdfoptions */ + + if (ret == WS_SUCCESS) + ret = GetUint32(&keyCount, buf, len, idx); /* key count */ + + if (ret == WS_SUCCESS) { + if (keyCount != WOLFSSH_KEY_QUANTITY_REQ) { + ret = WS_KEY_FORMAT_E; + } + } + + if (ret == WS_SUCCESS) { + strSz = 0; + ret = GetStringRef(&strSz, &str, buf, len, idx); + /* public buf */ + } + + if (ret == WS_SUCCESS) { + strSz = 0; + ret = GetStringRef(&strSz, &str, buf, len, idx); + /* list of private keys */ + + /* If there isn't a private key, the key file is bad. */ + if (ret == WS_SUCCESS && strSz == 0) { + ret = WS_KEY_FORMAT_E; + } + + if (ret == WS_SUCCESS) { + const byte* subStr = NULL; + word32 subStrSz = 0, subIdx = 0, check1 = 0, check2 = ~0; + byte keyId; + + ret = GetUint32(&check1, str, strSz, &subIdx); /* checkint 1 */ + if (ret == WS_SUCCESS) + ret = GetUint32(&check2, str, strSz, &subIdx); /* checkint 2 */ + if (ret == WS_SUCCESS) { + if (check1 != check2) { + ret = WS_KEY_CHECK_VAL_E; + } + } + if (ret == WS_SUCCESS) { + for (i = 0; i < keyCount; i++) { + ret = GetStringRef(&subStrSz, &subStr, + str, strSz, &subIdx); + if (ret == WS_SUCCESS) { + keyId = NameToId((const char*)subStr, subStrSz); + key->keyId = keyId; + } + if (ret == WS_SUCCESS) { + switch (keyId) { + #ifndef WOLFSSH_NO_RSA + case ID_SSH_RSA: + ret = GetOpenSshKeyRsa(&key->ks.rsa.key, + str, strSz, &subIdx); + break; + #endif + #ifndef WOLFSSH_NO_ECDSA + case ID_ECDSA_SHA2_NISTP256: + case ID_ECDSA_SHA2_NISTP384: + case ID_ECDSA_SHA2_NISTP521: + ret = GetOpenSshKeyEcc(&key->ks.ecc.key, + str, strSz, &subIdx); + break; + #endif + #ifndef WOLFSSH_NO_ED25519 + case ID_ED25519: + ret = GetOpenSshKeyEd25519(&key->ks.ed25519.key, + str, strSz, &subIdx); + break; + #endif + default: + ret = WS_UNIMPLEMENTED_E; + break; + } + if (ret == WS_SUCCESS) + ret = GetSkip(str, strSz, &subIdx); + /* key comment */ + } + } + /* Padding: Add increasing digits to pad to the nearest + * block size. Default block size is 8, but depends on + * the encryption algo. The private key chunk's length, + * and the length of the comment delimit the end of the + * encrypted blob. No added padding required. */ + if (ret == WS_SUCCESS) { + if (strSz % MIN_BLOCK_SZ == 0) { + if (strSz > subIdx) { + /* The padding starts at 1. */ + check2 = strSz - subIdx; + for (check1 = 1; + check1 <= check2; + check1++, subIdx++) { + if (check1 != str[subIdx]) { + /* Bad pad value. */ + ret = WS_KEY_FORMAT_E; + break; + } + } + } + } + } + } + } + } + + return ret; +} + + +/* + * Identifies the flavor of an OpenSSH key, RSA or ECDSA, and returns the + * key type ID. The process is to decode the key extracting the identifiers, + * and try to decode the key as the type indicated type. For RSA keys, the + * key format is described as "ssh-rsa". + * + * @param in key to identify + * @param inSz size of key + * @param heap heap to use for memory allocation + * @return keyId as int, WS_MEMORY_E, WS_UNIMPLEMENTED_E, + * WS_INVALID_ALGO_ID + */ +int IdentifyOpenSshKey(const byte* in, word32 inSz, void* heap) +{ + WS_KeySignature *key = NULL; + word32 idx = 0; + int ret; + + key = (WS_KeySignature*)WMALLOC(sizeof(WS_KeySignature), + heap, DYNTYPE_PRIVKEY); + + if (key == NULL) { + ret = WS_MEMORY_E; + } + else { + WMEMSET(key, 0, sizeof(*key)); + key->heap = heap; + key->keyId = ID_NONE; + + ret = GetOpenSshKey(key, in, inSz, &idx); + + if (ret == WS_SUCCESS) { + ret = key->keyId; + } + else if (key->keyId == ID_UNKNOWN) { + ret = WS_UNIMPLEMENTED_E; + } + + wolfSSH_KEY_clean(key); + WFREE(key, heap, DYNTYPE_PRIVKEY); + } + + return ret; +} + + +#ifdef WOLFSSH_OSSH_CERTS + +byte OsshCertBaseId(byte certId) +{ + byte id; + + switch (certId) { + #ifndef WOLFSSH_NO_RSA_SHA2_256 + case ID_OSSH_CERT_RSA: + id = ID_SSH_RSA; + break; + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + case ID_OSSH_CERT_ECDSA_SHA2_NISTP256: + id = ID_ECDSA_SHA2_NISTP256; + break; + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + case ID_OSSH_CERT_ECDSA_SHA2_NISTP384: + id = ID_ECDSA_SHA2_NISTP384; + break; + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + case ID_OSSH_CERT_ECDSA_SHA2_NISTP521: + id = ID_ECDSA_SHA2_NISTP521; + break; + #endif + #ifndef WOLFSSH_NO_ED25519 + case ID_OSSH_CERT_ED25519: + id = ID_ED25519; + break; + #endif + default: + id = ID_UNKNOWN; + } + + return id; +} + + +byte OsshRsaCertSigId(const byte* peerSigId, word32 peerSigIdSz) +{ + byte sigId = ID_RSA_SHA2_256; +#ifndef WOLFSSH_NO_RSA_SHA2_512 + word32 k; + + for (k = 0; k < peerSigIdSz; k++) { + if (peerSigId[k] == ID_RSA_SHA2_512) { + sigId = ID_RSA_SHA2_512; + break; + } + } +#else + (void)peerSigId; + (void)peerSigIdSz; +#endif + return sigId; +} + + +/* Skip the type-specific public key fields embedded in the certificate so the + * raw region can be captured for later reconstruction. Advances *idx. */ +static int OsshSkipUserKey(byte baseId, const byte* blob, word32 blobSz, + word32* idx) +{ + int ret = WS_SUCCESS; + word32 sz = 0; + const byte* p = NULL; + + switch (baseId) { + #ifndef WOLFSSH_NO_RSA + case ID_SSH_RSA: + ret = GetMpint(&sz, &p, blob, blobSz, idx); /* e */ + if (ret == WS_SUCCESS) + ret = GetMpint(&sz, &p, blob, blobSz, idx); /* n */ + break; + #endif + #ifndef WOLFSSH_NO_ECDSA + case ID_ECDSA_SHA2_NISTP256: + case ID_ECDSA_SHA2_NISTP384: + case ID_ECDSA_SHA2_NISTP521: + ret = GetStringRef(&sz, &p, blob, blobSz, idx); /* curve */ + if (ret == WS_SUCCESS) + ret = GetStringRef(&sz, &p, blob, blobSz, idx); /* Q */ + break; + #endif + #ifndef WOLFSSH_NO_ED25519 + case ID_ED25519: + ret = GetStringRef(&sz, &p, blob, blobSz, idx); /* pk */ + break; + #endif + default: + ret = WS_INVALID_ALGO_ID; + } + + return ret; +} + + +int OsshCertParse(WS_OsshCert* cert, byte typeId, const byte* blob, + word32 blobSz) +{ + int ret = WS_SUCCESS; + word32 idx = 0; + word32 ukStart = 0; + word32 caIdx = 0; + const byte* str = NULL; + word32 strSz = 0; + + if (cert == NULL || blob == NULL || blobSz == 0) { + return WS_BAD_ARGUMENT; + } + + WMEMSET(cert, 0, sizeof(*cert)); + cert->blob = blob; + cert->blobSz = blobSz; + cert->typeId = typeId; + cert->baseTypeId = OsshCertBaseId(typeId); + if (cert->baseTypeId == ID_UNKNOWN) { + ret = WS_INVALID_ALGO_ID; + } + + /* string: certificate type name, must match typeId */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&strSz, &str, blob, blobSz, &idx); + } + if (ret == WS_SUCCESS) { + if (NameToId((const char*)str, strSz) != typeId) { + WLOG(WS_LOG_DEBUG, "OSSH: cert type name mismatch"); + ret = WS_INVALID_ALGO_ID; + } + } + + /* string: nonce */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&cert->nonceSz, &cert->nonce, blob, blobSz, &idx); + } + + /* type-specific public key fields (captured as a raw region) */ + ukStart = idx; + if (ret == WS_SUCCESS) { + ret = OsshSkipUserKey(cert->baseTypeId, blob, blobSz, &idx); + } + if (ret == WS_SUCCESS) { + cert->userKeyParms = blob + ukStart; + cert->userKeyParmsSz = idx - ukStart; + } + + /* uint64: serial */ + if (ret == WS_SUCCESS) { + ret = GetUint64(&cert->serial, blob, blobSz, &idx); + } + /* uint32: type (user vs host) */ + if (ret == WS_SUCCESS) { + ret = GetUint32(&cert->certType, blob, blobSz, &idx); + } + /* string: key id */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&cert->keyIdSz, &cert->keyId, blob, blobSz, &idx); + } + /* string: valid principals (a run of inner SSH strings) */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&cert->principalsSz, &cert->principals, blob, blobSz, + &idx); + } + /* uint64: valid after / valid before */ + if (ret == WS_SUCCESS) { + ret = GetUint64(&cert->validAfter, blob, blobSz, &idx); + } + if (ret == WS_SUCCESS) { + ret = GetUint64(&cert->validBefore, blob, blobSz, &idx); + } + /* string: critical options */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&cert->critOptsSz, &cert->critOpts, blob, blobSz, + &idx); + } + /* string: extensions */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&cert->extensionsSz, &cert->extensions, blob, blobSz, + &idx); + } + /* string: reserved (ignored) */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&strSz, &str, blob, blobSz, &idx); + } + /* string: signature key (the CA public key blob) */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&cert->caKeySz, &cert->caKey, blob, blobSz, &idx); + } + /* The CA key blob itself begins with its own type name string. */ + if (ret == WS_SUCCESS) { + caIdx = 0; + ret = GetStringRef(&cert->caKeyTypeSz, &cert->caKeyType, cert->caKey, + cert->caKeySz, &caIdx); + } + + /* Everything up to (but not including) the signature is signed. */ + if (ret == WS_SUCCESS) { + cert->signedLen = idx; + } + + /* string: signature */ + if (ret == WS_SUCCESS) { + ret = GetStringRef(&cert->signatureSz, &cert->signature, blob, blobSz, + &idx); + } + + /* Reject trailing bytes after the signature, matching OpenSSH strictness. */ + if (ret == WS_SUCCESS && idx != blobSz) { + WLOG(WS_LOG_DEBUG, "OSSH: trailing bytes after certificate signature"); + ret = WS_PARSE_E; + } + + return ret; +} + + +int OsshCertCheckType(const WS_OsshCert* cert) +{ + int ret = WS_SUCCESS; + byte caId; + + if (cert == NULL) { + return WS_BAD_ARGUMENT; + } + + if (cert->certType != WOLFSSH_OSSH_CERT_TYPE_USER) { + WLOG(WS_LOG_DEBUG, "OSSH: not a user certificate"); + ret = WS_INVALID_ALGO_ID; + } + + if (ret == WS_SUCCESS) { + caId = NameToId((const char*)cert->caKeyType, cert->caKeyTypeSz); + if (caId != ID_SSH_RSA + && caId != ID_ECDSA_SHA2_NISTP256 + && caId != ID_ECDSA_SHA2_NISTP384 + && caId != ID_ECDSA_SHA2_NISTP521 + && caId != ID_ED25519) { + WLOG(WS_LOG_DEBUG, "OSSH: unsupported CA key type"); + ret = WS_INVALID_ALGO_ID; + } + } + + return ret; +} + + +/* Lexicographic comparison of two length-delimited names. */ +static int OsshNameCmp(const byte* a, word32 aSz, const byte* b, word32 bSz) +{ + word32 n = (aSz < bSz) ? aSz : bSz; + int c = 0; + + if (n > 0) { + c = WMEMCMP(a, b, n); + } + if (c == 0) { + if (aSz < bSz) { + c = -1; + } + else if (aSz > bSz) { + c = 1; + } + } + + return c; +} + + +static int OsshNameEq(const byte* a, word32 aSz, const char* lit) +{ + word32 litSz = (word32)WSTRLEN(lit); + return (aSz == litSz && WMEMCMP(a, lit, litSz) == 0); +} + + +int OsshCertCheckOptions(WS_OsshCert* cert) +{ + int ret = WS_SUCCESS; + word32 idx; + const byte* name = NULL; + word32 nameSz = 0; + const byte* data = NULL; + word32 dataSz = 0; + word32 di = 0; + const byte* prevName = NULL; + word32 prevNameSz = 0; + + if (cert == NULL) { + return WS_BAD_ARGUMENT; + } + + cert->forceCommand = NULL; + cert->forceCommandSz = 0; + cert->sourceAddress = NULL; + cert->sourceAddressSz = 0; + + /* Critical options: ascending order, no duplicates, all recognized. */ + idx = 0; + while (ret == WS_SUCCESS && idx < cert->critOptsSz) { + ret = GetStringRef(&nameSz, &name, cert->critOpts, cert->critOptsSz, + &idx); + if (ret == WS_SUCCESS && prevName != NULL && + OsshNameCmp(prevName, prevNameSz, name, nameSz) >= 0) { + WLOG(WS_LOG_DEBUG, "OSSH: critical options out of order"); + ret = WS_PARSE_E; + } + if (ret == WS_SUCCESS) { + ret = GetStringRef(&dataSz, &data, cert->critOpts, + cert->critOptsSz, &idx); + } + if (ret == WS_SUCCESS) { + di = 0; + if (OsshNameEq(name, nameSz, "force-command")) { + ret = GetStringRef(&cert->forceCommandSz, &cert->forceCommand, + data, dataSz, &di); + } + else if (OsshNameEq(name, nameSz, "source-address")) { + ret = GetStringRef(&cert->sourceAddressSz, + &cert->sourceAddress, data, dataSz, &di); + } + else { + WLOG(WS_LOG_DEBUG, + "OSSH: unsupported critical option, rejecting"); + ret = WS_UNIMPLEMENTED_E; + } + if (ret == WS_SUCCESS && di != dataSz) { + WLOG(WS_LOG_DEBUG, "OSSH: critical option data malformed"); + ret = WS_PARSE_E; + } + } + prevName = name; + prevNameSz = nameSz; + } + + /* Extensions: ascending order, no duplicates; unknown ones are ignored. */ + idx = 0; + prevName = NULL; + prevNameSz = 0; + while (ret == WS_SUCCESS && idx < cert->extensionsSz) { + ret = GetStringRef(&nameSz, &name, cert->extensions, + cert->extensionsSz, &idx); + if (ret == WS_SUCCESS && prevName != NULL && + OsshNameCmp(prevName, prevNameSz, name, nameSz) >= 0) { + WLOG(WS_LOG_DEBUG, "OSSH: extensions out of order"); + ret = WS_PARSE_E; + } + if (ret == WS_SUCCESS) { + ret = GetStringRef(&dataSz, &data, cert->extensions, + cert->extensionsSz, &idx); + } + if (ret == WS_SUCCESS) { + if (!OsshNameEq(name, nameSz, "permit-X11-forwarding") + && !OsshNameEq(name, nameSz, "permit-agent-forwarding") + && !OsshNameEq(name, nameSz, "permit-port-forwarding") + && !OsshNameEq(name, nameSz, "permit-pty") + && !OsshNameEq(name, nameSz, "permit-user-rc") + && !OsshNameEq(name, nameSz, "no-touch-required")) { + WLOG(WS_LOG_DEBUG, + "OSSH: ignoring unrecognized certificate extension"); + } + } + prevName = name; + prevNameSz = nameSz; + } + + return ret; +} + + +#ifndef WOLFSSH_NO_ED25519 +static int OsshVerifyEd25519(const WS_OsshCert* cert, void* heap) +{ + int ret = WS_SUCCESS; + int verified = 0; + word32 idx; + const byte* a = NULL; + word32 aSz = 0; + const byte* sig = NULL; + word32 sigSz = 0; + ed25519_key* key = NULL; + + key = (ed25519_key*)WMALLOC(sizeof(ed25519_key), heap, DYNTYPE_PUBKEY); + if (key == NULL) { + return WS_MEMORY_E; + } + + /* CA key blob: string "ssh-ed25519", string A */ + idx = cert->caKeyTypeSz + LENGTH_SZ; + ret = GetStringRef(&aSz, &a, cert->caKey, cert->caKeySz, &idx); + + /* signature: string "ssh-ed25519", string sig. The sig-type name is + * skipped. */ + if (ret == WS_SUCCESS) { + idx = 0; + ret = GetSkip(cert->signature, cert->signatureSz, &idx); + } + if (ret == WS_SUCCESS) { + ret = GetStringRef(&sigSz, &sig, cert->signature, cert->signatureSz, + &idx); + } + + if (ret == WS_SUCCESS) { + ret = wc_ed25519_init_ex(key, heap, INVALID_DEVID); + + if (ret == WS_SUCCESS) { + ret = wc_ed25519_import_public(a, aSz, key); + } + if (ret == WS_SUCCESS) { + ret = wc_ed25519_verify_msg(sig, sigSz, cert->blob, cert->signedLen, + &verified, key); + } + if (ret == WS_SUCCESS && verified != 1) { + WLOG(WS_LOG_DEBUG, "OSSH: ed25519 CA signature verify failed"); + ret = WS_ED25519_E; + } + + wc_ed25519_free(key); + } + WFREE(key, heap, DYNTYPE_PUBKEY); + + return ret; +} +#endif /* WOLFSSH_NO_ED25519 */ + + +#ifndef WOLFSSH_NO_RSA +static int OsshVerifyRsa(const WS_OsshCert* cert, void* heap) +{ + int ret = WS_SUCCESS; + word32 idx; + const byte* e = NULL; + const byte* n = NULL; + word32 eSz = 0; + word32 nSz = 0; + const byte* sigType = NULL; + word32 sigTypeSz = 0; + const byte* sig = NULL; + word32 sigSz = 0; + enum wc_HashType hashType = WC_HASH_TYPE_NONE; + int hashOid = 0; + byte digest[WC_MAX_DIGEST_SIZE]; + word32 digestSz = 0; + word32 encDigestSz = 0; + RsaKey* key = NULL; +#ifdef WOLFSSH_SMALL_STACK + byte* encDigest = NULL; +#else + byte encDigest[MAX_ENCODED_SIG_SZ]; +#endif + + key = (RsaKey*)WMALLOC(sizeof(RsaKey), heap, DYNTYPE_PUBKEY); + if (key == NULL) { + return WS_MEMORY_E; + } +#ifdef WOLFSSH_SMALL_STACK + encDigest = (byte*)WMALLOC(MAX_ENCODED_SIG_SZ, heap, DYNTYPE_TEMP); + if (encDigest == NULL) { + WFREE(key, heap, DYNTYPE_PUBKEY); + return WS_MEMORY_E; + } +#endif + + /* CA key blob: string "ssh-rsa", mpint e, mpint n */ + idx = cert->caKeyTypeSz + LENGTH_SZ; + ret = GetMpint(&eSz, &e, cert->caKey, cert->caKeySz, &idx); + if (ret == WS_SUCCESS) { + ret = GetMpint(&nSz, &n, cert->caKey, cert->caKeySz, &idx); + } + + /* signature: string sigType, string sig */ + if (ret == WS_SUCCESS) { + idx = 0; + ret = GetStringRef(&sigTypeSz, &sigType, cert->signature, + cert->signatureSz, &idx); + } + if (ret == WS_SUCCESS) { + ret = GetStringRef(&sigSz, &sig, cert->signature, cert->signatureSz, + &idx); + } + + /* select hash from the signature algorithm name */ + if (ret == WS_SUCCESS) { + if (sigTypeSz == 12 && WMEMCMP(sigType, "rsa-sha2-256", 12) == 0) { + hashType = WC_HASH_TYPE_SHA256; + } + else if (sigTypeSz == 12 && WMEMCMP(sigType, "rsa-sha2-512", 12) == 0) { + hashType = WC_HASH_TYPE_SHA512; + } + #ifndef WOLFSSH_NO_SSH_RSA_SHA1 + else if (sigTypeSz == 7 && WMEMCMP(sigType, "ssh-rsa", 7) == 0) { + hashType = WC_HASH_TYPE_SHA; + } + #endif + else { + WLOG(WS_LOG_DEBUG, "OSSH: unsupported RSA CA signature type"); + ret = WS_INVALID_ALGO_ID; + } + } + + if (ret == WS_SUCCESS) { + ret = wc_InitRsaKey(key, heap); + + if (ret == WS_SUCCESS) { + ret = wc_RsaPublicKeyDecodeRaw(n, nSz, e, eSz, key); + } + if (ret == WS_SUCCESS) { + digestSz = (word32)wc_HashGetDigestSize(hashType); + ret = wc_Hash(hashType, cert->blob, cert->signedLen, digest, + digestSz); + } + if (ret == WS_SUCCESS) { + hashOid = wc_HashGetOID(hashType); + if (hashOid <= 0) { + ret = WS_INVALID_ALGO_ID; + } + } + if (ret == WS_SUCCESS) { + encDigestSz = wc_EncodeSignature(encDigest, digest, digestSz, + hashOid); + if (encDigestSz == 0) { + ret = WS_CRYPTO_FAILED; + } + } + if (ret == WS_SUCCESS) { + ret = wolfSSH_RsaVerify(sig, sigSz, encDigest, encDigestSz, key, + heap, "OSSH CA"); + } + + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "OSSH: RSA CA signature verify failed (%d)", + ret); + } + + wc_FreeRsaKey(key); + } + WFREE(key, heap, DYNTYPE_PUBKEY); +#ifdef WOLFSSH_SMALL_STACK + WFREE(encDigest, heap, DYNTYPE_TEMP); +#endif + + return ret; +} +#endif /* WOLFSSH_NO_RSA */ + + +#ifndef WOLFSSH_NO_ECDSA +static int OsshVerifyEcc(const WS_OsshCert* cert, void* heap) +{ + int ret = WS_SUCCESS; + word32 idx; + const byte* q = NULL; + word32 qSz = 0; + const byte* sigBlob = NULL; + word32 sigBlobSz = 0; + const byte* r = NULL; + const byte* s = NULL; + word32 rSz = 0; + word32 sSz = 0; + word32 blobIdx = 0; + enum wc_HashType hashType = WC_HASH_TYPE_NONE; + byte digest[WC_MAX_DIGEST_SIZE]; + word32 digestSz = 0; + word32 asnSigSz = ECC_MAX_SIG_SIZE; + ecc_key* key = NULL; +#ifdef WOLFSSH_SMALL_STACK + byte* asnSig = NULL; +#else + byte asnSig[ECC_MAX_SIG_SIZE]; +#endif + + key = (ecc_key*)WMALLOC(sizeof(ecc_key), heap, DYNTYPE_PUBKEY); + if (key == NULL) { + return WS_MEMORY_E; + } +#ifdef WOLFSSH_SMALL_STACK + asnSig = (byte*)WMALLOC(ECC_MAX_SIG_SIZE, heap, DYNTYPE_TEMP); + if (asnSig == NULL) { + WFREE(key, heap, DYNTYPE_PUBKEY); + return WS_MEMORY_E; + } +#endif + + /* hash by the CA key's curve: the CA produced the signature, so the digest + * is bound to the CA key type, not the certified user key. */ + switch (NameToId((const char*)cert->caKeyType, cert->caKeyTypeSz)) { + case ID_ECDSA_SHA2_NISTP256: + hashType = WC_HASH_TYPE_SHA256; + break; + case ID_ECDSA_SHA2_NISTP384: + hashType = WC_HASH_TYPE_SHA384; + break; + case ID_ECDSA_SHA2_NISTP521: + hashType = WC_HASH_TYPE_SHA512; + break; + default: + ret = WS_INVALID_ALGO_ID; + } + + /* CA key blob: string type, string curve, string Q. The curve name is + * skipped; the hash comes from the CA key type and the curve is derived + * from Q by wc_ecc_import_x963. */ + if (ret == WS_SUCCESS) { + idx = cert->caKeyTypeSz + LENGTH_SZ; + ret = GetSkip(cert->caKey, cert->caKeySz, &idx); + } + if (ret == WS_SUCCESS) { + ret = GetStringRef(&qSz, &q, cert->caKey, cert->caKeySz, &idx); + } + + /* signature: string sigType, string (mpint r, mpint s). The sig-type name + * is skipped. */ + if (ret == WS_SUCCESS) { + idx = 0; + ret = GetSkip(cert->signature, cert->signatureSz, &idx); + } + if (ret == WS_SUCCESS) { + ret = GetStringRef(&sigBlobSz, &sigBlob, cert->signature, + cert->signatureSz, &idx); + } + if (ret == WS_SUCCESS) { + ret = GetMpint(&rSz, &r, sigBlob, sigBlobSz, &blobIdx); + } + if (ret == WS_SUCCESS) { + ret = GetMpint(&sSz, &s, sigBlob, sigBlobSz, &blobIdx); + } + + if (ret == WS_SUCCESS) { + ret = wc_ecc_init_ex(key, heap, INVALID_DEVID); + + if (ret == WS_SUCCESS) { + ret = wc_ecc_import_x963(q, qSz, key); + } + if (ret == WS_SUCCESS) { + ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz, asnSig, &asnSigSz); + } + if (ret == WS_SUCCESS) { + digestSz = (word32)wc_HashGetDigestSize(hashType); + ret = wc_Hash(hashType, cert->blob, cert->signedLen, digest, + digestSz); + } + if (ret == WS_SUCCESS) { + ret = wc_SignatureVerifyHash(hashType, WC_SIGNATURE_TYPE_ECC, + digest, digestSz, asnSig, asnSigSz, key, + (word32)sizeof(*key)); + } + if (ret != WS_SUCCESS) { + /* Preserve the specific error (import/hash/verify), like the RSA + * path, instead of collapsing every failure to WS_ECC_E. */ + WLOG(WS_LOG_DEBUG, "OSSH: ECDSA CA signature verify failed (%d)", + ret); + } + + wc_ecc_free(key); + } + WFREE(key, heap, DYNTYPE_PUBKEY); +#ifdef WOLFSSH_SMALL_STACK + WFREE(asnSig, heap, DYNTYPE_TEMP); +#endif + + return ret; +} +#endif /* WOLFSSH_NO_ECDSA */ + + +int OsshCertVerifySignature(const WS_OsshCert* cert, void* heap) +{ + int ret; + byte caId; + + if (cert == NULL || cert->caKey == NULL || cert->signature == NULL) { + return WS_BAD_ARGUMENT; + } + + caId = NameToId((const char*)cert->caKeyType, cert->caKeyTypeSz); + + switch (caId) { + #ifndef WOLFSSH_NO_RSA + case ID_SSH_RSA: + ret = OsshVerifyRsa(cert, heap); + break; + #endif + #ifndef WOLFSSH_NO_ECDSA + case ID_ECDSA_SHA2_NISTP256: + case ID_ECDSA_SHA2_NISTP384: + case ID_ECDSA_SHA2_NISTP521: + ret = OsshVerifyEcc(cert, heap); + break; + #endif + #ifndef WOLFSSH_NO_ED25519 + case ID_ED25519: + ret = OsshVerifyEd25519(cert, heap); + break; + #endif + default: + WLOG(WS_LOG_DEBUG, "OSSH: unsupported CA key type for verify"); + ret = WS_INVALID_ALGO_ID; + } + + return ret; +} + +#endif /* WOLFSSH_OSSH_CERTS */ diff --git a/src/ssh.c b/src/ssh.c index 45761d808..3d69d7745 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2579,6 +2579,26 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, #endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_OSSH_CERTS +/* Load an OpenSSH-format host certificate for the server to present during + * KEX. Host-side OpenSSH certificate presentation is not implemented yet; + * OpenSSH certificate support currently covers user authentication only. */ +int wolfSSH_CTX_UseOsshCert_buffer(WOLFSSH_CTX* ctx, + const byte* cert, word32 certSz) +{ + WOLFSSH_UNUSED(ctx); + WOLFSSH_UNUSED(cert); + WOLFSSH_UNUSED(certSz); + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_UseOsshCert_buffer()"); + WLOG(WS_LOG_DEBUG, + "[SSH] OpenSSH host certificates are not implemented"); + + return WS_UNIMPLEMENTED_E; +} +#endif /* WOLFSSH_OSSH_CERTS */ + + int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, word32 windowSz, word32 maxPacketSz) { diff --git a/tests/api.c b/tests/api.c index d511419ca..0ed86d048 100644 --- a/tests/api.c +++ b/tests/api.c @@ -51,6 +51,9 @@ #ifdef WOLFSSH_AGENT #include #endif +#ifdef WOLFSSH_OSSH_CERTS + #include +#endif #if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP) #define WOLFSSH_TEST_LOCKING @@ -1388,6 +1391,388 @@ static void test_wolfSSH_agent_signrequest_success(void) #endif /* WOLFSSH_AGENT */ +#ifdef WOLFSSH_OSSH_CERTS + +/* The committed positive vectors all certify an Ed25519 user key, so the + * parse/verify tests below require Ed25519. */ +#ifndef WOLFSSH_NO_ED25519 + +/* Committed positive test vectors: three OpenSSH user certificates, each over + * an Ed25519 user key with a "force-command" critical option and signed by a + * different CA (Ed25519, RSA, ECDSA), made non-expiring (always:forever). They + * are self-verifying: the signing CA public key is embedded in the cert, so the + * ossh.c parse and CA-signature verify paths get coverage under make check + * without ssh-keygen or the --enable-ossh-certs shell tests. */ +static const byte ossh_vec_ed[] = { + 0x00, 0x00, 0x00, 0x20, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, + 0x35, 0x31, 0x39, 0x2d, 0x63, 0x65, 0x72, 0x74, 0x2d, 0x76, 0x30, 0x31, + 0x40, 0x6f, 0x70, 0x65, 0x6e, 0x73, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, + 0x00, 0x00, 0x00, 0x20, 0xcb, 0x18, 0x51, 0x9f, 0x07, 0x48, 0x3d, 0x38, + 0xbc, 0x07, 0xa6, 0xcc, 0x2f, 0x0b, 0x92, 0x9a, 0x5a, 0x45, 0x36, 0x96, + 0x07, 0xc1, 0xc8, 0xc6, 0xf4, 0x4b, 0x7b, 0x59, 0x93, 0x08, 0x64, 0xc4, + 0x00, 0x00, 0x00, 0x20, 0xc1, 0xcd, 0x74, 0xac, 0x52, 0x54, 0xb6, 0x7d, + 0x1a, 0xcf, 0xab, 0xf3, 0x96, 0x90, 0x8a, 0xed, 0xea, 0x93, 0x1c, 0xdc, + 0xb7, 0x31, 0x73, 0x7d, 0xda, 0x3d, 0xd2, 0x5d, 0x0c, 0xcc, 0x41, 0x67, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x06, 0x76, 0x65, 0x63, 0x2d, 0x65, 0x64, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x66, 0x72, 0x65, 0x64, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0d, 0x66, 0x6f, + 0x72, 0x63, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x00, + 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x07, 0x65, 0x63, 0x68, 0x6f, 0x20, + 0x68, 0x69, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x15, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x58, 0x31, 0x31, 0x2d, 0x66, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x17, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2d, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, + 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x70, 0x6f, 0x72, 0x74, 0x2d, 0x66, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x70, + 0x74, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x72, 0x63, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, + 0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, + 0x35, 0x31, 0x39, 0x00, 0x00, 0x00, 0x20, 0x19, 0x7f, 0x5a, 0xb7, 0x58, + 0xfc, 0x3f, 0x57, 0xbd, 0x59, 0x77, 0x07, 0xe6, 0x97, 0x72, 0x27, 0xc8, + 0xa0, 0xe6, 0xa2, 0x07, 0xd7, 0xb0, 0xf7, 0x67, 0xe2, 0xc7, 0x50, 0x75, + 0x45, 0x96, 0x7d, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x0b, 0x73, + 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00, 0x00, + 0x00, 0x40, 0xc1, 0xb0, 0xee, 0x8a, 0xb3, 0xda, 0xbf, 0x87, 0x00, 0x9c, + 0xf1, 0x06, 0xdd, 0x57, 0xce, 0x2e, 0x33, 0xd7, 0xe9, 0x6f, 0x88, 0x6b, + 0xd0, 0x29, 0xcc, 0x90, 0xde, 0x0e, 0x48, 0x98, 0xd1, 0x3a, 0x66, 0x8b, + 0xe2, 0x61, 0x69, 0x0a, 0x78, 0xe2, 0x05, 0x59, 0x4c, 0x8e, 0x00, 0x3f, + 0xed, 0x9f, 0x3d, 0x38, 0xea, 0x27, 0x37, 0xe8, 0x2a, 0x64, 0xe2, 0x73, + 0xc9, 0xbd, 0x7f, 0xc0, 0x78, 0x0c +}; + +static const byte ossh_vec_rsa[] = { + 0x00, 0x00, 0x00, 0x20, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, + 0x35, 0x31, 0x39, 0x2d, 0x63, 0x65, 0x72, 0x74, 0x2d, 0x76, 0x30, 0x31, + 0x40, 0x6f, 0x70, 0x65, 0x6e, 0x73, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, + 0x00, 0x00, 0x00, 0x20, 0xeb, 0xfa, 0x09, 0x63, 0xae, 0x0c, 0xaf, 0xd5, + 0x04, 0xf0, 0xbc, 0x8f, 0xe1, 0x7f, 0x71, 0x66, 0x34, 0x56, 0x87, 0xce, + 0x3e, 0xce, 0xbf, 0x0f, 0xd5, 0x1a, 0xe0, 0x05, 0x33, 0xc3, 0xf1, 0x97, + 0x00, 0x00, 0x00, 0x20, 0xc1, 0xcd, 0x74, 0xac, 0x52, 0x54, 0xb6, 0x7d, + 0x1a, 0xcf, 0xab, 0xf3, 0x96, 0x90, 0x8a, 0xed, 0xea, 0x93, 0x1c, 0xdc, + 0xb7, 0x31, 0x73, 0x7d, 0xda, 0x3d, 0xd2, 0x5d, 0x0c, 0xcc, 0x41, 0x67, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x07, 0x76, 0x65, 0x63, 0x2d, 0x72, 0x73, 0x61, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x66, 0x72, 0x65, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0d, 0x66, + 0x6f, 0x72, 0x63, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x07, 0x65, 0x63, 0x68, 0x6f, + 0x20, 0x68, 0x69, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x15, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x58, 0x31, 0x31, 0x2d, 0x66, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x17, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2d, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x70, 0x6f, 0x72, 0x74, 0x2d, 0x66, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, + 0x70, 0x74, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x72, + 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x17, 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, + 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, + 0xaf, 0xce, 0x0f, 0xf3, 0x04, 0x75, 0xd3, 0x23, 0x93, 0x75, 0x08, 0x5f, + 0x35, 0xfa, 0x87, 0x23, 0xf0, 0x88, 0xce, 0x15, 0xc0, 0x68, 0xec, 0x4f, + 0x5b, 0x6d, 0x9d, 0x8f, 0x59, 0xc5, 0x16, 0x59, 0xdc, 0x0f, 0x28, 0xd7, + 0x92, 0x29, 0xfd, 0xf5, 0x80, 0xf3, 0x6f, 0xa4, 0xfc, 0xb3, 0x75, 0x84, + 0xe3, 0x5d, 0xf0, 0x84, 0xea, 0x15, 0x1c, 0x48, 0xec, 0x91, 0x25, 0x61, + 0x22, 0xc8, 0xa0, 0x74, 0xaf, 0x86, 0xc7, 0xb8, 0x0d, 0x6a, 0x17, 0x51, + 0x57, 0xda, 0x75, 0x9f, 0x7b, 0x7a, 0x3f, 0x7f, 0xd1, 0xcd, 0x64, 0xc6, + 0x89, 0xcf, 0x93, 0xfc, 0x49, 0x0f, 0x00, 0x82, 0x8c, 0xec, 0x8f, 0xd6, + 0x71, 0x23, 0x83, 0x35, 0x3a, 0x75, 0x95, 0x26, 0x36, 0x60, 0x10, 0xd0, + 0xfb, 0xd0, 0x3d, 0x3a, 0x5b, 0xb8, 0x9d, 0x68, 0x3f, 0x3c, 0xfc, 0xbc, + 0x44, 0x6c, 0xb7, 0x7d, 0x42, 0xed, 0xcd, 0xde, 0xd2, 0xfb, 0xd5, 0xbd, + 0x64, 0x35, 0xee, 0xf6, 0xf7, 0xe0, 0x09, 0xa6, 0xa5, 0x9b, 0x7a, 0x11, + 0xd9, 0xae, 0x7f, 0x86, 0x48, 0x7d, 0x77, 0x14, 0xc1, 0xbd, 0x2b, 0xbd, + 0x86, 0xf4, 0x23, 0x9a, 0x7a, 0x07, 0xa8, 0xdd, 0x11, 0x85, 0x43, 0xa3, + 0xb0, 0xcd, 0x9f, 0xfb, 0x77, 0x7e, 0x8d, 0x5d, 0xd1, 0xc0, 0x48, 0x9b, + 0x7a, 0x79, 0x2f, 0x8f, 0x4b, 0xa3, 0xb7, 0xe6, 0x10, 0x71, 0xde, 0x0c, + 0xa3, 0xf6, 0x71, 0xe9, 0xb4, 0x54, 0xd9, 0x82, 0x64, 0xf6, 0x76, 0x70, + 0x84, 0xab, 0xfc, 0xf3, 0x73, 0x1e, 0xc7, 0x60, 0xaa, 0x43, 0xbf, 0x59, + 0xbe, 0x1e, 0xc6, 0x5b, 0xda, 0x3e, 0x5c, 0x9a, 0xaa, 0xb7, 0x78, 0xbc, + 0x71, 0x24, 0x57, 0xad, 0xce, 0x1b, 0xf4, 0x84, 0x29, 0xa4, 0xaa, 0x22, + 0x3a, 0x80, 0xc6, 0x9f, 0x64, 0xfe, 0xd2, 0xcf, 0x87, 0x51, 0xa4, 0xcc, + 0x4e, 0x76, 0x3c, 0xf5, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x0c, + 0x72, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x35, 0x31, 0x32, + 0x00, 0x00, 0x01, 0x00, 0x3a, 0x8e, 0x29, 0xbb, 0xb8, 0x5f, 0xef, 0x07, + 0x12, 0x31, 0xd7, 0xc9, 0xd6, 0x88, 0xdf, 0x10, 0xcd, 0x79, 0x54, 0x0c, + 0x17, 0xab, 0xbf, 0xde, 0x00, 0xdb, 0x35, 0x7a, 0x0d, 0x67, 0x7d, 0x27, + 0xaf, 0x69, 0xa2, 0x3d, 0xaf, 0x44, 0xf8, 0xcd, 0xf9, 0xcc, 0xd9, 0xb2, + 0x01, 0xfd, 0xa6, 0x7a, 0x44, 0x4a, 0xcc, 0x77, 0xf8, 0xb8, 0xa4, 0xc2, + 0x5a, 0x0d, 0x03, 0x73, 0x71, 0x26, 0xf5, 0xf0, 0x24, 0x7d, 0xb9, 0xf4, + 0x99, 0x1b, 0xc5, 0xa0, 0x23, 0x12, 0xae, 0xe2, 0x0d, 0x36, 0xf5, 0x9c, + 0xf4, 0xba, 0xa4, 0x96, 0xaa, 0x57, 0xeb, 0xf4, 0x38, 0x79, 0x6e, 0xad, + 0x0c, 0x8a, 0x81, 0x6a, 0xc9, 0xda, 0xe7, 0x4a, 0x81, 0x5e, 0x4c, 0x61, + 0x64, 0x04, 0x3d, 0x0b, 0x29, 0x51, 0x91, 0x1f, 0xb5, 0x31, 0x5f, 0xf7, + 0x12, 0x95, 0xe9, 0x9b, 0x9b, 0x8e, 0xf8, 0x66, 0x48, 0xbd, 0x84, 0x4c, + 0x29, 0x91, 0x6d, 0xbd, 0x09, 0x72, 0xfd, 0x18, 0x83, 0x8c, 0xd4, 0x38, + 0x36, 0x7f, 0x1f, 0x31, 0x02, 0xc3, 0x83, 0xb0, 0x24, 0x24, 0x47, 0xca, + 0x2e, 0xf8, 0xe2, 0xf0, 0x70, 0x04, 0x31, 0x03, 0x74, 0x38, 0xd5, 0xce, + 0x85, 0x39, 0x39, 0x38, 0x87, 0x34, 0x75, 0xc1, 0x63, 0x4a, 0x1a, 0xed, + 0x7d, 0xa9, 0x8b, 0xee, 0x37, 0xf0, 0x45, 0xcb, 0x89, 0xb4, 0xce, 0x36, + 0x74, 0xd7, 0x04, 0x02, 0xcf, 0xad, 0x62, 0x21, 0x81, 0x1a, 0xc9, 0xf0, + 0x25, 0x91, 0xa7, 0xcb, 0xbe, 0xe0, 0xa8, 0x8a, 0x03, 0x3b, 0x20, 0xa7, + 0xb4, 0x72, 0xbe, 0xc2, 0xe6, 0x33, 0xd2, 0x5c, 0xc8, 0xeb, 0xba, 0x17, + 0xf6, 0x30, 0x59, 0x9f, 0x73, 0xc0, 0xee, 0xbf, 0xcc, 0x36, 0xa9, 0xf0, + 0x7c, 0xcf, 0x17, 0x9f, 0x07, 0x26, 0x41, 0xc1, 0x8f, 0x44, 0x67, 0xf6, + 0xc4, 0x56, 0x49, 0x95, 0x04, 0x3d, 0xc4, 0x7d +}; + +static const byte ossh_vec_ecc[] = { + 0x00, 0x00, 0x00, 0x20, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, + 0x35, 0x31, 0x39, 0x2d, 0x63, 0x65, 0x72, 0x74, 0x2d, 0x76, 0x30, 0x31, + 0x40, 0x6f, 0x70, 0x65, 0x6e, 0x73, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, + 0x00, 0x00, 0x00, 0x20, 0x70, 0x73, 0x37, 0x68, 0x54, 0x47, 0x3a, 0x25, + 0xe0, 0xc7, 0xd1, 0xfa, 0x68, 0xc8, 0xe6, 0x76, 0xb3, 0xd9, 0x88, 0x82, + 0x4e, 0x29, 0xaa, 0xbf, 0x7e, 0xa6, 0x9c, 0xd5, 0x7f, 0xeb, 0x7d, 0x3e, + 0x00, 0x00, 0x00, 0x20, 0xc1, 0xcd, 0x74, 0xac, 0x52, 0x54, 0xb6, 0x7d, + 0x1a, 0xcf, 0xab, 0xf3, 0x96, 0x90, 0x8a, 0xed, 0xea, 0x93, 0x1c, 0xdc, + 0xb7, 0x31, 0x73, 0x7d, 0xda, 0x3d, 0xd2, 0x5d, 0x0c, 0xcc, 0x41, 0x67, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x07, 0x76, 0x65, 0x63, 0x2d, 0x65, 0x63, 0x63, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x66, 0x72, 0x65, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0d, 0x66, + 0x6f, 0x72, 0x63, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x07, 0x65, 0x63, 0x68, 0x6f, + 0x20, 0x68, 0x69, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x15, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x58, 0x31, 0x31, 0x2d, 0x66, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x17, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2d, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x70, 0x6f, 0x72, 0x74, 0x2d, 0x66, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, + 0x70, 0x74, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x74, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x72, + 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, + 0x68, 0x61, 0x32, 0x2d, 0x6e, 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, + 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, + 0x00, 0x00, 0x00, 0x41, 0x04, 0x45, 0x2d, 0xd1, 0x42, 0x5a, 0x47, 0xeb, + 0x27, 0x6f, 0x78, 0xe4, 0xa8, 0x9e, 0x4e, 0x93, 0x21, 0x68, 0xff, 0xfb, + 0x7b, 0x06, 0x31, 0xf2, 0x30, 0xb7, 0xb0, 0x7e, 0xe2, 0x28, 0xc2, 0x18, + 0x99, 0x51, 0x24, 0xac, 0x55, 0x2b, 0xb3, 0x7f, 0xdb, 0x71, 0x5c, 0x34, + 0x68, 0xe0, 0x12, 0x06, 0x5a, 0x01, 0x12, 0xd0, 0x1b, 0x62, 0x19, 0x1c, + 0x14, 0xe3, 0xf0, 0xcb, 0xe8, 0xf5, 0x6c, 0xae, 0x70, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, + 0x68, 0x61, 0x32, 0x2d, 0x6e, 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, + 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x21, 0x00, 0xc9, 0x89, 0xf5, + 0xc2, 0xe1, 0x51, 0x4e, 0x2a, 0x68, 0x9b, 0xf1, 0x07, 0x17, 0xf1, 0x07, + 0x49, 0xbc, 0x28, 0xf2, 0x79, 0x71, 0xf9, 0x8b, 0xb3, 0x08, 0x51, 0x38, + 0x17, 0xb8, 0x5f, 0x8c, 0xe7, 0x00, 0x00, 0x00, 0x20, 0x70, 0xfa, 0xfa, + 0xf1, 0x0e, 0xd5, 0x70, 0x22, 0x47, 0xea, 0x6a, 0x7c, 0xa6, 0x98, 0xc3, + 0x2c, 0x9e, 0xca, 0x2c, 0xb5, 0xe0, 0xcc, 0x46, 0x36, 0x6a, 0xb7, 0x8c, + 0x49, 0xda, 0x40, 0x97, 0x30 +}; + +/* Parse, verify the CA signature, and validate the options of each committed + * certificate vector; then flip the final signature byte and confirm the + * verification fails while the parse still succeeds. */ +static void test_wolfSSH_OsshCert_valid(void) +{ + const byte* blobs[3]; + word32 sizes[3]; + byte tampered[1024]; + WS_OsshCert cert; + int v; + + blobs[0] = ossh_vec_ed; sizes[0] = (word32)sizeof(ossh_vec_ed); + blobs[1] = ossh_vec_rsa; sizes[1] = (word32)sizeof(ossh_vec_rsa); + blobs[2] = ossh_vec_ecc; sizes[2] = (word32)sizeof(ossh_vec_ecc); + + for (v = 0; v < 3; v++) { + WMEMSET(&cert, 0, sizeof(cert)); + AssertIntEQ(OsshCertParse(&cert, ID_OSSH_CERT_ED25519, blobs[v], + sizes[v]), WS_SUCCESS); + AssertIntEQ((int)cert.certType, WOLFSSH_OSSH_CERT_TYPE_USER); + AssertIntEQ(OsshCertCheckType(&cert), WS_SUCCESS); + AssertIntEQ(OsshCertVerifySignature(&cert, NULL), WS_SUCCESS); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_SUCCESS); + AssertNotNull(cert.forceCommand); + + AssertIntLE((int)sizes[v], (int)sizeof(tampered)); + WMEMCPY(tampered, blobs[v], sizes[v]); + tampered[sizes[v] - 1] ^= 0xFF; + WMEMSET(&cert, 0, sizeof(cert)); + AssertIntEQ(OsshCertParse(&cert, ID_OSSH_CERT_ED25519, tampered, + sizes[v]), WS_SUCCESS); + AssertIntNE(OsshCertVerifySignature(&cert, NULL), WS_SUCCESS); + } +} +/* OsshCertParse and OsshCertVerifySignature run on the attacker-controlled + * certificate blob before any CA-trust check, so a malformed/truncated blob + * from an unauthenticated peer must be rejected with a negative return and + * must not crash. Run under ASan to confirm no out-of-bounds read and no free + * of an uninitialized key (the CA verifiers allocate the key before parsing + * its inner fields). */ +static void test_wolfSSH_OsshCert_malformed(void) +{ + static const byte garbage[64] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + WS_OsshCert cert; + byte caKey[32]; + byte sig[16]; + word32 caTypeSz; + word32 i; + int t; + /* CA key type names whose bodies are then truncated. */ + static const char* caTypes[] = { + "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256" + }; + + /* Parser: every truncation of a hostile buffer must fail, never succeed + * and never read out of bounds. */ + WMEMSET(caKey, 0, sizeof(caKey)); + for (i = 0; i <= sizeof(garbage); i++) { + AssertIntNE(OsshCertParse(&cert, ID_OSSH_CERT_ED25519, garbage, i), + WS_SUCCESS); + } + + /* Verifiers: a CA key blob holding only its type string (no key body) + * parses far enough to dispatch by CA type, then fails on the truncated + * body. Exercises the allocate-key-before-parse path for all three. */ + WMEMSET(sig, 0xA5, sizeof(sig)); + for (t = 0; t < (int)(sizeof(caTypes) / sizeof(caTypes[0])); t++) { + caTypeSz = (word32)WSTRLEN(caTypes[t]); + + WMEMSET(&cert, 0, sizeof(cert)); + cert.blob = garbage; + cert.blobSz = (word32)sizeof(garbage); + cert.signedLen = 0; + cert.caKeyType = (const byte*)caTypes[t]; + cert.caKeyTypeSz = caTypeSz; + cert.caKey = caKey; + cert.caKeySz = LENGTH_SZ + caTypeSz; /* type only, body truncated */ + cert.signature = sig; + cert.signatureSz = (word32)sizeof(sig); + + AssertIntNE(OsshCertVerifySignature(&cert, NULL), WS_SUCCESS); + } +} + +#endif /* !WOLFSSH_NO_ED25519 */ + +/* Direct coverage for OsshCertCheckOptions: the ascending-order/no-duplicate + * rule, unknown-critical-option rejection, malformed option data, and the + * tolerate-unknown-extension behavior. These branches are otherwise only hit by + * the --enable-ossh-certs shell test, so cover them under plain make check. + * SSH strings are a 4-byte big-endian length followed by that many bytes; each + * option is a name string then a data string, and for force-command/ + * source-address the data string wraps the option value string. */ +static void test_wolfSSH_OsshCert_options(void) +{ + WS_OsshCert cert; + + /* force-command="hi" then source-address="x": ascending, well-formed. */ + static const byte critOk[] = { + 0,0,0,0x0D, 'f','o','r','c','e','-','c','o','m','m','a','n','d', + 0,0,0,0x06, 0,0,0,0x02, 'h','i', + 0,0,0,0x0E, 's','o','u','r','c','e','-','a','d','d','r','e','s','s', + 0,0,0,0x05, 0,0,0,0x01, 'x' + }; + /* source-address before force-command: not ascending. */ + static const byte critOutOfOrder[] = { + 0,0,0,0x0E, 's','o','u','r','c','e','-','a','d','d','r','e','s','s', + 0,0,0,0x05, 0,0,0,0x01, 'x', + 0,0,0,0x0D, 'f','o','r','c','e','-','c','o','m','m','a','n','d', + 0,0,0,0x06, 0,0,0,0x02, 'h','i' + }; + /* force-command twice: duplicate name. */ + static const byte critDup[] = { + 0,0,0,0x0D, 'f','o','r','c','e','-','c','o','m','m','a','n','d', + 0,0,0,0x06, 0,0,0,0x02, 'h','i', + 0,0,0,0x0D, 'f','o','r','c','e','-','c','o','m','m','a','n','d', + 0,0,0,0x06, 0,0,0,0x02, 'h','i' + }; + /* unrecognized critical option name: must be rejected. */ + static const byte critUnknown[] = { + 0,0,0,0x07, 'm','a','d','e','-','u','p', + 0,0,0,0x00 + }; + /* force-command value followed by a trailing byte (di != dataSz). */ + static const byte critMalformed[] = { + 0,0,0,0x0D, 'f','o','r','c','e','-','c','o','m','m','a','n','d', + 0,0,0,0x07, 0,0,0,0x02, 'h','i', 'Z' + }; + /* unknown extension with empty data: tolerated (ignored). */ + static const byte extUnknown[] = { + 0,0,0,0x0B, 'm','a','d','e','-','u','p','-','e','x','t', + 0,0,0,0x00 + }; + /* permit-pty before permit-agent-forwarding: not ascending. */ + static const byte extOutOfOrder[] = { + 0,0,0,0x0A, 'p','e','r','m','i','t','-','p','t','y', + 0,0,0,0x00, + 0,0,0,0x17, 'p','e','r','m','i','t','-','a','g','e','n','t','-', + 'f','o','r','w','a','r','d','i','n','g', + 0,0,0,0x00 + }; + + /* no options: valid, nothing extracted. */ + WMEMSET(&cert, 0, sizeof(cert)); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_SUCCESS); + AssertNull(cert.forceCommand); + AssertNull(cert.sourceAddress); + + /* happy path: both options extracted. */ + WMEMSET(&cert, 0, sizeof(cert)); + cert.critOpts = critOk; + cert.critOptsSz = (word32)sizeof(critOk); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_SUCCESS); + AssertNotNull(cert.forceCommand); + AssertIntEQ((int)cert.forceCommandSz, 2); + AssertIntEQ(WMEMCMP(cert.forceCommand, "hi", 2), 0); + AssertNotNull(cert.sourceAddress); + AssertIntEQ((int)cert.sourceAddressSz, 1); + + /* out-of-order critical options. */ + WMEMSET(&cert, 0, sizeof(cert)); + cert.critOpts = critOutOfOrder; + cert.critOptsSz = (word32)sizeof(critOutOfOrder); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_PARSE_E); + + /* duplicate critical option. */ + WMEMSET(&cert, 0, sizeof(cert)); + cert.critOpts = critDup; + cert.critOptsSz = (word32)sizeof(critDup); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_PARSE_E); + + /* unknown critical option. */ + WMEMSET(&cert, 0, sizeof(cert)); + cert.critOpts = critUnknown; + cert.critOptsSz = (word32)sizeof(critUnknown); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_UNIMPLEMENTED_E); + + /* malformed critical-option data (trailing byte). */ + WMEMSET(&cert, 0, sizeof(cert)); + cert.critOpts = critMalformed; + cert.critOptsSz = (word32)sizeof(critMalformed); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_PARSE_E); + + /* unknown extension is ignored. */ + WMEMSET(&cert, 0, sizeof(cert)); + cert.extensions = extUnknown; + cert.extensionsSz = (word32)sizeof(extUnknown); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_SUCCESS); + + /* out-of-order extensions. */ + WMEMSET(&cert, 0, sizeof(cert)); + cert.extensions = extOutOfOrder; + cert.extensionsSz = (word32)sizeof(extOutOfOrder); + AssertIntEQ(OsshCertCheckOptions(&cert), WS_PARSE_E); +} +#endif /* WOLFSSH_OSSH_CERTS */ + + #if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) && \ !defined(SINGLE_THREADED) @@ -3742,6 +4127,13 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_agent_signrequest_signature_too_large(); test_wolfSSH_agent_signrequest_success(); #endif +#ifdef WOLFSSH_OSSH_CERTS +#ifndef WOLFSSH_NO_ED25519 + test_wolfSSH_OsshCert_valid(); + test_wolfSSH_OsshCert_malformed(); +#endif + test_wolfSSH_OsshCert_options(); +#endif #ifdef WOLFSSH_KEYBOARD_INTERACTIVE test_wolfSSH_KeyboardInteractive(); #endif diff --git a/tests/unit.c b/tests/unit.c index e18981ef7..ca12af161 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -2784,6 +2784,7 @@ static int test_DoUserAuthRequestEd25519(void) word32 dataToSignSz = 0; word32 checkDataSz = 0; word32 sigSz = sizeof(sig); + word32 fieldSum = 0; word32 off; int result = 0; int ret; @@ -2884,7 +2885,8 @@ static int test_DoUserAuthRequestEd25519(void) authData.sf.publicKey.signature = sigBlob; authData.sf.publicKey.signatureSz = sigBlobSz; - /* Positive case: untouched signature must verify. */ + /* Positive case: untouched signature must verify. authData was zeroed, so + * dataToSignSz is 0 here and the field-sum fallback is exercised. */ ret = wolfSSH_TestDoUserAuthRequestEd25519(ssh, &authData); if (ret != WS_SUCCESS) { printf("DoUserAuthRequestEd25519 positive: ret=%d (expected %d)\n", @@ -2892,6 +2894,24 @@ static int test_DoUserAuthRequestEd25519(void) result = -605; goto done; } + /* The wire-derived signed length must equal the legacy field-sum for a + * plain Ed25519 request and verify identically; lock in that equivalence + * so the non-certificate path cannot regress. */ + fieldSum = usernameSz + serviceNameSz + authNameSz + BOOLEAN_SZ + + keyTypeNameSz + pubKeyBlobSz + (UINT32_SZ * 5); + if (fieldSum != dataToSignSz) { + printf("DoUserAuthRequestEd25519 length mismatch: wire=%u sum=%u\n", + dataToSignSz, fieldSum); + result = -607; goto done; + } + authData.sf.publicKey.dataToSignSz = dataToSignSz; + ret = wolfSSH_TestDoUserAuthRequestEd25519(ssh, &authData); + if (ret != WS_SUCCESS) { + printf("DoUserAuthRequestEd25519 wire-length: ret=%d (expected %d)\n", + ret, WS_SUCCESS); + result = -608; goto done; + } + /* Negative case: flip a byte inside the raw signature (skip past the * 4 + keyTypeNameSz + 4 header so we land in the actual signature * material). Must NOT return WS_SUCCESS. */ diff --git a/wolfssh/include.am b/wolfssh/include.am index f4013bf20..2abb44aa4 100644 --- a/wolfssh/include.am +++ b/wolfssh/include.am @@ -5,6 +5,7 @@ nobase_include_HEADERS+= \ wolfssh/agent.h \ wolfssh/certman.h \ + wolfssh/ossh.h \ wolfssh/version.h \ wolfssh/ssh.h \ wolfssh/keygen.h \ diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 858c9a9bc..4d4f4b518 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -378,6 +378,13 @@ enum { ID_X509V3_ECDSA_SHA2_NISTP384, ID_X509V3_ECDSA_SHA2_NISTP521, + /* OpenSSH certificate public key algorithms */ + ID_OSSH_CERT_RSA, + ID_OSSH_CERT_ECDSA_SHA2_NISTP256, + ID_OSSH_CERT_ECDSA_SHA2_NISTP384, + ID_OSSH_CERT_ECDSA_SHA2_NISTP521, + ID_OSSH_CERT_ED25519, + /* Service IDs */ ID_SERVICE_USERAUTH, ID_SERVICE_CONNECTION, @@ -1085,6 +1092,12 @@ WOLFSSH_LOCAL int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, vo WS_KeySignature **pkey); 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 +WOLFSSH_LOCAL int GetOpenSshPublicKey(WS_KeySignature *key, + const byte* buf, word32 len, word32* idx); +#endif /* Parsing functions */ @@ -1092,6 +1105,10 @@ WOLFSSH_LOCAL int GetBoolean(byte* v, const byte* buf, word32 len, word32* idx); WOLFSSH_LOCAL int GetUint32(word32* v, const byte* buf, word32 len, word32* idx); +#ifdef WOLFSSH_OSSH_CERTS +WOLFSSH_LOCAL int GetUint64(word64* v, + const byte* buf, word32 len, word32* idx); +#endif WOLFSSH_LOCAL int GetSize(word32* v, const byte* buf, word32 len, word32* idx); WOLFSSH_LOCAL int GetSkip(const byte* buf, word32 len, word32* idx); diff --git a/wolfssh/ossh.h b/wolfssh/ossh.h new file mode 100644 index 000000000..3708f8c50 --- /dev/null +++ b/wolfssh/ossh.h @@ -0,0 +1,113 @@ +/* ossh.h + * + * Copyright (C) 2014-2026 wolfSSL Inc. + * + * This file is part of wolfSSH. + * + * wolfSSH is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfSSH. If not, see . + */ + + +/* + * The ossh module parses and verifies OpenSSH ("*-cert-v01@openssh.com") + * user certificates. + */ + + +#ifndef _WOLFSSH_OSSH_H_ +#define _WOLFSSH_OSSH_H_ + +#include +#include + +#ifdef WOLFSSH_OSSH_CERTS + +#ifdef __cplusplus +extern "C" { +#endif + +/* OpenSSH user certificate, version 1. SSH2_CERT_TYPE_USER == 1. */ +#define WOLFSSH_OSSH_CERT_TYPE_USER 1 + +/* Parsed view of an OpenSSH certificate. All pointer members reference memory + * inside the input blob passed to OsshCertParse(); they are valid only while + * that blob is. */ +typedef struct WS_OsshCert { + const byte* blob; /* whole cert blob (for signature hashing) */ + word32 blobSz; + word32 signedLen; /* bytes covered by the CA signature */ + byte typeId; /* ID_OSSH_CERT_* of the certificate */ + byte baseTypeId; /* ID_SSH_RSA / ID_ECDSA_* / ID_ED25519 */ + + const byte* nonce; word32 nonceSz; + const byte* userKeyParms;/* type-specific public key fields (inline) */ + word32 userKeyParmsSz; + word64 serial; + word32 certType; /* must be WOLFSSH_OSSH_CERT_TYPE_USER */ + const byte* keyId; word32 keyIdSz; + const byte* principals; word32 principalsSz; /* run of SSH strings */ + word64 validAfter; + word64 validBefore; + const byte* critOpts; word32 critOptsSz; + const byte* extensions; word32 extensionsSz; + const byte* caKey; word32 caKeySz; /* signature_key contents */ + const byte* caKeyType; word32 caKeyTypeSz; + const byte* signature; word32 signatureSz; + + /* Critical option values extracted by OsshCertCheckOptions(); NULL when the + * option is absent. Both reference memory inside the input blob. */ + const byte* forceCommand; word32 forceCommandSz; + const byte* sourceAddress; word32 sourceAddressSz; +} WS_OsshCert; + +/* Map the certificate algorithm ID (ID_OSSH_CERT_*) to its base public key + * algorithm ID (ID_SSH_RSA, ID_ECDSA_SHA2_*, ID_ED25519). Returns ID_UNKNOWN + * when not an OpenSSH certificate ID. */ +WOLFSSH_LOCAL byte OsshCertBaseId(byte certId); + +/* Select the rsa-sha2-* signature algorithm to use for an OpenSSH RSA + * certificate: the strongest the peer advertised (peerSigId/peerSigIdSz). + * Returns ID_RSA_SHA2_512 when the peer supports it, else ID_RSA_SHA2_256. */ +WOLFSSH_LOCAL byte OsshRsaCertSigId(const byte* peerSigId, word32 peerSigIdSz); + +/* Parse an OpenSSH certificate blob of algorithm typeId. Fills cert with + * references into blob. Returns WS_SUCCESS or a negative error. */ +WOLFSSH_LOCAL int OsshCertParse(WS_OsshCert* cert, byte typeId, + const byte* blob, word32 blobSz); + +/* Verify the CA signature over the certificate's signed body using the + * embedded signature_key. Returns WS_SUCCESS when the signature is valid. */ +WOLFSSH_LOCAL int OsshCertVerifySignature(const WS_OsshCert* cert, void* heap); + +/* Check that certType is a user certificate and that the CA signature_key + * type is one of the supported algorithms. The embedded user-key-type vs + * certificate-algorithm consistency is enforced in OsshCertParse(). Returns + * WS_SUCCESS when valid. */ +WOLFSSH_LOCAL int OsshCertCheckType(const WS_OsshCert* cert); + +/* Validate the critical options and extensions, following the OpenSSH + * convention: both lists must be in ascending name order with no duplicates; + * any unrecognized CRITICAL option is rejected (fail closed); unrecognized + * extensions are ignored. Extracts the force-command and source-address + * critical option values into the certificate. Returns WS_SUCCESS when the + * options are acceptable. */ +WOLFSSH_LOCAL int OsshCertCheckOptions(WS_OsshCert* cert); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFSSH_OSSH_CERTS */ + +#endif /* _WOLFSSH_OSSH_H_ */ diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 3b3f2279b..71e2cf1f1 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -369,6 +369,29 @@ typedef struct WS_UserAuthData_PublicKey { const byte* signature; word32 signatureSz; byte isCert:1; +#ifdef WOLFSSH_OSSH_CERTS + /* OpenSSH cert fields (valid only while isOsshCert is set, for the parse + * lifetime). The library proves only cert self-consistency + key + * possession; the callback MUST trust-check caKey and enforce policy. */ + byte isOsshCert:1; + const byte* caKey; + word32 caKeySz; + const byte* principals; + word32 principalsSz; + word64 validAfter; /* cert valid_after, seconds since epoch */ + word64 validBefore; /* cert valid_before, seconds since epoch */ + /* Critical option values (NULL when absent), referencing the parsed cert. + * forceCommand overrides the session command; sourceAddress is a + * comma-separated CIDR list the peer address must match. */ + const byte* forceCommand; + word32 forceCommandSz; + const byte* sourceAddress; + word32 sourceAddressSz; +#endif /* WOLFSSH_OSSH_CERTS */ + /* Appended last so the original members keep their offsets when + * WOLFSSH_OSSH_CERTS is disabled; enabling it changes the layout, so app + * and library must share the setting. Signed request length, or 0. */ + word32 dataToSignSz; } WS_UserAuthData_PublicKey; typedef struct WS_UserAuthData { @@ -431,6 +454,10 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); #endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_OSSH_CERTS + WOLFSSH_API int wolfSSH_CTX_UseOsshCert_buffer(WOLFSSH_CTX* ctx, + const byte* cert, word32 certSz); +#endif /* WOLFSSH_OSSH_CERTS */ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, word32 windowSz, word32 maxPacketSz);