xref: /openbsd/usr.sbin/rpki-client/cms.c (revision d415bd75)
1 /*	$OpenBSD: cms.c,v 1.40 2023/10/19 17:05:54 job Exp $ */
2 /*
3  * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  */
17 
18 #include <assert.h>
19 #include <err.h>
20 #include <stdint.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <unistd.h>
24 
25 #include <openssl/bio.h>
26 #include <openssl/cms.h>
27 
28 #include "extern.h"
29 
30 extern ASN1_OBJECT	*cnt_type_oid;
31 extern ASN1_OBJECT	*msg_dgst_oid;
32 extern ASN1_OBJECT	*sign_time_oid;
33 extern ASN1_OBJECT	*bin_sign_time_oid;
34 
35 static int
36 cms_extract_econtent(const char *fn, CMS_ContentInfo *cms, unsigned char **res,
37     size_t *rsz)
38 {
39 	ASN1_OCTET_STRING		**os = NULL;
40 
41 	/* Detached signature case: no eContent to extract, so do nothing. */
42 	if (res == NULL || rsz == NULL)
43 		return 1;
44 
45 	if ((os = CMS_get0_content(cms)) == NULL || *os == NULL) {
46 		warnx("%s: RFC 6488 section 2.1.4: "
47 		    "eContent: zero-length content", fn);
48 		return 0;
49 	}
50 
51 	/*
52 	 * Extract and duplicate the eContent.
53 	 * The CMS framework offers us no other way of easily managing
54 	 * this information; and since we're going to d2i it anyway,
55 	 * simply pass it as the desired underlying types.
56 	 */
57 	if ((*res = malloc((*os)->length)) == NULL)
58 		err(1, NULL);
59 	memcpy(*res, (*os)->data, (*os)->length);
60 	*rsz = (*os)->length;
61 
62 	return 1;
63 }
64 
65 static int
66 cms_get_signtime(const char *fn, X509_ATTRIBUTE *attr, time_t *signtime)
67 {
68 	const ASN1_TIME		*at;
69 	const char		*time_str = "UTCtime";
70 	int			 time_type = V_ASN1_UTCTIME;
71 
72 	*signtime = 0;
73 	at = X509_ATTRIBUTE_get0_data(attr, 0, time_type, NULL);
74 	if (at == NULL) {
75 		time_str = "GeneralizedTime";
76 		time_type = V_ASN1_GENERALIZEDTIME;
77 		at = X509_ATTRIBUTE_get0_data(attr, 0, time_type, NULL);
78 		if (at == NULL) {
79 			warnx("%s: CMS signing-time issue", fn);
80 			return 0;
81 		}
82 		warnx("%s: GeneralizedTime instead of UTCtime", fn);
83 	}
84 
85 	if (!x509_get_time(at, signtime)) {
86 		warnx("%s: failed to convert %s", fn, time_str);
87 		return 0;
88 	}
89 
90 	return 1;
91 }
92 
93 static int
94 cms_parse_validate_internal(X509 **xp, const char *fn, const unsigned char *der,
95     size_t len, const ASN1_OBJECT *oid, BIO *bio, unsigned char **res,
96     size_t *rsz, time_t *signtime)
97 {
98 	const unsigned char		*oder;
99 	char				 buf[128], obuf[128];
100 	const ASN1_OBJECT		*obj, *octype;
101 	ASN1_OCTET_STRING		*kid = NULL;
102 	CMS_ContentInfo			*cms;
103 	long				 version;
104 	STACK_OF(X509)			*certs = NULL;
105 	STACK_OF(X509_CRL)		*crls;
106 	STACK_OF(CMS_SignerInfo)	*sinfos;
107 	CMS_SignerInfo			*si;
108 	EVP_PKEY			*pkey;
109 	X509_ALGOR			*pdig, *psig;
110 	int				 i, nattrs, nid;
111 	int				 has_ct = 0, has_md = 0, has_st = 0,
112 					 has_bst = 0;
113 	time_t				 notafter;
114 	int				 rc = 0;
115 
116 	*xp = NULL;
117 	if (rsz != NULL)
118 		*rsz = 0;
119 	*signtime = 0;
120 
121 	/* just fail for empty buffers, the warning was printed elsewhere */
122 	if (der == NULL)
123 		return 0;
124 
125 	oder = der;
126 	if ((cms = d2i_CMS_ContentInfo(NULL, &der, len)) == NULL) {
127 		warnx("%s: RFC 6488: failed CMS parse", fn);
128 		goto out;
129 	}
130 	if (der != oder + len) {
131 		warnx("%s: %td bytes trailing garbage", fn, oder + len - der);
132 		goto out;
133 	}
134 
135 	/*
136 	 * The CMS is self-signed with a signing certificate.
137 	 * Verify that the self-signage is correct.
138 	 */
139 	if (!CMS_verify(cms, NULL, NULL, bio, NULL,
140 	    CMS_NO_SIGNER_CERT_VERIFY)) {
141 		warnx("%s: CMS verification error", fn);
142 		goto out;
143 	}
144 
145 	/* RFC 6488 section 3 verify the CMS */
146 
147 	/* Should only return NULL if cms is not of type SignedData. */
148 	if ((sinfos = CMS_get0_SignerInfos(cms)) == NULL) {
149 		if ((obj = CMS_get0_type(cms)) == NULL) {
150 			warnx("%s: RFC 6488: missing content-type", fn);
151 			goto out;
152 		}
153 		OBJ_obj2txt(buf, sizeof(buf), obj, 1);
154 		warnx("%s: RFC 6488: no signerInfo in CMS object of type %s",
155 		    fn, buf);
156 		goto out;
157 	}
158 	if (sk_CMS_SignerInfo_num(sinfos) != 1) {
159 		warnx("%s: RFC 6488: CMS has multiple signerInfos", fn);
160 		goto out;
161 	}
162 	si = sk_CMS_SignerInfo_value(sinfos, 0);
163 
164 	if (!CMS_get_version(cms, &version)) {
165 		warnx("%s: Failed to retrieve SignedData version", fn);
166 		goto out;
167 	}
168 	if (version != 3) {
169 		warnx("%s: SignedData version %ld != 3", fn, version);
170 		goto out;
171 	}
172 	if (!CMS_SignerInfo_get_version(si, &version)) {
173 		warnx("%s: Failed to retrieve SignerInfo version", fn);
174 		goto out;
175 	}
176 	if (version != 3) {
177 		warnx("%s: SignerInfo version %ld != 3", fn, version);
178 		goto out;
179 	}
180 
181 	nattrs = CMS_signed_get_attr_count(si);
182 	if (nattrs <= 0) {
183 		warnx("%s: RFC 6488: error extracting signedAttrs", fn);
184 		goto out;
185 	}
186 	for (i = 0; i < nattrs; i++) {
187 		X509_ATTRIBUTE *attr;
188 
189 		attr = CMS_signed_get_attr(si, i);
190 		if (attr == NULL || X509_ATTRIBUTE_count(attr) != 1) {
191 			warnx("%s: RFC 6488: bad signed attribute encoding",
192 			    fn);
193 			goto out;
194 		}
195 
196 		obj = X509_ATTRIBUTE_get0_object(attr);
197 		if (obj == NULL) {
198 			warnx("%s: RFC 6488: bad signed attribute", fn);
199 			goto out;
200 		}
201 		if (OBJ_cmp(obj, cnt_type_oid) == 0) {
202 			if (has_ct++ != 0) {
203 				warnx("%s: RFC 6488: duplicate "
204 				    "signed attribute", fn);
205 				goto out;
206 			}
207 		} else if (OBJ_cmp(obj, msg_dgst_oid) == 0) {
208 			if (has_md++ != 0) {
209 				warnx("%s: RFC 6488: duplicate "
210 				    "signed attribute", fn);
211 				goto out;
212 			}
213 		} else if (OBJ_cmp(obj, sign_time_oid) == 0) {
214 			if (has_st++ != 0) {
215 				warnx("%s: RFC 6488: duplicate "
216 				    "signed attribute", fn);
217 				goto out;
218 			}
219 			if (!cms_get_signtime(fn, attr, signtime))
220 				goto out;
221 		} else if (OBJ_cmp(obj, bin_sign_time_oid) == 0) {
222 			if (has_bst++ != 0) {
223 				warnx("%s: RFC 6488: duplicate "
224 				    "signed attribute", fn);
225 				goto out;
226 			}
227 		} else {
228 			OBJ_obj2txt(buf, sizeof(buf), obj, 1);
229 			warnx("%s: RFC 6488: "
230 			    "CMS has unexpected signed attribute %s",
231 			    fn, buf);
232 			goto out;
233 		}
234 	}
235 
236 	if (!has_ct || !has_md) {
237 		warnx("%s: RFC 6488: CMS missing required "
238 		    "signed attribute", fn);
239 		goto out;
240 	}
241 
242 	if (has_bst)
243 		warnx("%s: unsupported CMS signing-time attribute", fn);
244 
245 	if (!has_st)
246 		warnx("%s: missing CMS signing-time attribute", fn);
247 
248 	if (CMS_unsigned_get_attr_count(si) != -1) {
249 		warnx("%s: RFC 6488: CMS has unsignedAttrs", fn);
250 		goto out;
251 	}
252 
253 	/* Check digest and signature algorithms (RFC 7935) */
254 	CMS_SignerInfo_get0_algs(si, &pkey, NULL, &pdig, &psig);
255 	if (!valid_ca_pkey(fn, pkey))
256 		goto out;
257 
258 	X509_ALGOR_get0(&obj, NULL, NULL, pdig);
259 	nid = OBJ_obj2nid(obj);
260 	if (nid != NID_sha256) {
261 		warnx("%s: RFC 6488: wrong digest %s, want %s", fn,
262 		    OBJ_nid2ln(nid), OBJ_nid2ln(NID_sha256));
263 		goto out;
264 	}
265 	X509_ALGOR_get0(&obj, NULL, NULL, psig);
266 	nid = OBJ_obj2nid(obj);
267 	/* RFC7935 last paragraph of section 2 specifies the allowed psig */
268 	if (nid == NID_ecdsa_with_SHA256) {
269 		if (verbose)
270 			warn("%s: P-256 support is experimental", fn);
271 	} else if (nid != NID_rsaEncryption &&
272 	    nid != NID_sha256WithRSAEncryption) {
273 		warnx("%s: RFC 6488: wrong signature algorithm %s, want %s",
274 		    fn, OBJ_nid2ln(nid), OBJ_nid2ln(NID_rsaEncryption));
275 		goto out;
276 	}
277 
278 	/* RFC 6488 section 2.1.3.1: check the object's eContentType. */
279 
280 	obj = CMS_get0_eContentType(cms);
281 	if (obj == NULL) {
282 		warnx("%s: RFC 6488 section 2.1.3.1: eContentType: "
283 		    "OID object is NULL", fn);
284 		goto out;
285 	}
286 	if (OBJ_cmp(obj, oid) != 0) {
287 		OBJ_obj2txt(buf, sizeof(buf), obj, 1);
288 		OBJ_obj2txt(obuf, sizeof(obuf), oid, 1);
289 		warnx("%s: RFC 6488 section 2.1.3.1: eContentType: "
290 		    "unknown OID: %s, want %s", fn, buf, obuf);
291 		goto out;
292 	}
293 
294 	/* Compare content-type with eContentType */
295 	octype = CMS_signed_get0_data_by_OBJ(si, cnt_type_oid,
296 	    -3, V_ASN1_OBJECT);
297 	assert(octype != NULL);
298 	if (OBJ_cmp(obj, octype) != 0) {
299 		OBJ_obj2txt(buf, sizeof(buf), obj, 1);
300 		OBJ_obj2txt(obuf, sizeof(obuf), octype, 1);
301 		warnx("%s: RFC 6488: eContentType does not match Content-Type "
302 		    "OID: %s, want %s", fn, buf, obuf);
303 		goto out;
304 	}
305 
306 	/*
307 	 * Check that there are no CRLS in this CMS message.
308 	 */
309 	crls = CMS_get1_crls(cms);
310 	if (crls != NULL) {
311 		sk_X509_CRL_pop_free(crls, X509_CRL_free);
312 		warnx("%s: RFC 6488: CMS has CRLs", fn);
313 		goto out;
314 	}
315 
316 	/*
317 	 * The self-signing certificate is further signed by the input
318 	 * signing authority according to RFC 6488, 2.1.4.
319 	 * We extract that certificate now for later verification.
320 	 */
321 
322 	certs = CMS_get0_signers(cms);
323 	if (certs == NULL || sk_X509_num(certs) != 1) {
324 		warnx("%s: RFC 6488 section 2.1.4: eContent: "
325 		    "want 1 signer, have %d", fn, sk_X509_num(certs));
326 		goto out;
327 	}
328 	*xp = sk_X509_value(certs, 0);
329 	if (!X509_up_ref(*xp)) {
330 		*xp = NULL;
331 		goto out;
332 	}
333 
334 	/* Cache X509v3 extensions, see X509_check_ca(3). */
335 	if (X509_check_purpose(*xp, -1, -1) <= 0) {
336 		warnx("%s: could not cache X509v3 extensions", fn);
337 		goto out;
338 	}
339 
340 	if (!x509_get_notafter(*xp, fn, &notafter))
341 		goto out;
342 	if (*signtime > notafter)
343 		warnx("%s: dating issue: CMS signing-time after X.509 notAfter",
344 		    fn);
345 
346 	if (CMS_SignerInfo_get0_signer_id(si, &kid, NULL, NULL) != 1 ||
347 	    kid == NULL) {
348 		warnx("%s: RFC 6488: could not extract SKI from SID", fn);
349 		goto out;
350 	}
351 	if (CMS_SignerInfo_cert_cmp(si, *xp) != 0) {
352 		warnx("%s: RFC 6488: wrong cert referenced by SignerInfo", fn);
353 		goto out;
354 	}
355 
356 	if (!cms_extract_econtent(fn, cms, res, rsz))
357 		goto out;
358 
359 	rc = 1;
360  out:
361 	if (rc == 0) {
362 		X509_free(*xp);
363 		*xp = NULL;
364 	}
365 	sk_X509_free(certs);
366 	CMS_ContentInfo_free(cms);
367 	return rc;
368 }
369 
370 /*
371  * Parse and validate a self-signed CMS message.
372  * Conforms to RFC 6488.
373  * The eContentType of the message must be an oid object.
374  * Return the eContent as a string and set "rsz" to be its length.
375  */
376 unsigned char *
377 cms_parse_validate(X509 **xp, const char *fn, const unsigned char *der,
378     size_t derlen, const ASN1_OBJECT *oid, size_t *rsz, time_t *st)
379 {
380 	unsigned char *res = NULL;
381 
382 	if (!cms_parse_validate_internal(xp, fn, der, derlen, oid, NULL, &res,
383 	    rsz, st))
384 		return NULL;
385 
386 	return res;
387 }
388 
389 /*
390  * Parse and validate a detached CMS signature.
391  * bio must contain the original message, der must contain the CMS.
392  * Return the 1 on success, 0 on failure.
393  */
394 int
395 cms_parse_validate_detached(X509 **xp, const char *fn, const unsigned char *der,
396     size_t derlen, const ASN1_OBJECT *oid, BIO *bio, time_t *st)
397 {
398 	return cms_parse_validate_internal(xp, fn, der, derlen, oid, bio, NULL,
399 	    NULL, st);
400 }
401