Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-certificate"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <time.h>
9 : :
10 : : #include <gio/gio.h>
11 : : #include <gnutls/gnutls.h>
12 : : #include <gnutls/abstract.h>
13 : : #include <gnutls/crypto.h>
14 : : #include <gnutls/x509.h>
15 : :
16 : : #include <libvalent-core.h>
17 : : #include "valent-certificate.h"
18 : : #include "valent-device.h"
19 : :
20 : : #define ACTIVATION_TIMESPAN (60L*60L*24L*365L)
21 : : #define EXPIRATION_TIMESPAN (60L*60L*24L*10L*365L)
22 : :
23 : : /**
24 : : * valent_certificate_generate:
25 : : * @cert_path: (type filename): file path to the certificate
26 : : * @key_path: (type filename): file path to the private key
27 : : * @common_name: common name for the certificate
28 : : * @error: (nullable): a `GError`
29 : : *
30 : : * Generate a private key and certificate for @common_name, saving them at
31 : : * @key_path and @cert_path respectively.
32 : : *
33 : : * Returns: %TRUE if successful, or %FALSE with @error set
34 : : *
35 : : * Since: 1.0
36 : : */
37 : : static gboolean
38 : 219 : valent_certificate_generate (const char *cert_path,
39 : : const char *key_path,
40 : : const char *common_name,
41 : : GError **error)
42 : : {
43 : 219 : gnutls_x509_crt_t crt = NULL;
44 : 219 : gnutls_x509_privkey_t privkey = NULL;
45 : 219 : gnutls_datum_t crt_out = { 0, };
46 : 219 : gnutls_datum_t privkey_out = { 0, };
47 : 438 : g_autofree char *dn = NULL;
48 : 219 : time_t now;
49 : 219 : unsigned char serial[20];
50 : 219 : int rc;
51 : 219 : gboolean ret = FALSE;
52 : :
53 : 219 : VALENT_ENTRY;
54 : :
55 : : /* The private key is a 256-bit ECC key. This is `NID_X9_62_prime256v1` in
56 : : * OpenSSL and `GNUTLS_ECC_CURVE_SECP256R1` in GnuTLS.
57 : : */
58 [ + - ]: 219 : if ((rc = gnutls_x509_privkey_init (&privkey)) != GNUTLS_E_SUCCESS ||
59 [ + - ]: 219 : (rc = gnutls_x509_privkey_generate (privkey,
60 : : GNUTLS_PK_ECDSA,
61 : : GNUTLS_CURVE_TO_BITS (GNUTLS_ECC_CURVE_SECP256R1),
62 : 219 : 0)) != GNUTLS_E_SUCCESS ||
63 [ - + ]: 219 : (rc = gnutls_x509_privkey_export2 (privkey,
64 : : GNUTLS_X509_FMT_PEM,
65 : : &privkey_out)) != GNUTLS_E_SUCCESS)
66 : : {
67 : 0 : g_set_error (error,
68 : : G_IO_ERROR,
69 : : G_IO_ERROR_FAILED,
70 : : "Generating private key: %s",
71 : : gnutls_strerror (rc));
72 : 0 : VALENT_GOTO (out);
73 : : }
74 : :
75 [ + - ]: 219 : if ((rc = gnutls_x509_crt_init (&crt)) != GNUTLS_E_SUCCESS ||
76 [ + - ]: 219 : (rc = gnutls_x509_crt_set_key (crt, privkey)) != GNUTLS_E_SUCCESS ||
77 [ - + ]: 219 : (rc = gnutls_x509_crt_set_version (crt, 3)) != GNUTLS_E_SUCCESS)
78 : : {
79 : 0 : g_set_error (error,
80 : : G_IO_ERROR,
81 : : G_IO_ERROR_FAILED,
82 : : "Generating certificate: %s",
83 : : gnutls_strerror (rc));
84 : 0 : VALENT_GOTO (out);
85 : : }
86 : :
87 : : /* The certificate is set to be activated 1 year in the past, with an
88 : : * expiration date 10 years in the future.
89 : : */
90 : 219 : now = time (NULL);
91 [ + - ]: 219 : if ((rc = gnutls_x509_crt_set_activation_time (crt, now - ACTIVATION_TIMESPAN)) != GNUTLS_E_SUCCESS ||
92 [ - + ]: 219 : (rc = gnutls_x509_crt_set_expiration_time (crt, now + EXPIRATION_TIMESPAN)) != GNUTLS_E_SUCCESS)
93 : : {
94 : 0 : g_set_error (error,
95 : : G_IO_ERROR,
96 : : G_IO_ERROR_FAILED,
97 : : "Setting certificate activation and expiration: %s",
98 : : gnutls_strerror (rc));
99 : 0 : VALENT_GOTO (out);
100 : : }
101 : :
102 : : /* While kdeconnect-android uses the static serial `1`, kdeconnect-kde uses
103 : : * a randomized serial, which presumably has some obscure security benefit.
104 : : */
105 [ + - ]: 219 : if ((rc = gnutls_rnd (GNUTLS_RND_RANDOM, serial, sizeof (serial))) != GNUTLS_E_SUCCESS ||
106 [ - + ]: 219 : (rc = gnutls_x509_crt_set_serial (crt, &serial, sizeof (serial))) != GNUTLS_E_SUCCESS)
107 : : {
108 : 0 : g_set_error (error,
109 : : G_IO_ERROR,
110 : : G_IO_ERROR_FAILED,
111 : : "Setting certificate serial: %s",
112 : : gnutls_strerror (rc));
113 : 0 : VALENT_RETURN (ret);
114 : : }
115 : :
116 : : /* KDE Connect sets this to `O=KDE,OU=KDE Connect,CN=<device-id>`, where
117 : : * `<device-id>` matches the pattern `/^[a-zA-Z0-9_]{32,38}$/`.
118 : : */
119 : 219 : dn = g_strdup_printf ("O=%s,OU=%s,CN=%s", "Valent", "Valent", common_name);
120 [ - + ]: 219 : if ((rc = gnutls_x509_crt_set_dn (crt, dn, NULL)) != GNUTLS_E_SUCCESS)
121 : : {
122 : 0 : g_set_error (error,
123 : : G_IO_ERROR,
124 : : G_IO_ERROR_FAILED,
125 : : "Setting certificate common name: %s",
126 : : gnutls_strerror (rc));
127 : 0 : VALENT_GOTO (out);
128 : : }
129 : :
130 : : /* The signature is a 512-bit SHA512 with ECDSA. This is `EVP_sha512` in
131 : : * OpenSSL and `GNUTLS_DIG_SHA512` in GnuTLS.
132 : : */
133 [ + - ]: 219 : if ((rc = gnutls_x509_crt_sign2 (crt, crt, privkey, GNUTLS_DIG_SHA512, 0)) != GNUTLS_E_SUCCESS ||
134 [ - + ]: 219 : (rc = gnutls_x509_crt_export2 (crt, GNUTLS_X509_FMT_PEM, &crt_out)) != GNUTLS_E_SUCCESS)
135 : : {
136 : 0 : g_set_error (error,
137 : : G_IO_ERROR,
138 : : G_IO_ERROR_FAILED,
139 : : "Signing certificate: %s",
140 : : gnutls_strerror (rc));
141 : 0 : VALENT_GOTO (out);
142 : : }
143 : :
144 : : /* Write the certificate and private key to disk
145 : : */
146 : 438 : ret = g_file_set_contents_full (cert_path,
147 : 219 : (const char *)crt_out.data,
148 : 219 : crt_out.size,
149 : : G_FILE_SET_CONTENTS_DURABLE,
150 : : 0600,
151 : : error);
152 [ - + ]: 219 : if (ret)
153 : : {
154 : 219 : ret = g_file_set_contents_full (key_path,
155 : 219 : (const char *)privkey_out.data,
156 : 219 : privkey_out.size,
157 : : G_FILE_SET_CONTENTS_DURABLE,
158 : : 0600,
159 : : error);
160 : : }
161 : :
162 : 0 : out:
163 [ + - ]: 219 : g_clear_pointer (&crt, gnutls_x509_crt_deinit);
164 [ + - ]: 219 : g_clear_pointer (&privkey, gnutls_x509_privkey_deinit);
165 [ + - ]: 219 : g_clear_pointer (&crt_out.data, gnutls_free);
166 [ + - ]: 219 : g_clear_pointer (&privkey_out.data, gnutls_free);
167 : :
168 : 219 : VALENT_RETURN (ret);
169 : : }
170 : :
171 : : static void
172 : 1 : valent_certificate_new_task (GTask *task,
173 : : gpointer source_object,
174 : : gpointer task_data,
175 : : GCancellable *cancellable)
176 : : {
177 : 1 : g_autoptr (GTlsCertificate) certificate = NULL;
178 : 1 : const char *path = task_data;
179 : 1 : GError *error = NULL;
180 : :
181 [ - + ]: 1 : if ((certificate = valent_certificate_new_sync (path, &error)) == NULL)
182 : 0 : return g_task_return_error (task, error);
183 : :
184 : 1 : g_task_return_pointer (task, g_steal_pointer (&certificate), g_object_unref);
185 : : }
186 : :
187 : : /**
188 : : * valent_certificate_new:
189 : : * @path: (type filename): a directory path
190 : : * @cancellable: (nullable): `GCancellable`
191 : : * @callback: (scope async): a `GAsyncReadyCallback`
192 : : * @user_data: user supplied data
193 : : *
194 : : * Get a TLS certificate and private key pair.
195 : : *
196 : : * This ensures a TLS certificate with the filename `certificate.pem` and
197 : : * private key with filename `private.pem` exist in a directory at @path.
198 : : *
199 : : * If either one doesn't exist, a new certificate and private key pair will be
200 : : * generated. The common name will be set to a string returned by
201 : : * [func@GLib.uuid_string_random].
202 : : *
203 : : * Get the result with [func@Valent.certificate_new_finish].
204 : : *
205 : : * Since: 1.0
206 : : */
207 : : void
208 : 1 : valent_certificate_new (const char *path,
209 : : GCancellable *cancellable,
210 : : GAsyncReadyCallback callback,
211 : : gpointer user_data)
212 : : {
213 : 2 : g_autoptr (GTask) task = NULL;
214 : :
215 [ + - + - ]: 1 : g_return_if_fail (path != NULL && *path != '\0');
216 [ - + - - : 1 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
- - - - ]
217 : :
218 : 1 : task = g_task_new (NULL, cancellable, callback, user_data);
219 [ + - ]: 1 : g_task_set_source_tag (task, valent_certificate_new);
220 [ - + ]: 2 : g_task_set_task_data (task, g_strdup (path), g_free);
221 [ + - ]: 1 : g_task_run_in_thread (task, valent_certificate_new_task);
222 : : }
223 : :
224 : : /**
225 : : * valent_certificate_new_finish:
226 : : * @result: a `GAsyncResult` provided to callback
227 : : * @error: (nullable): a `GError`
228 : : *
229 : : * Finish an operation started by [func@Valent.certificate_new].
230 : : *
231 : : * If either generating or loading the certificate failed, %NULL will be
232 : : * returned with @error set.
233 : : *
234 : : * Returns: (transfer full) (nullable): a `GTlsCertificate`
235 : : *
236 : : * Since: 1.0
237 : : */
238 : : GTlsCertificate *
239 : 1 : valent_certificate_new_finish (GAsyncResult *result,
240 : : GError **error)
241 : : {
242 [ + - ]: 1 : g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
243 : :
244 : 1 : return g_task_propagate_pointer (G_TASK (result), error);
245 : : }
246 : :
247 : : /**
248 : : * valent_certificate_new_sync:
249 : : * @path: (type filename): a directory path
250 : : * @error: (nullable): a `GError`
251 : : *
252 : : * Get a TLS certificate and private key pair.
253 : : *
254 : : * This ensures a TLS certificate with the filename `certificate.pem` and
255 : : * private key with filename `private.pem` exist in a directory at @path.
256 : : *
257 : : * If either one doesn't exist, a new certificate and private key pair will be
258 : : * generated. The common name will be set to a string returned by
259 : : * [func@Valent.Device.generate_id].
260 : : *
261 : : * If either generating or loading the certificate fails, %NULL will be returned
262 : : * with @error set.
263 : : *
264 : : * Returns: (transfer full) (nullable): a `GTlsCertificate`
265 : : *
266 : : * Since: 1.0
267 : : */
268 : : GTlsCertificate *
269 : 233 : valent_certificate_new_sync (const char *path,
270 : : GError **error)
271 : : {
272 : 466 : g_autofree char *cert_path = NULL;
273 : 233 : g_autofree char *key_path = NULL;
274 : :
275 [ + - + - ]: 233 : g_return_val_if_fail (path != NULL && *path != '\0', NULL);
276 [ + + - + ]: 233 : g_return_val_if_fail (error == NULL || *error == NULL, NULL);
277 : :
278 : 233 : cert_path = g_build_filename (path, "certificate.pem", NULL);
279 : 233 : key_path = g_build_filename (path, "private.pem", NULL);
280 : :
281 [ + + - + ]: 247 : if (!g_file_test (cert_path, G_FILE_TEST_IS_REGULAR) ||
282 : 14 : !g_file_test (key_path, G_FILE_TEST_IS_REGULAR))
283 : : {
284 : 219 : g_autofree char *cn = NULL;
285 : :
286 : 219 : cn = valent_device_generate_id ();
287 : :
288 [ - + ]: 219 : if (!valent_certificate_generate (cert_path, key_path, cn, error))
289 : 0 : return NULL;
290 : : }
291 : :
292 : 233 : return g_tls_certificate_new_from_files (cert_path, key_path, error);
293 : : }
294 : :
295 : : /**
296 : : * valent_certificate_get_common_name:
297 : : * @certificate: a `GTlsCertificate`
298 : : *
299 : : * Get the common name from @certificate, which by convention in KDE Connect is
300 : : * the single source of truth for a device's ID.
301 : : *
302 : : * Returns: (transfer none) (nullable): the certificate ID
303 : : *
304 : : * Since: 1.0
305 : : */
306 : : const char *
307 : 244 : valent_certificate_get_common_name (GTlsCertificate *certificate)
308 : : {
309 : 488 : g_autoptr (GByteArray) certificate_der = NULL;
310 : 244 : gnutls_x509_crt_t crt = NULL;
311 : 244 : gnutls_datum_t crt_der = { 0, };
312 : 244 : char buf[64] = { 0, };
313 : 244 : size_t buf_size = 64;
314 : 244 : const char *cn;
315 : 244 : int rc;
316 : :
317 [ + - + - : 244 : g_return_val_if_fail (G_IS_TLS_CERTIFICATE (certificate), NULL);
+ - - + ]
318 : :
319 : 244 : cn = g_object_get_data (G_OBJECT (certificate), "valent-certificate-cn");
320 [ + + ]: 244 : if (cn != NULL)
321 : : return cn;
322 : :
323 : 238 : g_object_get (certificate, "certificate", &certificate_der, NULL);
324 : 238 : crt_der.data = certificate_der->data;
325 : 238 : crt_der.size = certificate_der->len;
326 : :
327 [ + - ]: 238 : if ((rc = gnutls_x509_crt_init (&crt)) != GNUTLS_E_SUCCESS ||
328 [ + - ]: 238 : (rc = gnutls_x509_crt_import (crt, &crt_der, GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS ||
329 [ - + ]: 238 : (rc = gnutls_x509_crt_get_dn_by_oid (crt,
330 : : GNUTLS_OID_X520_COMMON_NAME,
331 : : 0,
332 : : 0,
333 : : &buf,
334 : : &buf_size)) != GNUTLS_E_SUCCESS)
335 : : {
336 : 0 : g_warning ("%s(): %s", G_STRFUNC, gnutls_strerror (rc));
337 [ # # ]: 0 : g_clear_pointer (&crt, gnutls_x509_crt_deinit);
338 : :
339 : 0 : return NULL;
340 : : }
341 : :
342 : : /* Intern the common name as private data
343 : : */
344 : 238 : g_object_set_data_full (G_OBJECT (certificate),
345 : : "valent-certificate-cn",
346 : 238 : g_strndup (buf, buf_size),
347 : : g_free);
348 [ + - ]: 238 : g_clear_pointer (&crt, gnutls_x509_crt_deinit);
349 : :
350 : 238 : return g_object_get_data (G_OBJECT (certificate), "valent-certificate-cn");
351 : : }
352 : :
353 : : /**
354 : : * valent_certificate_get_public_key:
355 : : * @certificate: a `GTlsCertificate`
356 : : *
357 : : * Get the public key of @certificate.
358 : : *
359 : : * Returns: (transfer none): a DER-encoded public key
360 : : *
361 : : * Since: 1.0
362 : : */
363 : : GByteArray *
364 : 5 : valent_certificate_get_public_key (GTlsCertificate *certificate)
365 : : {
366 : 10 : g_autoptr (GByteArray) certificate_der = NULL;
367 [ + + ]: 5 : g_autoptr (GByteArray) pk = NULL;
368 : 5 : gnutls_x509_crt_t crt = NULL;
369 : 5 : gnutls_datum_t crt_der = { 0, };
370 : 5 : gnutls_pubkey_t pubkey = NULL;
371 : 5 : size_t size;
372 : 5 : int rc;
373 : :
374 [ + - + - : 5 : g_return_val_if_fail (G_IS_TLS_CERTIFICATE (certificate), NULL);
+ - - + ]
375 : :
376 : 5 : pk = g_object_get_data (G_OBJECT (certificate), "valent-certificate-pk");
377 [ + + ]: 5 : if (pk != NULL)
378 : : return g_steal_pointer (&pk);
379 : :
380 : 3 : g_object_get (certificate, "certificate", &certificate_der, NULL);
381 : 3 : crt_der.data = certificate_der->data;
382 : 3 : crt_der.size = certificate_der->len;
383 : :
384 [ + - ]: 3 : if ((rc = gnutls_x509_crt_init (&crt)) != GNUTLS_E_SUCCESS ||
385 [ - + ]: 3 : (rc = gnutls_x509_crt_import (crt, &crt_der, GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS)
386 : : {
387 : 0 : g_warning ("%s(): %s", G_STRFUNC, gnutls_strerror (rc));
388 : 0 : VALENT_GOTO (out);
389 : : }
390 : :
391 [ + - ]: 3 : if ((rc = gnutls_pubkey_init (&pubkey)) != GNUTLS_E_SUCCESS ||
392 [ - + ]: 3 : (rc = gnutls_pubkey_import_x509 (pubkey, crt, 0)) != GNUTLS_E_SUCCESS)
393 : : {
394 : 0 : g_warning ("%s(): %s", G_STRFUNC, gnutls_strerror (rc));
395 : 0 : VALENT_GOTO (out);
396 : : }
397 : :
398 : : /* First call to get the size, since GByteArray.len is an `unsigned int`,
399 : : * while the output is a `size_t`.
400 : : */
401 : 3 : rc = gnutls_pubkey_export (pubkey, GNUTLS_X509_FMT_DER, NULL, &size);
402 [ - + ]: 3 : if (rc != GNUTLS_E_SUCCESS && rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
403 : : {
404 : 0 : g_warning ("%s(): %s", G_STRFUNC, gnutls_strerror (rc));
405 : 0 : VALENT_GOTO (out);
406 : : }
407 : :
408 [ - + ]: 3 : g_assert (size <= (size_t)UINT_MAX);
409 : 3 : pk = g_byte_array_sized_new (size);
410 : 3 : pk->len = (unsigned int)size;
411 [ - + ]: 3 : if ((rc = gnutls_pubkey_export (pubkey,
412 : : GNUTLS_X509_FMT_DER,
413 : 3 : pk->data, &size)) != GNUTLS_E_SUCCESS)
414 : : {
415 : 0 : g_warning ("%s(): %s", G_STRFUNC, gnutls_strerror (rc));
416 : 0 : VALENT_GOTO (out);
417 : : }
418 : :
419 : : /* Intern the DER as private data
420 : : */
421 : 3 : g_object_set_data_full (G_OBJECT (certificate),
422 : : "valent-certificate-pk",
423 : : g_steal_pointer (&pk),
424 : : (GDestroyNotify)g_byte_array_unref);
425 : :
426 : 3 : out:
427 [ + - ]: 3 : g_clear_pointer (&crt, gnutls_x509_crt_deinit);
428 [ + - ]: 3 : g_clear_pointer (&pubkey, gnutls_pubkey_deinit);
429 : :
430 : 3 : return g_object_get_data (G_OBJECT (certificate), "valent-certificate-pk");
431 : : }
432 : :
|