/* SSSD - certificate handling utils - NSS version The calls defined here should be useable outside of SSSD as well, e.g. in libsss_certmap. Copyright (C) Sumit Bose 2017 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "util/crypto/sss_crypto.h" #include "util/crypto/nss/nss_util.h" #include "util/cert.h" #include "lib/certmap/sss_certmap.h" #include "lib/certmap/sss_certmap_int.h" /* The following two functions are copied from NSS's lib/certdb/secname.c * becasue CERT_AddAVA is not exported. I just renamed it and made it static * to avoid issues if the call gets exported some time in future. */ static void ** AddToArray(PLArenaPool *arena, void **array, void *element) { unsigned count; void **ap; /* Count up number of slots already in use in the array */ count = 0; ap = array; if (ap) { while (*ap++) { count++; } } if (array) { array = (void**) PORT_ArenaGrow(arena, array, (count + 1) * sizeof(void *), (count + 2) * sizeof(void *)); } else { array = (void**) PORT_ArenaAlloc(arena, (count + 2) * sizeof(void *)); } if (array) { array[count] = element; array[count+1] = 0; } return array; } static SECStatus sss_CERT_AddAVA(PLArenaPool *arena, CERTRDN *rdn, CERTAVA *ava) { rdn->avas = (CERTAVA**) AddToArray(arena, (void**) rdn->avas, ava); return rdn->avas ? SECSuccess : SECFailure; } static SECItem * cert_get_ext_by_tag(CERTCertificate *cert, SECOidTag tag) { SECOidData *oid; int i; oid = SECOID_FindOIDByTag(tag); for (i = 0; (cert->extensions != NULL) && (cert->extensions[i] != NULL); i++) if (SECITEM_ItemsAreEqual(&cert->extensions[i]->id, &oid->oid)) return &cert->extensions[i]->value; return NULL; } static int get_extended_key_usage_oids(TALLOC_CTX *mem_ctx, CERTCertificate *cert, const char ***_oids) { PLArenaPool *pool; SECItem *ext; SECItem **oids = NULL; const char **oids_list = NULL; size_t c; SECStatus rv; char *tmp_str; int ret; pool = PORT_NewArena(sizeof(double)); ext = cert_get_ext_by_tag(cert, SEC_OID_X509_EXT_KEY_USAGE); if (ext != NULL) { rv = SEC_ASN1DecodeItem(pool, &oids, SEC_ASN1_GET(SEC_SequenceOfObjectIDTemplate), ext); if (rv != SECSuccess) { ret = EINVAL; goto done; } } for (c = 0; (oids != NULL && oids[c] != NULL); c++); oids_list = talloc_zero_array(mem_ctx, const char *, c + 1); if (oids_list == NULL) { return ENOMEM; } for (c = 0; (oids != NULL && oids[c] != NULL); c++) { tmp_str = CERT_GetOidString(oids[c]); /* is it expexted that NSS OID strings start with "OID." but we * prefer the plain dotted-decimal version so the prefix is skipped */ if (tmp_str == NULL || strncmp(tmp_str, "OID.", 4) != 0) { PR_smprintf_free(tmp_str); ret = EINVAL; goto done; } oids_list[c] = talloc_strdup(oids_list, tmp_str + 4); PR_smprintf_free(tmp_str); if(oids_list[c] == NULL) { ret = ENOMEM; goto done; } } ret = 0; done: PORT_FreeArena(pool, PR_TRUE); if (ret == 0) { *_oids = oids_list; } else { talloc_free(oids_list); } return ret; } static int get_rdn_str(TALLOC_CTX *mem_ctx, CERTAVA **avas, const char **rdn_str) { size_t c; char *tmp_name = NULL; const char *tmp_str = NULL; int ret; SECStatus rv; CERTRDN rdn = { 0 }; CERTName *name = NULL; PLArenaPool *arena = NULL; arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE); if (arena == NULL) { ret = ENOMEM; goto done; } /* Multiple AVAs should be avoided becasue there is no general ordering * rule and the RDN strings are not reproducible */ for (c = 0; avas[c] != NULL; c++) { rv = sss_CERT_AddAVA(arena, &rdn, avas[c]); if (rv != SECSuccess) { ret = EIO; goto done; } } name = CERT_CreateName(&rdn, NULL); if (name == NULL) { ret = EIO; goto done; } tmp_name = CERT_NameToAscii(name); CERT_DestroyName(name); if (tmp_name == NULL) { ret = EIO; goto done; } tmp_str = talloc_strdup(mem_ctx, tmp_name); PORT_Free(tmp_name); if (tmp_str == NULL) { ret = ENOMEM; goto done; } ret = 0; done: if (ret == 0) { *rdn_str = tmp_str; } else { talloc_free(discard_const(tmp_str)); } PORT_FreeArena(arena, PR_FALSE); return ret; } static int get_rdn_list(TALLOC_CTX *mem_ctx, CERTRDN **rdns, const char ***rdn_list) { int ret; size_t c; const char **list = NULL; for (c = 0; rdns[c] != NULL; c++); list = talloc_zero_array(mem_ctx, const char *, c + 1); if (list == NULL) { ret = ENOMEM; goto done; } for (c = 0; rdns[c] != NULL; c++) { ret = get_rdn_str(list, rdns[c]->avas, &(list[c])); if (ret != 0) { goto done; } } ret = 0; done: if (ret == 0) { *rdn_list = list; } else { talloc_free(list); } return ret; } enum san_opt nss_name_type_to_san_opt(CERTGeneralNameType type) { switch (type) { case certOtherName: return SAN_OTHER_NAME; case certRFC822Name: return SAN_RFC822_NAME; case certDNSName: return SAN_DNS_NAME; case certX400Address: return SAN_X400_ADDRESS; case certDirectoryName: return SAN_DIRECTORY_NAME; case certEDIPartyName: return SAN_EDIPART_NAME; case certURI: return SAN_URI; case certIPAddress: return SAN_IP_ADDRESS; case certRegisterID: return SAN_REGISTERED_ID; default: return SAN_INVALID; } } static int add_to_san_list(TALLOC_CTX *mem_ctx, bool is_bin, enum san_opt san_opt, uint8_t *data, size_t len, struct san_list **item) { struct san_list *i; if (data == NULL || len == 0 || san_opt == SAN_INVALID) { return EINVAL; } i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { return ENOMEM; } i->san_opt = san_opt; if (is_bin) { i->bin_val = talloc_memdup(i, data, len); i->bin_val_len = len; } else { i->val = talloc_strndup(i, (char *) data, len); } if (i->val == NULL) { talloc_free(i); return ENOMEM; } *item = i; return 0; } /* taken from pkinit_crypto_nss.c of MIT Kerberos */ /* KerberosString: RFC 4120, 5.2.1. */ static const SEC_ASN1Template kerberos_string_template[] = { { SEC_ASN1_GENERAL_STRING, 0, NULL, sizeof(SECItem), } }; /* Realm: RFC 4120, 5.2.2. */ struct realm { SECItem name; }; static const SEC_ASN1Template realm_template[] = { { SEC_ASN1_GENERAL_STRING, 0, NULL, sizeof(SECItem), } }; /* PrincipalName: RFC 4120, 5.2.2. */ static const SEC_ASN1Template sequence_of_kerberos_string_template[] = { { SEC_ASN1_SEQUENCE_OF, 0, &kerberos_string_template, 0, } }; struct principal_name { SECItem name_type; SECItem **name_string; }; static const SEC_ASN1Template principal_name_template[] = { { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(struct principal_name), }, { SEC_ASN1_CONTEXT_SPECIFIC | 0 | SEC_ASN1_CONSTRUCTED | SEC_ASN1_EXPLICIT, offsetof(struct principal_name, name_type), &SEC_IntegerTemplate, sizeof(SECItem), }, { SEC_ASN1_CONTEXT_SPECIFIC | 1 | SEC_ASN1_CONSTRUCTED | SEC_ASN1_EXPLICIT, offsetof(struct principal_name, name_string), sequence_of_kerberos_string_template, sizeof(struct SECItem **), }, {0, 0, NULL, 0}, }; /* KRB5PrincipalName: RFC 4556, 3.2.2. */ struct kerberos_principal_name { SECItem realm; struct principal_name principal_name; }; static const SEC_ASN1Template kerberos_principal_name_template[] = { { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(struct kerberos_principal_name), }, { SEC_ASN1_CONTEXT_SPECIFIC | 0 | SEC_ASN1_CONSTRUCTED | SEC_ASN1_EXPLICIT, offsetof(struct kerberos_principal_name, realm), &realm_template, sizeof(struct realm), }, { SEC_ASN1_CONTEXT_SPECIFIC | 1 | SEC_ASN1_CONSTRUCTED | SEC_ASN1_EXPLICIT, offsetof(struct kerberos_principal_name, principal_name), &principal_name_template, sizeof(struct principal_name), }, {0, 0, NULL, 0} }; #define PKINIT_OID "1.3.6.1.5.2.2" #define NT_PRINCIPAL_OID "1.3.6.1.4.1.311.20.2.3" static int add_string_other_name_to_san_list(TALLOC_CTX *mem_ctx, enum san_opt san_opt, CERTGeneralName *current, struct san_list **item) { struct san_list *i = NULL; int ret; char *tmp_str; tmp_str = CERT_GetOidString(&(current->name.OthName.oid)); /* is it expexted that NSS OID strings start with "OID." but we * prefer the plain dotted-decimal version so the prefix is skipped */ if (tmp_str == NULL || strncmp(tmp_str, "OID.", 4) != 0) { PR_smprintf_free(tmp_str); return EINVAL; } i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { PR_smprintf_free(tmp_str); return ENOMEM; } i->san_opt = san_opt; i->other_name_oid = talloc_strdup(i, tmp_str + 4); PR_smprintf_free(tmp_str); if (i->other_name_oid == NULL) { ret = ENOMEM; goto done; } i->bin_val = talloc_memdup(i, current->name.OthName.name.data, current->name.OthName.name.len); if (i->bin_val == NULL) { ret = ENOMEM; goto done; } i->bin_val_len = current->name.OthName.name.len; ret = 0; done: if (ret == 0) { *item = i; } else { talloc_free(i); } return ret; } static int get_short_name(TALLOC_CTX *mem_ctx, const char *full_name, char delim, char **short_name) { char *at; char *s; if (full_name == NULL || delim == '\0' || short_name == NULL) { return EINVAL; } at = strchr(full_name, delim); if (at != NULL) { s = talloc_strndup(mem_ctx, full_name, (at - full_name)); } else { s = talloc_strdup(mem_ctx, full_name); } if (s == NULL) { return ENOMEM; } *short_name = s; return 0; } static int add_nt_princ_to_san_list(TALLOC_CTX *mem_ctx, PLArenaPool *pool, enum san_opt san_opt, CERTGeneralName *current, struct san_list **item) { struct san_list *i = NULL; SECStatus rv; SECItem tmp_secitem = { 0 }; int ret; rv = SEC_ASN1DecodeItem(pool, &tmp_secitem, SEC_ASN1_GET(SEC_UTF8StringTemplate), &(current->name.OthName.name)); if (rv != SECSuccess) { return EINVAL; } i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { return ENOMEM; } i->san_opt = san_opt; i->val = talloc_strndup(i, (char *) tmp_secitem.data, tmp_secitem.len); if (i->val == NULL) { ret = ENOMEM; goto done; } ret = get_short_name(i, i->val, '@', &(i->short_name)); if (ret != 0) { goto done; } ret = 0; done: if (ret == 0) { *item = i; } else { talloc_free(i); } return ret; } static int add_pkinit_princ_to_san_list(TALLOC_CTX *mem_ctx, PLArenaPool *pool, enum san_opt san_opt, CERTGeneralName *current, struct san_list **item) { struct san_list *i = NULL; SECStatus rv; struct kerberos_principal_name kname; int ret; size_t c; rv = SEC_ASN1DecodeItem(pool, &kname, kerberos_principal_name_template, &(current->name.OthName.name)); if (rv != SECSuccess) { return EINVAL; } i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { return ENOMEM; } i->san_opt = san_opt; if (kname.principal_name.name_string != NULL) { i->val = talloc_strdup(i, ""); if (i->val == NULL) { ret = ENOMEM; goto done; } for (c = 0; kname.principal_name.name_string[c] != NULL; c++) { if (c > 0) { i->val = talloc_strdup_append(i->val, "/"); if (i->val == NULL) { ret = ENOMEM; goto done; } } i->val = talloc_strndup_append(i->val, (char *) kname.principal_name.name_string[c]->data, kname.principal_name.name_string[c]->len); if (i->val == NULL) { ret = ENOMEM; goto done; } } i->val = talloc_strndup_append(i->val, (char *) kname.realm.data, kname.realm.len); if (i->val == NULL) { ret = ENOMEM; goto done; } ret = get_short_name(i, i->val, '@', &(i->short_name)); if (ret != 0) { goto done; } } ret = 0; done: if (ret == 0) { *item = i; } else { talloc_free(i); } return ret; } static int add_oid_to_san_list(TALLOC_CTX *mem_ctx, enum san_opt san_opt, SECItem oid, struct san_list **item) { struct san_list *i = NULL; char *tmp_str; tmp_str = CERT_GetOidString(&oid); /* is it expexted that NSS OID strings start with "OID." but we * prefer the plain dotted-decimal version so the prefix is skipped */ if (tmp_str == NULL || strncmp(tmp_str, "OID.", 4) != 0) { PR_smprintf_free(tmp_str); return EINVAL; } i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { PR_smprintf_free(tmp_str); return ENOMEM; } i->san_opt = san_opt; i->val = talloc_strdup(i, tmp_str + 4); PR_smprintf_free(tmp_str); if (i->val == NULL) { talloc_free(i); return ENOMEM; } *item = i; return 0; } static int add_rdn_list_to_san_list(TALLOC_CTX *mem_ctx, enum san_opt san_opt, CERTName name, struct san_list **item) { struct san_list *i = NULL; int ret; i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { return ENOMEM; } i->san_opt = san_opt; ret = get_rdn_list(i, name.rdns, &(i->rdn_list)); if (ret != 0) { talloc_free(i); return ret; } *item = i; return 0; } static int add_ip_to_san_list(TALLOC_CTX *mem_ctx, enum san_opt san_opt, uint8_t *data, size_t len, struct san_list **item) { struct san_list *i; PRStatus st; PRNetAddr addr; char addrBuf[80]; if (data == NULL || len == 0 || san_opt == SAN_INVALID) { return EINVAL; } /* taken from secu_PrintIPAddress() */ memset(&addr, 0, sizeof addr); if (len == 4) { addr.inet.family = PR_AF_INET; memcpy(&addr.inet.ip, data, len); } else if (len == 16) { addr.ipv6.family = PR_AF_INET6; memcpy(addr.ipv6.ip.pr_s6_addr, data, len); if (PR_IsNetAddrType(&addr, PR_IpAddrV4Mapped)) { /* convert to IPv4. */ addr.inet.family = PR_AF_INET; memcpy(&addr.inet.ip, &addr.ipv6.ip.pr_s6_addr[12], 4); memset(&addr.inet.pad[0], 0, sizeof addr.inet.pad); } } else { return EINVAL; } st = PR_NetAddrToString(&addr, addrBuf, sizeof addrBuf); if (st != PR_SUCCESS) { return EIO; } i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { return ENOMEM; } i->san_opt = san_opt; i->val = talloc_strdup(i, addrBuf); if (i->val == NULL) { talloc_free(i); return ENOMEM; } *item = i; return 0; } static int add_principal_to_san_list(TALLOC_CTX *mem_ctx, enum san_opt san_opt, const char *princ, struct san_list **item) { struct san_list *i = NULL; int ret; i = talloc_zero(mem_ctx, struct san_list); if (i == NULL) { return ENOMEM; } i->san_opt = san_opt; i->val = talloc_strdup(i, princ); if (i->val == NULL) { ret = ENOMEM; goto done; } ret = get_short_name(i, i->val, '@', &(i->short_name)); if (ret != 0) { goto done; } ret = 0; done: if (ret == 0) { *item = i; } else { talloc_free(i); } return ret; } static int get_san(TALLOC_CTX *mem_ctx, CERTCertificate *cert, struct san_list **san_list) { SECItem subAltName = { 0 }; SECStatus rv; CERTGeneralName *name_list = NULL; CERTGeneralName *current; PLArenaPool *pool = NULL; int ret; struct san_list *list = NULL; struct san_list *item = NULL; struct san_list *item_s = NULL; struct san_list *item_p = NULL; struct san_list *item_pb = NULL; rv = CERT_FindCertExtension(cert, SEC_OID_X509_SUBJECT_ALT_NAME, &subAltName); if (rv != SECSuccess) { if (rv == SECFailure && PORT_GetError() == SEC_ERROR_EXTENSION_NOT_FOUND) { ret = EOK; } else { ret = EIO; } goto done; } pool = PORT_NewArena(DER_DEFAULT_CHUNKSIZE); if (pool == NULL) { ret = ENOMEM; goto done; } name_list = CERT_DecodeAltNameExtension(pool, &subAltName); if (name_list == NULL ) { ret = EIO; goto done; } current = name_list; do { switch (current->type) { case certOtherName: ret = add_string_other_name_to_san_list(mem_ctx, SAN_STRING_OTHER_NAME, current, &item_s); if (ret != 0) { goto done; } DLIST_ADD(list, item_s); item_p = NULL; if (strcmp(item_s->other_name_oid, NT_PRINCIPAL_OID) == 0) { ret = add_nt_princ_to_san_list(mem_ctx, pool, SAN_NT, current, &item_p); if (ret != 0) { goto done; } DLIST_ADD(list, item_p); } else if (strcmp(item_s->other_name_oid, PKINIT_OID) == 0) { ret = add_pkinit_princ_to_san_list(mem_ctx, pool, SAN_PKINIT, current, &item_p); if (ret != 0) { goto done; } DLIST_ADD(list, item_p); } if (item_p != NULL) { ret = add_principal_to_san_list(mem_ctx, SAN_PRINCIPAL, item_p->val, &item_pb); if (ret != 0) { goto done; } DLIST_ADD(list, item_pb); } break; case certRFC822Name: case certDNSName: case certURI: ret = add_to_san_list(mem_ctx, false, nss_name_type_to_san_opt(current->type), current->name.other.data, current->name.other.len, &item); if (ret != 0) { goto done; } if (current->type == certRFC822Name || current->type == certDNSName) { ret = get_short_name(item, item->val, (current->type == certRFC822Name ? '@' : '.'), &(item->short_name)); if (ret != 0) { goto done; } } DLIST_ADD(list, item); break; case certIPAddress: ret = add_ip_to_san_list(mem_ctx, nss_name_type_to_san_opt(current->type), current->name.other.data, current->name.other.len, &item); if (ret != 0) { goto done; } DLIST_ADD(list, item); break; case certDirectoryName: ret = add_rdn_list_to_san_list(mem_ctx, nss_name_type_to_san_opt(current->type), current->name.directoryName, &item); if (ret != 0) { goto done; } DLIST_ADD(list, item); break; case certRegisterID: ret = add_oid_to_san_list(mem_ctx, nss_name_type_to_san_opt(current->type), current->name.other, &item); if (ret != 0) { goto done; } DLIST_ADD(list, item); break; case certX400Address: case certEDIPartyName: ret = add_to_san_list(mem_ctx, true, nss_name_type_to_san_opt(current->type), current->name.other.data, current->name.other.len, &item); if (ret != 0) { goto done; } DLIST_ADD(list, item); break; default: ret = EINVAL; } current = CERT_GetNextGeneralName(current); if (current == NULL) { ret = EIO; goto done; } } while (current != name_list); done: /* Don't free nameList, it's part of the arena. */ if (pool != NULL) { PORT_FreeArena(pool, PR_FALSE); } if (subAltName.data != NULL) { SECITEM_FreeItem(&subAltName, PR_FALSE); } if (ret == EOK) { *san_list = list; } return ret; } int sss_cert_get_content(TALLOC_CTX *mem_ctx, const uint8_t *der_blob, size_t der_size, struct sss_cert_content **content) { int ret; struct sss_cert_content *cont = NULL; CERTCertDBHandle *handle; CERTCertificate *cert = NULL; SECItem der_item; NSSInitContext *nss_ctx; if (der_blob == NULL || der_size == 0) { return EINVAL; } nss_ctx = NSS_InitContext("", "", "", "", NULL, NSS_INIT_READONLY | NSS_INIT_NOCERTDB | NSS_INIT_NOMODDB | NSS_INIT_FORCEOPEN | NSS_INIT_NOROOTINIT | NSS_INIT_OPTIMIZESPACE); if (nss_ctx == NULL) { return EIO; } cont = talloc_zero(mem_ctx, struct sss_cert_content); if (cont == NULL) { return ENOMEM; } handle = CERT_GetDefaultCertDB(); der_item.len = der_size; der_item.data = discard_const(der_blob); cert = CERT_NewTempCertificate(handle, &der_item, NULL, PR_FALSE, PR_TRUE); if (cert == NULL) { ret = EINVAL; goto done; } cont->issuer_str = talloc_strdup(cont, cert->issuerName); if (cont->issuer_str == NULL) { ret = ENOMEM; goto done; } ret = get_rdn_list(cont, cert->issuer.rdns, &cont->issuer_rdn_list); if (ret != 0) { goto done; } cont->subject_str = talloc_strdup(cont, cert->subjectName); if (cont->subject_str == NULL) { ret = ENOMEM; goto done; } ret = get_rdn_list(cont, cert->subject.rdns, &cont->subject_rdn_list); if (ret != 0) { goto done; } cont->key_usage = cert->keyUsage; ret = get_extended_key_usage_oids(cont, cert, &(cont->extended_key_usage_oids)); if (ret != 0) { goto done; } ret = get_san(cont, cert, &(cont->san_list)); if (ret != 0) { goto done; } cont->cert_der = talloc_memdup(cont, der_blob, der_size); if (cont->cert_der == NULL) { ret = ENOMEM; goto done; } cont->cert_der_size = der_size; ret = EOK; done: CERT_DestroyCertificate(cert); NSS_ShutdownContext(nss_ctx); if (ret == EOK) { *content = cont; } else { talloc_free(cont); } return ret; }