Skip to content

Commit 4cd4f10

Browse files
committed
Add support for certificate profiles
Closes #90 See also https://letsencrypt.org/2025/01/09/acme-profiles/
1 parent 0fc608d commit 4cd4f10

File tree

6 files changed

+88
-37
lines changed

6 files changed

+88
-37
lines changed

crypto.c

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,7 +2134,8 @@ bool is_ip(const char *s, unsigned char *ip, size_t *ip_len)
21342134
return ret;
21352135
}
21362136

2137-
char *csr_gen(char * const *names, bool status_req, privkey_t key)
2137+
char *csr_gen(char * const *names, bool status_req, bool no_key_usage,
2138+
privkey_t key)
21382139
{
21392140
char *req = NULL;
21402141
unsigned char *csrdata = NULL;
@@ -2259,11 +2260,13 @@ char *csr_gen(char * const *names, bool status_req, privkey_t key)
22592260
names++;
22602261
}
22612262

2262-
r = gnutls_x509_crq_set_key_usage(crq, key_usage);
2263-
if (r != GNUTLS_E_SUCCESS) {
2264-
warnx("csr_gen: gnutls_x509_crq_set_key_usage: %s",
2265-
gnutls_strerror(r));
2266-
goto out;
2263+
if (!no_key_usage) {
2264+
r = gnutls_x509_crq_set_key_usage(crq, key_usage);
2265+
if (r != GNUTLS_E_SUCCESS) {
2266+
warnx("csr_gen: gnutls_x509_crq_set_key_usage: %s",
2267+
gnutls_strerror(r));
2268+
goto out;
2269+
}
22672270
}
22682271

22692272
if (status_req) {
@@ -2397,12 +2400,14 @@ char *csr_gen(char * const *names, bool status_req, privkey_t key)
23972400
goto out;
23982401
}
23992402
sk_X509_EXTENSION_push(exts, ext);
2400-
ext = X509V3_EXT_conf_nid(NULL, NULL, NID_key_usage, key_usage);
2401-
if (!ext) {
2402-
openssl_error("csr_gen");
2403-
goto out;
2403+
if (!no_key_usage) {
2404+
ext = X509V3_EXT_conf_nid(NULL, NULL, NID_key_usage, key_usage);
2405+
if (!ext) {
2406+
openssl_error("csr_gen");
2407+
goto out;
2408+
}
2409+
sk_X509_EXTENSION_push(exts, ext);
24042410
}
2405-
sk_X509_EXTENSION_push(exts, ext);
24062411
if (status_req) {
24072412
#if defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x3050000fL
24082413
warnx("csr_gen: -m, --must-staple is not supported by LibreSSL "
@@ -2452,11 +2457,13 @@ char *csr_gen(char * const *names, bool status_req, privkey_t key)
24522457
goto out;
24532458
}
24542459

2455-
r = mbedtls_x509write_csr_set_key_usage(&csr, key_usage);
2456-
if (r) {
2457-
warnx("csr_gen: mbedtls_x509write_csr_set_key_usage failed: %s",
2458-
_mbedtls_strerror(r));
2459-
goto out;
2460+
if (!no_key_usage) {
2461+
r = mbedtls_x509write_csr_set_key_usage(&csr, key_usage);
2462+
if (r) {
2463+
warnx("csr_gen: mbedtls_x509write_csr_set_key_usage failed: %s",
2464+
_mbedtls_strerror(r));
2465+
goto out;
2466+
}
24602467
}
24612468

24622469
r = mbedtls_x509write_csr_set_subject_name(&csr, cn);

crypto.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ char *jws_encode_hmac(const char *, const char *, size_t, const char *);
7979
keytype_t key_type(privkey_t);
8080
privkey_t key_load(keytype_t, int bits, const char *, ...);
8181
bool is_ip(const char *, unsigned char *, size_t *);
82-
char *csr_gen(char * const *, bool, privkey_t);
82+
char *csr_gen(char * const *, bool, bool, privkey_t);
8383
char *csr_load(const char *, char ***);
8484
char *cert_der_base64url(const char *);
8585
bool cert_valid(const char *, char * const *, const char *, int, bool);

docs/uacme.html

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -750,9 +750,9 @@ <h2 id="_synopsis">SYNOPSIS</h2>
750750
[<strong>-c</strong>|<strong>--confdir</strong> <em>DIR</em>] [<strong>-d</strong>|<strong>--days</strong> <em>DAYS</em>] [<strong>-e</strong>|<strong>--eab</strong> KEYID:KEY]
751751
[<strong>-f</strong>|<strong>--force</strong>] [<strong>-h</strong>|<strong>--hook</strong> <em>PROGRAM</em>] [<strong>-i</strong>|<strong>--no-ari</strong>]
752752
[<strong>-l</strong>|<strong>--alternate</strong> <em>N</em> | <em>FP</em>] [<strong>-m</strong>|<strong>--must-staple</strong>]
753-
[<strong>-n</strong>|<strong>--never-create</strong>] [<strong>-o</strong>|<strong>--no-ocsp</strong>] [<strong>-r</strong>|<strong>--reason</strong> CODE]
754-
[<strong>-s</strong>|<strong>--staging</strong>] [<strong>-t</strong>|<strong>--type</strong> <strong>RSA</strong>|<strong>EC</strong>] [<strong>-v</strong>|<strong>--verbose</strong> &#8230;]
755-
[<strong>-V</strong>|<strong>--version</strong>] [<strong>-y</strong>|<strong>--yes</strong>] [<strong>-?</strong>|<strong>--help</strong>]
753+
[<strong>-n</strong>|<strong>--never-create</strong>] [<strong>-o</strong>|<strong>--no-ocsp</strong>] [<strong>-p</strong>|<strong>--profile</strong> PROFILE]
754+
[<strong>-r</strong>|<strong>--reason</strong> CODE] [<strong>-s</strong>|<strong>--staging</strong>] [<strong>-t</strong>|<strong>--type</strong> <strong>RSA</strong>|<strong>EC</strong>]
755+
[<strong>-v</strong>|<strong>--verbose</strong> &#8230;] [<strong>-V</strong>|<strong>--version</strong>] [<strong>-y</strong>|<strong>--yes</strong>] [<strong>-?</strong>|<strong>--help</strong>]
756756
<strong>new</strong> [<em>EMAIL</em>] | <strong>update</strong> [<em>EMAIL</em>] | <strong>deactivate</strong> | <strong>newkey</strong> |
757757
<strong>issue</strong> <em>IDENTIFIER</em> [<em>ALTNAME</em> &#8230;]] | <strong>issue</strong> <em>CSRFILE</em> |
758758
<strong>revoke</strong> <em>CERTFILE</em> [<em>CERTKEYFILE</em>]</p></div>
@@ -1032,6 +1032,16 @@ <h2 id="_options">OPTIONS</h2>
10321032
</p>
10331033
</dd>
10341034
<dt class="hdlist1">
1035+
<strong>-p, --profile</strong> <em>PROFILE</em>
1036+
</dt>
1037+
<dd>
1038+
<p>
1039+
If the server supports it, use <em>PROFILE</em>, a collection of attributes
1040+
about the certificate that will be issued, such as what extensions it
1041+
will contain, how long it will be valid for, and more.
1042+
</p>
1043+
</dd>
1044+
<dt class="hdlist1">
10351045
<strong>-r, --reason</strong> <em>CODE</em>
10361046
</dt>
10371047
<dd>
@@ -1472,9 +1482,9 @@ <h2 id="_copyright">COPYRIGHT</h2>
14721482
<div id="footnotes"><hr></div>
14731483
<div id="footer">
14741484
<div id="footer-text">
1475-
Version 1.7.6<br>
1485+
Version 1.7.6-dev<br>
14761486
Last updated
1477-
2024-12-29 17:25:16 CET
1487+
2025-01-19 12:57:02 CET
14781488
</div>
14791489
</div>
14801490
</body>

uacme.1

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
.\" Title: uacme
33
.\" Author: [see the "AUTHOR" section]
44
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
5-
.\" Date: 12/29/2024
5+
.\" Date: 01/19/2025
66
.\" Manual: User Commands
7-
.\" Source: uacme 1.7.6
7+
.\" Source: uacme 1.7.6-dev
88
.\" Language: English
99
.\"
10-
.TH "UACME" "1" "12/29/2024" "uacme 1\&.7\&.6" "User Commands"
10+
.TH "UACME" "1" "01/19/2025" "uacme 1\&.7\&.6\-dev" "User Commands"
1111
.\" -----------------------------------------------------------------
1212
.\" * Define some portability stuff
1313
.\" -----------------------------------------------------------------
@@ -31,7 +31,7 @@
3131
uacme \- ACMEv2 client written in plain C with minimal dependencies
3232
.SH "SYNOPSIS"
3333
.sp
34-
\fBuacme\fR [\fB\-a\fR|\fB\-\-acme\-url\fR \fIURL\fR] [\fB\-b\fR|\fB\-\-bits\fR \fIBITS\fR] [\fB\-c\fR|\fB\-\-confdir\fR \fIDIR\fR] [\fB\-d\fR|\fB\-\-days\fR \fIDAYS\fR] [\fB\-e\fR|\fB\-\-eab\fR KEYID:KEY] [\fB\-f\fR|\fB\-\-force\fR] [\fB\-h\fR|\fB\-\-hook\fR \fIPROGRAM\fR] [\fB\-i\fR|\fB\-\-no\-ari\fR] [\fB\-l\fR|\fB\-\-alternate\fR \fIN\fR | \fIFP\fR] [\fB\-m\fR|\fB\-\-must\-staple\fR] [\fB\-n\fR|\fB\-\-never\-create\fR] [\fB\-o\fR|\fB\-\-no\-ocsp\fR] [\fB\-r\fR|\fB\-\-reason\fR CODE] [\fB\-s\fR|\fB\-\-staging\fR] [\fB\-t\fR|\fB\-\-type\fR \fBRSA\fR|\fBEC\fR] [\fB\-v\fR|\fB\-\-verbose\fR \&...] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-y\fR|\fB\-\-yes\fR] [\fB\-?\fR|\fB\-\-help\fR] \fBnew\fR [\fIEMAIL\fR] | \fBupdate\fR [\fIEMAIL\fR] | \fBdeactivate\fR | \fBnewkey\fR | \fBissue\fR \fIIDENTIFIER\fR [\fIALTNAME\fR \&...]] | \fBissue\fR \fICSRFILE\fR | \fBrevoke\fR \fICERTFILE\fR [\fICERTKEYFILE\fR]
34+
\fBuacme\fR [\fB\-a\fR|\fB\-\-acme\-url\fR \fIURL\fR] [\fB\-b\fR|\fB\-\-bits\fR \fIBITS\fR] [\fB\-c\fR|\fB\-\-confdir\fR \fIDIR\fR] [\fB\-d\fR|\fB\-\-days\fR \fIDAYS\fR] [\fB\-e\fR|\fB\-\-eab\fR KEYID:KEY] [\fB\-f\fR|\fB\-\-force\fR] [\fB\-h\fR|\fB\-\-hook\fR \fIPROGRAM\fR] [\fB\-i\fR|\fB\-\-no\-ari\fR] [\fB\-l\fR|\fB\-\-alternate\fR \fIN\fR | \fIFP\fR] [\fB\-m\fR|\fB\-\-must\-staple\fR] [\fB\-n\fR|\fB\-\-never\-create\fR] [\fB\-o\fR|\fB\-\-no\-ocsp\fR] [\fB\-p\fR|\fB\-\-profile\fR PROFILE] [\fB\-r\fR|\fB\-\-reason\fR CODE] [\fB\-s\fR|\fB\-\-staging\fR] [\fB\-t\fR|\fB\-\-type\fR \fBRSA\fR|\fBEC\fR] [\fB\-v\fR|\fB\-\-verbose\fR \&...] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-y\fR|\fB\-\-yes\fR] [\fB\-?\fR|\fB\-\-help\fR] \fBnew\fR [\fIEMAIL\fR] | \fBupdate\fR [\fIEMAIL\fR] | \fBdeactivate\fR | \fBnewkey\fR | \fBissue\fR \fIIDENTIFIER\fR [\fIALTNAME\fR \&...]] | \fBissue\fR \fICSRFILE\fR | \fBrevoke\fR \fICERTFILE\fR [\fICERTKEYFILE\fR]
3535
.SH "DESCRIPTION"
3636
.sp
3737
\fBuacme\fR is a client for the ACMEv2 protocol described in RFC8555, written in plain C with minimal dependencies (libcurl and one of GnuTLS, OpenSSL or mbedTLS)\&. The ACMEv2 protocol allows a Certificate Authority (https://letsencrypt\&.org is a popular one) and an applicant to automate the process of verification and certificate issuance\&. The protocol also provides facilities for other certificate management functions, such as certificate revocation\&. For more information see https://tools\&.ietf\&.org/html/rfc8555
@@ -238,6 +238,12 @@ forces reissuance regardless of the expiration date\&. See also
238238
\fB\-d, \-\-days\fR\&.
239239
.RE
240240
.PP
241+
\fB\-p, \-\-profile\fR \fIPROFILE\fR
242+
.RS 4
243+
If the server supports it, use
244+
\fIPROFILE\fR, a collection of attributes about the certificate that will be issued, such as what extensions it will contain, how long it will be valid for, and more\&.
245+
.RE
246+
.PP
241247
\fB\-r, \-\-reason\fR \fICODE\fR
242248
.RS 4
243249
Use

uacme.1.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ SYNOPSIS
1717
[*-c*|*--confdir* 'DIR'] [*-d*|*--days* 'DAYS'] [*-e*|*--eab* KEYID:KEY]
1818
[*-f*|*--force*] [*-h*|*--hook* 'PROGRAM'] [*-i*|*--no-ari*]
1919
[*-l*|*--alternate* 'N' | 'FP'] [*-m*|*--must-staple*]
20-
[*-n*|*--never-create*] [*-o*|*--no-ocsp*] [*-r*|*--reason* CODE]
21-
[*-s*|*--staging*] [*-t*|*--type* *RSA*|*EC*] [*-v*|*--verbose* ...]
22-
[*-V*|*--version*] [*-y*|*--yes*] [*-?*|*--help*]
20+
[*-n*|*--never-create*] [*-o*|*--no-ocsp*] [*-p*|*--profile* PROFILE]
21+
[*-r*|*--reason* CODE] [*-s*|*--staging*] [*-t*|*--type* *RSA*|*EC*]
22+
[*-v*|*--verbose* ...] [*-V*|*--version*] [*-y*|*--yes*] [*-?*|*--help*]
2323
*new* ['EMAIL'] | *update* ['EMAIL'] | *deactivate* | *newkey* |
2424
*issue* 'IDENTIFIER' ['ALTNAME' ...]] | *issue* 'CSRFILE' |
2525
*revoke* 'CERTFILE' ['CERTKEYFILE']
@@ -137,6 +137,11 @@ OPTIONS
137137
reported as revoked *uacme* forces reissuance regardless of the
138138
expiration date. See also *-d, --days*.
139139

140+
*-p, --profile* 'PROFILE'::
141+
If the server supports it, use 'PROFILE', a collection of attributes
142+
about the certificate that will be issued, such as what extensions it
143+
will contain, how long it will be valid for, and more.
144+
140145
*-r, --reason* 'CODE'::
141146
Use 'CODE' (default 0) as reason code in revocation requests. A list
142147
of values is at <https://tools.ietf.org/html/rfc5280#section-5.3.1>.

uacme.c

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ typedef struct acme {
6969
const char *directory;
7070
const char *hook;
7171
const char *email;
72+
const char *profile;
7273
char *keyprefix;
7374
char *certprefix;
7475
} acme_t;
@@ -417,7 +418,7 @@ char *identifiers(char * const *names)
417418
{
418419
char *ids = NULL;
419420
char *tmp = NULL;
420-
if (asprintf(&tmp, "{\"identifiers\":[") < 0) {
421+
if (asprintf(&tmp, "\"identifiers\":[") < 0) {
421422
warnx("identifiers: asprintf failed");
422423
return NULL;
423424
}
@@ -434,7 +435,7 @@ char *identifiers(char * const *names)
434435
names++;
435436
}
436437
tmp[strlen(tmp)-1] = 0;
437-
if (asprintf(&ids, "%s]}", tmp) < 0) {
438+
if (asprintf(&ids, "%s]", tmp) < 0) {
438439
warnx("identifiers: asprintf failed");
439440
ids = NULL;
440441
}
@@ -455,6 +456,20 @@ bool acme_bootstrap(acme_t *a)
455456
a->dir = a->json;
456457
a->json = NULL;
457458

459+
if (a->profile) {
460+
const json_value_t *meta = json_find(a->dir, "meta");
461+
const json_value_t *profiles = json_find(meta, "profiles");
462+
if (!profiles) {
463+
warnx("server does not support profiles");
464+
return false;
465+
}
466+
if (!json_find_string(profiles, a->profile)) {
467+
warnx("profile must be a supported one:");
468+
json_dump(stderr, profiles);
469+
return false;
470+
}
471+
}
472+
458473
return true;
459474
}
460475

@@ -1021,7 +1036,9 @@ bool cert_issue(acme_t *a, char * const *names, const char *csr)
10211036
}
10221037

10231038
msg(1, "creating new order at %s", url);
1024-
if (acme_post(a, url, ids) != 201)
1039+
if (acme_post(a, url, "{%s%s%s%s}", a->profile ? "\"profile\": \"" : "",
1040+
a->profile ? a->profile : "",
1041+
a->profile ? "\", " : "", ids) != 201)
10251042
{
10261043
warnx("failed to create new order at %s", url);
10271044
acme_error(a);
@@ -1419,9 +1436,10 @@ void usage(const char *progname)
14191436
"usage: %s [-a|--acme-url URL] [-b|--bits BITS] [-c|--confdir DIR]\n"
14201437
"\t[-d|--days DAYS] [-e|--eab KEYID:KEY] [-f|--force] [-h|--hook PROG]\n"
14211438
"\t[-i|--no-ari] [-l|--alternate [N | SHA256]] [-m|--must-staple]\n"
1422-
"\t[-n|--never-create] [-o|--no-ocsp] [-r|--reason CODE] [-s|--staging]\n"
1423-
"\t[-t|--type RSA | EC] [-v|--verbose ...] [-V|--version] [-y|--yes]\n"
1424-
"\t[-?|--help] new [EMAIL] | update [EMAIL] | deactivate | newkey |\n"
1439+
"\t[-n|--never-create] [-o|--no-ocsp] [-p|--profile profile]\n"
1440+
"\t[-r|--reason CODE] [-s|--staging] [-t|--type RSA | EC]\n"
1441+
"\t[-v|--verbose ...] [-V|--version] [-y|--yes] [-?|--help]\n"
1442+
"\tnew [EMAIL] | update [EMAIL] | deactivate | newkey |\n"
14251443
"\tissue IDENTIFIER [ALTNAME ...]] | issue CSRFILE |\n"
14261444
"\trevoke CERTFILE [CERTKEYFILE]\n", progname);
14271445
}
@@ -1457,6 +1475,7 @@ int main(int argc, char **argv)
14571475
{"must-staple", no_argument, NULL, 'm'},
14581476
{"never-create", no_argument, NULL, 'n'},
14591477
{"no-ocsp", no_argument, NULL, 'o'},
1478+
{"profile", no_argument, NULL, 'p'},
14601479
{"reason", required_argument, NULL, 'r'},
14611480
{"staging", no_argument, NULL, 's'},
14621481
{"type", required_argument, NULL, 't'},
@@ -1520,7 +1539,7 @@ int main(int argc, char **argv)
15201539
while (1) {
15211540
char *endptr;
15221541
int option_index;
1523-
int c = getopt_long(argc, argv, "a:b:c:d:e:f?h:il:mnor:st:vVy",
1542+
int c = getopt_long(argc, argv, "a:b:c:d:e:f?h:il:mnop:r:st:vVy",
15241543
options, &option_index);
15251544
if (c == -1) break;
15261545
switch (c) {
@@ -1587,6 +1606,10 @@ int main(int argc, char **argv)
15871606
status_check = false;
15881607
break;
15891608

1609+
case 'p':
1610+
a.profile = optarg;
1611+
break;
1612+
15901613
case 'v':
15911614
g_loglevel++;
15921615
break;
@@ -1892,7 +1915,7 @@ int main(int argc, char **argv)
18921915

18931916
if (!csr) {
18941917
msg(1, "generating certificate request");
1895-
csr = csr_gen(names, status_req, key);
1918+
csr = csr_gen(names, status_req, a.profile != NULL, key);
18961919
if (!csr) {
18971920
warnx("failed to generate certificate request");
18981921
goto out;

0 commit comments

Comments
 (0)