Index | Thread | Search

From:
Stuart Henderson <stu@spacehopper.org>
Subject:
Re: acme-client(1): add support for let's encrypt iPAddress certificates
To:
Lloyd <ng2d68@proton.me>
Cc:
Peter Hessler <phessler@theapt.org>, "tech@openbsd.org" <tech@openbsd.org>
Date:
Sun, 14 Dec 2025 14:04:09 +0000

Download raw body.

Thread
On 2025/12/13 21:20, Lloyd wrote:
> Peter Hessler wrote:
> 
> > I was looking at this topic and noticed thet LetsEncrypt is planning on
> > releasing this to production soon, so IMHO this is a good time to clean
> > up the diffs and get it in.
> > 
> > Can you update the patches and re-send?
> 
> With holidays around the corner, I don't have time to update this right
> now. The diff was built against an older branch and will not merge
> cleanly against -current because different profile code was merged in
> the meantime. So this will take some time to re-familiarize myself. That
> said, I've been running this against preprod for a while with no issues.
> 
> This feature is expected to be enabled in LE prod real soon now.
> 
> Regards
> Lloyd
> 

Diff below merges this to -current. Works for me with a shortlived IP
address cert on letsencrypt staging, with a standard cert on letsencrypt
prod, and src/regress/usr.sbin/acme-client (using pebble) is still
happy.

I have fixed "new sentence new line" in the manpage changes but not
made any substantial changes including any of the things I suggested;
comments on those bits:

: > Config files and the manual will be simpler if the parser is changed to
: > allow using ip:XXX directly here, i.e. "domain ip:192.0.2.0", without
: > needing to specify "domain name". Then we could also bring in the ip:
: > text
: > here,
: 
: This was intentional. The handle should be a static identifier. I felt
: that whether or not "ip:" gets stripped from the handle becomes
: ambiguous. Which is the real handle - with or without "ip:"? Which do
: you specify on the command line? The handle gets screened as a valid

I think "with ip:".

I suppose the config for a cert where you have both IP and domain names
in the same cert won't look too bad,

domain example.org {
        alternative names { ip:192.0.2.0 ip:2001:db8:0:1::1234 }
        domain key "/etc/ssl/private/server.key"
        domain chain certificate "/etc/ssl/server.crt"
        sign with letsencrypt-staging
        profile shortlived
}

but as things stand it's pretty ugly for an ip-only cert

domain ip_only_cert {
        domain name ip:192.0.2.0
        alternative names { ip:2001:db8:0:1::1234 }
        domain key "/etc/ssl/private/iponly.key"
        domain chain certificate "/etc/ssl/iponly.pem"
        sign with letsencrypt-staging
        profile tlsserver
}

: domain name so cannot contain colons - but that needs to be supported
: for IPv6 but still prevent you from entering something invalid like
: www:openbsd:org. I thought it would make the parser unnecessarily
: complex.

: > I don't think any current CAs supported by acme-client are requiring
: > this? I'm thinking the only thing that might require CN is software
: > using the cert which might (though shouldn't) get confused if it's not
: > there.
: 
: I left this as an on/off knob for legacy support. Maybe someone out
: there prefers CN's in their certs and they are still supported for the
: default long-lived profiles.

AFAIK the CA will generate CN themselves rather than copying from the
CSR. Certainly letsencrypt does (with the default profile).

Since the CSR is not stored, I think the only reason for having a knob
would be if a CA fails to issue a cert if there's no CN (letsencrypt
staging, prod, and pebble work) so I think I'd prefer to drop the knob
unless/until it's proven that we need it. Simplifies the docs too.


Index: acme-client.conf.5
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/acme-client.conf.5,v
diff -u -p -r1.32 acme-client.conf.5
--- acme-client.conf.5	16 Sep 2025 15:06:02 -0000	1.32
+++ acme-client.conf.5	14 Dec 2025 13:37:50 -0000
@@ -115,6 +115,10 @@ The certificates to be obtained through 
 Each domain section begins with the
 .Ic domain
 keyword followed by an identifier for this domain block.
