Skip to content

Commit 18ca6b5

Browse files
refactoring, expand test cases, adjust to authorized key file, minor dead code adjustments
1 parent 51629a7 commit 18ca6b5

15 files changed

Lines changed: 704 additions & 281 deletions

File tree

.github/workflows/windows-cert-store-test.yml

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,19 @@ jobs:
180180
grep -q -- '-lncrypt' Makefile
181181
182182
- name: Rejects a non-Windows host and a missing --enable-certs
183+
# Each assertion exits explicitly: bash errexit exempts a command
184+
# inverted with '!', and the step status comes from the last command.
183185
run: |
184-
! ./configure --enable-certs --enable-windows-cert-store $WOLFSSL_CACHE
185-
! ./configure --host=x86_64-w64-mingw32 --enable-windows-cert-store \
186-
$WOLFSSL_CACHE
186+
if ./configure --enable-certs --enable-windows-cert-store \
187+
$WOLFSSL_CACHE; then
188+
echo 'ERROR: configure should have failed on a non-Windows host'
189+
exit 1
190+
fi
191+
if ./configure --host=x86_64-w64-mingw32 \
192+
--enable-windows-cert-store $WOLFSSL_CACHE; then
193+
echo 'ERROR: configure should have failed without --enable-certs'
194+
exit 1
195+
fi
187196
188197
test:
189198
needs: build
@@ -205,23 +214,32 @@ jobs:
205214
client_key_source: x509
206215
key_algorithm: rsa
207216
test_name: "Server-Store-Client-X509"
217+
# key_algorithm is the server host key; client_key_algorithm is the
218+
# testuser client certificate key. Both are stated explicitly on the
219+
# store-client entries so neither depends on which key renewcerts.sh
220+
# happens to copy.
208221
- server_key_source: file
209222
client_key_source: store
210223
key_algorithm: rsa
224+
client_key_algorithm: ecdsa
211225
test_name: "Server-File-Client-Store"
212226
- server_key_source: store
213227
client_key_source: store
214228
key_algorithm: rsa
229+
client_key_algorithm: ecdsa
215230
test_name: "Server-Store-Client-Store"
216231
- server_key_source: store
217232
client_key_source: x509
218233
key_algorithm: ecdsa
219234
test_name: "Server-Store-Client-X509-ECDSA"
235+
# RSA client certificate, covering the x509v3-ssh-rsa user-auth and
236+
# client-side RSA cert store signing paths that the ECDSA entries
237+
# above cannot reach.
220238
- server_key_source: file
221239
client_key_source: store
222240
key_algorithm: rsa
223-
client_key_algorithm: ecdsa
224-
test_name: "Server-File-Client-Store-ECDSA"
241+
client_key_algorithm: rsa
242+
test_name: "Server-File-Client-Store-RSA"
225243

226244
steps:
227245
- uses: actions/checkout@v4
@@ -254,12 +272,20 @@ jobs:
254272
cd keys
255273
bash renewcerts.sh testuser
256274
257-
# renewcerts.sh always gives testuser fred's RSA key. Re-issue it with
258-
# an EC key when the client store entry is meant to be ECDSA.
259-
if [ "${{ matrix.client_key_algorithm }}" = "ecdsa" ]; then
275+
# renewcerts.sh copies fred's key, which is EC prime256v1, so testuser
276+
# comes out ECDSA. Re-issue it explicitly for whichever algorithm the
277+
# matrix entry asks for, rather than inheriting whatever fred's key
278+
# happens to be.
279+
ALG="${{ matrix.client_key_algorithm }}"
280+
if [ -n "$ALG" ]; then
260281
touch index.txt
261282
sed 's/fred/testuser/g' renewcerts.cnf > renewcerts-testuser.cnf
262-
openssl ecparam -name prime256v1 -genkey -noout -out testuser-key.pem
283+
if [ "$ALG" = "rsa" ]; then
284+
openssl genrsa -out testuser-key.pem 2048
285+
else
286+
openssl ecparam -name prime256v1 -genkey -noout \
287+
-out testuser-key.pem
288+
fi
263289
openssl req -subj "/C=US/ST=WA/L=Seattle/O=wolfSSL Inc/OU=Development/CN=testuser/emailAddress=testuser@example.com" \
264290
-key testuser-key.pem -out testuser-cert.csr \
265291
-config renewcerts-testuser.cnf -new -nodes
@@ -268,7 +294,11 @@ jobs:
268294
-CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -out testuser-cert.pem \
269295
-set_serial 7
270296
openssl x509 -in testuser-cert.pem -outform DER -out testuser-cert.der
271-
openssl ec -in testuser-key.pem -outform DER -out testuser-key.der
297+
if [ "$ALG" = "rsa" ]; then
298+
openssl rsa -in testuser-key.pem -outform DER -out testuser-key.der
299+
else
300+
openssl ec -in testuser-key.pem -outform DER -out testuser-key.der
301+
fi
272302
rm -f renewcerts-testuser.cnf testuser-cert.csr index.*
273303
fi
274304
cd ..
@@ -278,6 +308,29 @@ jobs:
278308
ls -la keys/
279309
exit 1
280310
fi
311+
312+
# Assert the key really is the algorithm this entry asked for, so a
313+
# change to renewcerts.sh cannot silently turn an entry into a
314+
# duplicate of another one. Unset means whatever renewcerts.sh gives,
315+
# which is fred's EC key.
316+
EXPECT="${{ matrix.client_key_algorithm }}"
317+
[ -n "$EXPECT" ] || EXPECT=ecdsa
318+
if openssl rsa -inform DER -in keys/testuser-key.der -noout 2>/dev/null
319+
then
320+
ACTUAL=rsa
321+
elif openssl ec -inform DER -in keys/testuser-key.der -noout 2>/dev/null
322+
then
323+
ACTUAL=ecdsa
324+
else
325+
echo "ERROR: testuser-key.der is neither RSA nor EC"
326+
exit 1
327+
fi
328+
if [ "$ACTUAL" != "$EXPECT" ]; then
329+
echo "ERROR: testuser client key is $ACTUAL, expected $EXPECT"
330+
exit 1
331+
fi
332+
echo "testuser client key algorithm: $ACTUAL"
333+
281334
echo "CLIENT_CERT_FILE=keys/testuser-cert.der" >> $GITHUB_ENV
282335
echo "CLIENT_KEY_FILE=keys/testuser-key.der" >> $GITHUB_ENV
283336
@@ -493,8 +546,8 @@ jobs:
493546
}
494547
495548
if ("${{ matrix.server_key_source }}" -eq "store") {
496-
# The certificate is part of the store entry; do NOT specify
497-
# HostCertificate separately.
549+
# The certificate is part of the store entry. HostKey and
550+
# HostCertificate alongside HostKeyStore are rejected at startup.
498551
$configContent += @"
499552
500553
HostKeyStore My

apps/wolfsshd/auth.c

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,46 @@ static int IsAbsoluteAuthKeysPath(const char* path)
643643
return ret;
644644
}
645645

