pkinit_apple_cert_store.c   [plain text]


/*
 * Copyright (c) 2004 Apple Computer, 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@
 */

/*
 * pkinit_apple_cert_store.c - PKINIT certificate storage/retrieval utilities, 
 *			       MAC OS X version
 *
 * Created 26 May 2004 by Doug Mitchell at Apple.
 */
 
#include "pkinit_cert_store.h"
#include "pkinit_asn1.h"
#include "pkinit_apple_utils.h"
#include <CoreFoundation/CFString.h>
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#include <assert.h>
#include <CoreServices/../Frameworks/CarbonCore.framework/Headers/MacErrors.h>
#include <CommonCrypto/CommonDigest.h>
#include <sys/errno.h>

/*
 * Client cert info is stored in preferences with this following parameters:
 *
 * key      = kPkinitClientCertKey
 * appID    = kPkinitClientCertApp
 * username = kCFPreferencesCurrentUser
 * hostname = kCFPreferencesAnyHost   
 *
 * The stored property list is a CFDictionary. Keys in the dictionary are
 * principal names (e.g. foobar@REALM.LOCAL). 
 *
 * Values in the dictionary are raw data containing the DER-encoded issuer and
 * serial number of the certificate. 
 *
 * When obtaining a PKINIT cert, if an entry in the CFDictionary for the specified
 * principal is not found, the entry for the default will be used if it's there.
 */

/* 
 * NOTE: ANSI C code requires an Apple-Custom -fconstant-cfstrings CFLAGS to 
 * use CFSTR in a const declaration so we just declare the C strings here. 
 */
#define kPkinitClientCertKey		"KRBClientCert"
#define kPkinitClientCertApp		"edu.mit.Kerberos.pkinit"

/*
 * KDC cert stored in this keychain. It's linked to systemkeychain so that if
 * a root process tries to unlock it, it auto-unlocks.
 */
#define KDC_KEYCHAIN    "/var/db/krb5kdc/kdc.keychain"

/* 
 * Given a certificate, obtain the DER-encoded issuer and serial number. Result
 * is mallocd and must be freed by caller. 
 */
static OSStatus pkinit_get_cert_issuer_sn(
    SecCertificateRef certRef, 
    CSSM_DATA *issuerSerial)		/* mallocd and RETURNED */
{
    OSStatus ortn;
    CSSM_DATA certData;
    krb5_data INIT_KDATA(issuerSerialKrb);
    krb5_data certDataKrb;
    krb5_error_code krtn;
    
    assert(certRef != NULL);
    assert(issuerSerial != NULL);
    
    ortn = SecCertificateGetData(certRef, &certData);
    if(ortn) {
	pkiCssmErr("SecCertificateGetData", ortn);
	return ortn;
    }
    PKI_CSSM_TO_KRB_DATA(&certData, &certDataKrb);
    krtn = krb5int_pkinit_get_issuer_serial(&certDataKrb, &issuerSerialKrb);
    if(krtn) {
	return CSSMERR_CL_INVALID_DATA;
    }
    PKI_KRB_TO_CSSM_DATA(&issuerSerialKrb, issuerSerial);
    return noErr;
}

/* 
 * Determine if specified identity's cert's issuer and serial number match the
 * provided issuer and serial number. Returns nonzero on match, else returns zero.
 */
static int pkinit_issuer_sn_match(
    SecIdentityRef idRef, 
    const CSSM_DATA *matchIssuerSerial)
{
    OSStatus ortn;
    SecCertificateRef certRef = NULL;
    CSSM_DATA INIT_CDATA(certIssuerSerial);
    int ourRtn = 0;

    assert(idRef != NULL);
    assert(matchIssuerSerial != NULL);
    
    /* Get this cert's issuer/serial number */
    ortn = SecIdentityCopyCertificate(idRef, &certRef);
    if(ortn) {
	pkiCssmErr("SecIdentityCopyCertificate", ortn);
	return 0;
    }
    /* subsequent errors to errOut: */
    ortn = pkinit_get_cert_issuer_sn(certRef, &certIssuerSerial);
    if(ortn) {
	pkiCssmErr("SecIdentityCopyCertificate", ortn);
	goto errOut;
    }
    ourRtn = pkiCompareCssmData(matchIssuerSerial, &certIssuerSerial) ? 1 : 0;
errOut:
    if(certRef != NULL) {
	CFRelease(certRef);
    }
    if(certIssuerSerial.Data != NULL) {
	free(certIssuerSerial.Data);
    }
    return ourRtn;
}

