Branch data Line data Source code
1 : : /* SPDX-FileCopyrightText: Copyright 2026 Cozens Software Solutions Limited
2 : : * SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 OR LicenseRef-PolyForm-Internal-Use-1.0.0 OR LicenseRef-COSOSO-Commercial
3 : : */
4 : :
5 : : #include "SolidSyslogOpenSslStream.h"
6 : :
7 : : #include <openssl/bio.h>
8 : : #include <openssl/prov_ssl.h>
9 : : #include <openssl/ssl.h>
10 : : #include <openssl/evp.h>
11 : : #include <openssl/types.h>
12 : : #include <openssl/x509.h>
13 : : #include <openssl/x509_vfy.h>
14 : : #include <openssl/x509v3.h>
15 : : #include <stdbool.h>
16 : : #include <stddef.h>
17 : : #include <stdint.h>
18 : :
19 : : #include "SolidSyslogError.h"
20 : : #include "SolidSyslogErrorCategory.h"
21 : : #include "SolidSyslogNullStream.h"
22 : : #include "SolidSyslogPrival.h"
23 : : #include "SolidSyslogStream.h"
24 : : #include "SolidSyslogStreamDefinition.h"
25 : : #include "SolidSyslogOpenSslCredentialsDefinition.h"
26 : : #include "SolidSyslogTlsCredentialsInstalled.h"
27 : : #include "SolidSyslogTlsFingerprint.h"
28 : : #include "SolidSyslogTlsStreamCategories.h"
29 : : #include "SolidSyslogOpenSslStreamErrors.h"
30 : : #include "SolidSyslogOpenSslStreamPrivate.h"
31 : : #include "SolidSyslogTunables.h"
32 : :
33 : : const struct SolidSyslogErrorSource SolidSyslogOpenSslStreamErrorSource = {
34 : : "OpenSslStream",
35 : : SolidSyslogTlsStreamErrors_Name
36 : : };
37 : :
38 : : enum
39 : : {
40 : : HANDSHAKE_POLL_INTERVAL_MILLISECONDS = 10U,
41 : : OPENSSL_STREAM_SECURITY_LEVEL_FLOOR = 2,
42 : : OPENSSL_STREAM_DISCARD_BYTES = 16U,
43 : : OPENSSL_STREAM_MAX_READS_BEFORE_WRITE = 8U
44 : : };
45 : :
46 : : static uint32_t OpenSslStream_NullHandshakeTimeoutGetter(void* context);
47 : : static uint32_t OpenSslStream_NullVersion(void* context);
48 : : static void OpenSslStream_NullProfile(struct SolidSyslogOpenSslProfile* profile, void* context);
49 : : static inline void OpenSslStream_PullProfile(struct SolidSyslogOpenSslStream* self);
50 : : static inline bool OpenSslStream_ConfigProvidesHandshakeGetter(const struct SolidSyslogOpenSslStreamConfig* config);
51 : : static inline bool OpenSslStream_ConfigProvidesVersion(const struct SolidSyslogOpenSslStreamConfig* config);
52 : : static inline bool OpenSslStream_ConfigProvidesProfile(const struct SolidSyslogOpenSslStreamConfig* config);
53 : : static inline uint32_t OpenSslStream_ResolveHandshakeTimeoutMs(struct SolidSyslogOpenSslStream* self);
54 : :
55 : : struct SolidSyslogAddress;
56 : :
57 : : static inline struct SolidSyslogOpenSslStream* OpenSslStream_SelfFromBase(struct SolidSyslogStream* base);
58 : :
59 : : static inline bool OpenSslStream_AttachTransportBio(struct SolidSyslogOpenSslStream* self);
60 : : static inline void OpenSslStream_Close(struct SolidSyslogStream* base);
61 : : static uint32_t OpenSslStream_Version(struct SolidSyslogStream* base);
62 : : static inline bool OpenSslStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList);
63 : : static inline bool OpenSslStream_ConfigureCipherSuites(SSL_CTX* ctx, const char* cipherSuites);
64 : : static inline bool OpenSslStream_ConfigureExpectedHostname(struct SolidSyslogOpenSslStream* self);
65 : : static inline bool OpenSslStream_NameBeginsWithADot(const char* name);
66 : : static inline bool OpenSslStream_ConfigureProtocolFloor(SSL_CTX* ctx);
67 : : static inline bool OpenSslStream_ConfigureCipherPolicy(SSL_CTX* ctx, const struct SolidSyslogOpenSslProfile* profile);
68 : : static inline BIO* OpenSslStream_CreateTransportBio(struct SolidSyslogOpenSslStream* self);
69 : : static inline BIO_METHOD* OpenSslStream_CreateTransportBioMethod(void);
70 : : static inline bool OpenSslStream_InitSslContext(struct SolidSyslogOpenSslStream* self);
71 : : static inline bool OpenSslStream_InstallCredentials(struct SolidSyslogOpenSslStream* self);
72 : : static inline bool OpenSslStream_PeerIsAuthorisable(const struct SolidSyslogTlsCredentialsInstalled* installed);
73 : : static inline bool OpenSslStream_FingerprintsAreUsable(struct SolidSyslogOpenSslStream* self);
74 : : static inline void OpenSslStream_ReleaseCredentials(struct SolidSyslogOpenSslStream* self);
75 : : static inline void OpenSslStream_RequirePeerVerification(SSL_CTX* ctx);
76 : : static int OpenSslStream_VerifyPeer(int preverifyOk, X509_STORE_CTX* storeCtx);
77 : : static inline void OpenSslStream_RaiseSecurityLevel(SSL_CTX* ctx);
78 : : static inline void OpenSslStream_RequireSecureRenegotiation(SSL_CTX* ctx);
79 : : static inline void OpenSslStream_RefusePassphrasePrompts(SSL_CTX* ctx);
80 : : static int OpenSslStream_RefusePassphrase(char* buf, int size, int rwflag, void* userdata);
81 : : static inline void OpenSslStream_ApplySessionPolicy(SSL* ssl);
82 : : static inline bool OpenSslStream_PeerPresentedACertificate(struct SolidSyslogOpenSslStream* self);
83 : : static inline bool OpenSslStream_NoNameObjectionIsOutstanding(struct SolidSyslogOpenSslStream* self);
84 : : static inline int OpenSslStream_ResolveNameObjection(
85 : : struct SolidSyslogOpenSslStream* self,
86 : : int verdict,
87 : : X509_STORE_CTX* storeCtx
88 : : );
89 : : static inline bool OpenSslStream_CarryObjectionToTheLeaf(
90 : : struct SolidSyslogOpenSslStream* self,
91 : : const X509_STORE_CTX* storeCtx
92 : : );
93 : : static inline bool OpenSslStream_CarriedChainObjectionStands(const struct SolidSyslogOpenSslStream* self);
94 : : static inline bool OpenSslStream_CarriedValidityObjectionStands(
95 : : const struct SolidSyslogOpenSslStream* self,
96 : : int verdict
97 : : );
98 : : static inline bool OpenSslStream_IsValidityError(int error);
99 : : static inline bool OpenSslStream_ChainTrustWasWaivedByPolicy(const struct SolidSyslogOpenSslStream* self, long verdict);
100 : : static inline struct SolidSyslogOpenSslStream* OpenSslStream_SelfFromStoreCtx(X509_STORE_CTX* storeCtx);
101 : : static inline int OpenSslStream_VerifyPinnedLeaf(
102 : : struct SolidSyslogOpenSslStream* self,
103 : : int preverifyOk,
104 : : X509_STORE_CTX* storeCtx
105 : : );
106 : : static inline bool OpenSslStream_LeafMatchesAPin(struct SolidSyslogOpenSslStream* self, X509_STORE_CTX* storeCtx);
107 : : static inline const EVP_MD* OpenSslStream_DigestFor(enum SolidSyslogTlsHashAlgorithm algorithm);
108 : : static bool OpenSslStream_DigestCertificate(
109 : : void* context,
110 : : enum SolidSyslogTlsHashAlgorithm algorithm,
111 : : uint8_t* digest,
112 : : size_t* length
113 : : );
114 : : static inline bool OpenSslStream_IsChainTrustWaived(
115 : : const struct SolidSyslogOpenSslStream* self,
116 : : const X509_STORE_CTX* storeCtx
117 : : );
118 : : static inline bool OpenSslStream_IsChainTrustError(int error);
119 : : static inline bool OpenSslStream_InitSslSession(struct SolidSyslogOpenSslStream* self);
120 : : static inline bool OpenSslStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr);
121 : : static inline bool OpenSslStream_IsOpen(const struct SolidSyslogOpenSslStream* self);
122 : : static inline bool OpenSslStream_Establish(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr);
123 : : static inline bool OpenSslStream_PerformHandshake(struct SolidSyslogOpenSslStream* self);
124 : : static inline uint32_t OpenSslStream_HandshakePollMs(uint32_t totalSleptMs, uint32_t budgetMs);
125 : : static inline enum SolidSyslogTlsStreamErrors OpenSslStream_RefusalDetail(struct SolidSyslogOpenSslStream* self);
126 : : static inline bool OpenSslStream_IsPeerNameMismatch(long verdict);
127 : : static inline SolidSyslogSsize OpenSslStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size);
128 : : static inline void OpenSslStream_ReleaseBioMethod(struct SolidSyslogOpenSslStream* self);
129 : : static inline void OpenSslStream_ReleaseHandshakeState(struct SolidSyslogOpenSslStream* self);
130 : : static inline void OpenSslStream_ReleaseSsl(struct SolidSyslogOpenSslStream* self);
131 : : static inline void OpenSslStream_ReleaseSslContext(struct SolidSyslogOpenSslStream* self);
132 : : static inline bool OpenSslStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size);
133 : : static bool OpenSslStream_SessionIsStillOpen(struct SolidSyslogStream* base);
134 : : static inline int OpenSslStream_TransportBioCreate(BIO* bio);
135 : : static inline long OpenSslStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg);
136 : : static inline int OpenSslStream_TransportBioRead(BIO* bio, char* buffer, int size);
137 : : static inline int OpenSslStream_TransportBioWrite(BIO* bio, const char* buffer, int size);
138 : :
139 : 302 : void SolidSyslogOpenSslStream_Initialise(
140 : : struct SolidSyslogStream* base,
141 : : const struct SolidSyslogOpenSslStreamConfig* config
142 : : )
143 : : {
144 : 302 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base);
145 : 302 : self->Base.Open = OpenSslStream_Open;
146 : 302 : self->Base.Send = OpenSslStream_Send;
147 : 302 : self->Base.Read = OpenSslStream_Read;
148 : 302 : self->Base.Close = OpenSslStream_Close;
149 : 302 : self->Base.Version = OpenSslStream_Version;
150 : 302 : self->Config = *config;
151 [ + + ]: 302 : if (OpenSslStream_ConfigProvidesHandshakeGetter(config) == false)
152 : : {
153 : : /* Substitute the Null Object so the bounded-handshake loop has a
154 : : * single code path regardless of whether the integrator wired
155 : : * runtime tuning. */
156 : 298 : self->Config.GetHandshakeTimeoutMs = OpenSslStream_NullHandshakeTimeoutGetter;
157 : 298 : self->Config.HandshakeTimeoutContext = NULL;
158 : : }
159 [ + + ]: 302 : if (OpenSslStream_ConfigProvidesVersion(config) == false)
160 : : {
161 : 300 : self->Config.Version = OpenSslStream_NullVersion;
162 : 300 : self->Config.VersionContext = NULL;
163 : : }
164 [ + + ]: 302 : if (OpenSslStream_ConfigProvidesProfile(config) == false)
165 : : {
166 : 8 : self->Config.Profile = OpenSslStream_NullProfile;
167 : 8 : self->Config.ProfileContext = NULL;
168 : : }
169 : 302 : self->Ctx = NULL;
170 : 302 : self->Ssl = NULL;
171 : 302 : self->BioMethod = NULL;
172 : 302 : self->OpenHealth = (struct SolidSyslogStreamOpenHealth) {0};
173 : 302 : self->CredentialsInstalled = false;
174 : 302 : }
175 : :
176 : 1380 : static inline struct SolidSyslogOpenSslStream* OpenSslStream_SelfFromBase(struct SolidSyslogStream* base)
177 : : {
178 : 1380 : return (struct SolidSyslogOpenSslStream*) base;
179 : : }
180 : :
181 : 302 : void SolidSyslogOpenSslStream_Cleanup(struct SolidSyslogStream* base)
182 : : {
183 : : /* Close first so an integrator who destroys a still-Open stream doesn't
184 : : * leak the underlying transport. Close now releases the SSL, BIO_METHOD
185 : : * and SSL_CTX, and is idempotent (the TLS-side teardown guards on Ssl /
186 : : * Ctx != NULL; transport Close is itself idempotent on every Stream
187 : : * impl), so the normal Open -> Close -> Destroy lifecycle is unaffected. */
188 : 302 : OpenSslStream_Close(base);
189 : : /* Overwrite the abstract base with the shared NullStream vtable so
190 : : * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */
191 : 302 : *base = *SolidSyslogNullStream_Get();
192 : 302 : }
193 : :
194 : : /* Idempotent: Send/Read may close internally on failure, after which the
195 : : * StreamSender's reconnect path or the caller's Destroy may call Close
196 : : * again. Skipping when ssl is already NULL keeps that safe. */
197 : 427 : static inline void OpenSslStream_Close(struct SolidSyslogStream* base)
198 : : {
199 : 427 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base);
200 [ + + ]: 427 : if (self->Ssl != NULL)
201 : : {
202 : 226 : SSL_shutdown(self->Ssl);
203 : 226 : OpenSslStream_ReleaseHandshakeState(self);
204 : : }
205 : : /* Each Open rebuilds the CTX (cert-rotation contract), so Close must free
206 : : the current one or the fail-fast Open -> Close -> Open reconnect cycle
207 : : leaks an SSL_CTX every round. NULL-guarded, so the Open-failure tail and
208 : : Cleanup that also call it stay double-free safe. */
209 : 427 : OpenSslStream_ReleaseSslContext(self);
210 : 427 : OpenSslStream_ReleaseCredentials(self);
211 : 427 : SolidSyslogStream_Close(self->Config.Transport);
212 : 427 : }
213 : :
214 : 3 : static uint32_t OpenSslStream_Version(struct SolidSyslogStream* base)
215 : : {
216 : 3 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base);
217 : 3 : return self->Config.Version(self->Config.VersionContext);
218 : : }
219 : :
220 : 226 : static inline void OpenSslStream_ReleaseHandshakeState(struct SolidSyslogOpenSslStream* self)
221 : : {
222 : 226 : OpenSslStream_ReleaseSsl(self);
223 : 226 : OpenSslStream_ReleaseBioMethod(self);
224 : 226 : }
225 : :
226 : 226 : static inline void OpenSslStream_ReleaseSsl(struct SolidSyslogOpenSslStream* self)
227 : : {
228 : 226 : SSL_free(self->Ssl);
229 : 226 : self->Ssl = NULL;
230 : 226 : }
231 : :
232 : 230 : static inline void OpenSslStream_ReleaseBioMethod(struct SolidSyslogOpenSslStream* self)
233 : : {
234 [ + + ]: 230 : if (self->BioMethod != NULL)
235 : : {
236 : 224 : BIO_meth_free(self->BioMethod);
237 : 224 : self->BioMethod = NULL;
238 : : }
239 : 230 : }
240 : :
241 : : /* An anonymous key exchange sends no Certificate message, so verification never
242 : : * runs, the verify callback is never invoked, and a handshake completes against
243 : : * a peer nothing has authorised. SSL_VERIFY_PEER does not prevent it and the
244 : : * protocol floor does not either: OpenSSL's own default cipher list excludes
245 : : * aNULL, but a profile naming ALL, ADH or aNULL puts it back.
246 : : *
247 : : * Checked as an outcome rather than by screening the cipher string, which has
248 : : * too many spellings to screen and would not close any other route to the same
249 : : * place. Reported as a configuration fault because that is what it is - the
250 : : * integrator is pointed at their own profile rather than at the collector. */
251 : 167 : static inline bool OpenSslStream_PeerPresentedACertificate(struct SolidSyslogOpenSslStream* self)
252 : : {
253 : 167 : bool ok = SSL_get0_peer_certificate(self->Ssl) != NULL;
254 [ + + ]: 167 : if (!ok)
255 : : {
256 : 3 : OpenSslStream_ReportOpenFailure(
257 : : self,
258 : : SOLIDSYSLOG_SEVERITY_ERROR,
259 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
260 : : SOLIDSYSLOG_TLS_STREAM_ERROR_NO_PEER_AUTHORISATION
261 : : );
262 : : }
263 : 167 : return ok;
264 : : }
265 : :
266 : : /* A carried name mismatch refuses the leaf on OpenSSL's last call for it. A
267 : : * handshake that completed with one still carried never made that call, and is
268 : : * refused here rather than trusted to have. */
269 : 164 : static inline bool OpenSslStream_NoNameObjectionIsOutstanding(struct SolidSyslogOpenSslStream* self)
270 : : {
271 : 164 : bool ok = self->NameObjection == X509_V_OK;
272 [ + + ]: 164 : if (!ok)
273 : : {
274 : 1 : OpenSslStream_ReportOpenFailure(
275 : : self,
276 : : SOLIDSYSLOG_SEVERITY_ERROR,
277 : : SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED,
278 : : SOLIDSYSLOG_TLS_STREAM_ERROR_PEER_NAME_MISMATCHED
279 : : );
280 : : }
281 : 164 : return ok;
282 : : }
283 : :
284 : 439 : static inline void OpenSslStream_ReleaseSslContext(struct SolidSyslogOpenSslStream* self)
285 : : {
286 [ + + ]: 439 : if (self->Ctx != NULL)
287 : : {
288 : 254 : SSL_CTX_free(self->Ctx);
289 : 254 : self->Ctx = NULL;
290 : : }
291 : 439 : }
292 : :
293 : 262 : static inline bool OpenSslStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr)
294 : : {
295 : 262 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base);
296 : 262 : bool ok = false;
297 [ + + ]: 262 : if (OpenSslStream_IsOpen(self))
298 : : {
299 : : /* Refused by contract, not reconnected: what is held stays as it is. */
300 : 2 : OpenSslStream_Report(
301 : : SOLIDSYSLOG_BAD_ARGUMENT_SEVERITY,
302 : : SOLIDSYSLOG_CAT_BAD_ARGUMENT,
303 : : SOLIDSYSLOG_TLS_STREAM_ERROR_ALREADY_OPEN
304 : : );
305 : : }
306 : : else
307 : : {
308 : 260 : ok = OpenSslStream_Establish(base, addr);
309 [ + + ]: 260 : if (ok)
310 : : {
311 : 163 : SolidSyslogStreamOpenHealth_Opened(&self->OpenHealth);
312 : : }
313 : : }
314 : 262 : return ok;
315 : : }
316 : :
317 : : /* Every Open that succeeds leaves a context behind and every Close frees it,
318 : : * so holding one is the one mark of a session still open. */
319 : 262 : static inline bool OpenSslStream_IsOpen(const struct SolidSyslogOpenSslStream* self)
320 : : {
321 : 262 : return self->Ctx != NULL;
322 : : }
323 : :
324 : 260 : static inline bool OpenSslStream_Establish(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr)
325 : : {
326 : 260 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base);
327 : 260 : OpenSslStream_PullProfile(self);
328 : 260 : self->ChainObjection = X509_V_OK;
329 : 260 : self->ValidityObjection = X509_V_OK;
330 : 260 : self->NameObjection = X509_V_OK;
331 : 260 : self->PinVerdict = SOLIDSYSLOG_TLS_AUTHORISATION_NO_MATCH;
332 [ + + + + ]: 764 : bool ok = SolidSyslogStream_Open(self->Config.Transport, addr) && OpenSslStream_InitSslContext(self) &&
333 [ + + + + ]: 704 : OpenSslStream_InstallCredentials(self) && OpenSslStream_InitSslSession(self) &&
334 [ + + + + ]: 658 : OpenSslStream_AttachTransportBio(self) && OpenSslStream_ConfigureExpectedHostname(self) &&
335 [ + + + + : 895 : OpenSslStream_PerformHandshake(self) && OpenSslStream_PeerPresentedACertificate(self) &&
+ + ]
336 : 164 : OpenSslStream_NoNameObjectionIsOutstanding(self);
337 [ + + ]: 260 : if (!ok)
338 : : {
339 : 97 : OpenSslStream_Close(base);
340 : : }
341 : 260 : return ok;
342 : : }
343 : :
344 : : /* One snapshot per connection: every later step reads the same answer, however
345 : : * the integrator's own state moves while the handshake is in progress. */
346 : 260 : static inline void OpenSslStream_PullProfile(struct SolidSyslogOpenSslStream* self)
347 : : {
348 : 260 : self->Profile = (struct SolidSyslogOpenSslProfile) {0};
349 : 260 : self->Config.Profile(&self->Profile, self->Config.ProfileContext);
350 : 260 : }
351 : :
352 : 258 : static inline bool OpenSslStream_InitSslContext(struct SolidSyslogOpenSslStream* self)
353 : : {
354 : 258 : self->Ctx = SSL_CTX_new(TLS_client_method());
355 [ + + + + ]: 258 : bool ok = (self->Ctx != NULL) && OpenSslStream_ConfigureProtocolFloor(self->Ctx);
356 [ + + ]: 258 : if (!ok)
357 : : {
358 : 6 : OpenSslStream_ReportOpenFailure(
359 : : self,
360 : : SOLIDSYSLOG_SEVERITY_ERROR,
361 : : SOLIDSYSLOG_CAT_TLS_STREAM_INIT_FAILED,
362 : : SOLIDSYSLOG_TLS_STREAM_ERROR_CONTEXT_INIT_FAILED
363 : : );
364 : : }
365 [ + + ]: 252 : else if (!OpenSslStream_ConfigureCipherPolicy(self->Ctx, &self->Profile))
366 : : {
367 : : /* An integrator string the library rejected: their configuration, not
368 : : * the library failing to come up. */
369 : 6 : OpenSslStream_ReportOpenFailure(
370 : : self,
371 : : SOLIDSYSLOG_SEVERITY_ERROR,
372 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
373 : : SOLIDSYSLOG_TLS_STREAM_ERROR_CIPHER_POLICY_REJECTED
374 : : );
375 : 6 : ok = false;
376 : : }
377 : : else
378 : : {
379 : 246 : OpenSslStream_RequirePeerVerification(self->Ctx);
380 : 246 : OpenSslStream_RaiseSecurityLevel(self->Ctx);
381 : 246 : OpenSslStream_RequireSecureRenegotiation(self->Ctx);
382 : 246 : OpenSslStream_RefusePassphrasePrompts(self->Ctx);
383 : : }
384 [ + + ]: 258 : if (!ok)
385 : : {
386 : 12 : OpenSslStream_ReleaseSslContext(self);
387 : : }
388 : 258 : return ok;
389 : : }
390 : :
391 : 252 : static inline bool OpenSslStream_ConfigureCipherPolicy(SSL_CTX* ctx, const struct SolidSyslogOpenSslProfile* profile)
392 : : {
393 [ + + + + ]: 499 : return OpenSslStream_ConfigureCipherList(ctx, profile->CipherList) &&
394 : 247 : OpenSslStream_ConfigureCipherSuites(ctx, profile->CipherSuites);
395 : : }
396 : :
397 : : /* Asked once per connection, after the context exists and before the handshake,
398 : : * so material is fetched only for a connection actually being made. The flag is
399 : : * set before the call rather than after it: the contract is one Release per
400 : : * Install call whatever that call returned, which is what spares every backend
401 : : * a rollback path of its own. */
402 : 246 : static inline bool OpenSslStream_InstallCredentials(struct SolidSyslogOpenSslStream* self)
403 : : {
404 : 246 : self->Installed.TrustAnchorsInstalled = false;
405 : 246 : self->Installed.Fingerprints = NULL;
406 : 246 : self->Installed.FingerprintCount = 0U;
407 : 246 : self->CredentialsInstalled = true;
408 : 246 : bool ok = self->Config.Credentials->Install(self->Config.Credentials, self->Ctx, &self->Installed);
409 [ + + + + ]: 246 : if (ok && !OpenSslStream_PeerIsAuthorisable(&self->Installed))
410 : : {
411 : 4 : OpenSslStream_ReportOpenFailure(
412 : : self,
413 : : SOLIDSYSLOG_SEVERITY_ERROR,
414 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
415 : : SOLIDSYSLOG_TLS_STREAM_ERROR_NO_PEER_AUTHORISATION
416 : : );
417 : 4 : ok = false;
418 : : }
419 [ + + ]: 246 : if (ok)
420 : : {
421 : 238 : ok = OpenSslStream_FingerprintsAreUsable(self);
422 : : }
423 : 246 : return ok;
424 : : }
425 : :
426 : : /* A peer is authorised by a chain to trust anchors or by a pinned certificate
427 : : * fingerprint, and RFC 5425 4.2.1 makes the second sufficient on its own. With
428 : : * neither, there is nothing to check the peer against, so the connection stops
429 : : * rather than reaching a peer this stream cannot identify. */
430 : 242 : static inline bool OpenSslStream_PeerIsAuthorisable(const struct SolidSyslogTlsCredentialsInstalled* installed)
431 : : {
432 [ + + + + ]: 242 : return installed->TrustAnchorsInstalled || (installed->FingerprintCount > 0U);
433 : : }
434 : :
435 : : /* Inspected before the handshake, so a pin that cannot match is reported as
436 : : * bad configuration rather than as a refused peer. */
437 : 238 : static inline bool OpenSslStream_FingerprintsAreUsable(struct SolidSyslogOpenSslStream* self)
438 : : {
439 : 238 : bool ok = true;
440 : : enum SolidSyslogTlsFingerprintListState state =
441 : 238 : SolidSyslogTlsFingerprint_InspectList(self->Installed.Fingerprints, self->Installed.FingerprintCount);
442 [ + + ]: 238 : if (state == SOLIDSYSLOG_TLS_FINGERPRINT_LIST_MALFORMED)
443 : : {
444 : 5 : OpenSslStream_ReportOpenFailure(
445 : : self,
446 : : SOLIDSYSLOG_SEVERITY_ERROR,
447 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
448 : : SOLIDSYSLOG_TLS_STREAM_ERROR_FINGERPRINT_MALFORMED
449 : : );
450 : 5 : ok = false;
451 : : }
452 [ + + ]: 233 : else if (state == SOLIDSYSLOG_TLS_FINGERPRINT_LIST_USES_SHA1)
453 : : {
454 : 3 : OpenSslStream_Report(
455 : : SOLIDSYSLOG_SEVERITY_WARNING,
456 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
457 : : SOLIDSYSLOG_TLS_STREAM_ERROR_FINGERPRINT_SHA1
458 : : );
459 : : }
460 : : else
461 : : {
462 : : /* Well formed, or no pins at all. */
463 : : }
464 : 238 : return ok;
465 : : }
466 : :
467 : : /* Answers every Install, so the integrator is always told when the credential
468 : : * window has closed - including on the paths where Open failed part way. */
469 : 427 : static inline void OpenSslStream_ReleaseCredentials(struct SolidSyslogOpenSslStream* self)
470 : : {
471 [ + + ]: 427 : if (self->CredentialsInstalled)
472 : : {
473 : 246 : self->CredentialsInstalled = false;
474 : 246 : self->Config.Credentials->Release(self->Config.Credentials);
475 : : }
476 : 427 : }
477 : :
478 : : /* RFC 9325 s4.5: RSA and DH of 2048 bits, curves of 224, no SHA-1 signatures -
479 : : * OpenSSL's level 2. A floor rather than a setting: the build, openssl.cnf and
480 : : * a cipher string's @SECLEVEL=n decide where the context starts, a weaker
481 : : * start is raised and a stricter one stands. Read after the cipher policy,
482 : : * since the string is the last of those to move it: the profile's own
483 : : * @SECLEVEL overrides the host's, as it does anywhere in OpenSSL, and only
484 : : * this floor is beneath both. */
485 : 246 : static inline void OpenSslStream_RaiseSecurityLevel(SSL_CTX* ctx)
486 : : {
487 [ + + ]: 246 : if (SSL_CTX_get_security_level(ctx) < OPENSSL_STREAM_SECURITY_LEVEL_FLOOR)
488 : : {
489 : 212 : SSL_CTX_set_security_level(ctx, OPENSSL_STREAM_SECURITY_LEVEL_FLOOR);
490 : : }
491 : 246 : }
492 : :
493 : : /* RFC 5746: a TLS 1.2 peer that does not acknowledge secure renegotiation is
494 : : * refused. That is OpenSSL's default from 3.0, but its client accepts such a
495 : : * peer under either of two options, and openssl.cnf can set both -
496 : : * UnsafeLegacyServerConnect and UnsafeLegacyRenegotiation - so the stream
497 : : * clears them rather than inheriting the default. */
498 : 246 : static inline void OpenSslStream_RequireSecureRenegotiation(SSL_CTX* ctx)
499 : : {
500 : 246 : (void) SSL_CTX_clear_options(ctx, SSL_OP_LEGACY_SERVER_CONNECT | SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION);
501 : 246 : }
502 : :
503 : : /* An encrypted private key otherwise prompts on the controlling terminal, from
504 : : * the servicing thread. Refusing makes it fail to load instead, which the
505 : : * credentials source reports. */
506 : 246 : static inline void OpenSslStream_RefusePassphrasePrompts(SSL_CTX* ctx)
507 : : {
508 : 246 : SSL_CTX_set_default_passwd_cb(ctx, OpenSslStream_RefusePassphrase);
509 : 246 : }
510 : :
511 : : /* NOLINTBEGIN(bugprone-easily-swappable-parameters,readability-non-const-parameter) -- OpenSSL's pem_password_cb signature */
512 : 2 : static int OpenSslStream_RefusePassphrase(char* buf, int size, int rwflag, void* userdata)
513 : : /* NOLINTEND(bugprone-easily-swappable-parameters,readability-non-const-parameter) */
514 : : {
515 : : (void) buf;
516 : : (void) size;
517 : : (void) rwflag;
518 : : (void) userdata;
519 : 2 : return 0;
520 : : }
521 : :
522 : : /* Set outright rather than alongside loading trust anchors, because the peer is
523 : : * also authorisable by a pinned fingerprint with no anchors at all. Tying the
524 : : * two together is how a fingerprint-only configuration would come to verify
525 : : * nothing - the one place in this design that could fail open rather than
526 : : * closed. */
527 : 246 : static inline void OpenSslStream_RequirePeerVerification(SSL_CTX* ctx)
528 : : {
529 : 246 : SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, OpenSslStream_VerifyPeer);
530 : 246 : }
531 : :
532 : : /* Called for each certificate in the chain; only the leaf, at depth 0, is
533 : : * pinned. Returning zero above the leaf would stop verification before the
534 : : * leaf was reached, so a pinned peer with no anchors waives the chain-trust
535 : : * errors there as well. */
536 : 103 : static int OpenSslStream_VerifyPeer(int preverifyOk, X509_STORE_CTX* storeCtx)
537 : : {
538 : 103 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromStoreCtx(storeCtx);
539 : 103 : int verdict = preverifyOk;
540 : :
541 [ + + ]: 103 : if (X509_STORE_CTX_get_error_depth(storeCtx) == 0)
542 : : {
543 [ + + ]: 74 : if (self->Installed.FingerprintCount > 0U)
544 : : {
545 : 47 : verdict = OpenSslStream_VerifyPinnedLeaf(self, preverifyOk, storeCtx);
546 : : }
547 : 74 : verdict = OpenSslStream_ResolveNameObjection(self, verdict, storeCtx);
548 : : }
549 [ + + ]: 29 : else if (OpenSslStream_CarryObjectionToTheLeaf(self, storeCtx))
550 : : {
551 : 24 : verdict = 1;
552 : : }
553 : : else
554 : : {
555 : : /* OpenSSL's own verdict stands. */
556 : : }
557 : :
558 : 103 : return verdict;
559 : : }
560 : :
561 : : /* OpenSSL checks the name once the chain is built and before any certificate's
562 : : * dates, and abandons verification on the first refusal, so refusing the name
563 : : * where it is raised would name it ahead of the dates. It is carried instead,
564 : : * and refuses the leaf on the last call OpenSSL makes for it, once every other
565 : : * check has passed. */
566 : 74 : static inline int OpenSslStream_ResolveNameObjection(
567 : : struct SolidSyslogOpenSslStream* self,
568 : : int verdict,
569 : : X509_STORE_CTX* storeCtx
570 : : )
571 : : {
572 : 74 : int resolved = verdict;
573 : 74 : int error = X509_STORE_CTX_get_error(storeCtx);
574 [ + + + + ]: 74 : if ((verdict == 0) && OpenSslStream_IsPeerNameMismatch((long) error))
575 : : {
576 : 12 : self->NameObjection = error;
577 : 12 : resolved = 1;
578 : : }
579 [ + + + + ]: 62 : else if ((verdict == 1) && (self->NameObjection != X509_V_OK))
580 : : {
581 : 5 : X509_STORE_CTX_set_error(storeCtx, self->NameObjection);
582 : 5 : resolved = 0;
583 : : }
584 : : else
585 : : {
586 : : /* Refused for something else, or nothing was carried. */
587 : : }
588 : 74 : return resolved;
589 : : }
590 : :
591 : : /* Wherever pins are configured, an objection above the leaf is recorded and
592 : : * carried past rather than refused here. OpenSSL reports an unreachable chain
593 : : * at the top of what the peer presented, checks validity from the top down,
594 : : * and abandons verification on the first refusal, so refusing at depth would
595 : : * leave the leaf's pin uncompared - and the contract's order, a fingerprint
596 : : * before a chain that reaches no anchor or an issuer outside its dates, would
597 : : * be decided by how many certificates the peer happened to send. Forgiving it
598 : : * is the leaf's decision, not this one. */
599 : 29 : static inline bool OpenSslStream_CarryObjectionToTheLeaf(
600 : : struct SolidSyslogOpenSslStream* self,
601 : : const X509_STORE_CTX* storeCtx
602 : : )
603 : : {
604 : 29 : int error = X509_STORE_CTX_get_error(storeCtx);
605 : 29 : bool carry = self->Installed.FingerprintCount > 0U;
606 [ + + + + ]: 29 : if (carry && OpenSslStream_IsChainTrustError(error))
607 : : {
608 : 13 : self->ChainObjection = error;
609 : : }
610 [ + + + + ]: 16 : else if (carry && OpenSslStream_IsValidityError(error))
611 : : {
612 : 11 : self->ValidityObjection = error;
613 : : }
614 : : else
615 : : {
616 : 5 : carry = false;
617 : : }
618 : 29 : return carry;
619 : : }
620 : :
621 : 12 : static inline bool OpenSslStream_IsValidityError(int error)
622 : : {
623 [ + + + + ]: 12 : return (error == X509_V_ERR_CERT_HAS_EXPIRED) || (error == X509_V_ERR_CERT_NOT_YET_VALID);
624 : : }
625 : :
626 : : /* OpenSSL files the SSL running the handshake under a well-known ex_data index
627 : : * on the store context; the stream is that SSL's app data. */
628 : 103 : static inline struct SolidSyslogOpenSslStream* OpenSslStream_SelfFromStoreCtx(X509_STORE_CTX* storeCtx)
629 : : {
630 : 103 : SSL* ssl = (SSL*) X509_STORE_CTX_get_ex_data(storeCtx, SSL_get_ex_data_X509_STORE_CTX_idx());
631 : 103 : return (struct SolidSyslogOpenSslStream*) SSL_get_app_data(ssl);
632 : : }
633 : :
634 : 47 : static inline int OpenSslStream_VerifyPinnedLeaf(
635 : : struct SolidSyslogOpenSslStream* self,
636 : : int preverifyOk,
637 : : X509_STORE_CTX* storeCtx
638 : : )
639 : : {
640 : 47 : int verdict = preverifyOk;
641 [ + + ]: 47 : if (!OpenSslStream_LeafMatchesAPin(self, storeCtx))
642 : : {
643 : 13 : X509_STORE_CTX_set_error(storeCtx, X509_V_ERR_APPLICATION_VERIFICATION);
644 : 13 : verdict = 0;
645 : : }
646 [ + + ]: 34 : else if (OpenSslStream_CarriedChainObjectionStands(self))
647 : : {
648 : : /* The pin matched, but anchors were configured too and the chain
649 : : * reached none of them. Both were asked for, so both must pass. */
650 : 3 : X509_STORE_CTX_set_error(storeCtx, self->ChainObjection);
651 : 3 : verdict = 0;
652 : : }
653 [ + + ]: 31 : else if (OpenSslStream_IsChainTrustWaived(self, storeCtx))
654 : : {
655 : 17 : verdict = 1;
656 : : }
657 : : else
658 : : {
659 : : /* OpenSSL's own verdict on the leaf stands. */
660 : : }
661 : :
662 [ + + ]: 47 : if (OpenSslStream_CarriedValidityObjectionStands(self, verdict))
663 : : {
664 : 5 : X509_STORE_CTX_set_error(storeCtx, self->ValidityObjection);
665 : 5 : verdict = 0;
666 : : }
667 : 47 : return verdict;
668 : : }
669 : :
670 : : /* A chain objection carried past a deeper certificate is forgiven only where
671 : : * the pin is the whole of the authorisation. With anchors configured as well,
672 : : * it is the fault that refuses the peer once the pin has been found to match. */
673 : 34 : static inline bool OpenSslStream_CarriedChainObjectionStands(const struct SolidSyslogOpenSslStream* self)
674 : : {
675 [ + + + + ]: 34 : return self->Installed.TrustAnchorsInstalled && (self->ChainObjection != X509_V_OK);
676 : : }
677 : :
678 : : /* A validity objection carried past a deeper certificate is never forgiven: a
679 : : * pin does not extend an issuer's dates. It is the last fault named, so it
680 : : * refuses only a leaf that every earlier check would have passed. */
681 : 47 : static inline bool OpenSslStream_CarriedValidityObjectionStands(
682 : : const struct SolidSyslogOpenSslStream* self,
683 : : int verdict
684 : : )
685 : : {
686 [ + + + + ]: 47 : return (verdict == 1) && (self->ValidityObjection != X509_V_OK);
687 : : }
688 : :
689 : 47 : static inline bool OpenSslStream_LeafMatchesAPin(struct SolidSyslogOpenSslStream* self, X509_STORE_CTX* storeCtx)
690 : : {
691 : 47 : X509* leaf = X509_STORE_CTX_get_current_cert(storeCtx);
692 : 47 : self->PinVerdict = SolidSyslogTlsFingerprint_Authorise(
693 : : self->Installed.Fingerprints,
694 : : self->Installed.FingerprintCount,
695 : : OpenSslStream_DigestCertificate,
696 : : leaf
697 : : );
698 : 47 : return self->PinVerdict == SOLIDSYSLOG_TLS_AUTHORISATION_MATCHED;
699 : : }
700 : :
701 : 47 : static bool OpenSslStream_DigestCertificate(
702 : : /* cppcheck-suppress constParameterCallback -- the parameter is SolidSyslogTlsDigestFunction's, declared in Core. */
703 : : void* context,
704 : : enum SolidSyslogTlsHashAlgorithm algorithm,
705 : : uint8_t* digest,
706 : : size_t* length
707 : : )
708 : : {
709 : 47 : const X509* leaf = (const X509*) context;
710 : 47 : const EVP_MD* md = OpenSslStream_DigestFor(algorithm);
711 : 47 : unsigned int written = 0U;
712 [ + - + + ]: 47 : bool ok = (md != NULL) && (X509_digest(leaf, md, digest, &written) == 1);
713 : 47 : *length = (size_t) written;
714 : 47 : return ok;
715 : : }
716 : :
717 : : /* An algorithm this pack does not name has no digest, so a hash added to Core
718 : : * and not handled here refuses the peer rather than being digested as
719 : : * something else. */
720 : 47 : static inline const EVP_MD* OpenSslStream_DigestFor(enum SolidSyslogTlsHashAlgorithm algorithm)
721 : : {
722 : 47 : const EVP_MD* md = NULL;
723 [ + + ]: 47 : if (algorithm == SOLIDSYSLOG_TLS_HASH_SHA1)
724 : : {
725 : 3 : md = EVP_sha1();
726 : : }
727 [ + - ]: 44 : else if (algorithm == SOLIDSYSLOG_TLS_HASH_SHA256)
728 : : {
729 : 44 : md = EVP_sha256();
730 : : }
731 : : else
732 : : {
733 : : /* Left as NULL. */
734 : : }
735 : 47 : return md;
736 : : }
737 : :
738 : : /* Reached only once a pin has matched the leaf, so the pin is known to be
739 : : * configured; what remains to ask is whether it is the whole of the
740 : : * authorisation. */
741 : 31 : static inline bool OpenSslStream_IsChainTrustWaived(
742 : : const struct SolidSyslogOpenSslStream* self,
743 : : const X509_STORE_CTX* storeCtx
744 : : )
745 : : {
746 [ + + + + ]: 54 : return !self->Installed.TrustAnchorsInstalled &&
747 : 23 : OpenSslStream_IsChainTrustError(X509_STORE_CTX_get_error(storeCtx));
748 : : }
749 : :
750 : : /* The errors a missing trust anchor alone produces. Every other code describes
751 : : * the certificate itself, which a pin does not vouch for. */
752 : 51 : static inline bool OpenSslStream_IsChainTrustError(int error)
753 : : {
754 [ + + + + ]: 40 : return (error == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) || (error == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) ||
755 [ + + + + ]: 22 : (error == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) || (error == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT) ||
756 [ + + + + ]: 91 : (error == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE) || (error == X509_V_ERR_CERT_UNTRUSTED);
757 : : }
758 : :
759 : : /* A floor, and deliberately no ceiling: RFC 9662, which updates RFC 5425,
760 : : * requires TLS 1.3 to be preferred wherever it is implemented. A stricter
761 : : * minimum from openssl.cnf stands. One configured nowhere reads 0, OpenSSL's
762 : : * "the lowest the build supports", which is below the floor and so raised. */
763 : 254 : static inline bool OpenSslStream_ConfigureProtocolFloor(SSL_CTX* ctx)
764 : : {
765 : 254 : bool ok = true;
766 [ + + ]: 254 : if (SSL_CTX_get_min_proto_version(ctx) < TLS1_2_VERSION)
767 : : {
768 : 253 : ok = SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 1;
769 : : }
770 : 254 : return ok;
771 : : }
772 : :
773 : 252 : static inline bool OpenSslStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList)
774 : : {
775 : 252 : bool ok = true;
776 [ + + ]: 252 : if (cipherList != NULL)
777 : : {
778 : 9 : ok = SSL_CTX_set_cipher_list(ctx, cipherList) == 1;
779 : : }
780 : 252 : return ok;
781 : : }
782 : :
783 : : /* Separate from the list above because OpenSSL has kept TLS 1.3 ciphersuites in
784 : : * their own list since 1.1.1, and no ceiling is pinned - so a policy expressed
785 : : * only through CipherList would not bind the version usually negotiated. */
786 : 247 : static inline bool OpenSslStream_ConfigureCipherSuites(SSL_CTX* ctx, const char* cipherSuites)
787 : : {
788 : 247 : bool ok = true;
789 [ + + ]: 247 : if (cipherSuites != NULL)
790 : : {
791 : 3 : ok = SSL_CTX_set_ciphersuites(ctx, cipherSuites) == 1;
792 : : }
793 : 247 : return ok;
794 : : }
795 : :
796 : 233 : static inline bool OpenSslStream_InitSslSession(struct SolidSyslogOpenSslStream* self)
797 : : {
798 : 233 : self->Ssl = SSL_new(self->Ctx);
799 [ + + + + ]: 233 : bool ok = (self->Ssl != NULL) && (SSL_set_app_data(self->Ssl, self) == 1);
800 [ + + ]: 233 : if (ok)
801 : : {
802 : 225 : OpenSslStream_ApplySessionPolicy(self->Ssl);
803 : : }
804 : : else
805 : : {
806 : 8 : OpenSslStream_ReportOpenFailure(
807 : : self,
808 : : SOLIDSYSLOG_SEVERITY_ERROR,
809 : : SOLIDSYSLOG_CAT_TLS_STREAM_INIT_FAILED,
810 : : SOLIDSYSLOG_TLS_STREAM_ERROR_SESSION_INIT_FAILED
811 : : );
812 : : }
813 : 233 : return ok;
814 : : }
815 : :
816 : : /* RFC 9525 s6.3 and RFC 5425 s5.2: a wildcard is the whole of the left-most
817 : : * label, never part of one. And a client that never needs to renegotiate does
818 : : * not let the collector make it. */
819 : 225 : static inline void OpenSslStream_ApplySessionPolicy(SSL* ssl)
820 : : {
821 : 225 : SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
822 : 225 : (void) SSL_set_options(ssl, SSL_OP_NO_RENEGOTIATION);
823 : 225 : }
824 : :
825 : 225 : static inline bool OpenSslStream_AttachTransportBio(struct SolidSyslogOpenSslStream* self)
826 : : {
827 : 225 : BIO* bio = OpenSslStream_CreateTransportBio(self);
828 : 225 : bool ok = bio != NULL;
829 [ + + ]: 225 : if (ok)
830 : : {
831 : 220 : BIO_set_data(bio, self->Config.Transport);
832 : 220 : SSL_set_bio(self->Ssl, bio, bio);
833 : : }
834 : : else
835 : : {
836 : 5 : OpenSslStream_ReportOpenFailure(
837 : : self,
838 : : SOLIDSYSLOG_SEVERITY_ERROR,
839 : : SOLIDSYSLOG_CAT_TLS_STREAM_INIT_FAILED,
840 : : SOLIDSYSLOG_TLS_STREAM_ERROR_SESSION_INIT_FAILED
841 : : );
842 : : }
843 : 225 : return ok;
844 : : }
845 : :
846 : 225 : static inline BIO* OpenSslStream_CreateTransportBio(struct SolidSyslogOpenSslStream* self)
847 : : {
848 : 225 : self->BioMethod = OpenSslStream_CreateTransportBioMethod();
849 : 225 : BIO* bio = NULL;
850 [ + + ]: 225 : if (self->BioMethod != NULL)
851 : : {
852 : 224 : bio = BIO_new(self->BioMethod);
853 [ + + ]: 224 : if (bio == NULL)
854 : : {
855 : 4 : OpenSslStream_ReleaseBioMethod(self);
856 : : }
857 : : }
858 : 225 : return bio;
859 : : }
860 : :
861 : 225 : static inline BIO_METHOD* OpenSslStream_CreateTransportBioMethod(void)
862 : : {
863 : 225 : BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO");
864 [ + + ]: 225 : if (method != NULL)
865 : : {
866 : 224 : BIO_meth_set_create(method, OpenSslStream_TransportBioCreate);
867 : 224 : BIO_meth_set_read(method, OpenSslStream_TransportBioRead);
868 : 224 : BIO_meth_set_write(method, OpenSslStream_TransportBioWrite);
869 : 224 : BIO_meth_set_ctrl(method, OpenSslStream_TransportBioCtrl);
870 : : }
871 : 225 : return method;
872 : : }
873 : :
874 : : /* Called when BIO_new instantiates a BIO from our method. Marking init=1 tells
875 : : * OpenSSL the BIO is ready for I/O; without it SSL_connect bails early. */
876 : 31 : static inline int OpenSslStream_TransportBioCreate(BIO* bio)
877 : : {
878 : 31 : BIO_set_init(bio, 1);
879 : 31 : return 1;
880 : : }
881 : :
882 : : /* Translate the non-blocking transport's Read contract into the OpenSSL BIO
883 : : * contract:
884 : : * transport > 0 -> bytes available, BIO returns the same positive count.
885 : : * transport = 0 -> would-block. BIO must signal retry via BIO_set_retry_read
886 : : * and return -1; without this, OpenSSL treats the 0 as EOF
887 : : * and aborts the handshake on the first poll.
888 : : * transport < 0 -> EOF or error. BIO returns -1 with retry flags cleared so
889 : : * OpenSSL surfaces the failure rather than spinning. */
890 : 289 : static inline int OpenSslStream_TransportBioRead(BIO* bio, char* buffer, int size)
891 : : {
892 : 289 : struct SolidSyslogStream* transport = (struct SolidSyslogStream*) BIO_get_data(bio);
893 : 289 : SolidSyslogSsize n = SolidSyslogStream_Read(transport, buffer, (size_t) size);
894 : 289 : int result = -1;
895 : :
896 [ + + ]: 289 : if (n > 0)
897 : : {
898 : 285 : result = (int) n;
899 : : }
900 [ + + ]: 4 : else if (n == 0)
901 : : {
902 : 3 : BIO_set_retry_read(bio);
903 : : }
904 : : else
905 : : {
906 : 1 : BIO_clear_retry_flags(bio);
907 : : }
908 : 289 : return result;
909 : : }
910 : :
911 : 69 : static inline int OpenSslStream_TransportBioWrite(BIO* bio, const char* buffer, int size)
912 : : {
913 : 69 : struct SolidSyslogStream* transport = (struct SolidSyslogStream*) BIO_get_data(bio);
914 : 69 : int result = -1;
915 : :
916 [ + + ]: 69 : if (SolidSyslogStream_Send(transport, buffer, (size_t) size))
917 : : {
918 : 68 : result = size;
919 : : }
920 : : else
921 : : {
922 : 1 : BIO_clear_retry_flags(bio);
923 : : }
924 : 69 : return result;
925 : : }
926 : :
927 : : /* Minimal ctrl handler. OpenSSL calls this for a variety of control commands
928 : : * during normal operation; returning 1 for the common lifecycle commands lets
929 : : * SSL_connect / SSL_write / SSL_shutdown proceed. Unknown commands return 0. */
930 : : // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- signature fixed by OpenSSL BIO_ctrl_fn contract
931 : 132 : static inline long OpenSslStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg)
932 : : {
933 : : (void) bio;
934 : : (void) larg;
935 : : (void) parg;
936 : 132 : long result = 0;
937 [ + + ]: 132 : switch (cmd)
938 : : {
939 : 131 : case BIO_CTRL_FLUSH:
940 : : case BIO_CTRL_PUSH:
941 : : case BIO_CTRL_POP:
942 : : case BIO_CTRL_DUP:
943 : 131 : result = 1;
944 : 131 : break;
945 : 1 : default:
946 : 1 : break;
947 : : }
948 : 132 : return result;
949 : : }
950 : :
951 : 220 : static inline bool OpenSslStream_ConfigureExpectedHostname(struct SolidSyslogOpenSslStream* self)
952 : : {
953 : 220 : bool ok = true;
954 : 220 : const char* serverName = self->Profile.ServerName;
955 [ + + ]: 220 : if (serverName == NULL)
956 : : {
957 : : /* No expected identity supplied - the handshake will accept any cert that
958 : : * chains to a trusted CA, so the peer is unverified unless a pin names
959 : : * it. Surface it as a WARNING (still connect, preserving the IP-pinned /
960 : : * closed-network case) rather than swallowing the MITM-class default
961 : : * silently. */
962 [ + + ]: 151 : if (self->Installed.FingerprintCount == 0U)
963 : : {
964 : 129 : OpenSslStream_Report(
965 : : SOLIDSYSLOG_SEVERITY_WARNING,
966 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
967 : : SOLIDSYSLOG_TLS_STREAM_ERROR_SERVER_NAME_NOT_SET
968 : : );
969 : : }
970 : : }
971 [ + + ]: 69 : else if (OpenSslStream_NameBeginsWithADot(serverName))
972 : : {
973 : : /* X509_check_host reads a checked name beginning with a dot as a
974 : : * sub-domain pattern matching any depth below it - not the one identity
975 : : * the profile declares. Refused before it reaches SNI or the verifier. */
976 : 3 : OpenSslStream_ReportOpenFailure(
977 : : self,
978 : : SOLIDSYSLOG_SEVERITY_ERROR,
979 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
980 : : SOLIDSYSLOG_TLS_STREAM_ERROR_SERVER_NAME_NOT_APPLIED
981 : : );
982 : 3 : ok = false;
983 : : }
984 [ + + ]: 66 : else if (serverName[0] != '\0')
985 : : {
986 [ + + + + ]: 63 : ok = (SSL_set_tlsext_host_name(self->Ssl, serverName) == 1) && (SSL_set1_host(self->Ssl, serverName) == 1);
987 [ + + ]: 63 : if (!ok)
988 : : {
989 : 4 : OpenSslStream_ReportOpenFailure(
990 : : self,
991 : : SOLIDSYSLOG_SEVERITY_ERROR,
992 : : SOLIDSYSLOG_CAT_BAD_CONFIG,
993 : : SOLIDSYSLOG_TLS_STREAM_ERROR_SERVER_NAME_NOT_APPLIED
994 : : );
995 : : }
996 : : }
997 : : else
998 : : {
999 : : /* Empty string is the deliberate opt-out: the integrator has no name to
1000 : : * verify against (IP-pinning / private CA) and has said so explicitly, so
1001 : : * connect chain-only without a diagnostic. */
1002 : : }
1003 : 220 : return ok;
1004 : : }
1005 : :
1006 : 69 : static inline bool OpenSslStream_NameBeginsWithADot(const char* name)
1007 : : {
1008 : 69 : return name[0] == '.';
1009 : : }
1010 : :
1011 : 1556 : static inline bool OpenSslStream_IsRetryableSslError(int err)
1012 : : {
1013 [ + + + + ]: 1556 : return (err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE);
1014 : : }
1015 : :
1016 : 1515 : static inline bool OpenSslStream_IsHandshakeBudgetExhausted(uint32_t totalSleptMs, uint32_t budgetMs)
1017 : : {
1018 : 1515 : return totalSleptMs >= budgetMs;
1019 : : }
1020 : :
1021 : : /* Null Object substituted at Initialise when the integrator does not install a
1022 : : * getter - returns the compile-time tunable so the bounded-handshake path is a
1023 : : * single code path regardless of whether the integrator wired runtime tuning. */
1024 : 209 : static uint32_t OpenSslStream_NullHandshakeTimeoutGetter(void* context)
1025 : : {
1026 : : (void) context;
1027 : 209 : return (uint32_t) SOLIDSYSLOG_TLS_HANDSHAKE_TIMEOUT_MS;
1028 : : }
1029 : :
1030 : 302 : static inline bool OpenSslStream_ConfigProvidesHandshakeGetter(const struct SolidSyslogOpenSslStreamConfig* config)
1031 : : {
1032 : 302 : return config->GetHandshakeTimeoutMs != NULL;
1033 : : }
1034 : :
1035 : : /* Null Object substituted at Initialise when the integrator installs no version
1036 : : * function - reports an unchanging configuration, so the sender never reconnects
1037 : : * on this stream's account. */
1038 : 1 : static uint32_t OpenSslStream_NullVersion(void* context)
1039 : : {
1040 : : (void) context;
1041 : 1 : return 0U;
1042 : : }
1043 : :
1044 : 302 : static inline bool OpenSslStream_ConfigProvidesVersion(const struct SolidSyslogOpenSslStreamConfig* config)
1045 : : {
1046 : 302 : return config->Version != NULL;
1047 : : }
1048 : :
1049 : : /* Null Object substituted at Initialise when the integrator supplies no profile -
1050 : : * leaves every field at the library default. */
1051 : 1 : static void OpenSslStream_NullProfile(struct SolidSyslogOpenSslProfile* profile, void* context)
1052 : : {
1053 : : (void) context;
1054 : : (void) profile;
1055 : 1 : }
1056 : :
1057 : 302 : static inline bool OpenSslStream_ConfigProvidesProfile(const struct SolidSyslogOpenSslStreamConfig* config)
1058 : : {
1059 : 302 : return config->Profile != NULL;
1060 : : }
1061 : :
1062 : : /* Bridges the integrator-installed getter (or the Null Object substituted at
1063 : : * config-copy time) to the bounded handshake deadline. Invoked at the start
1064 : : * of each handshake attempt so runtime-tunable values take effect on the next
1065 : : * reconnect. */
1066 : 213 : static inline uint32_t OpenSslStream_ResolveHandshakeTimeoutMs(struct SolidSyslogOpenSslStream* self)
1067 : : {
1068 : 213 : return self->Config.GetHandshakeTimeoutMs(self->Config.HandshakeTimeoutContext);
1069 : : }
1070 : :
1071 : : /* Drive SSL_connect to completion under non-blocking transport. Each call may
1072 : : * return WANT_READ/WANT_WRITE while waiting for the multi-RTT handshake to
1073 : : * progress; we sleep briefly between attempts (avoiding a busy spin) until
1074 : : * either the handshake completes, hits a hard error, or the bounded budget
1075 : : * expires. */
1076 : 213 : static inline bool OpenSslStream_PerformHandshake(struct SolidSyslogOpenSslStream* self)
1077 : : {
1078 : 213 : uint32_t budgetMs = OpenSslStream_ResolveHandshakeTimeoutMs(self);
1079 : 213 : uint32_t totalSleptMs = 0U;
1080 : 213 : bool result = false;
1081 : 213 : bool done = false;
1082 : :
1083 [ + + ]: 1936 : while (!done)
1084 : : {
1085 : 1723 : int rc = SSL_connect(self->Ssl);
1086 [ + + ]: 1723 : if (rc > 0)
1087 : : {
1088 : 167 : result = true;
1089 : 167 : done = true;
1090 : : }
1091 : : else
1092 : : {
1093 : 1556 : int err = SSL_get_error(self->Ssl, rc);
1094 [ + + ]: 1556 : if (!OpenSslStream_IsRetryableSslError(err))
1095 : : {
1096 : 41 : OpenSslStream_ReportOpenFailure(
1097 : : self,
1098 : : SOLIDSYSLOG_SEVERITY_ERROR,
1099 : : SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED,
1100 : : OpenSslStream_RefusalDetail(self)
1101 : : );
1102 : 41 : done = true;
1103 : : }
1104 [ + + ]: 1515 : else if (OpenSslStream_IsHandshakeBudgetExhausted(totalSleptMs, budgetMs))
1105 : : {
1106 : 5 : OpenSslStream_ReportOpenFailure(
1107 : : self,
1108 : : SOLIDSYSLOG_SEVERITY_WARNING,
1109 : : SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED,
1110 : : SOLIDSYSLOG_TLS_STREAM_ERROR_HANDSHAKE_TIMEOUT
1111 : : );
1112 : 5 : done = true;
1113 : : }
1114 : : else
1115 : : {
1116 : 1510 : uint32_t pollMs = OpenSslStream_HandshakePollMs(totalSleptMs, budgetMs);
1117 : 1510 : self->Config.Sleep(pollMs);
1118 : 1510 : totalSleptMs += pollMs;
1119 : : }
1120 : : }
1121 : : }
1122 : 213 : return result;
1123 : : }
1124 : :
1125 : : /* The poll interval, or what is left of the budget where that is less, so the
1126 : : * sleeps requested never add up to more than the budget. How long each sleep
1127 : : * really takes, and the handshake itself, can still carry the attempt past it.
1128 : : * Called only while budget remains. */
1129 : 1510 : static inline uint32_t OpenSslStream_HandshakePollMs(uint32_t totalSleptMs, uint32_t budgetMs)
1130 : : {
1131 : 1510 : uint32_t pollMs = (uint32_t) HANDSHAKE_POLL_INTERVAL_MILLISECONDS;
1132 : 1510 : uint32_t remainingMs = budgetMs - totalSleptMs;
1133 [ + + ]: 1510 : if (remainingMs < pollMs)
1134 : : {
1135 : 1 : pollMs = remainingMs;
1136 : : }
1137 : 1510 : return pollMs;
1138 : : }
1139 : :
1140 : : /* The verdict outlives the failed handshake - OpenSSL records it on the
1141 : : * connection as path validation runs - so the refusal can name the check that
1142 : : * produced it rather than the handshake that carried it. A verification failure
1143 : : * this does not name individually reads as untrusted: the certificate did not
1144 : : * validate, which is what the integrator has to act on. */
1145 : 41 : static inline enum SolidSyslogTlsStreamErrors OpenSslStream_RefusalDetail(struct SolidSyslogOpenSslStream* self)
1146 : : {
1147 : 41 : enum SolidSyslogTlsStreamErrors detail = SOLIDSYSLOG_TLS_STREAM_ERROR_HANDSHAKE_REJECTED;
1148 : 41 : long verdict = SSL_get_verify_result(self->Ssl);
1149 [ + + ]: 41 : if (self->PinVerdict == SOLIDSYSLOG_TLS_AUTHORISATION_DIGEST_UNAVAILABLE)
1150 : : {
1151 : 1 : detail = SOLIDSYSLOG_TLS_STREAM_ERROR_FINGERPRINT_DIGEST_UNAVAILABLE;
1152 : : }
1153 [ + + ]: 40 : else if (verdict == X509_V_ERR_APPLICATION_VERIFICATION)
1154 : : {
1155 : 7 : detail = SOLIDSYSLOG_TLS_STREAM_ERROR_PEER_FINGERPRINT_MISMATCHED;
1156 : : }
1157 [ + + ]: 33 : else if (OpenSslStream_IsPeerNameMismatch(verdict))
1158 : : {
1159 : 5 : detail = SOLIDSYSLOG_TLS_STREAM_ERROR_PEER_NAME_MISMATCHED;
1160 : : }
1161 [ + + ]: 28 : else if (verdict == X509_V_ERR_CERT_HAS_EXPIRED)
1162 : : {
1163 : 8 : detail = SOLIDSYSLOG_TLS_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED;
1164 : : }
1165 [ + + ]: 20 : else if (verdict == X509_V_ERR_CERT_NOT_YET_VALID)
1166 : : {
1167 : 2 : detail = SOLIDSYSLOG_TLS_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID;
1168 : : }
1169 [ + + + + ]: 18 : else if ((verdict != X509_V_OK) && !OpenSslStream_ChainTrustWasWaivedByPolicy(self, verdict))
1170 : : {
1171 : 5 : detail = SOLIDSYSLOG_TLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED;
1172 : : }
1173 : : else
1174 : : {
1175 : : /* Verification passed or never ran, so the refusal is a protocol or
1176 : : * transport fault rather than one the peer's certificate explains. */
1177 : : }
1178 : 41 : return detail;
1179 : : }
1180 : :
1181 : : /* Authorising by pin alone waives the chain-trust objection, and OpenSSL keeps
1182 : : * the waived code in verify_result for the rest of the connection - it is
1183 : : * deliberately sticky. So a refusal arriving later, when the collector rejects
1184 : : * us or the connection drops, must not be read back out of it as a fault in the
1185 : : * peer's certificate: that names the wrong end. */
1186 : 7 : static inline bool OpenSslStream_ChainTrustWasWaivedByPolicy(const struct SolidSyslogOpenSslStream* self, long verdict)
1187 : : {
1188 [ + + + + : 10 : return (self->Installed.FingerprintCount > 0U) && !self->Installed.TrustAnchorsInstalled &&
+ + ]
1189 : 3 : OpenSslStream_IsChainTrustError((int) verdict);
1190 : : }
1191 : :
1192 : : /* An expected identity that parses as an IP literal is checked as an address
1193 : : * rather than a name - SSL_set1_host installs it that way - so the two verdicts
1194 : : * are one fault in the portable vocabulary, and an integrator configuring a
1195 : : * collector by address is told the same thing as one configuring it by name. */
1196 : 76 : static inline bool OpenSslStream_IsPeerNameMismatch(long verdict)
1197 : : {
1198 [ + + + + ]: 76 : return (verdict == X509_V_ERR_HOSTNAME_MISMATCH) || (verdict == X509_V_ERR_IP_ADDRESS_MISMATCH);
1199 : : }
1200 : :
1201 : 17 : static inline bool OpenSslStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size)
1202 : : {
1203 : 17 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base);
1204 : 17 : bool ok = false;
1205 : :
1206 [ + + ]: 17 : if (OpenSslStream_SessionIsStillOpen(base))
1207 : : {
1208 : 13 : int rc = SSL_write(self->Ssl, buffer, (int) size);
1209 [ + + + + ]: 13 : ok = (rc > 0) && ((size_t) rc == size);
1210 : :
1211 [ + + ]: 13 : if (!ok)
1212 : : {
1213 : 5 : OpenSslStream_Close(base);
1214 : : }
1215 : : }
1216 : 17 : return ok;
1217 : : }
1218 : :
1219 : : /* SSL_write does not look at what the peer has sent, so a collector that has
1220 : : * closed the session - a close_notify and a FIN - still has the record
1221 : : * accepted, and it is lost unless the peer's reset beats the write. Read what
1222 : : * is waiting first: an alert or a closed transport ends the session here, and
1223 : : * Read closes it. A collector sends no application data, so any that arrives is
1224 : : * discarded; a peer that keeps sending is plainly still there, and the record
1225 : : * goes out once the bound is reached. */
1226 : 17 : static bool OpenSslStream_SessionIsStillOpen(struct SolidSyslogStream* base)
1227 : : {
1228 : : char discard[OPENSSL_STREAM_DISCARD_BYTES];
1229 : 17 : SolidSyslogSsize pending = OpenSslStream_Read(base, discard, sizeof(discard));
1230 : 17 : uint32_t reads = 1U;
1231 [ + + + + ]: 102 : while ((pending > 0) && (reads < OPENSSL_STREAM_MAX_READS_BEFORE_WRITE))
1232 : : {
1233 : 85 : pending = OpenSslStream_Read(base, discard, sizeof(discard));
1234 : 85 : reads++;
1235 : : }
1236 : 17 : return pending >= 0;
1237 : : }
1238 : :
1239 : : /* SSL_read has two distinct modes worth keeping straight:
1240 : : * 1. Steady-state application read: bytes available -> return them; nothing
1241 : : * to read right now -> SSL_ERROR_WANT_READ -> return 0 mirrors the transport
1242 : : * Read contract.
1243 : : * 2. Renegotiation or alerts mid-stream: SSL_read may need to write (server
1244 : : * requested re-key), surfacing as SSL_ERROR_WANT_WRITE. Under fail-fast
1245 : : * semantics this is a transport failure - close internally; the caller
1246 : : * reopens, store-and-forward replays. Same rule for any other SSL error.
1247 : : * Anything below the WANT_READ branch therefore takes the Close path. */
1248 : 109 : static inline SolidSyslogSsize OpenSslStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size)
1249 : : {
1250 : 109 : struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base);
1251 : 109 : int rc = SSL_read(self->Ssl, buffer, (int) size);
1252 : 109 : SolidSyslogSsize result = -1;
1253 : :
1254 [ + + ]: 109 : if (rc > 0)
1255 : : {
1256 : 101 : result = (SolidSyslogSsize) rc;
1257 : : }
1258 [ + + ]: 8 : else if (SSL_get_error(self->Ssl, rc) == SSL_ERROR_WANT_READ)
1259 : : {
1260 : 2 : result = 0;
1261 : : }
1262 : : else
1263 : : {
1264 : 6 : OpenSslStream_Close(base);
1265 : : }
1266 : 109 : return result;
1267 : : }
|