646+
#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32))
647+
/* True when the AuthorizedKeysFile pattern is guaranteed to resolve to a
648+
* different file for every account, which is what makes an entry in it an
649+
* implicit user-to-credential binding. A relative pattern resolves under the
650+
* account's home directory, and an absolute one qualifies only when it carries
651+
* a %u or %h token. An absolute pattern with neither (e.g.
652+
* "/etc/ssh/authorized_keys_all") is one shared file for every account and
653+
* binds a credential to nothing. */
654+
static int IsPerUserAuthKeysPattern(const char* pattern)
655+
{
656+
word32 i;
657+
word32 patSz;
658+
659+
if (pattern == NULL || *pattern == '\0') {
660+
/* the built-in ~/.ssh/authorized_keys default */
661+
return 1;
662+
}
663+
664+
if (!IsAbsoluteAuthKeysPath(pattern)) {
665+
return 1;
666+
}
667+
668+
patSz = (word32)WSTRLEN(pattern);
669+
for (i = 0; (i + 1) < patSz; i++) {
670+
if (pattern[i] != '%') {
671+
continue;
672+
}
673+
if (pattern[i + 1] == 'u' || pattern[i + 1] == 'h') {
674+
return 1;
675+
}
676+
/* "%%" is a literal percent, step over both characters */
677+
if (pattern[i + 1] == '%') {
678+
i++;
679+
}
680+
}
681+
682+
return 0;
683+
}
684+
#endif /* WOLFSSH_CERTS && (WOLFSSL_FPKI || _WIN32) */
685+
646686
/* Resolve the authorized keys file path for a user. The pattern is passed in
647687
* explicitly so concurrent authentications cannot race on it, and its tokens
648688
* are expanded so each user resolves to a distinct path. */
@@ -2090,11 +2130,16 @@ static int RequestAuthentication(WS_UserAuthData* authData,
20902130
if (ret == WOLFSSH_USERAUTH_SUCCESS &&
20912131
authData->type == WOLFSSH_USERAUTH_PUBLICKEY) {
20922132
/* Bind the certificate to the requested user name via UPN with FPKI or
2093-
* CN without FPKI. Only done when relying on the CA; an
2094-
* AuthorizedKeysFile entry is itself an explicit user to cert binding
2095-
* and is checked below. */
2133+
* CN without FPKI. Skipped only when a per-user AuthorizedKeysFile is
2134+
* configured, because such an entry is itself an explicit user to cert
2135+
* binding and is checked below. A shared AuthorizedKeysFile (an
2136+
* absolute pattern with no %u or %h) resolves to one file for every
2137+
* account and binds the certificate to nothing, so the identity check
2138+
* still has to run. */
20962139
if (authData->sf.publicKey.isCert &&
2097-
!wolfSSHD_ConfigGetAuthKeysFileSet(usrConf)) {
2140+
!(wolfSSHD_ConfigGetAuthKeysFileSet(usrConf) &&
2141+
IsPerUserAuthKeysPattern(
2142+
wolfSSHD_ConfigGetAuthKeysFile(usrConf)))) {
20982143
DecodedCert* dCert;
20992144
#ifdef WOLFSSH_SMALL_STACK
21002145
dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), NULL,

apps/wolfsshd/configuration.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,8 +1862,10 @@ int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value)
18621862
conf->useSystemCA = 0;
18631863
}
18641864
else {
1865-
wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs unexpected flag");
1866-
ret = WS_FATAL_ERROR;
1865+
wolfSSH_Log(WS_LOG_ERROR,
1866+
"[SSHD] wolfSSH_TrustedSystemCAKeys: expected 'yes' or 'no', "
1867+
"got '%s'", value);
1868+
ret = WS_BAD_ARGUMENT;
18671869
}
18681870
}
18691871

@@ -1903,8 +1905,10 @@ int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value)
19031905
conf->useUserCAStore = 0;
19041906
}
19051907
else {
1906-
wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store unexpected flag");
1907-
ret = WS_FATAL_ERROR;
1908+
wolfSSH_Log(WS_LOG_ERROR,
1909+
"[SSHD] wolfSSH_TrustedUserCAStore: expected 'yes' or 'no', "
1910+
"got '%s'", value);
1911+
ret = WS_BAD_ARGUMENT;
19081912
}
19091913
}
19101914

0 commit comments

Comments
 (0)