nameconstraints.c   [plain text]


/*
 * 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);
}