1*a0b97782Stb /* $OpenBSD: cms.c,v 1.50 2024/11/27 15:19:26 tb Exp $ */ 29a7e9e7fSjob /* 39a7e9e7fSjob * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv> 49a7e9e7fSjob * 59a7e9e7fSjob * Permission to use, copy, modify, and distribute this software for any 69a7e9e7fSjob * purpose with or without fee is hereby granted, provided that the above 79a7e9e7fSjob * copyright notice and this permission notice appear in all copies. 89a7e9e7fSjob * 99a7e9e7fSjob * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 109a7e9e7fSjob * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 119a7e9e7fSjob * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 129a7e9e7fSjob * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 139a7e9e7fSjob * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 149a7e9e7fSjob * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 159a7e9e7fSjob * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 169a7e9e7fSjob */ 179a7e9e7fSjob 189a7e9e7fSjob #include <err.h> 199a7e9e7fSjob #include <stdint.h> 209a7e9e7fSjob #include <stdlib.h> 219a7e9e7fSjob #include <string.h> 229a7e9e7fSjob #include <unistd.h> 239a7e9e7fSjob 24ef3f6f56Sjob #include <openssl/bio.h> 259a7e9e7fSjob #include <openssl/cms.h> 269a7e9e7fSjob 279a7e9e7fSjob #include "extern.h" 289a7e9e7fSjob 299025e295Sclaudio extern ASN1_OBJECT *cnt_type_oid; 309025e295Sclaudio extern ASN1_OBJECT *msg_dgst_oid; 319025e295Sclaudio extern ASN1_OBJECT *sign_time_oid; 329025e295Sclaudio 33ef3f6f56Sjob static int 34f8cba460Stb cms_extract_econtent(const char *fn, CMS_ContentInfo *cms, unsigned char **res, 35f8cba460Stb size_t *rsz) 36f8cba460Stb { 37f8cba460Stb ASN1_OCTET_STRING **os = NULL; 38f8cba460Stb 39f8cba460Stb /* Detached signature case: no eContent to extract, so do nothing. */ 40f8cba460Stb if (res == NULL || rsz == NULL) 41f8cba460Stb return 1; 42f8cba460Stb 43f8cba460Stb if ((os = CMS_get0_content(cms)) == NULL || *os == NULL) { 44f8cba460Stb warnx("%s: RFC 6488 section 2.1.4: " 45f8cba460Stb "eContent: zero-length content", fn); 46f8cba460Stb return 0; 47f8cba460Stb } 48f8cba460Stb 49f8cba460Stb /* 50f8cba460Stb * Extract and duplicate the eContent. 51f8cba460Stb * The CMS framework offers us no other way of easily managing 52f8cba460Stb * this information; and since we're going to d2i it anyway, 53f8cba460Stb * simply pass it as the desired underlying types. 54f8cba460Stb */ 55f8cba460Stb if ((*res = malloc((*os)->length)) == NULL) 56f8cba460Stb err(1, NULL); 57f8cba460Stb memcpy(*res, (*os)->data, (*os)->length); 58f8cba460Stb *rsz = (*os)->length; 59f8cba460Stb 60f8cba460Stb return 1; 61f8cba460Stb } 62f8cba460Stb 63f8cba460Stb static int 641bb1e509Sjob cms_get_signtime(const char *fn, X509_ATTRIBUTE *attr, time_t *signtime) 651bb1e509Sjob { 661bb1e509Sjob const ASN1_TIME *at; 671bb1e509Sjob const char *time_str = "UTCtime"; 681bb1e509Sjob int time_type = V_ASN1_UTCTIME; 691bb1e509Sjob 7081c43103Stb *signtime = 0; 711bb1e509Sjob at = X509_ATTRIBUTE_get0_data(attr, 0, time_type, NULL); 721bb1e509Sjob if (at == NULL) { 731bb1e509Sjob time_str = "GeneralizedTime"; 741bb1e509Sjob time_type = V_ASN1_GENERALIZEDTIME; 751bb1e509Sjob at = X509_ATTRIBUTE_get0_data(attr, 0, time_type, NULL); 761bb1e509Sjob if (at == NULL) { 771bb1e509Sjob warnx("%s: CMS signing-time issue", fn); 781bb1e509Sjob return 0; 791bb1e509Sjob } 801bb1e509Sjob warnx("%s: GeneralizedTime instead of UTCtime", fn); 811bb1e509Sjob } 821bb1e509Sjob 831bb1e509Sjob if (!x509_get_time(at, signtime)) { 841bb1e509Sjob warnx("%s: failed to convert %s", fn, time_str); 851bb1e509Sjob return 0; 861bb1e509Sjob } 871bb1e509Sjob 881bb1e509Sjob return 1; 891bb1e509Sjob } 901bb1e509Sjob 911bb1e509Sjob static int 92ef3f6f56Sjob cms_parse_validate_internal(X509 **xp, const char *fn, const unsigned char *der, 93797cceeeStb size_t len, const ASN1_OBJECT *oid, BIO *bio, unsigned char **res, 941bb1e509Sjob size_t *rsz, time_t *signtime) 959a7e9e7fSjob { 96797cceeeStb const unsigned char *oder; 979025e295Sclaudio char buf[128], obuf[128]; 989025e295Sclaudio const ASN1_OBJECT *obj, *octype; 99f8cba460Stb ASN1_OCTET_STRING *kid = NULL; 1009a7e9e7fSjob CMS_ContentInfo *cms; 101de232e1dStb long version; 1029a7e9e7fSjob STACK_OF(X509) *certs = NULL; 103475ff837Sjob STACK_OF(X509_CRL) *crls = NULL; 1049025e295Sclaudio STACK_OF(CMS_SignerInfo) *sinfos; 1059025e295Sclaudio CMS_SignerInfo *si; 106ae36eebeSjob EVP_PKEY *pkey; 1079025e295Sclaudio X509_ALGOR *pdig, *psig; 1089025e295Sclaudio int i, nattrs, nid; 109968e2494Sjob int has_ct = 0, has_md = 0, has_st = 0; 110db13aa11Sjob time_t notafter; 111ef3f6f56Sjob int rc = 0; 1129a7e9e7fSjob 1139a7e9e7fSjob *xp = NULL; 114ef3f6f56Sjob if (rsz != NULL) 115ef3f6f56Sjob *rsz = 0; 11681c43103Stb *signtime = 0; 1179a7e9e7fSjob 118cabf3a3bSclaudio /* just fail for empty buffers, the warning was printed elsewhere */ 119cabf3a3bSclaudio if (der == NULL) 120ef3f6f56Sjob return 0; 1219a7e9e7fSjob 122797cceeeStb oder = der; 123797cceeeStb if ((cms = d2i_CMS_ContentInfo(NULL, &der, len)) == NULL) { 124c0528901Stb warnx("%s: RFC 6488: failed CMS parse", fn); 1259a7e9e7fSjob goto out; 1269a7e9e7fSjob } 127797cceeeStb if (der != oder + len) { 128797cceeeStb warnx("%s: %td bytes trailing garbage", fn, oder + len - der); 129797cceeeStb goto out; 130797cceeeStb } 1319a7e9e7fSjob 1329a7e9e7fSjob /* 1333a50f0a9Sjmc * The CMS is self-signed with a signing certificate. 1349a7e9e7fSjob * Verify that the self-signage is correct. 1359a7e9e7fSjob */ 136ef3f6f56Sjob if (!CMS_verify(cms, NULL, NULL, bio, NULL, 137cabf3a3bSclaudio CMS_NO_SIGNER_CERT_VERIFY)) { 138c0528901Stb warnx("%s: CMS verification error", fn); 1399a7e9e7fSjob goto out; 1409a7e9e7fSjob } 1419a7e9e7fSjob 1429025e295Sclaudio /* RFC 6488 section 3 verify the CMS */ 1439025e295Sclaudio 14458ffc3d7Stb /* Should only return NULL if cms is not of type SignedData. */ 14558ffc3d7Stb if ((sinfos = CMS_get0_SignerInfos(cms)) == NULL) { 14658ffc3d7Stb if ((obj = CMS_get0_type(cms)) == NULL) { 14758ffc3d7Stb warnx("%s: RFC 6488: missing content-type", fn); 14858ffc3d7Stb goto out; 14958ffc3d7Stb } 15058ffc3d7Stb OBJ_obj2txt(buf, sizeof(buf), obj, 1); 15158ffc3d7Stb warnx("%s: RFC 6488: no signerInfo in CMS object of type %s", 15258ffc3d7Stb fn, buf); 15358ffc3d7Stb goto out; 15458ffc3d7Stb } 1559025e295Sclaudio if (sk_CMS_SignerInfo_num(sinfos) != 1) { 156c0528901Stb warnx("%s: RFC 6488: CMS has multiple signerInfos", fn); 1579025e295Sclaudio goto out; 1589025e295Sclaudio } 1599025e295Sclaudio si = sk_CMS_SignerInfo_value(sinfos, 0); 1609025e295Sclaudio 161de232e1dStb if (!CMS_get_version(cms, &version)) { 162de232e1dStb warnx("%s: Failed to retrieve SignedData version", fn); 163de232e1dStb goto out; 164de232e1dStb } 165de232e1dStb if (version != 3) { 166de232e1dStb warnx("%s: SignedData version %ld != 3", fn, version); 167de232e1dStb goto out; 168de232e1dStb } 169de232e1dStb if (!CMS_SignerInfo_get_version(si, &version)) { 170de232e1dStb warnx("%s: Failed to retrieve SignerInfo version", fn); 171de232e1dStb goto out; 172de232e1dStb } 173de232e1dStb if (version != 3) { 174de232e1dStb warnx("%s: SignerInfo version %ld != 3", fn, version); 175de232e1dStb goto out; 176de232e1dStb } 177de232e1dStb 1789025e295Sclaudio nattrs = CMS_signed_get_attr_count(si); 1799025e295Sclaudio if (nattrs <= 0) { 180c0528901Stb warnx("%s: RFC 6488: error extracting signedAttrs", fn); 1819025e295Sclaudio goto out; 1829025e295Sclaudio } 1839025e295Sclaudio for (i = 0; i < nattrs; i++) { 1849025e295Sclaudio X509_ATTRIBUTE *attr; 1859025e295Sclaudio 1869025e295Sclaudio attr = CMS_signed_get_attr(si, i); 1879025e295Sclaudio if (attr == NULL || X509_ATTRIBUTE_count(attr) != 1) { 188c0528901Stb warnx("%s: RFC 6488: bad signed attribute encoding", 189c0528901Stb fn); 1909025e295Sclaudio goto out; 1919025e295Sclaudio } 1929025e295Sclaudio 1939025e295Sclaudio obj = X509_ATTRIBUTE_get0_object(attr); 1949025e295Sclaudio if (obj == NULL) { 195c0528901Stb warnx("%s: RFC 6488: bad signed attribute", fn); 1969025e295Sclaudio goto out; 1979025e295Sclaudio } 1989025e295Sclaudio if (OBJ_cmp(obj, cnt_type_oid) == 0) { 1999025e295Sclaudio if (has_ct++ != 0) { 200c0528901Stb warnx("%s: RFC 6488: duplicate " 2019025e295Sclaudio "signed attribute", fn); 2029025e295Sclaudio goto out; 2039025e295Sclaudio } 2049025e295Sclaudio } else if (OBJ_cmp(obj, msg_dgst_oid) == 0) { 2059025e295Sclaudio if (has_md++ != 0) { 206c0528901Stb warnx("%s: RFC 6488: duplicate " 2079025e295Sclaudio "signed attribute", fn); 2089025e295Sclaudio goto out; 2099025e295Sclaudio } 2109025e295Sclaudio } else if (OBJ_cmp(obj, sign_time_oid) == 0) { 2119025e295Sclaudio if (has_st++ != 0) { 212c0528901Stb warnx("%s: RFC 6488: duplicate " 2139025e295Sclaudio "signed attribute", fn); 2149025e295Sclaudio goto out; 2159025e295Sclaudio } 2161bb1e509Sjob if (!cms_get_signtime(fn, attr, signtime)) 2171bb1e509Sjob goto out; 2189025e295Sclaudio } else { 2199025e295Sclaudio OBJ_obj2txt(buf, sizeof(buf), obj, 1); 220c0528901Stb warnx("%s: RFC 6488: " 2219025e295Sclaudio "CMS has unexpected signed attribute %s", 2229025e295Sclaudio fn, buf); 2239025e295Sclaudio goto out; 2249025e295Sclaudio } 2259025e295Sclaudio } 2261bb1e509Sjob 2279025e295Sclaudio if (!has_ct || !has_md) { 228c7a965b3Stb /* RFC 9589, section 4 */ 229c0528901Stb warnx("%s: RFC 6488: CMS missing required " 2309025e295Sclaudio "signed attribute", fn); 2319025e295Sclaudio goto out; 2329025e295Sclaudio } 2331bb1e509Sjob 234968e2494Sjob if (!has_st) { 235c7a965b3Stb /* RFC 9589, section 4 */ 2361bce4d34Sjob warnx("%s: missing CMS signing-time attribute", fn); 237968e2494Sjob goto out; 238968e2494Sjob } 2391bce4d34Sjob 240974c6fafStb if (CMS_unsigned_get_attr_count(si) != -1) { 241c0528901Stb warnx("%s: RFC 6488: CMS has unsignedAttrs", fn); 2429025e295Sclaudio goto out; 2439025e295Sclaudio } 2449025e295Sclaudio 245ae36eebeSjob /* Check digest and signature algorithms (RFC 7935) */ 246ae36eebeSjob CMS_SignerInfo_get0_algs(si, &pkey, NULL, &pdig, &psig); 247ae36eebeSjob if (!valid_ca_pkey(fn, pkey)) 248ae36eebeSjob goto out; 249ae36eebeSjob 2509025e295Sclaudio X509_ALGOR_get0(&obj, NULL, NULL, pdig); 2519025e295Sclaudio nid = OBJ_obj2nid(obj); 2529025e295Sclaudio if (nid != NID_sha256) { 2539025e295Sclaudio warnx("%s: RFC 6488: wrong digest %s, want %s", fn, 25478de3577Stb nid2str(nid), LN_sha256); 2559025e295Sclaudio goto out; 2569025e295Sclaudio } 2579025e295Sclaudio X509_ALGOR_get0(&obj, NULL, NULL, psig); 2589025e295Sclaudio nid = OBJ_obj2nid(obj); 259e5fe0df7Sjob /* RFC7935 last paragraph of section 2 specifies the allowed psig */ 26081a06611Sclaudio if (experimental && nid == NID_ecdsa_with_SHA256) { 261ec1cc732Sjob if (verbose) 2628fcc9cc2Sjob warnx("%s: P-256 support is experimental", fn); 263ec1cc732Sjob } else if (nid != NID_rsaEncryption && 264ec1cc732Sjob nid != NID_sha256WithRSAEncryption) { 2659025e295Sclaudio warnx("%s: RFC 6488: wrong signature algorithm %s, want %s", 26678de3577Stb fn, nid2str(nid), LN_rsaEncryption); 2679025e295Sclaudio goto out; 2689025e295Sclaudio } 2699025e295Sclaudio 2709a7e9e7fSjob /* RFC 6488 section 2.1.3.1: check the object's eContentType. */ 2719a7e9e7fSjob 2729a7e9e7fSjob obj = CMS_get0_eContentType(cms); 273d2e465bbSclaudio if (obj == NULL) { 2749a7e9e7fSjob warnx("%s: RFC 6488 section 2.1.3.1: eContentType: " 275d2e465bbSclaudio "OID object is NULL", fn); 276d2e465bbSclaudio goto out; 277d2e465bbSclaudio } 278d2e465bbSclaudio if (OBJ_cmp(obj, oid) != 0) { 279d2e465bbSclaudio OBJ_obj2txt(buf, sizeof(buf), obj, 1); 280d2e465bbSclaudio OBJ_obj2txt(obuf, sizeof(obuf), oid, 1); 281d2e465bbSclaudio warnx("%s: RFC 6488 section 2.1.3.1: eContentType: " 282d2e465bbSclaudio "unknown OID: %s, want %s", fn, buf, obuf); 2839a7e9e7fSjob goto out; 2849a7e9e7fSjob } 2859a7e9e7fSjob 2869025e295Sclaudio /* Compare content-type with eContentType */ 2879025e295Sclaudio octype = CMS_signed_get0_data_by_OBJ(si, cnt_type_oid, 2889025e295Sclaudio -3, V_ASN1_OBJECT); 289233f224dStb /* 290233f224dStb * Since lastpos == -3, octype can be NULL for 4 reasons: 291233f224dStb * 1. requested attribute OID is missing 292233f224dStb * 2. signedAttrs contains multiple attributes with requested OID 293233f224dStb * 3. attribute with requested OID has multiple values (malformed) 294c026bd36Stb * 4. X509_ATTRIBUTE_get0_data() returned NULL. This is also malformed, 295233f224dStb * but libcrypto will create, sign, and verify such objects. 296233f224dStb * Reasons 1 and 2 are excluded because has_ct == 1. We don't know which 297233f224dStb * one of 3 or 4 we hit. Doesn't matter, drop the garbage on the floor. 298233f224dStb */ 299233f224dStb if (octype == NULL) { 300233f224dStb warnx("%s: RFC 6488, section 2.1.6.4.1: malformed value " 301233f224dStb "for content-type attribute", fn); 302233f224dStb goto out; 303233f224dStb } 3049025e295Sclaudio if (OBJ_cmp(obj, octype) != 0) { 3059025e295Sclaudio OBJ_obj2txt(buf, sizeof(buf), obj, 1); 30677251745Sjob OBJ_obj2txt(obuf, sizeof(obuf), octype, 1); 3079025e295Sclaudio warnx("%s: RFC 6488: eContentType does not match Content-Type " 3089025e295Sclaudio "OID: %s, want %s", fn, buf, obuf); 3099025e295Sclaudio goto out; 3109025e295Sclaudio } 3119025e295Sclaudio 3129025e295Sclaudio /* 3139025e295Sclaudio * Check that there are no CRLS in this CMS message. 314475ff837Sjob * XXX - can only error check for OpenSSL >= 3.4. 3159025e295Sclaudio */ 3169025e295Sclaudio crls = CMS_get1_crls(cms); 317475ff837Sjob if (crls != NULL && sk_X509_CRL_num(crls) != 0) { 318c0528901Stb warnx("%s: RFC 6488: CMS has CRLs", fn); 3199025e295Sclaudio goto out; 3209025e295Sclaudio } 3219025e295Sclaudio 3229a7e9e7fSjob /* 3239a7e9e7fSjob * The self-signing certificate is further signed by the input 3249a7e9e7fSjob * signing authority according to RFC 6488, 2.1.4. 3259a7e9e7fSjob * We extract that certificate now for later verification. 3269a7e9e7fSjob */ 3279a7e9e7fSjob 3289a7e9e7fSjob certs = CMS_get0_signers(cms); 3299a7e9e7fSjob if (certs == NULL || sk_X509_num(certs) != 1) { 33080272c49Sderaadt warnx("%s: RFC 6488 section 2.1.4: eContent: " 33180272c49Sderaadt "want 1 signer, have %d", fn, sk_X509_num(certs)); 3329a7e9e7fSjob goto out; 3339a7e9e7fSjob } 33491b15523Stb *xp = sk_X509_value(certs, 0); 33591b15523Stb if (!X509_up_ref(*xp)) { 33691b15523Stb *xp = NULL; 33791b15523Stb goto out; 33891b15523Stb } 3399a7e9e7fSjob 340e891962dStb if (!x509_cache_extensions(*xp, fn)) 341b2d6bcdcStb goto out; 342b2d6bcdcStb 343db13aa11Sjob if (!x509_get_notafter(*xp, fn, ¬after)) 344db13aa11Sjob goto out; 345b3614c08Sjob if (*signtime > notafter) 346db13aa11Sjob warnx("%s: dating issue: CMS signing-time after X.509 notAfter", 347db13aa11Sjob fn); 348db13aa11Sjob 349570c9579Sclaudio if (CMS_SignerInfo_get0_signer_id(si, &kid, NULL, NULL) != 1 || 350570c9579Sclaudio kid == NULL) { 351570c9579Sclaudio warnx("%s: RFC 6488: could not extract SKI from SID", fn); 352570c9579Sclaudio goto out; 353570c9579Sclaudio } 354570c9579Sclaudio if (CMS_SignerInfo_cert_cmp(si, *xp) != 0) { 355570c9579Sclaudio warnx("%s: RFC 6488: wrong cert referenced by SignerInfo", fn); 356570c9579Sclaudio goto out; 357570c9579Sclaudio } 358570c9579Sclaudio 359f8cba460Stb if (!cms_extract_econtent(fn, cms, res, rsz)) 360ef3f6f56Sjob goto out; 3619a7e9e7fSjob 3629a7e9e7fSjob rc = 1; 3639a7e9e7fSjob out: 3649a7e9e7fSjob if (rc == 0) { 3659a7e9e7fSjob X509_free(*xp); 3669a7e9e7fSjob *xp = NULL; 3679a7e9e7fSjob } 368475ff837Sjob sk_X509_CRL_pop_free(crls, X509_CRL_free); 369ef3f6f56Sjob sk_X509_free(certs); 370ef3f6f56Sjob CMS_ContentInfo_free(cms); 371ef3f6f56Sjob return rc; 372ef3f6f56Sjob } 373ef3f6f56Sjob 374ef3f6f56Sjob /* 375ef3f6f56Sjob * Parse and validate a self-signed CMS message. 376ef3f6f56Sjob * Conforms to RFC 6488. 377ef3f6f56Sjob * The eContentType of the message must be an oid object. 378ef3f6f56Sjob * Return the eContent as a string and set "rsz" to be its length. 379ef3f6f56Sjob */ 380ef3f6f56Sjob unsigned char * 381ef3f6f56Sjob cms_parse_validate(X509 **xp, const char *fn, const unsigned char *der, 3821bb1e509Sjob size_t derlen, const ASN1_OBJECT *oid, size_t *rsz, time_t *st) 383ef3f6f56Sjob { 384ef3f6f56Sjob unsigned char *res = NULL; 385ef3f6f56Sjob 386ef3f6f56Sjob if (!cms_parse_validate_internal(xp, fn, der, derlen, oid, NULL, &res, 3871bb1e509Sjob rsz, st)) 388ef3f6f56Sjob return NULL; 3899a7e9e7fSjob 3909a7e9e7fSjob return res; 3919a7e9e7fSjob } 392ef3f6f56Sjob 393ef3f6f56Sjob /* 394ef3f6f56Sjob * Parse and validate a detached CMS signature. 395ef3f6f56Sjob * bio must contain the original message, der must contain the CMS. 396ef3f6f56Sjob * Return the 1 on success, 0 on failure. 397ef3f6f56Sjob */ 398ef3f6f56Sjob int 399ef3f6f56Sjob cms_parse_validate_detached(X509 **xp, const char *fn, const unsigned char *der, 4001bb1e509Sjob size_t derlen, const ASN1_OBJECT *oid, BIO *bio, time_t *st) 401ef3f6f56Sjob { 402ef3f6f56Sjob return cms_parse_validate_internal(xp, fn, der, derlen, oid, bio, NULL, 4031bb1e509Sjob NULL, st); 404ef3f6f56Sjob } 405