/*
 * Search specified keychain/array/NULL (NULL meaning the default search list) for
 * an Identity matching specified key usage and optional Issuer/Serial number. 
 * If issuer/serial is specified and no identities match, or if no identities found
 * matching specified Key usage, errSecItemNotFound is returned.
 *
 * Caller must CFRelease a non-NULL returned idRef. 
 */
static OSStatus pkinit_search_ident(
    CFTypeRef		keychainOrArray,
    CSSM_KEYUSE		keyUsage,
    const CSSM_DATA     *issuerSerial,  /* optional */
    SecIdentityRef      *foundId)	/* RETURNED */
{
    OSStatus ortn;
    SecIdentityRef idRef = NULL;
    SecIdentitySearchRef srchRef = NULL;
    
    ortn = SecIdentitySearchCreate(keychainOrArray, keyUsage, &srchRef);
    if(ortn) {
	pkiCssmErr("SecIdentitySearchCreate", ortn);
	return ortn;
    }
    do {
	ortn = SecIdentitySearchCopyNext(srchRef, &idRef);
	if(ortn != noErr) {
	    break;
	}
	if(issuerSerial == NULL) {
	    /* no match needed, we're done - this is the KDC cert case */
	    break;
	}
	else if(pkinit_issuer_sn_match(idRef, issuerSerial)) {
	    /* match, we're done */
	    break;
	}
	/* finished with this one */
	CFRelease(idRef);
	idRef = NULL;
    } while(ortn == noErr);
    
    CFRelease(srchRef);
    if(idRef == NULL) {
	return errSecItemNotFound;
    }
    else {
	*foundId = idRef;
	return noErr;
    }
}

/*
 * In Mac OS terms, get the keychain on which a given identity resides. 
 */
static krb5_error_code pkinit_cert_to_db(
    krb5_pkinit_signing_cert_t   idRef,
    krb5_pkinit_cert_db_t	 *dbRef)
{
    SecKeychainRef kcRef = NULL;
    SecKeyRef keyRef = NULL;
    OSStatus ortn;

    /* that's an identity - get the associated key's keychain */
    ortn = SecIdentityCopyPrivateKey((SecIdentityRef)idRef, &keyRef);
    if(ortn) {
	pkiCssmErr("SecIdentityCopyPrivateKey", ortn);
	return ortn;
    }
    ortn = SecKeychainItemCopyKeychain((SecKeychainItemRef)keyRef, &kcRef);
    if(ortn) {
	pkiCssmErr("SecKeychainItemCopyKeychain", ortn);
    }
    else {
	*dbRef = (krb5_pkinit_cert_db_t)kcRef;
    }
    CFRelease(keyRef);
    return ortn;
}

/* 
 * Obtain the CFDictionary representing this user's PKINIT client cert prefs, if it 
 * exists. Returns noErr or errSecItemNotFound as appropriate. 
 */
