403 lines
16 KiB
Objective-C
403 lines
16 KiB
Objective-C
//
|
|
// MYAnonymousIdentity.m
|
|
// MYUtilities
|
|
//
|
|
// Created by Jens Alfke on 12/5/14.
|
|
//
|
|
|
|
#import "MYAnonymousIdentity.h"
|
|
#import "HammerspoonCertTemplate.h"
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
#import <Security/Security.h>
|
|
|
|
|
|
// Key size of kCertTemplate:
|
|
#define kKeySizeInBits 2048
|
|
|
|
// These are offsets into kCertTemplate where values need to be substituted:
|
|
#define kSerialLength 1
|
|
#define kDateLength 13
|
|
#define kPublicKeyLength 270u
|
|
#define kCSROffset 0
|
|
#define kSignatureLength 256u
|
|
|
|
|
|
static BOOL checkErr(OSStatus err, NSError** outError);
|
|
static NSData* generateAnonymousCert(SecKeyRef publicKey, SecKeyRef privateKey,
|
|
NSTimeInterval expirationInterval,
|
|
NSError** outError);
|
|
static BOOL checkCertValid(SecCertificateRef cert, NSTimeInterval expirationInterval);
|
|
static BOOL generateRSAKeyPair(int sizeInBits,
|
|
BOOL permanent,
|
|
NSString* label,
|
|
SecKeyRef *publicKey,
|
|
SecKeyRef *privateKey,
|
|
NSError** outError);
|
|
static NSData* getPublicKeyData(SecKeyRef publicKey);
|
|
static NSData* signData(SecKeyRef privateKey, NSData* inputData);
|
|
static SecCertificateRef addCertToKeychain(NSData* certData, NSString* label,
|
|
NSError** outError);
|
|
static SecIdentityRef findIdentity(NSString* label, NSTimeInterval expirationInterval);
|
|
|
|
#if TARGET_OS_IPHONE
|
|
static void removePublicKey(SecKeyRef publicKey);
|
|
#endif
|
|
|
|
|
|
SecIdentityRef MYGetOrCreateAnonymousIdentity(NSString* label,
|
|
NSTimeInterval expirationInterval,
|
|
NSError** outError)
|
|
{
|
|
NSCParameterAssert(label);
|
|
SecIdentityRef ident = findIdentity(label, expirationInterval);
|
|
if (!ident) {
|
|
NSLog(@"Generating new anonymous self-signed SSL identity labeled \"%@\"...", label);
|
|
SecKeyRef publicKey, privateKey;
|
|
if (!generateRSAKeyPair(kKeySizeInBits, YES, label, &publicKey, &privateKey, outError))
|
|
return NULL;
|
|
NSData* certData = generateAnonymousCert(publicKey,privateKey, expirationInterval,outError);
|
|
if (!certData)
|
|
return NULL;
|
|
SecCertificateRef certRef = addCertToKeychain(certData, label, outError);
|
|
if (!certRef)
|
|
return NULL;
|
|
#if TARGET_OS_IPHONE
|
|
removePublicKey(publicKey); // workaround for Radar 18205627
|
|
ident = findIdentity(label, expirationInterval);
|
|
if (!ident)
|
|
checkErr(errSecItemNotFound, outError);
|
|
#else
|
|
if (checkErr(SecIdentityCreateWithCertificate(NULL, certRef, &ident), outError))
|
|
CFAutorelease(ident);
|
|
#endif
|
|
if (!ident)
|
|
NSLog(@"MYAnonymousIdentity: Can't find identity we just created");
|
|
}
|
|
return ident;
|
|
}
|
|
|
|
|
|
static BOOL checkErr(OSStatus err, NSError** outError) {
|
|
if (err == noErr)
|
|
return YES;
|
|
NSDictionary* info = nil;
|
|
#if !TARGET_OS_IPHONE
|
|
NSString* message = CFBridgingRelease(SecCopyErrorMessageString(err, NULL));
|
|
if (message)
|
|
info = @{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ (%d)", message, (int)err]};
|
|
#endif
|
|
if (outError)
|
|
*outError = [NSError errorWithDomain: NSOSStatusErrorDomain code: err userInfo: info];
|
|
return NO;
|
|
}
|
|
|
|
|
|
// Generates an RSA key-pair, optionally adding it to the keychain.
|
|
static BOOL generateRSAKeyPair(int sizeInBits,
|
|
BOOL permanent,
|
|
NSString* label,
|
|
SecKeyRef *publicKey,
|
|
SecKeyRef *privateKey,
|
|
NSError** outError)
|
|
{
|
|
#if TARGET_OS_IPHONE
|
|
NSDictionary *keyAttrs = @{(__bridge id)kSecAttrIsPermanent: @(permanent),
|
|
(__bridge id)kSecAttrLabel: label};
|
|
#endif
|
|
NSDictionary *pairAttrs = @{(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
|
|
(__bridge id)kSecAttrKeySizeInBits: @(sizeInBits),
|
|
(__bridge id)kSecAttrLabel: label,
|
|
#if TARGET_OS_IPHONE
|
|
(__bridge id)kSecPublicKeyAttrs: keyAttrs,
|
|
(__bridge id)kSecPrivateKeyAttrs: keyAttrs
|
|
#else
|
|
(__bridge id)kSecAttrIsPermanent: @(permanent)
|
|
#endif
|
|
};
|
|
if (!checkErr(SecKeyGeneratePair((__bridge CFDictionaryRef)pairAttrs, publicKey, privateKey),
|
|
outError))
|
|
return NO;
|
|
CFAutorelease(*publicKey);
|
|
CFAutorelease(*privateKey);
|
|
return YES;
|
|
}
|
|
|
|
|
|
// Generates a self-signed certificate, returning the cert data.
|
|
static NSData* generateAnonymousCert(SecKeyRef publicKey, SecKeyRef privateKey,
|
|
NSTimeInterval expirationInterval,
|
|
NSError** outError __unused)
|
|
{
|
|
// Read the original template certificate file:
|
|
NSMutableData* data = [NSMutableData dataWithBytes: kCertTemplate length: sizeof(kCertTemplate)];
|
|
uint8_t* buf = data.mutableBytes;
|
|
|
|
// Write the serial number:
|
|
if (SecRandomCopyBytes(kSecRandomDefault, kSerialLength, &buf[kSerialOffset]) != 0) {
|
|
NSLog(@"SecRandomCopyBytes() failed");
|
|
return nil;
|
|
}
|
|
buf[kSerialOffset] &= 0x7F; // non-negative
|
|
|
|
// Write the issue and expiration dates:
|
|
NSDateFormatter *x509DateFormatter = [[NSDateFormatter alloc] init];
|
|
x509DateFormatter.dateFormat = @"yyMMddHHmmss'Z'";
|
|
x509DateFormatter.timeZone = [NSTimeZone timeZoneWithName: @"GMT"];
|
|
NSDate* date = [NSDate date];
|
|
const char* dateStr = [[x509DateFormatter stringFromDate: date] UTF8String];
|
|
memcpy(&buf[kIssueDateOffset], dateStr, kDateLength);
|
|
date = [date dateByAddingTimeInterval: expirationInterval];
|
|
dateStr = [[x509DateFormatter stringFromDate: date] UTF8String];
|
|
memcpy(&buf[kExpDateOffset], dateStr, kDateLength);
|
|
|
|
// Copy the public key:
|
|
NSData* keyData = getPublicKeyData(publicKey);
|
|
if (keyData.length != kPublicKeyLength) {
|
|
NSLog(@"ERROR: keyData.length (%lu) != kPublicKeyLength (%i)", keyData.length, kPublicKeyLength);
|
|
return nil;
|
|
}
|
|
memcpy(&buf[kPublicKeyOffset], keyData.bytes, kPublicKeyLength);
|
|
|
|
// Sign the cert:
|
|
NSData* csr = [data subdataWithRange: NSMakeRange(kCSROffset, kCSRLength)];
|
|
NSData* sig = signData(privateKey, csr);
|
|
if (sig.length != kSignatureLength) {
|
|
NSLog(@"ERROR: sig.length (%lu) != kSignatureLength (%i)", sig.length, kSignatureLength);
|
|
return nil;
|
|
}
|
|
[data appendData: sig];
|
|
|
|
return data;
|
|
}
|
|
|
|
|
|
// Returns the data of an RSA public key, in the format used in an X.509 certificate.
|
|
static NSData* getPublicKeyData(SecKeyRef publicKey) {
|
|
#if TARGET_OS_IPHONE
|
|
NSDictionary *info = @{(__bridge id)kSecValueRef: (__bridge id)publicKey,
|
|
(__bridge id)kSecReturnData: @YES};
|
|
CFTypeRef data;
|
|
if (SecItemCopyMatching((__bridge CFDictionaryRef)info, &data) != noErr) {
|
|
Log(@"SecItemCopyMatching failed; input = %@", info);
|
|
return nil;
|
|
}
|
|
Assert(data!=NULL);
|
|
return CFBridgingRelease(data);
|
|
#else
|
|
CFDataRef data = NULL;
|
|
if (SecItemExport(publicKey, kSecFormatBSAFE, 0, NULL, &data) != noErr)
|
|
return nil;
|
|
return (NSData*)CFBridgingRelease(data);
|
|
#endif
|
|
}
|
|
|
|
|
|
#if TARGET_OS_IPHONE
|
|
// workaround for Radar 18205627: When iOS reads an identity from the keychain, it may accidentally
|
|
// get the public key instead of the private key. The workaround is to remove the public key so
|
|
// that only the private one is obtainable. --jpa 6/2015
|
|
static void removePublicKey(SecKeyRef publicKey) {
|
|
NSDictionary* query = @{(__bridge id)kSecValueRef: (__bridge id)publicKey};
|
|
OSStatus err = SecItemDelete((__bridge CFDictionaryRef)query);
|
|
if (err)
|
|
NSLog(@"Couldn't delete public key: err %d", (int)err);
|
|
}
|
|
#endif
|
|
|
|
|
|
// Signs a data blob using a private key. Padding is PKCS1 with SHA-1 digest.
|
|
static NSData* signData(SecKeyRef privateKey, NSData* inputData) {
|
|
#if TARGET_OS_IPHONE
|
|
uint8_t digest[CC_SHA1_DIGEST_LENGTH];
|
|
CC_SHA1(inputData.bytes, (CC_LONG)inputData.length, digest);
|
|
|
|
size_t sigLen = 1024;
|
|
uint8_t sigBuf[sigLen];
|
|
OSStatus err = SecKeyRawSign(privateKey, kSecPaddingPKCS1SHA1,
|
|
digest, sizeof(digest),
|
|
sigBuf, &sigLen);
|
|
if(err) {
|
|
NSLog(@"SecKeyRawSign failed: %ld", (long)err);
|
|
return nil;
|
|
}
|
|
return [NSData dataWithBytes: sigBuf length: sigLen];
|
|
|
|
#else
|
|
SecTransformRef transform = SecSignTransformCreate(privateKey, NULL);
|
|
if (!transform)
|
|
return nil;
|
|
NSData* resultData = nil;
|
|
if (SecTransformSetAttribute(transform, kSecDigestTypeAttribute, kSecDigestSHA1, NULL)
|
|
&& SecTransformSetAttribute(transform, kSecTransformInputAttributeName,
|
|
(__bridge CFDataRef)inputData, NULL)) {
|
|
resultData = CFBridgingRelease(SecTransformExecute(transform, NULL));
|
|
}
|
|
CFRelease(transform);
|
|
return resultData;
|
|
#endif
|
|
}
|
|
|
|
|
|
// Adds a certificate to the keychain, tagged with a label for future lookup.
|
|
static SecCertificateRef addCertToKeychain(NSData* certData, NSString* label,
|
|
NSError** outError) {
|
|
SecCertificateRef certRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
|
|
if (!certRef) {
|
|
checkErr(errSecIO, outError);
|
|
return NULL;
|
|
}
|
|
CFAutorelease(certRef);
|
|
NSDictionary* attrs = @{(__bridge id)kSecClass: (__bridge id)kSecClassCertificate,
|
|
(__bridge id)kSecValueRef: (__bridge id)certRef,
|
|
#if TARGET_OS_IPHONE
|
|
(__bridge id)kSecAttrLabel: label
|
|
#endif
|
|
};
|
|
CFTypeRef result;
|
|
OSStatus err = SecItemAdd((__bridge CFDictionaryRef)attrs, &result);
|
|
if (err != noErr) {
|
|
NSLog(@"ERROR: SecItemAdd() returned %i", err);
|
|
}
|
|
|
|
#if !TARGET_OS_IPHONE
|
|
// kSecAttrLabel is not settable on Mac OS (it's automatically generated from the principal
|
|
// name.) Instead we use the "preference" mapping mechanism, which only exists on Mac OS.
|
|
if (!err)
|
|
err = SecCertificateSetPreferred(certRef, (__bridge CFStringRef)label, NULL);
|
|
if (!err) {
|
|
// Check if this is an identity cert, i.e. we have the corresponding private key.
|
|
// If so, we'll also set the preference for the resulting SecIdentityRef.
|
|
SecIdentityRef identRef;
|
|
if (SecIdentityCreateWithCertificate(NULL, certRef, &identRef) == noErr) {
|
|
err = SecIdentitySetPreferred(identRef, (__bridge CFStringRef)label, NULL);
|
|
CFRelease(identRef);
|
|
}
|
|
}
|
|
#endif
|
|
checkErr(err, outError);
|
|
return certRef;
|
|
}
|
|
|
|
|
|
// Looks up an identity (cert + private key) by the cert's label.
|
|
static SecIdentityRef findIdentity(NSString* label, NSTimeInterval expirationInterval) {
|
|
SecIdentityRef identity;
|
|
#if TARGET_OS_IPHONE
|
|
NSDictionary* query = @{(__bridge id)kSecClass: (__bridge id)kSecClassIdentity,
|
|
(__bridge id)kSecAttrLabel: label,
|
|
(__bridge id)kSecReturnRef: @YES};
|
|
CFTypeRef ref = NULL;
|
|
OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)query, &ref);
|
|
if (err) {
|
|
AssertEq(err, errSecItemNotFound); // other err indicates query dict is malformed
|
|
return NULL;
|
|
}
|
|
identity = (SecIdentityRef)ref;
|
|
#else
|
|
identity = SecIdentityCopyPreferred((__bridge CFStringRef)label, NULL, NULL);
|
|
#endif
|
|
|
|
if (identity) {
|
|
// Check that the cert hasn't expired yet:
|
|
CFAutorelease(identity);
|
|
SecCertificateRef cert;
|
|
if (SecIdentityCopyCertificate(identity, &cert) == noErr) {
|
|
if (!checkCertValid(cert, expirationInterval)) {
|
|
NSLog(@"SSL identity labeled \"%@\" has expired", label);
|
|
identity = NULL;
|
|
MYDeleteAnonymousIdentity(label);
|
|
}
|
|
CFRelease(cert);
|
|
} else {
|
|
identity = NULL;
|
|
}
|
|
}
|
|
return identity;
|
|
}
|
|
|
|
|
|
NSData* MYGetCertificateDigest(SecCertificateRef cert) {
|
|
CFDataRef data = SecCertificateCopyData(cert);
|
|
uint8_t digest[CC_SHA1_DIGEST_LENGTH];
|
|
CC_SHA1(CFDataGetBytePtr(data), (CC_LONG)CFDataGetLength(data), digest);
|
|
CFRelease(data);
|
|
return [NSData dataWithBytes: digest length: sizeof(digest)];
|
|
}
|
|
|
|
|
|
#if TARGET_OS_IPHONE
|
|
static NSDictionary* getItemAttributes(CFTypeRef cert) {
|
|
NSDictionary* query = @{(__bridge id)kSecValueRef: (__bridge id)cert,
|
|
(__bridge id)kSecReturnAttributes: @YES};
|
|
CFDictionaryRef attrs = NULL;
|
|
OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef*)&attrs);
|
|
if (err) {
|
|
AssertEq(err, errSecItemNotFound);
|
|
return NULL;
|
|
}
|
|
Assert(attrs);
|
|
return CFBridgingRelease(attrs);
|
|
}
|
|
#endif
|
|
|
|
|
|
#if !TARGET_OS_IPHONE
|
|
static double relativeTimeFromOID(NSDictionary* values, CFTypeRef oid) {
|
|
NSNumber* dateNum = values[(__bridge id)oid][@"value"];
|
|
if (!dateNum)
|
|
return 0.0;
|
|
return dateNum.doubleValue - CFAbsoluteTimeGetCurrent();
|
|
}
|
|
#endif
|
|
|
|
|
|
// Returns YES if the cert has not yet expired.
|
|
static BOOL checkCertValid(SecCertificateRef cert, NSTimeInterval expirationInterval __unused) {
|
|
#if TARGET_OS_IPHONE
|
|
NSDictionary* attrs = getItemAttributes(cert);
|
|
// The fucked-up iOS Keychain API doesn't expose the cert expiration date, only the date the
|
|
// item was added to the keychain. So derive it based on the current expiration interval:
|
|
NSDate* creationDate = attrs[(__bridge id)kSecAttrCreationDate];
|
|
return creationDate && -[creationDate timeIntervalSinceNow] < expirationInterval;
|
|
#else
|
|
CFArrayRef oids = (__bridge CFArrayRef)@[(__bridge id)kSecOIDX509V1ValidityNotAfter,
|
|
(__bridge id)kSecOIDX509V1ValidityNotBefore];
|
|
NSDictionary* values = CFBridgingRelease(SecCertificateCopyValues(cert, oids, NULL));
|
|
return relativeTimeFromOID(values, kSecOIDX509V1ValidityNotAfter) >= 0.0
|
|
&& relativeTimeFromOID(values, kSecOIDX509V1ValidityNotBefore) <= 0.0;
|
|
#endif
|
|
}
|
|
|
|
|
|
BOOL MYDeleteAnonymousIdentity(NSString* label) {
|
|
NSDictionary* attrs = @{(__bridge id)kSecClass: (__bridge id)kSecClassIdentity,
|
|
(__bridge id)kSecAttrLabel: label};
|
|
OSStatus err = SecItemDelete((__bridge CFDictionaryRef)attrs);
|
|
if (err != noErr && err != errSecItemNotFound)
|
|
NSLog(@"Unexpected error %d deleting identity from keychain", (int)err);
|
|
return (err == noErr);
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright (c) 2014-15, Jens Alfke <jens@mooseyard.com>. All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without modification, are permitted
|
|
provided that the following conditions are met:
|
|
|
|
* Redistributions of source code must retain the above copyright notice, this list of conditions
|
|
and the following disclaimer.
|
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions
|
|
and the following disclaimer in the documentation and/or other materials provided with the
|
|
distribution.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
|
|
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
|
|
BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|