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:
"tech@openbsd.org" <tech@openbsd.org>
Date:
Thu, 18 Sep 2025 08:31:38 +0100

Download raw body.

Thread
On 2025/09/18 00:50, Lloyd wrote:
> Fixed a bug with IPv6 parsing on revocation. As is, this should work with
> fully padded IPv6 addresses in the config; e.g.
> 
> domain name ip:fe80:0000:0000:0000:0000:0000:0000:0001
> 
> The correct way to store this in memory should probably be in network
> byte order so users can specify compact v6 addresses and we don't have
> to worry about comparing strings.
> 
> Either way, the current method of cramming all the SAN hostnames into alts[]
> is really ugly and needs to go. This gets you about 90% of the way there.

Thanks.

We can still work on it in the meantime, but this is a big enough change
that it should wait until after release for commit - if there is any
problem we want to be able to find it before some users are stuck with
it for 6 months.

> Lloyd wrote:
> 
> > The below diff adds full support for IP address certs to acme-client(1).
> > 
> <snip>
> 
> Index: acme-client.conf.5
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/acme-client.conf.5,v
> retrieving revision 1.29
> diff -u -p -u -p -r1.29 acme-client.conf.5
> --- acme-client.conf.5	11 Jan 2021 07:23:42 -0000	1.29
> +++ acme-client.conf.5	18 Sep 2025 00:28:40 -0000
> @@ -110,7 +110,10 @@ The certificates to be obtained through 
>  .It Ic domain Ar handle Brq ...
>  Each domain section begins with the
>  .Ic domain
> -keyword followed by an identifier for this domain block.
> +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.

Pity we already have the keyword "domain" as this gets a little bit
confusing.

I don't think talking about iPAddress is needed here. Going straight
into the abbreviation SAN is confusing to a user who is unfamiliar
with the term. (and, while correct in the context of SANs, I can
guarantee we will be sent diffs "you should have used IPAddress" ;)

Perhaps ..

By default, a name of
.Ar handle
is included in the certificate.
Where this is not wanted, for example when requesting both EC and RSA
certificates for the same name, also set
.Cm domain name
to override this default.

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,

+If
+.Ar handle
+begins with
+.Cm ip: ,
+it is treated as an IP address rather than a domain name.
+Normally CAs do not issue certificates for IP addresses, but this is
+sometimes supported if a non-default
+.Ic profile
+is requested.

Actually, the type is unambiguous, so maybe we don't need the prefix?
If there are only digits and ., it's an ip4 address. Hex digits and :,
ip6. Otherwise fqdn. (Just try parsing as the various types rather than
look at those characteristics though).

I don't think we need to go into the nuts and bolts of SAN type here
do we?


>  .El
>  .Pp
>  It is followed by a block of options enclosed in curly brackets:
> @@ -118,19 +121,23 @@ 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.

+to be used as the primary name in the X.509 certificate.
 This is optional.

(assuming the above-mentioned parser change)

>  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

again a bit overcomplicated I think.

new sentence -> new line

+of the domain block will be used as the primary name.

> +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: .

But "the name from either the 'domain' or 'domain name' line" is still
included and that should be reflected, along with the "www. is not added
automatically".

Manuals should talk about what it does now, not what it used to do ("no
longer..").

>  .It Ic domain key Ar file Op Ar keytype
>  The private key file for which the certificate will be obtained.
>  .Ar keytype
> @@ -187,6 +194,13 @@ A backup with name
>  is created if
>  .Ar file
>  exists.
> +.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.

new sentence -> new line, whitespace at eol

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.

> +.It Ic profile Ar profile
> +The certificate profile to be requested.
> +If this setting is absent, no profile request is made.

already committed

>  .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
> retrieving revision 1.21
> diff -u -p -u -p -r1.21 extern.h
> --- extern.h	21 May 2024 05:00:48 -0000	1.21
> +++ extern.h	18 Sep 2025 00:28:40 -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);
> +			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);
> +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
> retrieving revision 1.21
> diff -u -p -u -p -r1.21 json.c
> --- json.c	14 Sep 2020 16:00:17 -0000	1.21
> +++ json.c	18 Sep 2025 00:28:40 -0000
> @@ -647,20 +647,31 @@ json_fmt_newacc(const char *contact)
>   * Format the "newOrder" resource request
>   */
>  char *
> -json_fmt_neworder(const char *const *alts, size_t altsz)
> +json_fmt_neworder(struct domain_c *domain)

I need to make tea before I can even think about looking at the code
so will stop here for now.

