1/*	$OpenBSD: constraints.c,v 1.4 2024/03/15 05:14:16 tb Exp $ */
2/*
3 * Copyright (c) 2023 Job Snijders <job@openbsd.org>
4 * Copyright (c) 2023 Theo Buehler <tb@openbsd.org>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19#include <sys/socket.h>
20
21#include <arpa/inet.h>
22
23#include <ctype.h>
24#include <err.h>
25#include <errno.h>
26#include <fcntl.h>
27#include <libgen.h>
28#include <stdint.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32#include <unistd.h>
33
34#include <openssl/asn1.h>
35#include <openssl/x509v3.h>
36
37#include "extern.h"
38
39struct tal_constraints {
40	int		 fd;		/* constraints file descriptor or -1. */
41	char		*fn;		/* constraints filename */
42	char		*warn;		/* warning msg used for violations */
43	struct cert_ip	*allow_ips;	/* list of allowed IP address ranges */
44	size_t		 allow_ipsz;	/* length of "allow_ips" */
45	struct cert_as	*allow_as;	/* allowed AS numbers and ranges */
46	size_t		 allow_asz;	/* length of "allow_as" */
47	struct cert_ip	*deny_ips;	/* forbidden IP address ranges */
48	size_t		 deny_ipsz;	/* length of "deny_ips" */
49	struct cert_as	*deny_as;	/* forbidden AS numbers and ranges */
50	size_t		 deny_asz;	/* length of "deny_as" */
51} tal_constraints[TALSZ_MAX];
52
53/*
54 * If there is a .constraints file next to a .tal file, load its contents
55 * into into tal_constraints[talid]. The load function only opens the fd
56 * and stores the filename. The actual parsing happens in constraints_parse().
57 * Resources of EE certs can then be constrained using constraints_validate().
58 */
59
60static void
61constraints_load_talid(int talid)
62{
63	const char	*tal = tals[talid];
64	char		*constraints = NULL, *warning = NULL, *cbn;
65	int		 fd;
66	size_t		 len;
67	int		 saved_errno;
68
69	tal_constraints[talid].fd = -1;
70
71	if (rtype_from_file_extension(tal) != RTYPE_TAL)
72		return;
73
74	/* Replace .tal suffix with .constraints. */
75	len = strlen(tal) - 4;
76	if (asprintf(&constraints, "%.*s.constraints", (int)len, tal) == -1)
77		err(1, NULL);
78
79	/* prepare warning message for when violations are detected */
80	if ((cbn = basename(constraints)) == NULL)
81		err(1, "basename");
82	if (asprintf(&warning, "resource violates %s", cbn) == -1)
83		err(1, NULL);
84
85	saved_errno = errno;
86
87	fd = open(constraints, O_RDONLY);
88	if (fd == -1 && errno != ENOENT)
89		err(1, "failed to load constraints for %s", tal);
90
91	tal_constraints[talid].fn = constraints;
92	tal_constraints[talid].fd = fd;
93	tal_constraints[talid].warn = warning;
94
95	errno = saved_errno;
96}
97
98/*
99 * Iterate over all TALs and load the corresponding constraints files.
100 */
101void
102constraints_load(void)
103{
104	int	 talid;
105
106	for (talid = 0; talid < talsz; talid++)
107		constraints_load_talid(talid);
108}
109
110void
111constraints_unload(void)
112{
113	int	 saved_errno, talid;
114
115	saved_errno = errno;
116	for (talid = 0; talid < talsz; talid++) {
117		if (tal_constraints[talid].fd != -1)
118			close(tal_constraints[talid].fd);
119		free(tal_constraints[talid].fn);
120		free(tal_constraints[talid].warn);
121		tal_constraints[talid].fd = -1;
122		tal_constraints[talid].fn = NULL;
123		tal_constraints[talid].warn = NULL;
124	}
125	errno = saved_errno;
126}
127
128/*
129 * Split a string at '-' and trim whitespace around the '-'.
130 * Assumes leading and trailing whitespace in p has already been trimmed.
131 */
132static int
133constraints_split_range(char *p, const char **min, const char **max)
134{
135	char	*pp;
136
137	*min = p;
138	if ((*max = pp = strchr(p, '-')) == NULL)
139		return 0;
140
141	/* Trim whitespace before '-'. */
142	while (pp > *min && isspace((unsigned char)pp[-1]))
143		pp--;
144	*pp = '\0';
145
146	/* Skip past '-' and whitespace following it. */
147	(*max)++;
148	while (isspace((unsigned char)**max))
149		(*max)++;
150
151	return 1;
152}
153
154/*
155 * Helper functions to parse textual representations of IP prefixes or ranges.
156 * The RFC 3779 API has poor error reporting, so as a debugging aid, we call
157 * the prohibitively expensive X509v3_addr_canonize() in high verbosity mode.
158 */
159
160static void
161constraints_parse_ip_prefix(const char *fn, const char *prefix, enum afi afi,
162    IPAddrBlocks *addrs)
163{
164	unsigned char	 addr[16] = { 0 };
165	int		 af = afi == AFI_IPV4 ? AF_INET : AF_INET6;
166	int		 plen;
167
168	if ((plen = inet_net_pton(af, prefix, addr, sizeof(addr))) == -1)
169		errx(1, "%s: failed to parse %s", fn, prefix);
170
171	if (!X509v3_addr_add_prefix(addrs, afi, NULL, addr, plen))
172		errx(1, "%s: failed to add prefix %s", fn, prefix);
173
174	if (verbose < 3)
175		return;
176
177	if (!X509v3_addr_canonize(addrs))
178		errx(1, "%s: failed to canonize with prefix %s", fn, prefix);
179}
180
181static void
182constraints_parse_ip_range(const char *fn, const char *min, const char *max,
183    enum afi afi, IPAddrBlocks *addrs)
184{
185	unsigned char	 min_addr[16] = {0}, max_addr[16] = {0};
186	int		 af = afi == AFI_IPV4 ? AF_INET : AF_INET6;
187
188	if (inet_pton(af, min, min_addr) != 1)
189		errx(1, "%s: failed to parse %s", fn, min);
190	if (inet_pton(af, max, max_addr) != 1)
191		errx(1, "%s: failed to parse %s", fn, max);
192
193	if (!X509v3_addr_add_range(addrs, afi, NULL, min_addr, max_addr))
194		errx(1, "%s: failed to add range %s--%s", fn, min, max);
195
196	if (verbose < 3)
197		return;
198
199	if (!X509v3_addr_canonize(addrs))
200		errx(1, "%s: failed to canonize with range %s--%s", fn,
201		    min, max);
202}
203
204static void
205constraints_parse_ip(const char *fn, char *p, enum afi afi, IPAddrBlocks *addrs)
206{
207	const char	*min, *max;
208
209	if (strchr(p, '-') == NULL) {
210		constraints_parse_ip_prefix(fn, p, afi, addrs);
211		return;
212	}
213
214	if (!constraints_split_range(p, &min, &max))
215		errx(1, "%s: failed to split range: %s", fn, p);
216
217	constraints_parse_ip_range(fn, min, max, afi, addrs);
218}
219
220/*
221 * Helper functions to parse textual representations of AS numbers or ranges.
222 * The RFC 3779 API has poor error reporting, so as a debugging aid, we call
223 * the prohibitively expensive X509v3_asid_canonize() in high verbosity mode.
224 */
225
226static void
227constraints_parse_asn(const char *fn, const char *asn, ASIdentifiers *asids)
228{
229	ASN1_INTEGER	*id;
230
231	if ((id = s2i_ASN1_INTEGER(NULL, asn)) == NULL)
232		errx(1, "%s: failed to parse AS %s", fn, asn);
233
234	if (!X509v3_asid_add_id_or_range(asids, V3_ASID_ASNUM, id, NULL))
235		errx(1, "%s: failed to add AS %s", fn, asn);
236
237	if (verbose < 3)
238		return;
239
240	if (!X509v3_asid_canonize(asids))
241		errx(1, "%s: failed to canonize with AS %s", fn, asn);
242}
243
244static void
245constraints_parse_asn_range(const char *fn, const char *min, const char *max,
246    ASIdentifiers *asids)
247{
248	ASN1_INTEGER	*min_as, *max_as;
249
250	if ((min_as = s2i_ASN1_INTEGER(NULL, min)) == NULL)
251		errx(1, "%s: failed to parse AS %s", fn, min);
252	if ((max_as = s2i_ASN1_INTEGER(NULL, max)) == NULL)
253		errx(1, "%s: failed to parse AS %s", fn, max);
254
255	if (!X509v3_asid_add_id_or_range(asids, V3_ASID_ASNUM, min_as, max_as))
256		errx(1, "%s: failed to add AS range %s--%s", fn, min, max);
257
258	if (verbose < 3)
259		return;
260
261	if (!X509v3_asid_canonize(asids))
262		errx(1, "%s: failed to canonize with AS range %s--%s", fn,
263		    min, max);
264}
265
266static void
267constraints_parse_as(const char *fn, char *p, ASIdentifiers *asids)
268{
269	const char	*min, *max;
270
271	if (strchr(p, '-') == NULL) {
272		constraints_parse_asn(fn, p, asids);
273		return;
274	}
275
276	if (!constraints_split_range(p, &min, &max))
277		errx(1, "%s: failed to split range: %s", fn, p);
278
279	constraints_parse_asn_range(fn, min, max, asids);
280}
281
282/*
283 * Work around an annoying bug in X509v3_addr_add_range(). The upper bound
284 * of a range can have unused bits set in its ASN1_BIT_STRING representation.
285 * This triggers a check in ip_addr_parse(). A round trip through DER fixes
286 * this mess up. For extra special fun, {d2i,i2d}_IPAddrBlocks() isn't part
287 * of the API and implementing them for OpenSSL 3 is hairy, so do the round
288 * tripping once per address family.
289 */
290static void
291constraints_normalize_ip_addrblocks(const char *fn, IPAddrBlocks **addrs)
292{
293	IPAddrBlocks		*new_addrs;
294	IPAddressFamily		*af;
295	const unsigned char	*p;
296	unsigned char		*der;
297	int			 der_len, i;
298
299	if ((new_addrs = IPAddrBlocks_new()) == NULL)
300		err(1, NULL);
301
302	for (i = 0; i < sk_IPAddressFamily_num(*addrs); i++) {
303		af = sk_IPAddressFamily_value(*addrs, i);
304
305		der = NULL;
306		if ((der_len = i2d_IPAddressFamily(af, &der)) <= 0)
307			errx(1, "%s: failed to convert to DER", fn);
308		p = der;
309		if ((af = d2i_IPAddressFamily(NULL, &p, der_len)) == NULL)
310			errx(1, "%s: failed to convert from DER", fn);
311		free(der);
312
313		if (!sk_IPAddressFamily_push(new_addrs, af))
314			errx(1, "%s: failed to push constraints", fn);
315	}
316
317	IPAddrBlocks_free(*addrs);
318	*addrs = new_addrs;
319}
320
321/*
322 * If there is a constraints file for tals[talid], load it into a buffer
323 * and parse it line by line. Leverage the above parse helpers to build up
324 * IPAddrBlocks and ASIdentifiers. We use the RFC 3779 API to benefit from
325 * the limited abilities of X509v3_{addr,asid}_canonize() to sort and merge
326 * adjacent ranges. This doesn't deal with overlaps or duplicates, but it's
327 * better than nothing.
328 */
329
330static void
331constraints_parse_talid(int talid)
332{
333	IPAddrBlocks	*allow_addrs, *deny_addrs;
334	ASIdentifiers	*allow_asids, *deny_asids;
335	FILE		*f;
336	char		*fn, *p, *pp;
337	struct cert_as	*allow_as = NULL, *deny_as = NULL;
338	struct cert_ip	*allow_ips = NULL, *deny_ips = NULL;
339	size_t		 allow_asz = 0, allow_ipsz = 0,
340			 deny_asz = 0, deny_ipsz = 0;
341	char		*line = NULL;
342	size_t		 len = 0;
343	ssize_t		 n;
344	int		 fd, have_allow_as = 0, have_allow_ips = 0,
345			 have_deny_as = 0, have_deny_ips = 0;
346
347	fd = tal_constraints[talid].fd;
348	fn = tal_constraints[talid].fn;
349	tal_constraints[talid].fd = -1;
350	tal_constraints[talid].fn = NULL;
351
352	if (fd == -1) {
353		free(fn);
354		return;
355	}
356
357	if ((f = fdopen(fd, "r")) == NULL)
358		err(1, "fdopen");
359
360	if ((allow_addrs = IPAddrBlocks_new()) == NULL)
361		err(1, NULL);
362	if ((allow_asids = ASIdentifiers_new()) == NULL)
363		err(1, NULL);
364	if ((deny_addrs = IPAddrBlocks_new()) == NULL)
365		err(1, NULL);
366	if ((deny_asids = ASIdentifiers_new()) == NULL)
367		err(1, NULL);
368
369	while ((n = getline(&line, &len, f)) != -1) {
370		if (line[n - 1] == '\n')
371			line[n - 1] = '\0';
372
373		p = line;
374
375		/* Zap leading whitespace */
376		while (isspace((unsigned char)*p))
377			p++;
378
379		/* Zap comments */
380		if ((pp = strchr(p, '#')) != NULL)
381			*pp = '\0';
382
383		/* Zap trailing whitespace */
384		if (pp == NULL)
385			pp = p + strlen(p);
386		while (pp > p && isspace((unsigned char)pp[-1]))
387			pp--;
388		*pp = '\0';
389
390		if (strlen(p) == 0)
391			continue;
392
393		if (strncmp(p, "allow", strlen("allow")) == 0) {
394			p += strlen("allow");
395
396			/* Ensure there's whitespace and jump over it. */
397			if (!isspace((unsigned char)*p))
398				errx(1, "%s: failed to parse %s", fn, p);
399			while (isspace((unsigned char)*p))
400				p++;
401
402			if (strchr(p, '.') != NULL) {
403				constraints_parse_ip(fn, p, AFI_IPV4,
404				    allow_addrs);
405				have_allow_ips = 1;
406			} else if (strchr(p, ':') != NULL) {
407				constraints_parse_ip(fn, p, AFI_IPV6,
408				    allow_addrs);
409				have_allow_ips = 1;
410			} else {
411				constraints_parse_as(fn, p, allow_asids);
412				have_allow_as = 1;
413			}
414		} else if (strncmp(p, "deny", strlen("deny")) == 0) {
415			p += strlen("deny");
416
417			/* Ensure there's whitespace and jump over it. */
418			if (!isspace((unsigned char)*p))
419				errx(1, "%s: failed to parse %s", fn, p);
420			/* Zap leading whitespace */
421			while (isspace((unsigned char)*p))
422				p++;
423
424			if (strchr(p, '.') != NULL) {
425				constraints_parse_ip(fn, p, AFI_IPV4,
426				    deny_addrs);
427				have_deny_ips = 1;
428			} else if (strchr(p, ':') != NULL) {
429				constraints_parse_ip(fn, p, AFI_IPV6,
430				    deny_addrs);
431				have_deny_ips = 1;
432			} else {
433				constraints_parse_as(fn, p, deny_asids);
434				have_deny_as = 1;
435			}
436		} else
437			errx(1, "%s: failed to parse %s", fn, p);
438	}
439	free(line);
440
441	if (ferror(f))
442		err(1, "%s", fn);
443	fclose(f);
444
445	if (!X509v3_addr_canonize(allow_addrs))
446		errx(1, "%s: failed to canonize IP addresses allowlist", fn);
447	if (!X509v3_asid_canonize(allow_asids))
448		errx(1, "%s: failed to canonize AS numbers allowlist", fn);
449	if (!X509v3_addr_canonize(deny_addrs))
450		errx(1, "%s: failed to canonize IP addresses denylist", fn);
451	if (!X509v3_asid_canonize(deny_asids))
452		errx(1, "%s: failed to canonize AS numbers denylist", fn);
453
454	if (have_allow_as) {
455		if (!sbgp_parse_assysnum(fn, allow_asids, &allow_as,
456		    &allow_asz))
457			errx(1, "%s: failed to parse AS identifiers allowlist",
458			    fn);
459	}
460	if (have_deny_as) {
461		if (!sbgp_parse_assysnum(fn, deny_asids, &deny_as,
462		    &deny_asz))
463			errx(1, "%s: failed to parse AS identifiers denylist",
464			    fn);
465	}
466	if (have_allow_ips) {
467		constraints_normalize_ip_addrblocks(fn, &allow_addrs);
468
469		if (!sbgp_parse_ipaddrblk(fn, allow_addrs, &allow_ips,
470		    &allow_ipsz))
471			errx(1, "%s: failed to parse IP addresses allowlist",
472			    fn);
473	}
474	if (have_deny_ips) {
475		constraints_normalize_ip_addrblocks(fn, &deny_addrs);
476
477		if (!sbgp_parse_ipaddrblk(fn, deny_addrs, &deny_ips,
478		    &deny_ipsz))
479			errx(1, "%s: failed to parse IP addresses denylist",
480			    fn);
481	}
482
483	tal_constraints[talid].allow_as = allow_as;
484	tal_constraints[talid].allow_asz = allow_asz;
485	tal_constraints[talid].allow_ips = allow_ips;
486	tal_constraints[talid].allow_ipsz = allow_ipsz;
487	tal_constraints[talid].deny_as = deny_as;
488	tal_constraints[talid].deny_asz = deny_asz;
489	tal_constraints[talid].deny_ips = deny_ips;
490	tal_constraints[talid].deny_ipsz = deny_ipsz;
491
492	IPAddrBlocks_free(allow_addrs);
493	IPAddrBlocks_free(deny_addrs);
494	ASIdentifiers_free(allow_asids);
495	ASIdentifiers_free(deny_asids);
496
497	free(fn);
498}
499
500/*
501 * Iterate over all TALs and parse the constraints files loaded previously.
502 */
503void
504constraints_parse(void)
505{
506	int	 talid;
507
508	for (talid = 0; talid < talsz; talid++)
509		constraints_parse_talid(talid);
510}
511
512static int
513constraints_check_as(const char *fn, struct cert_as *cert,
514    const struct cert_as *allow_as, size_t allow_asz,
515    const struct cert_as *deny_as, size_t deny_asz)
516{
517	uint32_t min, max;
518
519	/* Inheriting EE resources are not to be constrained. */
520	if (cert->type == CERT_AS_INHERIT)
521		return 1;
522
523	if (cert->type == CERT_AS_ID) {
524		min = cert->id;
525		max = cert->id;
526	} else {
527		min = cert->range.min;
528		max = cert->range.max;
529	}
530
531	if (deny_as != NULL) {
532		if (!as_check_overlap(cert, fn, deny_as, deny_asz, 1))
533			return 0;
534	}
535	if (allow_as != NULL) {
536		if (as_check_covered(min, max, allow_as, allow_asz) <= 0)
537			return 0;
538	}
539	return 1;
540}
541
542static int
543constraints_check_ips(const char *fn, struct cert_ip *cert,
544    const struct cert_ip *allow_ips, size_t allow_ipsz,
545    const struct cert_ip *deny_ips, size_t deny_ipsz)
546{
547	/* Inheriting EE resources are not to be constrained. */
548	if (cert->type == CERT_IP_INHERIT)
549		return 1;
550
551	if (deny_ips != NULL) {
552		if (!ip_addr_check_overlap(cert, fn, deny_ips, deny_ipsz, 1))
553			return 0;
554	}
555	if (allow_ips != NULL) {
556		if (ip_addr_check_covered(cert->afi, cert->min, cert->max,
557		    allow_ips, allow_ipsz) <= 0)
558			return 0;
559	}
560	return 1;
561}
562
563/*
564 * Check whether an EE cert's resources are covered by its TAL's constraints.
565 * We accept certs with a negative talid as "unknown TAL" for filemode. The
566 * logic nearly duplicates valid_cert().
567 */
568int
569constraints_validate(const char *fn, const struct cert *cert)
570{
571	int		 talid = cert->talid;
572	struct cert_as	*allow_as, *deny_as;
573	struct cert_ip	*allow_ips, *deny_ips;
574	size_t		 i, allow_asz, allow_ipsz, deny_asz, deny_ipsz;
575
576	/* Accept negative talid to bypass validation. */
577	if (talid < 0)
578		return 1;
579	if (talid >= talsz)
580		errx(1, "%s: talid out of range %d", fn, talid);
581
582	allow_as = tal_constraints[talid].allow_as;
583	allow_asz = tal_constraints[talid].allow_asz;
584	deny_as = tal_constraints[talid].deny_as;
585	deny_asz = tal_constraints[talid].deny_asz;
586
587	for (i = 0; i < cert->asz; i++) {
588		if (constraints_check_as(fn, &cert->as[i], allow_as, allow_asz,
589		    deny_as, deny_asz))
590			continue;
591
592		as_warn(fn, tal_constraints[talid].warn, &cert->as[i]);
593		return 0;
594	}
595
596	allow_ips = tal_constraints[talid].allow_ips;
597	allow_ipsz = tal_constraints[talid].allow_ipsz;
598	deny_ips = tal_constraints[talid].deny_ips;
599	deny_ipsz = tal_constraints[talid].deny_ipsz;
600
601	for (i = 0; i < cert->ipsz; i++) {
602		if (constraints_check_ips(fn, &cert->ips[i], allow_ips,
603		    allow_ipsz, deny_ips, deny_ipsz))
604			continue;
605
606		ip_warn(fn, tal_constraints[talid].warn, &cert->ips[i]);
607		return 0;
608	}
609
610	return 1;
611}
612