static OSStatus pkinit_get_pref_dict(
    CFDictionaryRef *dict)
{
    CFDictionaryRef theDict;
    theDict = (CFDictionaryRef)CFPreferencesCopyValue(CFSTR(kPkinitClientCertKey),
	CFSTR(kPkinitClientCertApp), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
    if(theDict == NULL) {
	pkiDebug("pkinit_get_pref_dict: no kPkinitClientCertKey\n");
	return errSecItemNotFound;
    }
    if(CFGetTypeID(theDict) != CFDictionaryGetTypeID()) {
	pkiDebug("pkinit_get_pref_dict: bad kPkinitClientCertKey pref\n");
	CFRelease(theDict);
	return errSecItemNotFound;
    }
    *dict = theDict;
    return noErr;
}

#pragma mark --- Public client side functions ---

/*
 * Obtain signing cert for specified principal. On successful return, 
 * caller must eventually release the cert with krb5_pkinit_release_cert().
 */
krb5_error_code krb5_pkinit_get_client_cert(
    const char			*principal,     /* full principal string */
    krb5_pkinit_signing_cert_t	*client_cert)
{
    CFDataRef issuerSerial = NULL;
    CSSM_DATA issuerSerialData;
    SecIdentityRef idRef = NULL;
    OSStatus ortn;
    CFDictionaryRef theDict = NULL;
    krb5_error_code ourRtn = 0;
    
    if(principal == NULL) {
	return KRB5_PRINC_NOMATCH;
    }
    
    /* Is there a stored preference for PKINIT certs for this user? */
    ortn = pkinit_get_pref_dict(&theDict);
    if(ortn) {
	return KRB5_PRINC_NOMATCH;
    }
    
    /* Entry in the dictionary for specified principal? */
    CFStringRef cfPrinc = CFStringCreateWithCString(NULL, principal, 
	kCFStringEncodingASCII);
    issuerSerial = (CFDataRef)CFDictionaryGetValue(theDict, cfPrinc);
    CFRelease(cfPrinc);
    if(issuerSerial == NULL) {
	pkiDebug("krb5_pkinit_get_client_cert: no identity found\n");
	ourRtn = KRB5_PRINC_NOMATCH;
	goto errOut;
    }
    if(CFGetTypeID(issuerSerial) != CFDataGetTypeID()) {
	pkiDebug("krb5_pkinit_get_client_cert: bad kPkinitClientCertKey value\n");
	ourRtn = KRB5_PRINC_NOMATCH;
	goto errOut;
    }
    
    issuerSerialData.Data = (uint8 *)CFDataGetBytePtr(issuerSerial);
    issuerSerialData.Length = CFDataGetLength(issuerSerial);
    
    /* find a cert with that issuer/serial number in default search list */
    ortn = pkinit_search_ident(NULL, CSSM_KEYUSE_SIGN | CSSM_KEYUSE_ENCRYPT, 
	&issuerSerialData, &idRef);
    if(ortn) {
	pkiDebug("krb5_pkinit_get_client_cert: no identity found!\n");
	pkiCssmErr("pkinit_search_ident", ortn);
	ourRtn = KRB5_PRINC_NOMATCH;
    }
    else {
	*client_cert = (krb5_pkinit_signing_cert_t)idRef;
    }
errOut:
    if(theDict) {
	CFRelease(theDict);
    }
    return ourRtn;
}

/* 
 * Determine if the specified client has a signing cert. Returns TRUE
 * if so, else returns FALSE.
 */
krb5_boolean krb5_pkinit_have_client_cert(
    const char			*principal)	/* full principal string */
{
    krb5_pkinit_signing_cert_t signing_cert = NULL;
    krb5_error_code krtn;
    
    krtn = krb5_pkinit_get_client_cert(principal, &signing_cert);
    if(krtn) {
	return FALSE;
    }
    if(signing_cert != NULL) {
	krb5_pkinit_release_cert(signing_cert);
	return TRUE;
    }
    else {
	return FALSE;
    }
}

/*
 * Store the specified certificate (or, more likely, some platform-dependent
 * reference to it) as the specified principal's signing certificate. Passing
 * in NULL for the client_cert has the effect of deleting the relevant entry
 * in the cert storage.
 */
krb5_error_code krb5_pkinit_set_client_cert_from_signing_cert(
    const char			*principal,     /* full principal string */
    krb5_pkinit_signing_cert_t	client_cert)
{
    SecIdentityRef idRef = (SecIdentityRef)client_cert;
    SecCertificateRef certRef = NULL;
    OSStatus ortn;
    krb5_error_code ourRtn = 0;

    if (NULL != idRef) {
	if (CFGetTypeID(idRef) != SecIdentityGetTypeID()) {
	    ourRtn = KRB5KRB_ERR_GENERIC;
	    goto fin;
	}
	/* Get the cert */
	ortn = SecIdentityCopyCertificate(idRef, &certRef);
	if (ortn) {
	    pkiCssmErr("SecIdentityCopyCertificate", ortn);
	    ourRtn = KRB5KRB_ERR_GENERIC;
	    goto fin;
	}
    }
    ourRtn = krb5_pkinit_set_client_cert(principal, (krb5_pkinit_cert_t)certRef);
fin:
    if (certRef)
	CFRelease(certRef);
    return ourRtn;
}


/*
 * Store the specified certificate (or, more likely, some platform-dependent
 * reference to it) as the specified principal's certificate. Passing
 * in NULL for the client_cert has the effect of deleting the relevant entry
 * in the cert storage.
 */
krb5_error_code krb5_pkinit_set_client_cert(
    const char			*principal,     /* full principal string */
    krb5_pkinit_cert_t		client_cert)
{
    SecCertificateRef certRef = (SecCertificateRef)client_cert;
    OSStatus ortn;
    CSSM_DATA issuerSerial = {0, NULL};
    CFDataRef cfIssuerSerial = NULL;
    CFDictionaryRef existDict = NULL;
    CFMutableDictionaryRef newDict = NULL;
    CFStringRef keyStr = NULL;
    krb5_error_code ourRtn = 0;
    
    if(certRef != NULL) {
	if(CFGetTypeID(certRef) != SecCertificateGetTypeID()) {
	    return KRB5KRB_ERR_GENERIC;
	}
    
	/* Cook up DER-encoded issuer/serial number */
	ortn = pkinit_get_cert_issuer_sn(certRef, &issuerSerial);
	if(ortn) {
	    ourRtn = KRB5KRB_ERR_GENERIC;
	    goto errOut;
	}
    }
    
    /* 
     * Obtain the existing pref for kPkinitClientCertKey as a CFDictionary, or
     * cook up a new one. 
     */
    ortn = pkinit_get_pref_dict(&existDict);
    if(ortn == noErr) {
	/* dup to a mutable dictionary */
	newDict = CFDictionaryCreateMutableCopy(NULL, 0, existDict);
    }
    else {
	if(certRef == NULL) {
	    /* no existing entry, nothing to delete, we're done */
	    return 0;
	}
	newDict = CFDictionaryCreateMutable(NULL, 0,
	    &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    }
    if(newDict == NULL) {
	ourRtn = ENOMEM;
	goto errOut;
    }

    /* issuer / serial number ==> that dictionary */
    keyStr = CFStringCreateWithCString(NULL, principal, kCFStringEncodingASCII);
    if(certRef == NULL) {
	CFDictionaryRemoveValue(newDict, keyStr);
    }
    else {
	cfIssuerSerial = CFDataCreate(NULL, issuerSerial.Data, issuerSerial.Length);
	CFDictionarySetValue(newDict, keyStr, cfIssuerSerial);
    }
    
    /* dictionary ==> prefs */
    CFPreferencesSetValue(CFSTR(kPkinitClientCertKey), newDict, 
	CFSTR(kPkinitClientCertApp), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
    if(CFPreferencesSynchronize(CFSTR(kPkinitClientCertApp), kCFPreferencesCurrentUser, 
	    kCFPreferencesAnyHost)) {
	ourRtn = 0;
    }
    else {
	ourRtn = EACCES;   /* any better ideas? */
    }
errOut:
    if(cfIssuerSerial) {
	CFRelease(cfIssuerSerial);
    }
    if(issuerSerial.Data) {
	free(issuerSerial.Data);
    }
    if(existDict) {
	CFRelease(existDict);
    }
    if(newDict) {
	CFRelease(newDict);
    }
    if(keyStr) {
	CFRelease(keyStr);
    }
    return ourRtn;
}

/* 
 * Obtain a reference to the client's cert database. Specify either principal
 * name or client_cert as obtained from krb5_pkinit_get_client_cert().
 */
krb5_error_code krb5_pkinit_get_client_cert_db(
    const char			*principal,     /* full principal string */
    krb5_pkinit_signing_cert_t	client_cert,    /* optional, from krb5_pkinit_get_client_cert() */
    krb5_pkinit_cert_db_t	*client_cert_db)/* RETURNED */
{
    krb5_error_code krtn;
    krb5_pkinit_signing_cert_t local_cert;
    
    assert((client_cert != NULL) || (principal != NULL));
    if(client_cert == NULL) {
	/* caller didn't provide, look it up */
	krtn = krb5_pkinit_get_client_cert(principal, &local_cert);
	if(krtn) {
	    return krtn;
	}
    }
    else {
	/* easy case */
	local_cert = client_cert;
    }
    krtn = pkinit_cert_to_db(local_cert, client_cert_db);
    if(client_cert == NULL) {
	krb5_pkinit_release_cert(local_cert);
    }
    return krtn;
}

#pragma mark --- Public server side functions ---

/*
 * Obtain the KDC signing cert, with optional CA and specific cert specifiers.
 * CAs and cert specifiers are in the form of DER-encoded issuerAndSerialNumbers.
 *
 * The client_spec argument is typically provided by the client as kdcPkId.
 */
krb5_error_code krb5_pkinit_get_kdc_cert(
    krb5_ui_4			num_trusted_CAs,    /* sizeof *trusted_CAs */
    krb5_data			*trusted_CAs,	    /* optional */
    krb5_data			*client_spec,	    /* optional */
    krb5_pkinit_signing_cert_t *kdc_cert)
{
    SecIdentityRef idRef = NULL;
    OSStatus ortn;
    krb5_error_code ourRtn = 0;
    
    /* OS X: trusted_CAs and client_spec ignored */
    
    ortn = SecIdentityCopySystemIdentity(kSecIdentityDomainKerberosKDC,
	&idRef, NULL);
    if(ortn) {
	pkiCssmErr("SecIdentityCopySystemIdentity", ortn);
	return KRB5_PRINC_NOMATCH;
    }
    *kdc_cert = (krb5_pkinit_signing_cert_t)idRef;
    return ourRtn;
}

/* 
 * Obtain a reference to the KDC's cert database.
 */
krb5_error_code krb5_pkinit_get_kdc_cert_db(
    krb5_pkinit_cert_db_t   *kdc_cert_db)
{
    krb5_pkinit_signing_cert_t kdcCert = NULL;
    krb5_error_code krtn;
    
    krtn = krb5_pkinit_get_kdc_cert(0, NULL, NULL, &kdcCert);
    if(krtn) {
	return krtn;
    }
    krtn = pkinit_cert_to_db(kdcCert, kdc_cert_db);
    krb5_pkinit_release_cert(kdcCert);
    return krtn;
}

/*
 * Release certificate references obtained via krb5_pkinit_get_client_cert() and
 * krb5_pkinit_get_kdc_cert().
 */
void krb5_pkinit_release_cert(
    krb5_pkinit_signing_cert_t   cert)
{
    if(cert == NULL) {
	return;
    }
    CFRelease((CFTypeRef)cert);
}

/*
 * Release database references obtained via krb5_pkinit_get_client_cert_db() and
 * krb5_pkinit_get_kdc_cert_db().
 */
extern void krb5_pkinit_release_cert_db(
    krb5_pkinit_cert_db_t	    cert_db)
{
    if(cert_db == NULL) {
	return;
    }
    CFRelease((CFTypeRef)cert_db);
}


/* 
 * Obtain a mallocd C-string representation of a certificate's SHA1 digest. 
 * Only error is a NULL return indicating memory failure. 
 * Caller must free the returned string.
 */
char *krb5_pkinit_cert_hash_str(
    const krb5_data *cert)
{
    CC_SHA1_CTX ctx;
    char *outstr;
    char *cpOut;
    unsigned char digest[CC_SHA1_DIGEST_LENGTH];
    unsigned dex;
    
    assert(cert != NULL);
    CC_SHA1_Init(&ctx);
    CC_SHA1_Update(&ctx, cert->data, cert->length);
    CC_SHA1_Final(digest, &ctx);
    
    outstr = (char *)malloc((2 * CC_SHA1_DIGEST_LENGTH) + 1);
    if(outstr == NULL) {
	return NULL;
    }
    cpOut = outstr;
    for(dex=0; dex<CC_SHA1_DIGEST_LENGTH; dex++) {
	sprintf(cpOut, "%02X", (unsigned)(digest[dex]));
	cpOut += 2;
    }
    *cpOut = '\0';
    return outstr;
}

/* 
 * Obtain a client's optional list of trusted KDC CA certs (trustedCertifiers)
 * and/or trusted KDC cert (kdcPkId) for a given client and server. 
 * All returned values are mallocd and must be freed by caller; the contents 
 * of the krb5_datas are DER-encoded certificates. 
 */
krb5_error_code krb5_pkinit_get_server_certs(
    const char *client_principal,
    const char *server_principal,
    krb5_data **trusted_CAs,	    /* RETURNED, though return value may be NULL */
    krb5_ui_4 *num_trusted_CAs,	    /* RETURNED */
    krb5_data *kdc_cert)	    /* RETURNED, though may be 0/NULL */
{
    /* nothing for now */
    *trusted_CAs = NULL;
    *num_trusted_CAs = 0;
    kdc_cert->data = NULL;
    kdc_cert->length = 0;
    return 0;
}