+For certificates with primary SAN type iPAddress, an additional
+directive of type
+.Cm domain name ip:
+must follow.
 .El
 .Pp
 It is followed by a block of options enclosed in curly brackets:
@@ -122,19 +126,25 @@ It is followed by a block of options enc
 .It Ic domain name Ar name
 The
 .Ar name
-to be used as the common name component of the subject of the
-X.509 certificate.
-This is optional.
+to be used as the primary Subject Alternative Name (or common name if included)
+in the X.509 certificate.
+This is optional for SANs of type DNS.
 If not specified, the
 .Ar handle
-of the domain block will be used as common name.
+of the domain block will be used as the primary SAN (or common name).
+To specify a SAN of type iPAddress as the
+.Ar name ,
+prefix the address with
+.Cm ip: .
+To use a primary SAN of type iPAddress, this field is mandatory.
 .It Ic alternative names Brq ...
 A list of alternative names,
 comma or space separated,
 for which the certificate will be valid.
-The common name is included automatically if this option is present,
-but there is no automatic conversion/inclusion between "www." and
-plain domain name forms.
+The common name is no longer included by default.
+To specify a SAN of type iPAddress as an alternative name, prefix the
+address with
+.Cm ip: .
 .It Ic domain key Ar file Op Ar keytype
 The private key file for which the certificate will be obtained.
 .Ar keytype
@@ -194,6 +204,11 @@ exists.
 .It Ic profile Ar profile
 The certificate profile to be requested.
 If this setting is absent, no profile request is made.