>  {
> -	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);
> +				    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,7 +680,11 @@ json_fmt_neworder(const char *const *alt
>  		}
>  		t = p;
>  	}
> -	c = asprintf(&p, "%s ] }", t);
> +	if (domain->profile == NULL)
> +		c = asprintf(&p, "%s ] }", t);
> +	else
> +		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
> retrieving revision 1.18
> diff -u -p -u -p -r1.18 keyproc.c
> --- keyproc.c	28 Aug 2022 18:30:29 -0000	1.18
> +++ keyproc.c	18 Sep 2025 00:28:40 -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
> retrieving revision 1.56
> diff -u -p -u -p -r1.56 main.c
> --- main.c	19 Jun 2024 13:13:25 -0000	1.56
> +++ main.c	18 Sep 2025 00:28:40 -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;
> @@ -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,8 +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);
> +		    revocate, authority, domain);
>  		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
>  	}
>  
> @@ -249,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);
>  	}
>  
> @@ -354,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
> retrieving revision 1.37.4.1
> diff -u -p -u -p -r1.37.4.1 netproc.c
> --- netproc.c	10 Jun 2025 18:21:49 -0000	1.37.4.1
> +++ netproc.c	18 Sep 2025 00:28:40 -0000
> @@ -440,15 +440,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)
> +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)) == 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);
> @@ -671,7 +671,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)
> +    struct domain_c *domain)    
>  {
>  	int		 rc = 0, retries = 0;
>  	size_t		 i;
> @@ -762,7 +762,7 @@ netproc(int kfd, int afd, int Cfd, int c
>  	 * Following that, submit the request to the CA then notify the
>  	 * certproc, which will in turn notify the fileproc.
>  	 * XXX currently we can only sign with the account key, the RFC
> -	 * also mentions signing with the privat key of the cert itself.
> +	 * also mentions signing with the private key of the cert itself.
>  	 */
>  	if (revocate) {
>  		if ((cert = readstr(rfd, COMM_CSR)) == NULL)
> @@ -776,7 +776,7 @@ netproc(int kfd, int afd, int Cfd, int c
>  
>  	memset(&order, 0, sizeof(order));
>  
> -	if (!doneworder(&c, alts, altsz, &order, &paths))
> +	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
> retrieving revision 1.15
> diff -u -p -u -p -r1.15 parse.h
> --- parse.h	14 Sep 2020 16:00:17 -0000	1.15
> +++ parse.h	18 Sep 2025 00:28:40 -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;
> @@ -54,11 +59,15 @@ struct domain_c {
>  	char			*fullchain;
>  	char			*auth;
>  	char			*challengedir;
> +	char			*profile;
> +	enum identifiertype	 idtype;
> +	int			 with_cn;
>  };
>  
>  struct altname_c {
>  	TAILQ_ENTRY(altname_c)	 entry;
>  	char		       	*domain;
> +	enum identifiertype	 idtype;
>  };
>  
>  struct keyfile {
> @@ -85,5 +94,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
> retrieving revision 1.45
> diff -u -p -u -p -r1.45 parse.y
> --- parse.y	15 Dec 2022 08:06:13 -0000	1.45
> +++ parse.y	18 Sep 2025 00:28:41 -0000
> @@ -101,8 +101,8 @@ typedef struct {
>  %}
>  
>  %token	AUTHORITY URL API ACCOUNT CONTACT
> -%token	DOMAIN ALTERNATIVE NAME NAMES CERT FULL CHAIN KEY SIGN WITH CHALLENGEDIR
> -%token	YES NO
> +%token	DOMAIN ALTERNATIVE NAME NAMES CERT FULL CHAIN KEY SIGN WITH
> +%token	CHALLENGEDIR PROFILE CN
>  %token	INCLUDE
>  %token	ERROR
>  %token	RSA ECDSA
> @@ -219,7 +219,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");
> @@ -294,8 +294,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 {
> @@ -393,6 +406,19 @@ domainoptsl	: ALTERNATIVE NAMES '{' optn
>  				err(EXIT_FAILURE, "strdup");
>  			domain->challengedir = 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
> @@ -402,15 +428,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");
> +				err(EXIT_FAILURE, "calloc");	
> +			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);
> @@ -462,6 +499,7 @@ lookup(char *s)
>  		{"certificate",		CERT},
>  		{"chain",		CHAIN},
>  		{"challengedir",	CHALLENGEDIR},
> +		{"cn",			CN},
>  		{"contact",		CONTACT},
>  		{"domain",		DOMAIN},
>  		{"ecdsa",		ECDSA},
> @@ -470,6 +508,7 @@ lookup(char *s)
>  		{"key",			KEY},
>  		{"name",		NAME},
>  		{"names",		NAMES},
> +		{"profile",		PROFILE},
>  		{"rsa",			RSA},
>  		{"sign",		SIGN},
>  		{"url",			URL},
> @@ -1060,12 +1099,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;
>  			}
>  		}
> @@ -1081,6 +1123,10 @@ print_config(struct acme_conf *xconf)
>  		if (d->fullchain != NULL)
>  			printf("\tdomain full chain certificate \"%s\"\n",
>  			    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)
> @@ -1102,6 +1148,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
> retrieving revision 1.25
> diff -u -p -u -p -r1.25 revokeproc.c
> --- revokeproc.c	18 Dec 2022 12:04:55 -0000	1.25
> +++ revokeproc.c	18 Sep 2025 00:28:41 -0000
> @@ -60,18 +60,18 @@ X509expires(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;
> +	int				*found_altnames = NULL;
>  	FILE				*f = NULL;
>  	X509				*x = NULL;
>  	long				 lval;
>  	enum revokeop			 op, rop;
>  	time_t				 t;
> -	size_t				 j;
> +	int				 j, k;
>  
>  	/*
>  	 * First try to open the certificate before we drop privileges
> @@ -142,7 +142,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;
>  	}
> @@ -152,49 +153,79 @@ 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)
> -			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);
> -
> -		for (j = 0; j < altsz; j++) {
> -			if ((size_t)name_len != strlen(alts[j]))
> +		
> +		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;
> -			if (memcmp(name_buf, alts[j], name_len) == 0)
> +		}	
> +		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;
> +		
> +		/* now we have a real C string */
> +		name_len = strlen(name_buf);
> +		
> +		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);
> @@ -204,11 +235,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;
> @@ -299,7 +339,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();
>