blob: c1d514a98943360ded7c09d449fc03232b413565 [file] [log] [blame]
Dmitry Shmidtd5dc24e2014-03-12 14:22:04 -07001/*
2 * Hotspot 2.0 OSU client - EST client
3 * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
4 *
5 * This software may be distributed under the terms of the BSD license.
6 * See README for more details.
7 */
8
9#include "includes.h"
10#include <openssl/err.h>
11#include <openssl/evp.h>
12#include <openssl/pem.h>
13#include <openssl/pkcs7.h>
14#include <openssl/rsa.h>
15#include <openssl/asn1.h>
16#include <openssl/asn1t.h>
17#include <openssl/x509.h>
18#include <openssl/x509v3.h>
19
20#include "common.h"
21#include "utils/base64.h"
22#include "utils/xml-utils.h"
23#include "utils/http-utils.h"
24#include "osu_client.h"
25
26
27static int pkcs7_to_cert(struct hs20_osu_client *ctx, const u8 *pkcs7,
28 size_t len, char *pem_file, char *der_file)
29{
30 PKCS7 *p7 = NULL;
31 const unsigned char *p = pkcs7;
32 STACK_OF(X509) *certs;
33 int i, num, ret = -1;
34 BIO *out = NULL;
35
36 p7 = d2i_PKCS7(NULL, &p, len);
37 if (p7 == NULL) {
38 wpa_printf(MSG_INFO, "Could not parse PKCS#7 object: %s",
39 ERR_error_string(ERR_get_error(), NULL));
40 write_result(ctx, "Could not parse PKCS#7 object from EST");
41 goto fail;
42 }
43
44 switch (OBJ_obj2nid(p7->type)) {
45 case NID_pkcs7_signed:
46 certs = p7->d.sign->cert;
47 break;
48 case NID_pkcs7_signedAndEnveloped:
49 certs = p7->d.signed_and_enveloped->cert;
50 break;
51 default:
52 certs = NULL;
53 break;
54 }
55
56 if (!certs || ((num = sk_X509_num(certs)) == 0)) {
57 wpa_printf(MSG_INFO, "No certificates found in PKCS#7 object");
58 write_result(ctx, "No certificates found in PKCS#7 object");
59 goto fail;
60 }
61
62 if (der_file) {
63 FILE *f = fopen(der_file, "wb");
64 if (f == NULL)
65 goto fail;
66 i2d_X509_fp(f, sk_X509_value(certs, 0));
67 fclose(f);
68 }
69
70 if (pem_file) {
71 out = BIO_new(BIO_s_file());
72 if (out == NULL ||
73 BIO_write_filename(out, pem_file) <= 0)
74 goto fail;
75
76 for (i = 0; i < num; i++) {
77 X509 *cert = sk_X509_value(certs, i);
78 X509_print(out, cert);
79 PEM_write_bio_X509(out, cert);
80 BIO_puts(out, "\n");
81 }
82 }
83
84 ret = 0;
85
86fail:
87 PKCS7_free(p7);
88 if (out)
89 BIO_free_all(out);
90
91 return ret;
92}
93
94
95int est_load_cacerts(struct hs20_osu_client *ctx, const char *url)
96{
97 char *buf, *resp;
98 size_t buflen;
99 unsigned char *pkcs7;
100 size_t pkcs7_len, resp_len;
101 int res;
102
103 buflen = os_strlen(url) + 100;
104 buf = os_malloc(buflen);
105 if (buf == NULL)
106 return -1;
107
108 os_snprintf(buf, buflen, "%s/cacerts", url);
109 wpa_printf(MSG_INFO, "Download EST cacerts from %s", buf);
110 write_summary(ctx, "Download EST cacerts from %s", buf);
111 res = http_download_file(ctx->http, buf, "Cert/est-cacerts.txt",
112 ctx->ca_fname);
113 if (res < 0) {
114 wpa_printf(MSG_INFO, "Failed to download EST cacerts from %s",
115 buf);
116 write_result(ctx, "Failed to download EST cacerts from %s",
117 buf);
118 os_free(buf);
119 return -1;
120 }
121 os_free(buf);
122
123 resp = os_readfile("Cert/est-cacerts.txt", &resp_len);
124 if (resp == NULL) {
125 wpa_printf(MSG_INFO, "Could not read Cert/est-cacerts.txt");
126 write_result(ctx, "Could not read EST cacerts");
127 return -1;
128 }
129
130 pkcs7 = base64_decode((unsigned char *) resp, resp_len, &pkcs7_len);
131 if (pkcs7 && pkcs7_len < resp_len / 2) {
132 wpa_printf(MSG_INFO, "Too short base64 decode (%u bytes; downloaded %u bytes) - assume this was binary",
133 (unsigned int) pkcs7_len, (unsigned int) resp_len);
134 os_free(pkcs7);
135 pkcs7 = NULL;
136 }
137 if (pkcs7 == NULL) {
138 wpa_printf(MSG_INFO, "EST workaround - Could not decode base64, assume this is DER encoded PKCS7");
139 pkcs7 = os_malloc(resp_len);
140 if (pkcs7) {
141 os_memcpy(pkcs7, resp, resp_len);
142 pkcs7_len = resp_len;
143 }
144 }
145 os_free(resp);
146
147 if (pkcs7 == NULL) {
148 wpa_printf(MSG_INFO, "Could not fetch PKCS7 cacerts");
149 write_result(ctx, "Could not fetch EST PKCS#7 cacerts");
150 return -1;
151 }
152
153 res = pkcs7_to_cert(ctx, pkcs7, pkcs7_len, "Cert/est-cacerts.pem",
154 NULL);
155 os_free(pkcs7);
156 if (res < 0) {
157 wpa_printf(MSG_INFO, "Could not parse CA certs from PKCS#7 cacerts response");
158 write_result(ctx, "Could not parse CA certs from EST PKCS#7 cacerts response");
159 return -1;
160 }
161 unlink("Cert/est-cacerts.txt");
162
163 return 0;
164}
165
166
167/*
168 * CsrAttrs ::= SEQUENCE SIZE (0..MAX) OF AttrOrOID
169 *
170 * AttrOrOID ::= CHOICE {
171 * oid OBJECT IDENTIFIER,
172 * attribute Attribute }
173 *
174 * Attribute ::= SEQUENCE {
175 * type OBJECT IDENTIFIER,
176 * values SET SIZE(1..MAX) OF OBJECT IDENTIFIER }
177 */
178
179typedef struct {
180 ASN1_OBJECT *type;
181 STACK_OF(ASN1_OBJECT) *values;
182} Attribute;
183
184typedef struct {
185 int type;
186 union {
187 ASN1_OBJECT *oid;
188 Attribute *attribute;
189 } d;
190} AttrOrOID;
191
192typedef struct {
193 int type;
194 STACK_OF(AttrOrOID) *attrs;
195} CsrAttrs;
196
197ASN1_SEQUENCE(Attribute) = {
198 ASN1_SIMPLE(Attribute, type, ASN1_OBJECT),
199 ASN1_SET_OF(Attribute, values, ASN1_OBJECT)
200} ASN1_SEQUENCE_END(Attribute);
201
202ASN1_CHOICE(AttrOrOID) = {
203 ASN1_SIMPLE(AttrOrOID, d.oid, ASN1_OBJECT),
204 ASN1_SIMPLE(AttrOrOID, d.attribute, Attribute)
205} ASN1_CHOICE_END(AttrOrOID);
206
207ASN1_CHOICE(CsrAttrs) = {
208 ASN1_SEQUENCE_OF(CsrAttrs, attrs, AttrOrOID)
209} ASN1_CHOICE_END(CsrAttrs);
210
211IMPLEMENT_ASN1_FUNCTIONS(CsrAttrs);
212
213
214static void add_csrattrs_oid(struct hs20_osu_client *ctx, ASN1_OBJECT *oid,
215 STACK_OF(X509_EXTENSION) *exts)
216{
217 char txt[100];
218 int res;
219
220 if (!oid)
221 return;
222
223 res = OBJ_obj2txt(txt, sizeof(txt), oid, 1);
224 if (res < 0 || res >= (int) sizeof(txt))
225 return;
226
227 if (os_strcmp(txt, "1.2.840.113549.1.9.7") == 0) {
228 wpa_printf(MSG_INFO, "TODO: csrattr challengePassword");
229 } else if (os_strcmp(txt, "1.2.840.113549.1.1.11") == 0) {
230 wpa_printf(MSG_INFO, "csrattr sha256WithRSAEncryption");
231 } else {
232 wpa_printf(MSG_INFO, "Ignore unsupported csrattr oid %s", txt);
233 }
234}
235
236
237static void add_csrattrs_ext_req(struct hs20_osu_client *ctx,
238 STACK_OF(ASN1_OBJECT) *values,
239 STACK_OF(X509_EXTENSION) *exts)
240{
241 char txt[100];
242 int i, num, res;
243
244 num = sk_ASN1_OBJECT_num(values);
245 for (i = 0; i < num; i++) {
246 ASN1_OBJECT *oid = sk_ASN1_OBJECT_value(values, i);
247
248 res = OBJ_obj2txt(txt, sizeof(txt), oid, 1);
249 if (res < 0 || res >= (int) sizeof(txt))
250 continue;
251
252 if (os_strcmp(txt, "1.3.6.1.1.1.1.22") == 0) {
253 wpa_printf(MSG_INFO, "TODO: extReq macAddress");
254 } else if (os_strcmp(txt, "1.3.6.1.4.1.40808.1.1.3") == 0) {
255 wpa_printf(MSG_INFO, "TODO: extReq imei");
256 } else if (os_strcmp(txt, "1.3.6.1.4.1.40808.1.1.4") == 0) {
257 wpa_printf(MSG_INFO, "TODO: extReq meid");
258 } else if (os_strcmp(txt, "1.3.6.1.4.1.40808.1.1.5") == 0) {
259 wpa_printf(MSG_INFO, "TODO: extReq DevId");
260 } else {
261 wpa_printf(MSG_INFO, "Ignore unsupported cstattr extensionsRequest %s",
262 txt);
263 }
264 }
265}
266
267
268static void add_csrattrs_attr(struct hs20_osu_client *ctx, Attribute *attr,
269 STACK_OF(X509_EXTENSION) *exts)
270{
271 char txt[100], txt2[100];
272 int i, num, res;
273
274 if (!attr || !attr->type || !attr->values)
275 return;
276
277 res = OBJ_obj2txt(txt, sizeof(txt), attr->type, 1);
278 if (res < 0 || res >= (int) sizeof(txt))
279 return;
280
281 if (os_strcmp(txt, "1.2.840.113549.1.9.14") == 0) {
282 add_csrattrs_ext_req(ctx, attr->values, exts);
283 return;
284 }
285
286 num = sk_ASN1_OBJECT_num(attr->values);
287 for (i = 0; i < num; i++) {
288 ASN1_OBJECT *oid = sk_ASN1_OBJECT_value(attr->values, i);
289
290 res = OBJ_obj2txt(txt2, sizeof(txt2), oid, 1);
291 if (res < 0 || res >= (int) sizeof(txt2))
292 continue;
293
294 wpa_printf(MSG_INFO, "Ignore unsupported cstattr::attr %s oid %s",
295 txt, txt2);
296 }
297}
298
299
300static void add_csrattrs(struct hs20_osu_client *ctx, CsrAttrs *csrattrs,
301 STACK_OF(X509_EXTENSION) *exts)
302{
303 int i, num;
304
305 if (!csrattrs || ! csrattrs->attrs)
306 return;
307
308 num = SKM_sk_num(AttrOrOID, csrattrs->attrs);
309 for (i = 0; i < num; i++) {
310 AttrOrOID *ao = SKM_sk_value(AttrOrOID, csrattrs->attrs, i);
311 switch (ao->type) {
312 case 0:
313 add_csrattrs_oid(ctx, ao->d.oid, exts);
314 break;
315 case 1:
316 add_csrattrs_attr(ctx, ao->d.attribute, exts);
317 break;
318 }
319 }
320}
321
322
323static int generate_csr(struct hs20_osu_client *ctx, char *key_pem,
324 char *csr_pem, char *est_req, char *old_cert,
325 CsrAttrs *csrattrs)
326{
327 EVP_PKEY_CTX *pctx = NULL;
328 EVP_PKEY *pkey = NULL;
329 RSA *rsa;
330 X509_REQ *req = NULL;
331 int ret = -1;
332 unsigned int val;
333 X509_NAME *subj = NULL;
334 char name[100];
335 STACK_OF(X509_EXTENSION) *exts = NULL;
336 X509_EXTENSION *ex;
337 BIO *out;
338
339 wpa_printf(MSG_INFO, "Generate RSA private key");
340 write_summary(ctx, "Generate RSA private key");
341 pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
342 if (!pctx)
343 return -1;
344
345 if (EVP_PKEY_keygen_init(pctx) <= 0)
346 goto fail;
347
348 if (EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0)
349 goto fail;
350
351 if (EVP_PKEY_keygen(pctx, &pkey) <= 0)
352 goto fail;
353 EVP_PKEY_CTX_free(pctx);
354 pctx = NULL;
355
356 rsa = EVP_PKEY_get1_RSA(pkey);
357 if (rsa == NULL)
358 goto fail;
359
360 if (key_pem) {
361 FILE *f = fopen(key_pem, "wb");
362 if (f == NULL)
363 goto fail;
364 if (!PEM_write_RSAPrivateKey(f, rsa, NULL, NULL, 0, NULL,
365 NULL)) {
366 wpa_printf(MSG_INFO, "Could not write private key: %s",
367 ERR_error_string(ERR_get_error(), NULL));
368 fclose(f);
369 goto fail;
370 }
371 fclose(f);
372 }
373
374 wpa_printf(MSG_INFO, "Generate CSR");
375 write_summary(ctx, "Generate CSR");
376 req = X509_REQ_new();
377 if (req == NULL)
378 goto fail;
379
380 if (old_cert) {
381 FILE *f;
382 X509 *cert;
383 int res;
384
385 f = fopen(old_cert, "r");
386 if (f == NULL)
387 goto fail;
388 cert = PEM_read_X509(f, NULL, NULL, NULL);
389 fclose(f);
390
391 if (cert == NULL)
392 goto fail;
393 res = X509_REQ_set_subject_name(req,
394 X509_get_subject_name(cert));
395 X509_free(cert);
396 if (!res)
397 goto fail;
398 } else {
399 os_get_random((u8 *) &val, sizeof(val));
400 os_snprintf(name, sizeof(name), "cert-user-%u", val);
401 subj = X509_NAME_new();
402 if (subj == NULL ||
403 !X509_NAME_add_entry_by_txt(subj, "CN", MBSTRING_ASC,
404 (unsigned char *) name,
405 -1, -1, 0) ||
406 !X509_REQ_set_subject_name(req, subj))
407 goto fail;
408 X509_NAME_free(subj);
409 subj = NULL;
410 }
411
412 if (!X509_REQ_set_pubkey(req, pkey))
413 goto fail;
414
415 exts = sk_X509_EXTENSION_new_null();
416 if (!exts)
417 goto fail;
418
419 ex = X509V3_EXT_conf_nid(NULL, NULL, NID_basic_constraints,
420 "CA:FALSE");
421 if (ex == NULL ||
422 !sk_X509_EXTENSION_push(exts, ex))
423 goto fail;
424
425 ex = X509V3_EXT_conf_nid(NULL, NULL, NID_key_usage,
426 "nonRepudiation,digitalSignature,keyEncipherment");
427 if (ex == NULL ||
428 !sk_X509_EXTENSION_push(exts, ex))
429 goto fail;
430
431 ex = X509V3_EXT_conf_nid(NULL, NULL, NID_ext_key_usage,
432 "1.3.6.1.4.1.40808.1.1.2");
433 if (ex == NULL ||
434 !sk_X509_EXTENSION_push(exts, ex))
435 goto fail;
436
437 add_csrattrs(ctx, csrattrs, exts);
438
439 if (!X509_REQ_add_extensions(req, exts))
440 goto fail;
441 sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
442 exts = NULL;
443
444 if (!X509_REQ_sign(req, pkey, EVP_sha256()))
445 goto fail;
446
447 out = BIO_new(BIO_s_mem());
448 if (out) {
449 char *txt;
450 size_t rlen;
451
452 X509_REQ_print(out, req);
453 rlen = BIO_ctrl_pending(out);
454 txt = os_malloc(rlen + 1);
455 if (txt) {
456 int res = BIO_read(out, txt, rlen);
457 if (res > 0) {
458 txt[res] = '\0';
459 wpa_printf(MSG_MSGDUMP, "OpenSSL: Certificate request:\n%s",
460 txt);
461 }
462 os_free(txt);
463 }
464 BIO_free(out);
465 }
466
467 if (csr_pem) {
468 FILE *f = fopen(csr_pem, "w");
469 if (f == NULL)
470 goto fail;
471 X509_REQ_print_fp(f, req);
472 if (!PEM_write_X509_REQ(f, req)) {
473 fclose(f);
474 goto fail;
475 }
476 fclose(f);
477 }
478
479 if (est_req) {
480 BIO *mem = BIO_new(BIO_s_mem());
481 BUF_MEM *ptr;
482 char *pos, *end, *buf_end;
483 FILE *f;
484
485 if (mem == NULL)
486 goto fail;
487 if (!PEM_write_bio_X509_REQ(mem, req)) {
488 BIO_free(mem);
489 goto fail;
490 }
491
492 BIO_get_mem_ptr(mem, &ptr);
493 pos = ptr->data;
494 buf_end = pos + ptr->length;
495
496 /* Remove START/END lines */
497 while (pos < buf_end && *pos != '\n')
498 pos++;
499 if (pos == buf_end) {
500 BIO_free(mem);
501 goto fail;
502 }
503 pos++;
504
505 end = pos;
506 while (end < buf_end && *end != '-')
507 end++;
508
509 f = fopen(est_req, "w");
510 if (f == NULL) {
511 BIO_free(mem);
512 goto fail;
513 }
514 fwrite(pos, end - pos, 1, f);
515 fclose(f);
516
517 BIO_free(mem);
518 }
519
520 ret = 0;
521fail:
522 if (exts)
523 sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
524 if (subj)
525 X509_NAME_free(subj);
526 if (req)
527 X509_REQ_free(req);
528 if (pkey)
529 EVP_PKEY_free(pkey);
530 if (pctx)
531 EVP_PKEY_CTX_free(pctx);
532 return ret;
533}
534
535
536int est_build_csr(struct hs20_osu_client *ctx, const char *url)
537{
538 char *buf;
539 size_t buflen;
540 int res;
541 char old_cert_buf[200];
542 char *old_cert = NULL;
543 CsrAttrs *csrattrs = NULL;
544
545 buflen = os_strlen(url) + 100;
546 buf = os_malloc(buflen);
547 if (buf == NULL)
548 return -1;
549
550 os_snprintf(buf, buflen, "%s/csrattrs", url);
551 wpa_printf(MSG_INFO, "Download csrattrs from %s", buf);
552 write_summary(ctx, "Download EST csrattrs from %s", buf);
553 res = http_download_file(ctx->http, buf, "Cert/est-csrattrs.txt",
554 ctx->ca_fname);
555 os_free(buf);
556 if (res < 0) {
557 wpa_printf(MSG_INFO, "Failed to download EST csrattrs - assume no extra attributes are needed");
558 } else {
559 size_t resp_len;
560 char *resp;
561 unsigned char *attrs;
562 const unsigned char *pos;
563 size_t attrs_len;
564
565 resp = os_readfile("Cert/est-csrattrs.txt", &resp_len);
566 if (resp == NULL) {
567 wpa_printf(MSG_INFO, "Could not read csrattrs");
568 return -1;
569 }
570
571 attrs = base64_decode((unsigned char *) resp, resp_len,
572 &attrs_len);
573 os_free(resp);
574
575 if (attrs == NULL) {
576 wpa_printf(MSG_INFO, "Could not base64 decode csrattrs");
577 return -1;
578 }
579 unlink("Cert/est-csrattrs.txt");
580
581 pos = attrs;
582 csrattrs = d2i_CsrAttrs(NULL, &pos, attrs_len);
583 os_free(attrs);
584 if (csrattrs == NULL) {
585 wpa_printf(MSG_INFO, "Failed to parse csrattrs ASN.1");
586 /* Continue assuming no additional requirements */
587 }
588 }
589
590 if (ctx->client_cert_present) {
591 os_snprintf(old_cert_buf, sizeof(old_cert_buf),
592 "SP/%s/client-cert.pem", ctx->fqdn);
593 old_cert = old_cert_buf;
594 }
595
596 res = generate_csr(ctx, "Cert/privkey-plain.pem", "Cert/est-req.pem",
597 "Cert/est-req.b64", old_cert, csrattrs);
598 if (csrattrs)
599 CsrAttrs_free(csrattrs);
600
601 return res;
602}
603
604
605int est_simple_enroll(struct hs20_osu_client *ctx, const char *url,
606 const char *user, const char *pw)
607{
608 char *buf, *resp, *req, *req2;
609 size_t buflen, resp_len, len, pkcs7_len;
610 unsigned char *pkcs7;
611 FILE *f;
612 char client_cert_buf[200];
613 char client_key_buf[200];
614 const char *client_cert = NULL, *client_key = NULL;
615 int res;
616
617 req = os_readfile("Cert/est-req.b64", &len);
618 if (req == NULL) {
619 wpa_printf(MSG_INFO, "Could not read Cert/req.b64");
620 return -1;
621 }
622 req2 = os_realloc(req, len + 1);
623 if (req2 == NULL) {
624 os_free(req);
625 return -1;
626 }
627 req2[len] = '\0';
628 req = req2;
629 wpa_printf(MSG_DEBUG, "EST simpleenroll request: %s", req);
630
631 buflen = os_strlen(url) + 100;
632 buf = os_malloc(buflen);
633 if (buf == NULL) {
634 os_free(req);
635 return -1;
636 }
637
638 if (ctx->client_cert_present) {
639 os_snprintf(buf, buflen, "%s/simplereenroll", url);
640 os_snprintf(client_cert_buf, sizeof(client_cert_buf),
641 "SP/%s/client-cert.pem", ctx->fqdn);
642 client_cert = client_cert_buf;
643 os_snprintf(client_key_buf, sizeof(client_key_buf),
644 "SP/%s/client-key.pem", ctx->fqdn);
645 client_key = client_key_buf;
646 } else
647 os_snprintf(buf, buflen, "%s/simpleenroll", url);
648 wpa_printf(MSG_INFO, "EST simpleenroll URL: %s", buf);
649 write_summary(ctx, "EST simpleenroll URL: %s", buf);
650 resp = http_post(ctx->http, buf, req, "application/pkcs10",
651 "Content-Transfer-Encoding: base64",
652 ctx->ca_fname, user, pw, client_cert, client_key,
653 &resp_len);
654 os_free(buf);
655 if (resp == NULL) {
656 wpa_printf(MSG_INFO, "EST certificate enrollment failed");
657 write_result(ctx, "EST certificate enrollment failed");
658 return -1;
659 }
660 wpa_printf(MSG_DEBUG, "EST simpleenroll response: %s", resp);
661 f = fopen("Cert/est-resp.raw", "w");
662 if (f) {
663 fwrite(resp, resp_len, 1, f);
664 fclose(f);
665 }
666
667 pkcs7 = base64_decode((unsigned char *) resp, resp_len, &pkcs7_len);
668 if (pkcs7 == NULL) {
669 wpa_printf(MSG_INFO, "EST workaround - Could not decode base64, assume this is DER encoded PKCS7");
670 pkcs7 = os_malloc(resp_len);
671 if (pkcs7) {
672 os_memcpy(pkcs7, resp, resp_len);
673 pkcs7_len = resp_len;
674 }
675 }
676 os_free(resp);
677
678 if (pkcs7 == NULL) {
679 wpa_printf(MSG_INFO, "Failed to parse simpleenroll base64 response");
680 write_result(ctx, "Failed to parse EST simpleenroll base64 response");
681 return -1;
682 }
683
684 res = pkcs7_to_cert(ctx, pkcs7, pkcs7_len, "Cert/est_cert.pem",
685 "Cert/est_cert.der");
686 os_free(pkcs7);
687
688 if (res < 0) {
689 wpa_printf(MSG_INFO, "EST: Failed to extract certificate from PKCS7 file");
690 write_result(ctx, "EST: Failed to extract certificate from EST PKCS7 file");
691 return -1;
692 }
693
694 wpa_printf(MSG_INFO, "EST simple%senroll completed successfully",
695 ctx->client_cert_present ? "re" : "");
696 write_summary(ctx, "EST simple%senroll completed successfully",
697 ctx->client_cert_present ? "re" : "");
698
699 return 0;
700}