]> git.saurik.com Git - apple/security.git/blobdiff - OSX/sec/securityd/nameconstraints.c
Security-57336.1.9.tar.gz
[apple/security.git] / OSX / sec / securityd / nameconstraints.c
diff --git a/OSX/sec/securityd/nameconstraints.c b/OSX/sec/securityd/nameconstraints.c
new file mode 100644 (file)
index 0000000..65407bb
--- /dev/null
@@ -0,0 +1,554 @@
+/*
+ * Copyright (c) 2015 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+/*
+ * nameconstraints.c - rfc5280 section 4.2.1.10 and later name constraints implementation.
+ */
+
+#include "nameconstraints.h"
+#include <AssertMacros.h>
+#include <utilities/SecCFWrappers.h>
+#include <Security/SecCertificateInternal.h>
+#include <securityd/SecPolicyServer.h>
+#include <libDER/asn1Types.h>
+
+/* RFC 5280 Section 4.2.1.10:
+ For URIs, the constraint applies to the host part of the name.  The
+ constraint MUST be specified as a fully qualified domain name and MAY
+ specify a host or a domain.  Examples would be "host.example.com" and
+ ".example.com".  When the constraint begins with a period, it MAY be
+ expanded with one or more labels.  That is, the constraint
+ ".example.com" is satisfied by both host.example.com and
+ my.host.example.com.  However, the constraint ".example.com" is not
+ satisfied by "example.com".  When the constraint does not begin with
+ a period, it specifies a host.
+ */
+static bool SecURIMatch(CFStringRef URI, CFStringRef hostname) {
+    bool result = false;
+    CFStringRef URI_hostname =  NULL;
+    CFCharacterSetRef port_or_path_separator = NULL;
+    /* URI must have scheme specified */
+    CFRange URI_scheme = CFStringFind(URI, CFSTR("://"), 0);
+    require_quiet(URI_scheme.location != kCFNotFound, out);
+    
+    /* Remove scheme prefix and port or resource path suffix */
+    CFRange URI_hostname_range = { URI_scheme.location + URI_scheme.length,
+        CFStringGetLength(URI) - URI_scheme.location - URI_scheme.length };
+    port_or_path_separator = CFCharacterSetCreateWithCharactersInString(kCFAllocatorDefault, CFSTR(":/"));
+    CFRange separator = {kCFNotFound, 0};
+    if(CFStringFindCharacterFromSet(URI, port_or_path_separator, URI_hostname_range, 0, &separator)) {
+        URI_hostname_range.length -= (CFStringGetLength(URI) - separator.location);
+    }
+    URI_hostname = CFStringCreateWithSubstring(kCFAllocatorDefault, URI, URI_hostname_range);
+    
+    /* Hostname in URI must not begin with '.' */
+    require_quiet('.' != CFStringGetCharacterAtIndex(URI_hostname, 0), out);
+    
+    CFIndex ulength = CFStringGetLength(URI_hostname);
+    CFIndex hlength = CFStringGetLength(hostname);
+    require_quiet(ulength >= hlength, out);
+    CFRange compare_range = { 0, hlength };
+    
+    /* Allow one or more preceding labels */
+    if ('.' == CFStringGetCharacterAtIndex(hostname, 0)) {
+        compare_range.location = ulength - hlength;
+    }
+    
+    if(kCFCompareEqualTo == CFStringCompareWithOptions(URI_hostname,
+                                                       hostname,
+                                                       compare_range,
+                                                       kCFCompareCaseInsensitive)) {
+        result = true;
+    }
+    
+out:
+    CFReleaseNull(port_or_path_separator);
+    CFReleaseNull(URI_hostname);
+    return result;
+}
+
+/* RFC 5280 Section 4.2.1.10:
+ A name constraint for Internet mail addresses MAY specify a
+ particular mailbox, all addresses at a particular host, or all
+ mailboxes in a domain.  To indicate a particular mailbox, the
+ constraint is the complete mail address.  For example,
+ "root@example.com" indicates the root mailbox on the host
+ "example.com".  To indicate all Internet mail addresses on a
+ particular host, the constraint is specified as the host name.  For
+ example, the constraint "example.com" is satisfied by any mail
+ address at the host "example.com".  To specify any address within a
+ domain, the constraint is specified with a leading period (as with
+ URIs).
+ */
+static bool SecRFC822NameMatch(CFStringRef emailAddress, CFStringRef constraint) {
+    CFRange mailbox_range = CFStringFind(constraint,CFSTR("@"),0);
+
+    /* Constraint specifies a particular mailbox. Perform full comparison. */
+    if (mailbox_range.location != kCFNotFound) {
+        if (!CFStringCompare(emailAddress, constraint, kCFCompareCaseInsensitive)) {
+            return true;
+        }
+        else return false;
+    }
+
+    mailbox_range = CFStringFind(emailAddress, CFSTR("@"), 0);
+    require_quiet(mailbox_range.location != kCFNotFound, out);
+    CFRange hostname_range = {mailbox_range.location + 1,
+        CFStringGetLength(emailAddress) - mailbox_range.location - 1 };
+
+    /* Constraint specificies a particular host. Compare hostname of address. */
+    if ('.' != CFStringGetCharacterAtIndex(constraint, 0)) {
+        if (!CFStringCompareWithOptions(emailAddress, constraint, hostname_range, kCFCompareCaseInsensitive)) {
+            return true;
+        }
+        else return false;
+    }
+
+    /* Constraint specificies a domain. Match hostname of address to domain name. */
+    require_quiet('.' != CFStringGetCharacterAtIndex(emailAddress, mailbox_range.location +1), out);
+    if (CFStringHasSuffix(emailAddress, constraint)) {
+        return true;
+    }
+
+out:
+    return false;
+}
+
+static bool nc_compare_directoryNames(const DERItem *certName, const DERItem *subtreeName) {
+    /* Get content of certificate name and subtree name */
+    DERDecodedInfo certName_content;
+    require_noerr_quiet(DERDecodeItem(certName, &certName_content), out);
+    
+    DERDecodedInfo subtreeName_content;
+    require_noerr_quiet(DERDecodeItem(subtreeName, &subtreeName_content), out);
+    
+    if (certName->length > subtreeName->length) {
+        if(0 == memcmp(certName_content.content.data,
+                       subtreeName_content.content.data,
+                       subtreeName_content.content.length)) {
+            return true;
+        }
+    }
+    
+out:
+    return false;
+}
+
+static bool nc_compare_DNSNames(const DERItem *certName, const DERItem *subtreeName) {
+    bool result = false;
+    CFStringRef subtreeName_with_wildcard = NULL;
+    CFStringRef certName_str = CFStringCreateWithBytes(kCFAllocatorDefault,
+                                                       certName->data, certName->length,
+                                                       kCFStringEncodingUTF8, FALSE);
+    CFStringRef subtreeName_str = CFStringCreateWithBytes(kCFAllocatorDefault,
+                                                          subtreeName->data, subtreeName->length,
+                                                          kCFStringEncodingUTF8, FALSE);
+    require_quiet(certName_str, out);
+    require_quiet(subtreeName_str, out);
+    /*
+     * We'll also compose a string with a preceding wildcard label since:
+     *      "Any DNS name that can be constructed by simply adding zero or more labels to
+     *      the left-hand side of the name satisfies the name constraint."
+     */
+    subtreeName_with_wildcard = CFStringCreateWithFormat(kCFAllocatorDefault,
+                                                         NULL,
+                                                         CFSTR("*.%s"),
+                                                         CFStringGetCStringPtr(subtreeName_str,
+                                                                               kCFStringEncodingUTF8));
+    require_quiet(subtreeName_with_wildcard, out);
+    
+    if (SecDNSMatch(certName_str, subtreeName_str) || SecDNSMatch(certName_str, subtreeName_with_wildcard)) {
+        result = true;
+    }
+    
+out:
+    CFReleaseNull(certName_str) ;
+    CFReleaseNull(subtreeName_str);
+    CFReleaseNull(subtreeName_with_wildcard);
+    return result;
+}
+
+static bool nc_compare_URIs(const DERItem *certName, const DERItem *subtreeName) {
+    bool result = false;
+    CFStringRef certName_str = CFStringCreateWithBytes(kCFAllocatorDefault,
+                                                       certName->data, certName->length,
+                                                       kCFStringEncodingUTF8, FALSE);
+    CFStringRef subtreeName_str = CFStringCreateWithBytes(kCFAllocatorDefault,
+                                                          subtreeName->data, subtreeName->length,
+                                                          kCFStringEncodingUTF8, FALSE);
+    require_quiet(certName_str, out);
+    require_quiet(subtreeName_str, out);
+    
+    if (SecURIMatch(certName_str, subtreeName_str)) {
+        result = true;
+    }
+    
+out:
+    CFReleaseNull(certName_str);
+    CFReleaseNull(subtreeName_str);
+    return result;
+}
+
+static bool nc_compare_RFC822Names(const DERItem *certName, const DERItem *subtreeName) {
+    bool result = false;
+    CFStringRef certName_str = CFStringCreateWithBytes(kCFAllocatorDefault,
+                                                       certName->data, certName->length,
+                                                       kCFStringEncodingUTF8, FALSE);
+    CFStringRef subtreeName_str = CFStringCreateWithBytes(kCFAllocatorDefault,
+                                                          subtreeName->data, subtreeName->length,
+                                                          kCFStringEncodingUTF8, FALSE);
+    require_quiet(certName_str, out);
+    require_quiet(subtreeName_str, out);
+    
+    if (SecRFC822NameMatch(certName_str, subtreeName_str)) {
+        result = true;
+    }
+    
+out:
+    CFReleaseNull(certName_str);
+    CFReleaseNull(subtreeName_str);
+    return result;
+}
+
+static bool nc_compare_IPAddresses(const DERItem *certAddr, const DERItem *subtreeAddr) {
+    bool result = false;
+    
+    /* Verify Subtree Address has correct number of bytes for IP and mask */
+    require_quiet((subtreeAddr->length == 8) || (subtreeAddr->length == 32), out);
+    /* Verify Cert Address has correct number of bytes for IP */
+    require_quiet((certAddr->length == 4) || (certAddr->length ==16), out);
+    /* Verify Subtree Address and Cert Address are the same version */
+    require_quiet(subtreeAddr->length == 2*certAddr->length, out);
+    
+    DERByte * mask = subtreeAddr->data + certAddr->length;
+    for (DERSize i = 0; i < certAddr->length; i++) {
+        if((subtreeAddr->data[i] & mask[i]) != (certAddr->data[i] & mask[i])) {
+            return false;
+        }
+    }
+    return true;
+    
+out:
+    return result;
+}
+
+typedef struct {
+    bool present;
+    bool isMatch;
+} match_t;
+
+typedef struct {
+    const SecCEGeneralNameType gnType;
+    const DERItem *cert_item;
+    match_t *match;
+} nc_match_context_t;
+
+typedef struct {
+    const CFArrayRef subtrees;
+    match_t *match;
+} nc_san_match_context_t;
+
+static OSStatus nc_compare_subtree(void *context, SecCEGeneralNameType gnType, const DERItem *generalName) {
+    nc_match_context_t *item_context = context;
+    if (item_context && gnType == item_context->gnType
+        && item_context->match && item_context->cert_item) {
+        
+        item_context->match->present = true;
+        /*
+         * We set isMatch such that if there are multiple subtrees of the same type, matching to any one
+         * of them is considered a match.
+         */
+        switch (gnType) {
+            case GNT_DirectoryName: {
+                item_context->match->isMatch |= nc_compare_directoryNames(item_context->cert_item, generalName);
+                return errSecSuccess;
+            }
+            case GNT_DNSName: {
+                item_context->match->isMatch |= nc_compare_DNSNames(item_context->cert_item, generalName);
+                return errSecSuccess;
+            }
+            case GNT_URI: {
+                item_context->match->isMatch |= nc_compare_URIs(item_context->cert_item, generalName);
+                return errSecSuccess;
+            }
+            case GNT_RFC822Name: {
+                item_context->match->isMatch |= nc_compare_RFC822Names(item_context->cert_item, generalName);
+                return errSecSuccess;
+            }
+            case  GNT_IPAddress: {
+                item_context->match->isMatch |= nc_compare_IPAddresses(item_context->cert_item, generalName);
+                return errSecSuccess;
+            }
+            default: {
+                /* If the name form is not supported, reject the certificate. */
+                return errSecInvalidCertificate;
+            }
+        }
+    }
+    
+    return errSecInvalidCertificate;
+}
+
+static void nc_decode_and_compare_subtree(const void *value, void *context) {
+    CFDataRef subtree = value;
+    nc_match_context_t *match_context = context;
+    if(subtree) {
+        /* convert subtree to DERItem */
+        const DERItem general_name = { (unsigned char *)CFDataGetBytePtr(subtree), CFDataGetLength(subtree) };
+        DERDecodedInfo general_name_content;
+        require_noerr_quiet(DERDecodeItem(&general_name, &general_name_content),out);
+        
+        OSStatus status = SecCertificateParseGeneralNameContentProperty(general_name_content.tag,
+                                                                        &general_name_content.content,
+                                                                        match_context,
+                                                                        nc_compare_subtree);
+        if (status == errSecInvalidCertificate) {
+            secdebug("policy","can't parse general name or not a type we support");
+        }
+    }
+out:
+    return;
+}
+
+static bool isEmptySubject(CFDataRef subject) {
+    const DERItem subject_der = { (unsigned char *)CFDataGetBytePtr(subject), CFDataGetLength(subject) };
+    
+    /* Get content of certificate name */
+    DERDecodedInfo subject_content;
+    require_noerr_quiet(DERDecodeItem(&subject_der, &subject_content), out);
+    if (subject_content.content.length) return false;
+    
+out:
+    return true;
+}
+
+static bool nc_compare_subject_to_subtrees(CFDataRef subject, CFArrayRef subtrees) {
+    /* An empty subject name is considered a match */
+    if (isEmptySubject(subject))
+        return true;
+    
+    CFIndex num_trees = CFArrayGetCount(subtrees);
+    CFRange range = { 0, num_trees };
+    const DERItem subject_der = { (unsigned char *)CFDataGetBytePtr(subject), CFDataGetLength(subject) };
+    match_t match = { false, false };
+    nc_match_context_t context = {GNT_DirectoryName, &subject_der, &match};
+    CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &context);
+    
+    /* If no directory name amongst the subtrees, then return success. */
+    if (!match.present) return true;
+    else return match.isMatch;
+}
+
+static void nc_compare_RFC822Name_to_subtrees(const void *value, void *context) {
+    CFStringRef rfc822Name = value;
+    nc_san_match_context_t *san_context = context;
+    CFArrayRef subtrees = NULL;
+    if (san_context) {
+        subtrees = san_context->subtrees;
+    }
+    if (subtrees) {
+        CFIndex num_trees = CFArrayGetCount(subtrees);
+        CFRange range = { 0, num_trees };
+        match_t match = { false, false };
+        const DERItem addr = { (unsigned char *)CFStringGetCStringPtr(rfc822Name, kCFStringEncodingUTF8),
+                              CFStringGetLength(rfc822Name) };
+        nc_match_context_t match_context = {GNT_RFC822Name, &addr, &match};
+        CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &match_context);
+        
+        /*
+         * We set the SAN context match struct as follows:
+         * 'present' is true if there's any subtree of the same type as any SAN
+         * 'match' is false if the present type(s) is/are not supported or the subtree(s) and SAN(s) don't match.
+         * Note: the state of 'match' is meaningless without 'present' also being true.
+         */
+        if (match.present && san_context->match) {
+            san_context->match->present = true;
+            san_context->match->isMatch &= match.isMatch;
+        }
+    }
+
+}
+
+static OSStatus nc_compare_subjectAltName_to_subtrees(void *context, SecCEGeneralNameType gnType, const DERItem *generalName) {
+    nc_san_match_context_t *san_context = context;
+    CFArrayRef subtrees = NULL;
+    if (san_context) {
+        subtrees = san_context->subtrees;
+    }
+    if (subtrees) {
+        CFIndex num_trees = CFArrayGetCount(subtrees);
+        CFRange range = { 0, num_trees };
+        match_t match = { false, false };
+        nc_match_context_t match_context = {gnType, generalName, &match};
+        CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &match_context);
+        
+        /*
+         * We set the SAN context match struct as follows:
+         * 'present' is true if there's any subtree of the same type as any SAN
+         * 'match' is false if the present type(s) is/are not supported or the subtree(s) and SAN(s) don't match.
+         * Note: the state of 'match' is meaningless without 'present' also being true.
+         */
+        if (match.present && san_context->match) {
+            san_context->match->present = true;
+            san_context->match->isMatch &= match.isMatch;
+        }
+        
+        return errSecSuccess;
+    }
+    
+    return errSecInvalidCertificate;
+}
+
+OSStatus SecNameContraintsMatchSubtrees(SecCertificateRef certificate, CFArrayRef subtrees, bool *matched) {
+    CFDataRef subject = NULL;
+    OSStatus status = errSecSuccess;
+    CFArrayRef rfc822Names = NULL;
+    
+    require_action_quiet(subject = SecCertificateCopySubjectSequence(certificate),
+                         out,
+                         status = errSecInvalidCertificate);
+    const DERItem *subjectAltNames = SecCertificateGetSubjectAltName(certificate);
+
+    /* Reject certificates with neither Subject Name nor SubjectAltName */
+    require_action_quiet(!isEmptySubject(subject) || subjectAltNames, out, *matched = false);
+    
+    /* Verify that the subject name is within any of the subtrees for X.500 distinguished names */
+    require_action_quiet(nc_compare_subject_to_subtrees(subject,subtrees), out, *matched = false);
+    
+    match_t san_match = { false, true };
+    nc_san_match_context_t san_context = {subtrees, &san_match};
+    
+    /* If there are no subjectAltNames, then determine if there's a matching emailAddress in the Subject */
+    if (!subjectAltNames) {
+        rfc822Names = SecCertificateCopyRFC822Names(certificate);
+        /* If there's also no emailAddress field then subject match is enough. */
+        require_action_quiet(rfc822Names, out, *matched = true);
+        CFRange range = { 0 , CFArrayGetCount(rfc822Names) };
+        CFArrayApplyFunction(rfc822Names, range, nc_compare_RFC822Name_to_subtrees, &san_context);
+        
+        if (san_match.present && !san_match.isMatch) {
+            *matched = false;
+        }
+        else {
+            *matched = true;
+        }
+    }
+    else {
+        /* And verify that each of the alternative names in the subjectAltName extension (critical or non-critical)
+         * is within any of the subtrees for that name type. */
+        status = SecCertificateParseGeneralNames(subjectAltNames,
+                                                 &san_context,
+                                                 nc_compare_subjectAltName_to_subtrees);
+        /* If failed to parse or failed to match SAN(s) to subtree(s) of same type */
+        if((status != errSecSuccess) || (san_match.present && !san_match.isMatch)) {
+            *matched = false;
+        }
+        else {
+            *matched = true;
+        }
+    }
+    
+out:
+    CFReleaseNull(subject);
+    CFReleaseNull(rfc822Names);
+    return status;
+}
+
+/* The recommended processing algorithm states:
+ *    If permittedSubtrees is present in the certificate, set the permitted_subtrees state variable to the intersection
+ *    of its previous value and the value indicated in the extension field.
+ * However, in practice, certs are issued with permittedSubtrees whose intersection would be the empty set. Wherever
+ * a new permittedSubtree is a subset of an existing subtree, we'll replace the existing subtree; otherwise, we just
+ * append the new subtree.
+ */
+static void nc_intersect_tree_with_subtrees (const void *value, void *context) {
+    CFDataRef new_subtree = value;
+    CFMutableArrayRef *existing_subtrees = context;
+    
+    if (!new_subtree || !*existing_subtrees) return;
+    
+    /* convert new subtree to DERItem */
+    const DERItem general_name = { (unsigned char *)CFDataGetBytePtr(new_subtree), CFDataGetLength(new_subtree) };
+    DERDecodedInfo general_name_content;
+    if(DR_Success != DERDecodeItem(&general_name, &general_name_content)) return;
+    
+    SecCEGeneralNameType gnType;
+    DERItem *new_subtree_item = &general_name_content.content;
+    
+    /* Attempt to intersect if one of the supported types: DirectoryName and DNSName.
+     * Otherwise, just append the new tree. 
+     */
+    switch (general_name_content.tag) {
+        case ASN1_CONTEXT_SPECIFIC | 2: {
+            gnType = GNT_DNSName;
+            break;
+        }
+        case ASN1_CONTEXT_SPECIFIC | ASN1_CONSTRUCTED | 4: {
+            gnType = GNT_DirectoryName;
+            break;
+        }
+        default: {
+            CFArrayAppendValue(*existing_subtrees, new_subtree);
+            return;
+        }
+    }
+    
+    CFIndex subtreeIX;
+    CFIndex num_existing_subtrees = CFArrayGetCount(*existing_subtrees);
+    match_t match = { false, false };
+    nc_match_context_t match_context = { gnType, new_subtree_item, &match};
+    for (subtreeIX = 0; subtreeIX < num_existing_subtrees; subtreeIX++) {
+        CFDataRef candidate_subtree = CFArrayGetValueAtIndex(*existing_subtrees, subtreeIX);
+        /* Convert candidate subtree to DERItem */
+        const DERItem candidate = { (unsigned char *)CFDataGetBytePtr(candidate_subtree), CFDataGetLength(candidate_subtree) };
+        DERDecodedInfo candidate_content;
+        /* We could probably just delete any subtrees in the array that don't decode */
+        if(DR_Success != DERDecodeItem(&candidate, &candidate_content)) continue;
+        
+        OSStatus status = SecCertificateParseGeneralNameContentProperty(candidate_content.tag,
+                                                                        &candidate_content.content,
+                                                                        &match_context,
+                                                                        nc_compare_subtree);
+        if((status == errSecSuccess) && match.present && match.isMatch) {
+            break;
+        }
+    }
+    if (subtreeIX == num_existing_subtrees) {
+        /* No matches found. Append new subtree */
+        CFArrayAppendValue(*existing_subtrees, new_subtree);
+    }
+    else {
+        CFArraySetValueAtIndex(*existing_subtrees, subtreeIX, new_subtree);
+    }
+    return;
+    
+}
+
+void SecNameConstraintsIntersectSubtrees(CFMutableArrayRef subtrees_state, CFArrayRef subtrees_new) {
+    assert(subtrees_state);
+    assert(subtrees_new);
+    
+    CFIndex num_new_trees = CFArrayGetCount(subtrees_new);
+    CFRange range = { 0, num_new_trees };
+    CFArrayApplyFunction(subtrees_new, range, nc_intersect_tree_with_subtrees, &subtrees_state);
+}