+.It Ic with cn
+Include the X.509 Common Name attribute in the certificate
+request.
+Use this if your CA's Certificate Policy requires it.
+By default, the CN attribute is suppressed.
 .It Ic sign with Ar authority
 The certificate authority (as declared above in the
 .Sx Authorities
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/extern.h,v
diff -u -p -r1.22 extern.h
--- extern.h	16 Sep 2025 15:06:02 -0000	1.22
+++ extern.h	14 Dec 2025 13:37:50 -0000
@@ -204,15 +204,12 @@ int		 acctproc(int, const char *, enum k
 int		 certproc(int, int);
 int		 chngproc(int, const char *);
 int		 dnsproc(int);
-int		 revokeproc(int, const char *, int, int, const char *const *,
-			size_t);
+int		 revokeproc(int, const char *, int, int, struct domain_c *);
 int		 fileproc(int, const char *, const char *, const char *,
 			const char *);
-int		 keyproc(int, const char *, const char **, size_t,
-			enum keytype);
+int		 keyproc(int, struct domain_c *);
 int		 netproc(int, int, int, int, int, int, int,
-			struct authority_c *, const char *const *,
-			size_t, const char *);
+			struct authority_c *, struct domain_c *);
 
 /*
  * Debugging functions.
@@ -263,7 +260,7 @@ char		*json_getstr(struct jsmnn *, const
 char		*json_fmt_newcert(const char *);
 char		*json_fmt_chkacc(void);
 char		*json_fmt_newacc(const char *);
-char		*json_fmt_neworder(const char *const *, size_t, const char *);
+char		*json_fmt_neworder(struct domain_c *);
 char		*json_fmt_protected_rsa(const char *,
 			const char *, const char *, const char *);
 char		*json_fmt_protected_ec(const char *, const char *, const char *,
Index: json.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/json.c,v
diff -u -p -r1.22 json.c
--- json.c	16 Sep 2025 15:06:02 -0000	1.22
+++ json.c	14 Dec 2025 13:37:50 -0000
@@ -647,20 +647,32 @@ json_fmt_newacc(const char *contact)
  * Format the "newOrder" resource request
  */
 char *
-json_fmt_neworder(const char *const *alts, size_t altsz, const char *profile)
+json_fmt_neworder(struct domain_c *domain)
 {
-	size_t	 i;
-	int	 c;
-	char	*p, *t;
+	int			 c;
+	char			*p, *t;
+	struct altname_c	*ac, *first;
 
 	if ((p = strdup("{ \"identifiers\": [")) == NULL)
 		goto err;
 
 	t = p;
-	for (i = 0; i < altsz; i++) {
-		c = asprintf(&p,
-		    "%s { \"type\": \"dns\", \"value\": \"%s\" }%s",
-		    t, alts[i], i + 1 == altsz ? "" : ",");
+	first = TAILQ_FIRST(&domain->altname_list);
+	TAILQ_FOREACH(ac, &domain->altname_list, entry) {
+		switch (ac->idtype) {
+			case ID_DNS:
+				c = asprintf(&p,
+				    "%s%s { \"type\": \"dns\", "
+				    "\"value\": \"%s\" }",
+				    t, ac == first ? "" : ",", ac->domain);
+				break;
+			case ID_IP:
+				c = asprintf(&p,
+				    "%s%s { \"type\": \"ip\", "
+				    "\"value\": \"%s\" }",
+				    t, ac == first ? "" : ",", ac->domain);
+				break;
+		}
 		free(t);
 		if (c == -1) {
 			warn("asprintf");
@@ -669,10 +681,11 @@ json_fmt_neworder(const char *const *alt
 		}
 		t = p;
 	}
-	if (profile == NULL)
+	if (domain->profile == NULL)
 		c = asprintf(&p, "%s ] }", t);
 	else
-		c = asprintf(&p, "%s ], \"profile\": \"%s\" }", t, profile);
+		c = asprintf(&p, "%s ], \"profile\": \"%s\" }", t,
+		    domain->profile);
 	free(t);
 	if (c == -1) {
 		warn("asprintf");
Index: keyproc.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/keyproc.c,v
diff -u -p -r1.18 keyproc.c
--- keyproc.c	28 Aug 2022 18:30:29 -0000	1.18
+++ keyproc.c	14 Dec 2025 13:37:50 -0000
@@ -74,13 +74,12 @@ add_ext(STACK_OF(X509_EXTENSION) *sk, in
  * jail and, on success, ship it to "netsock" as an X509 request.
  */
 int
-keyproc(int netsock, const char *keyfile, const char **alts, size_t altsz,
-    enum keytype keytype)
+keyproc(int netsock, struct domain_c *domain)
 {
 	char		*der64 = NULL, *der = NULL, *dercp;
 	char		*sans = NULL, *san = NULL;
 	FILE		*f;
-	size_t		 i, sansz;
+	size_t		 sansz;
 	void		*pp;
 	EVP_PKEY	*pkey = NULL;
 	X509_REQ	*x = NULL;
@@ -88,6 +87,8 @@ keyproc(int netsock, const char *keyfile
 	int		 len, rc = 0, cc, nid, newkey = 0;
 	mode_t		 prev;
 	STACK_OF(X509_EXTENSION) *exts = NULL;
+	struct altname_c	 *ac, *first;
+	const char	*keyfile = domain->key;
 
 	/*
 	 * First, open our private key file read-only or write-only if
@@ -117,7 +118,7 @@ keyproc(int netsock, const char *keyfile
 	}
 
 	if (newkey) {
-		switch (keytype) {
+		switch (domain->keytype) {
 		case KT_ECDSA:
 			if ((pkey = ec_key_create(f, keyfile)) == NULL)
 				goto out;
@@ -155,20 +156,21 @@ keyproc(int netsock, const char *keyfile
 		goto out;
 	}
 
-	/* Now specify the common name that we'll request. */
-
-	if ((name = X509_NAME_new()) == NULL) {
-		warnx("X509_NAME_new");
-		goto out;
-	} else if (!X509_NAME_add_entry_by_txt(name, "CN",
-		MBSTRING_ASC, (u_char *)alts[0], -1, -1, 0)) {
-		warnx("X509_NAME_add_entry_by_txt: CN=%s", alts[0]);
-		goto out;
-	} else if (!X509_REQ_set_subject_name(x, name)) {
-		warnx("X509_req_set_issuer_name");
-		goto out;
+	if (domain->with_cn) {
+		/* Now specify the common name that we'll request. */
+		if ((name = X509_NAME_new()) == NULL) {
+			warnx("X509_NAME_new");
+			goto out;
+		} else if (!X509_NAME_add_entry_by_txt(name, "CN",
+			MBSTRING_ASC, (u_char *)domain->domain, -1, -1, 0)) {
+			warnx("X509_NAME_add_entry_by_txt: CN=%s",
+				domain->domain);
+			goto out;
+		} else if (!X509_REQ_set_subject_name(x, name)) {
+			warnx("X509_req_set_issuer_name");
+			goto out;
+		}
 	}
-
 	/*
 	 * Now add the SAN extensions.
 	 * This was lifted more or less directly from demos/x509/mkreq.c
@@ -195,9 +197,18 @@ keyproc(int netsock, const char *keyfile
 	 * domains: NOT an entry per domain!
 	 */
 
-	for (i = 0; i < altsz; i++) {
-		cc = asprintf(&san, "%sDNS:%s",
-		    i ? "," : "", alts[i]);
+	first = TAILQ_FIRST(&domain->altname_list);
+	TAILQ_FOREACH(ac, &domain->altname_list, entry) {
+		switch (ac->idtype) {
+			case ID_DNS:
+				cc = asprintf(&san, "%sDNS:%s",
+				    ac == first ? "" : ",", ac->domain);
+				break;
+			case ID_IP:
+				cc = asprintf(&san, "%sIP:%s",
+				    ac == first ? "" : ",", ac->domain);
+				break;
+		}
 		if (cc == -1) {
 			warn("asprintf");
 			goto out;
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/main.c,v
diff -u -p -r1.57 main.c
--- main.c	16 Sep 2025 15:06:02 -0000	1.57
+++ main.c	14 Dec 2025 13:37:50 -0000
@@ -40,7 +40,6 @@ enum comp 	 proccomp;
 int
 main(int argc, char *argv[])
 {
-	const char	 **alts = NULL;
 	char		 *certdir = NULL;
 	char		 *chngdir = NULL, *auth = NULL;
 	char		 *conffile = CONF_FILE;
@@ -51,7 +50,7 @@ main(int argc, char *argv[])
 	int		  c, rc, revocate = 0;
 	int		  popts = 0;
 	pid_t		  pids[COMP__MAX];
-	size_t		  i, altsz, ne;
+	size_t		  ne;
 
 	struct acme_conf	*conf = NULL;
 	struct authority_c	*authority = NULL;
@@ -112,7 +111,7 @@ main(int argc, char *argv[])
 	if ((tmpsd = dirname(tmps)) == NULL)
 		err(EXIT_FAILURE, "dirname");
 	if ((certdir = strdup(tmpsd)) == NULL)
-		err(EXIT_FAILURE, "strdup");	
+		err(EXIT_FAILURE, "strdup");
 	free(tmps);
 	tmps = tmpsd = NULL;
 
@@ -174,15 +173,12 @@ main(int argc, char *argv[])
 		return EXIT_SUCCESS;
 
 	/* Set the zeroth altname as our domain. */
-	altsz = domain->altname_count + 1;
-	alts = calloc(altsz, sizeof(char *));
-	if (alts == NULL)
-		err(EXIT_FAILURE, "calloc");
-	alts[0] = domain->domain;
-	i = 1;
-	/* XXX get rid of alts[] later */
-	TAILQ_FOREACH(ac, &domain->altname_list, entry)
-		alts[i++] = ac->domain;
+
+	ac = calloc(1, sizeof(struct altname_c));
+	ac->domain = domain->domain;
+	ac->idtype = domain->idtype;
+	TAILQ_INSERT_HEAD(&domain->altname_list, ac, entry);
+	domain->altname_count++;
 
 	/*
 	 * Open channels between our components.
@@ -223,9 +219,7 @@ main(int argc, char *argv[])
 		c = netproc(key_fds[1], acct_fds[1],
 		    chng_fds[1], cert_fds[1],
 		    dns_fds[1], rvk_fds[1],
-		    revocate, authority,
-		    (const char *const *)alts, altsz,
-		    domain->profile);
+		    revocate, authority, domain);
 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
 	}
 
@@ -250,9 +244,7 @@ main(int argc, char *argv[])
 		close(chng_fds[0]);
 		close(file_fds[0]);
 		close(file_fds[1]);
-		c = keyproc(key_fds[0], domain->key,
-		    (const char **)alts, altsz,
-		    domain->keytype);
+		c = keyproc(key_fds[0], domain);
 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
 	}
 
@@ -355,8 +347,7 @@ main(int argc, char *argv[])
 	if (pids[COMP_REVOKE] == 0) {
 		proccomp = COMP_REVOKE;
 		c = revokeproc(rvk_fds[0], domain->cert != NULL ? domain->cert :
-		    domain->fullchain, force, revocate,
-		    (const char *const *)alts, altsz);
+		    domain->fullchain, force, revocate, domain);
 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
 	}
 
Index: netproc.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/netproc.c,v
diff -u -p -r1.45 netproc.c
--- netproc.c	16 Sep 2025 15:06:02 -0000	1.45
+++ netproc.c	14 Dec 2025 13:37:50 -0000
@@ -492,15 +492,15 @@ dochkacc(struct conn *c, const struct ca
  * Submit a new order for a certificate.
  */
 static int
-doneworder(struct conn *c, const char *const *alts, size_t altsz,
-    struct order *order, const struct capaths *p, const char *profile)
+doneworder(struct conn *c, struct domain_c *domain, struct order *order,
+    const struct capaths *p)
 {
 	struct jsmnn	*j = NULL;
 	int		 rc = 0;
 	char		*req;
 	long		 lc;
 
-	if ((req = json_fmt_neworder(alts, altsz, profile)) == NULL)
+	if ((req = json_fmt_neworder(domain)) == NULL)
 		warnx("json_fmt_neworder");
 	else if ((lc = sreq(c, p->neworder, 1, req, &order->uri)) < 0)
 		warnx("%s: bad comm", p->neworder);
@@ -723,7 +723,7 @@ dodirs(struct conn *c, const char *addr,
 int
 netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd,
     int revocate, struct authority_c *authority,
-    const char *const *alts, size_t altsz, const char *profile)
+    struct domain_c *domain)
 {
 	int		 rc = 0, retries = 0;
 	size_t		 i;
@@ -828,7 +828,7 @@ netproc(int kfd, int afd, int Cfd, int c
 
 	memset(&order, 0, sizeof(order));
 
-	if (!doneworder(&c, alts, altsz, &order, &paths, profile))
+	if (!doneworder(&c, domain, &order, &paths))
 		goto out;
 
 	chngs = calloc(order.authsz, sizeof(struct chng));
Index: parse.h
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/parse.h,v
diff -u -p -r1.17 parse.h
--- parse.h	16 Sep 2025 15:06:02 -0000	1.17
+++ parse.h	14 Dec 2025 13:37:50 -0000
@@ -32,6 +32,11 @@ enum keytype {
 	KT_ECDSA
 };
 
+enum identifiertype {
+	ID_DNS = 0,	/* RFC 8555 */
+	ID_IP		/* RFC 8738 */
+};
+
 struct authority_c {
 	TAILQ_ENTRY(authority_c)	 entry;
 	char				*name;
@@ -46,7 +51,9 @@ struct domain_c {
 	TAILQ_ENTRY(domain_c)	 entry;
 	TAILQ_HEAD(, altname_c)	 altname_list;
 	int			 altname_count;
+	int			 with_cn;
 	enum keytype		 keytype;
+	enum identifiertype	 idtype;
 	char			*handle;
 	char			*domain;
 	char			*key;
@@ -60,7 +67,8 @@ struct domain_c {
 
 struct altname_c {
 	TAILQ_ENTRY(altname_c)	 entry;
-	char		       	*domain;
+	char			*domain;
+	enum identifiertype	 idtype;
 };
 
 struct keyfile {
@@ -87,5 +95,6 @@ struct authority_c	*authority_find0(stru
 struct domain_c		*domain_find_handle(struct acme_conf *, char *);
 
 int			 domain_valid(const char *);
+int			 altname_valid(const char *);
 
 #endif /* PARSE_H */
Index: parse.y
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/parse.y,v
diff -u -p -r1.47 parse.y
--- parse.y	16 Sep 2025 15:06:02 -0000	1.47
+++ parse.y	14 Dec 2025 13:37:50 -0000
@@ -102,8 +102,7 @@ typedef struct {
 
 %token	AUTHORITY URL API ACCOUNT CONTACT
 %token	DOMAIN ALTERNATIVE NAME NAMES CERT FULL CHAIN KEY SIGN WITH
-%token	CHALLENGEDIR PROFILE
-%token	YES NO
+%token	CHALLENGEDIR PROFILE CN
 %token	INCLUDE
 %token	ERROR
 %token	RSA ECDSA
@@ -221,7 +220,7 @@ authorityoptsl	: API URL STRING {
 				err(EXIT_FAILURE, "strdup");
 			auth->api = s;
 		}
-		| ACCOUNT KEY STRING keytype{
+		| ACCOUNT KEY STRING keytype {
 			char *s;
 			if (auth->account != NULL) {
 				yyerror("duplicate account");
@@ -299,8 +298,21 @@ domainoptsl	: ALTERNATIVE NAMES '{' optn
 				yyerror("duplicate domain name");
 				YYERROR;
 			}
-			if ((s = strdup($3)) == NULL)
-				err(EXIT_FAILURE, "strdup");
+			if (!altname_valid($3)) {
+				yyerror("bad domain name syntax");
+				YYERROR;
+			}
+			if ((strncmp($3, "ip:", 3) == 0) &&
+			    (strlen($3) > 3)) {
+				domain->idtype = ID_IP;
+				if ((s = strdup(($3) + 3)) == NULL)
+					err(EXIT_FAILURE, "strdup");
+			}
+			else {
+				domain->idtype = ID_DNS;
+				if ((s = strdup($3)) == NULL)
+					err(EXIT_FAILURE, "strdup");
+			}
 			domain->domain = s;
 		}
 		| DOMAIN KEY STRING keytype {
@@ -408,6 +420,19 @@ domainoptsl	: ALTERNATIVE NAMES '{' optn
 				err(EXIT_FAILURE, "strdup");
 			domain->profile = s;
 		}
+		| PROFILE STRING {
+			char *s;
+			if (domain->profile != NULL) {
+				yyerror("duplicate profile");
+				YYERROR;
+			}
+			if ((s = strdup($2)) == NULL)
+				err(EXIT_FAILURE, "strdup");
+			domain->profile = s;
+		}
+		| WITH CN {
+			domain->with_cn = 1;
+		}
 		;
 
 altname_l	: altname optcommanl altname_l
@@ -417,15 +442,26 @@ altname_l	: altname optcommanl altname_l
 altname		: STRING {
 			char			*s;
 			struct altname_c	*ac;
-			if (!domain_valid($1)) {
-				yyerror("bad domain syntax");
+			if (!altname_valid($1)) {
+				yyerror("bad altname syntax");
 				YYERROR;
 			}
 			if ((ac = calloc(1, sizeof(struct altname_c))) == NULL)
 				err(EXIT_FAILURE, "calloc");
-			if ((s = strdup($1)) == NULL) {
-				free(ac);
-				err(EXIT_FAILURE, "strdup");
+			if ((strncmp($1, "ip:", 3) == 0) &&
+			    (strlen($1) > 3)) {
+				ac->idtype = ID_IP;
+				if ((s = strdup(($1) + 3)) == NULL) {
+					free(ac);
+					err(EXIT_FAILURE, "strdup");
+				}
+			}
+			else {
+				ac->idtype = ID_DNS;
+				if ((s = strdup($1)) == NULL) {
+					free(ac);
+					err(EXIT_FAILURE, "strdup");
+				}
 			}
 			ac->domain = s;
 			TAILQ_INSERT_TAIL(&domain->altname_list, ac, entry);
@@ -477,6 +513,7 @@ lookup(char *s)
 		{"certificate",		CERT},
 		{"chain",		CHAIN},
 		{"challengedir",	CHALLENGEDIR},
+		{"cn",			CN},
 		{"contact",		CONTACT},
 		{"domain",		DOMAIN},
 		{"ecdsa",		ECDSA},
@@ -1079,12 +1116,15 @@ print_config(struct acme_conf *xconf)
 		f = 0;
 		printf("domain %s {\n", d->handle);
 		if (d->domain != NULL)
-			printf("\tdomain name \"%s\"\n", d->domain);
+			printf("\tdomain name \"%s%s\"\n",
+			    (d->idtype == ID_IP) ? "ip:" : "", d->domain);
 		TAILQ_FOREACH(ac, &d->altname_list, entry) {
 			if (!f)
 				printf("\talternative names {");
 			if (ac->domain != NULL) {
-				printf("%s%s", f ? ", " : " ", ac->domain);
+				printf("%s%s%s", f ? ", " : " ",
+				    (d->idtype == ID_IP) ? "ip:" : "",
+				    ac->domain);
 				f = 1;
 			}
 		}
@@ -1102,6 +1142,8 @@ print_config(struct acme_conf *xconf)
 			    d->fullchain);
 		if (d->profile != NULL)
 			printf("\tprofile \"%s\"\n", d->profile);
+		if (d->with_cn != 0)
+			printf("\twith cn\n");
 		if (d->auth != NULL)
 			printf("\tsign with \"%s\"\n", d->auth);
 		if (d->challengedir != NULL)
@@ -1123,6 +1165,17 @@ domain_valid(const char *cp)
 	for ( ; *cp != '\0'; cp++)
 		if (!(*cp == '.' || *cp == '-' ||
 		    *cp == '_' || isalnum((unsigned char)*cp)))
+			return 0;
+	return 1;
+}
+
+int
+altname_valid(const char *cp)
+{
+
+	for ( ; *cp != '\0'; cp++)
+		if (!(*cp == '.' || *cp == '-' ||
+		    *cp == ':' || isalnum((unsigned char)*cp)))
 			return 0;
 	return 1;
 }
Index: revokeproc.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/revokeproc.c,v
diff -u -p -r1.26 revokeproc.c
--- revokeproc.c	18 Sep 2025 13:22:36 -0000	1.26
+++ revokeproc.c	14 Dec 2025 13:37:50 -0000
@@ -74,19 +74,18 @@ X509notbefore(X509 *x)
 
 int
 revokeproc(int fd, const char *certfile, int force,
-    int revocate, const char *const *alts, size_t altsz)
+    int revocate, struct domain_c *domain)
 {
 	GENERAL_NAMES			*sans = NULL;
 	char				*der = NULL, *dercp, *der64 = NULL;
-	int				 rc = 0, cc, i, len;
-	size_t				*found = NULL;
+	int				 rc = 0, cc, sanidx, len, j, k;
+	int				*found_altnames = NULL;
 	FILE				*f = NULL;
 	X509				*x = NULL;
 	long				 lval;
 	enum revokeop			 op, rop;
 	time_t				 notafter, notbefore, cert_validity;
 	time_t				 remaining_validity, renew_allow;
-	size_t				 j;
 
 	/*
 	 * First try to open the certificate before we drop privileges
@@ -162,7 +161,8 @@ revokeproc(int fd, const char *certfile,
 
 	/* An array of buckets: the number of entries found. */
 
-	if ((found = calloc(altsz, sizeof(size_t))) == NULL) {
+	if ((found_altnames = (int *)calloc(domain->altname_count,
+	    sizeof(int))) == NULL) {
 		warn("calloc");
 		goto out;
 	}
@@ -172,49 +172,78 @@ revokeproc(int fd, const char *certfile,
 	 * configuration file and that all domains are represented only once.
 	 */
 
-	for (i = 0; i < sk_GENERAL_NAME_num(sans); i++) {
+	for (sanidx = 0; sanidx < sk_GENERAL_NAME_num(sans); sanidx++) {
 		GENERAL_NAME		*gen_name;
-		const ASN1_IA5STRING	*name;
-		const unsigned char	*name_buf;
+		unsigned char		*name_buf;
+		unsigned char		*p;
 		int			 name_len;
-		int			 name_type;
+		struct altname_c	*ac;
+		int			i;
 
-		gen_name = sk_GENERAL_NAME_value(sans, i);
+		gen_name = sk_GENERAL_NAME_value(sans, sanidx);
 		assert(gen_name != NULL);
 
-		name = GENERAL_NAME_get0_value(gen_name, &name_type);
-		if (name_type != GEN_DNS)
+		if (gen_name->type == GEN_IPADD) {
+			p = gen_name->d.iPAddress->data;
+			name_len = gen_name->d.iPAddress->length;
+
+			if (name_len == 4)
+				asprintf((char **)&name_buf, "%d.%d.%d.%d",
+				    p[0], p[1], p[2], p[3]);
+			else if (name_len == 16) {
+				name_buf = strdup("");
+				for (i = 0; i < 8; i++) {
+					asprintf((char **)&name_buf, "%s%s%04x",
+					    name_buf, i == 0 ? "" : ":",
+					    p[0] << 8 | p[1]);
+					p += 2;
+				}
+			}
+			else
+				continue;
+		}
+		else if (gen_name->type == GEN_DNS) {
+			name_len = gen_name->d.dNSName->length;
+			asprintf((char **)&name_buf, "%.*s",
+			    name_len, gen_name->d.dNSName->data);
+		}
+		else
 			continue;
 
-		/* name_buf isn't a C string and could contain embedded NULs. */
-		name_buf = ASN1_STRING_get0_data(name);
-		name_len = ASN1_STRING_length(name);
+		/* now we have a real C string */
+		name_len = strlen(name_buf);
 
-		for (j = 0; j < altsz; j++) {
-			if ((size_t)name_len != strlen(alts[j]))
-				continue;
-			if (memcmp(name_buf, alts[j], name_len) == 0)
+		j = 0;
+		TAILQ_FOREACH(ac, &domain->altname_list, entry) {
+			if (memcmp(name_buf, ac->domain, name_len) == 0) {
+				found_altnames[j]++;
 				break;
+			}
+			/* increment if didn't match */
+			j++;
 		}
-		if (j == altsz) {
+		if (j >= domain->altname_count) {
+			/* we haven't matched any */
 			if (revocate) {
 				char *visbuf;
 
 				visbuf = calloc(4, name_len + 1);
 				if (visbuf == NULL) {
-					warn("%s: unexpected SAN", certfile);
+					warn("%s: unexpected SAN in "
+					    "certificate", certfile);
 					goto out;
 				}
 				strvisx(visbuf, name_buf, name_len, VIS_SAFE);
-				warnx("%s: unexpected SAN entry: %s",
-				    certfile, visbuf);
+				warnx("%s: unexpected SAN entry in "
+				    "certificate: %s", certfile, visbuf);
 				free(visbuf);
 				goto out;
 			}
 			force = 2;
 			continue;
 		}
-		if (found[j]++) {
+		/* should not reach here if j is out of bounds */
+		if (found_altnames[j] > 1) {
 			if (revocate) {
 				warnx("%s: duplicate SAN entry: %.*s",
 				    certfile, name_len, name_buf);
@@ -224,11 +253,20 @@ revokeproc(int fd, const char *certfile,
 		}
 	}
 
-	for (j = 0; j < altsz; j++) {
-		if (found[j])
+	for (j = 0; j < domain->altname_count; j++) {
+		struct altname_c	*ac;
+
+		if (found_altnames[j])
 			continue;
 		if (revocate) {
-			warnx("%s: domain not listed: %s", certfile, alts[j]);
+			k = 0;
+			TAILQ_FOREACH(ac, &domain->altname_list, entry) {
+				if (j == k)
+					break;
+				k++;
+			}
+			warnx("%s: domain not listed: %s", certfile,
+			    ac->domain);
 			goto out;
 		}
 		force = 2;
@@ -340,7 +378,7 @@ out:
 	X509_free(x);
 	GENERAL_NAMES_free(sans);
 	free(der);
-	free(found);
+	free(found_altnames);
 	free(der64);
 	ERR_print_errors_fp(stderr);
 	ERR_free_strings();