Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0
import PackageDescription

let package = Package(
Expand All @@ -22,10 +22,16 @@ let package = Package(
targets: [
.target(
name: "TrustKit",
dependencies: ["TrustKitSwift"],
path: "TrustKit",
exclude: ["Swift"],
resources: [.process("PrivacyInfo.xcprivacy")],
publicHeadersPath: "public",
cSettings: [.define("NS_BLOCK_ASSERTIONS", to: "1", .when(configuration: .release))]
),
.target(
name: "TrustKitSwift",
path: "TrustKit/Swift"
),
]
)
30 changes: 28 additions & 2 deletions TrustKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion TrustKit/Pinning/TSKSPKIHashCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ NS_ASSUME_NONNULL_BEGIN

// The identifier used for the default shared hash cache. Use this identifier
// in the TSKSPKIHashCache constructor to use the shared cache.
static NSString * const kTSKSPKISharedHashCacheIdentifier = @"spki-hash.cache";
// Versioned so that hashes cached by TrustKit 3.x, which rebuilt the SPKI from the key, are not reused.
static NSString * const kTSKSPKISharedHashCacheIdentifier = @"spki-hash-v2.cache";

// Each key is a raw certificate data (for easy lookup) and each value is the certificate's raw SPKI data
typedef NSMutableDictionary<NSData *, NSData *> SPKICacheDictionnary;
Expand Down
203 changes: 9 additions & 194 deletions TrustKit/Pinning/TSKSPKIHashCache.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,122 +13,11 @@
#import "TSKSPKIHashCache.h"
#import "../TSKLog.h"
#import <CommonCrypto/CommonDigest.h>
#import "pinning_utils.h"

#pragma mark Missing ASN1 SPKI Headers

// These are the ASN1 headers for the Subject Public Key Info section of a certificate
// TODO(AD): Are they returned by the new iOS API https://developer.apple.com/documentation/security/2963103-seccertificatecopykey ?
static const unsigned char rsa2048Asn1Header[] =
{
0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
};

static const unsigned char rsa3072Asn1Header[] =
{
0x30, 0x82, 0x01, 0xA2, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86,
0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x8F, 0x00
};

static const unsigned char rsa4096Asn1Header[] =
{
0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00
};

static const unsigned char ecDsaSecp256r1Asn1Header[] =
{
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
0x42, 0x00
};

static const unsigned char ecDsaSecp384r1Asn1Header[] =
{
0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00
};


static BOOL isKeySupported(NSString *publicKeyType, NSNumber *publicKeySize)
{
if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 2048))
{
return YES;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 3072))
{
return YES;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 4096))
{
return YES;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeECSECPrimeRandom]) && ([publicKeySize integerValue] == 256))
{
return YES;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeECSECPrimeRandom]) && ([publicKeySize integerValue] == 384))
{
return YES;
}
return NO;
}


static char *getAsn1HeaderBytes(NSString *publicKeyType, NSNumber *publicKeySize)
{
if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 2048))
{
return (char *)rsa2048Asn1Header;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 3072))
{
return (char *)rsa3072Asn1Header;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 4096))
{
return (char *)rsa4096Asn1Header;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeECSECPrimeRandom]) && ([publicKeySize integerValue] == 256))
{
return (char *)ecDsaSecp256r1Asn1Header;
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeECSECPrimeRandom]) && ([publicKeySize integerValue] == 384))
{
return (char *)ecDsaSecp384r1Asn1Header;
}

@throw([NSException exceptionWithName:@"Unsupported public key algorithm" reason:@"Tried to generate the SPKI hash for an unsupported key algorithm" userInfo:nil]);
}

static unsigned int getAsn1HeaderSize(NSString *publicKeyType, NSNumber *publicKeySize)
{
if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 2048))
{
return sizeof(rsa2048Asn1Header);
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 3072))
{
return sizeof(rsa3072Asn1Header);
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeRSA]) && ([publicKeySize integerValue] == 4096))
{
return sizeof(rsa4096Asn1Header);
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeECSECPrimeRandom]) && ([publicKeySize integerValue] == 256))
{
return sizeof(ecDsaSecp256r1Asn1Header);
}
else if (([publicKeyType isEqualToString:(NSString *)kSecAttrKeyTypeECSECPrimeRandom]) && ([publicKeySize integerValue] == 384))
{
return sizeof(ecDsaSecp384r1Asn1Header);
}

@throw([NSException exceptionWithName:@"Unsupported public key algorithm" reason:@"Tried to generate the SPKI hash for an unsupported key algorithm" userInfo:nil]);
}

#if SWIFT_PACKAGE
@import TrustKitSwift;
#else
#import <TrustKit/TrustKit-Swift.h>
#endif

@interface TSKSPKIHashCache ()

Expand Down Expand Up @@ -234,54 +123,15 @@ - (NSData *)_hashSubjectPublicKeyInfoFromCertificate:(SecCertificateRef)certific
// We didn't this certificate in the cache so we need to generate its SPKI hash
os_log_debug(TSKLogHandle(), "Generating Subject Public Key Info hash...");

// First extract the public key
SecKeyRef publicKey = [self copyPublicKeyFromCertificate:certificate];
if (publicKey == nil)
{
os_log_error(TSKLogHandle(), "Could not copy the public key from the certificate");
return nil;
}

// Obtain the public key bytes from the key reference
NSData *publicKeyData = (__bridge_transfer NSData *)SecKeyCopyExternalRepresentation(publicKey, NULL);
if (publicKeyData == nil)
NSData *subjectPublicKeyInfo = [TSKSubjectPublicKeyInfo dataFromCertificate:certificateData];
Comment thread
alexanderdibenedetto marked this conversation as resolved.
Resolved
if (subjectPublicKeyInfo == nil)
{
os_log_error(TSKLogHandle(), "Could not extract the public key bytes");
CFRelease(publicKey);
os_log_error(TSKLogHandle(), "Could not read the Subject Public Key Info from the certificate");
return nil;
}

// Obtain the SPKI header based on the key's algorithm
CFDictionaryRef publicKeyAttributes = SecKeyCopyAttributes(publicKey);
NSString *publicKeyType = CFDictionaryGetValue(publicKeyAttributes, kSecAttrKeyType);
NSNumber *publicKeysize = CFDictionaryGetValue(publicKeyAttributes, kSecAttrKeySizeInBits);
CFRelease(publicKeyAttributes);

if (!isKeySupported(publicKeyType, publicKeysize))
{
os_log_error(TSKLogHandle(), "Public key algorithm or length is not supported");
CFRelease(publicKey);
return nil;
}

char *asn1HeaderBytes = getAsn1HeaderBytes(publicKeyType, publicKeysize);
unsigned int asn1HeaderSize = getAsn1HeaderSize(publicKeyType, publicKeysize);

CFRelease(publicKey);

// Generate a hash of the subject public key info
NSMutableData *subjectPublicKeyInfoHash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CC_SHA256_CTX shaCtx;
CC_SHA256_Init(&shaCtx);

// Add the missing ASN1 header for public keys to re-create the subject public key info
CC_SHA256_Update(&shaCtx, asn1HeaderBytes, asn1HeaderSize);


// Add the public key
CC_SHA256_Update(&shaCtx, [publicKeyData bytes], (unsigned int)[publicKeyData length]);
CC_SHA256_Final((unsigned char *)[subjectPublicKeyInfoHash bytes], &shaCtx);

CC_SHA256([subjectPublicKeyInfo bytes], (CC_LONG)[subjectPublicKeyInfo length], [subjectPublicKeyInfoHash mutableBytes]);

// Store the hash in our memory cache
self->_spkiCache[certificateData] = subjectPublicKeyInfoHash;
Expand Down Expand Up @@ -339,41 +189,6 @@ - (SPKICacheDictionnary *)loadSPKICacheFromFileSystem
return spkiCache;
}

- (SecKeyRef)copyPublicKeyFromCertificate:(SecCertificateRef)certificate
{
OSStatus status;

// Create an X509 trust using the using the certificate
SecTrustRef trust;
SecPolicyRef policy = SecPolicyCreateBasicX509();
status = SecTrustCreateWithCertificates(certificate, policy, &trust);
CFRelease(policy);

if (status != errSecSuccess)
{
os_log_error(TSKLogHandle(), "Could not create trust from certificate, got status %d", (int)status);
return nil;
}

// Get a public key reference for the certificate from the trust
// The certificate chain must be evaluated first in order to be able
// to determine which is the leaf certificate of the chain, and only
// then SecTrustCopyKey can be called
NSError *error = NULL;
SecTrustResultType trustResult = 0;
evaluateCertificateChainTrust(trust, &trustResult, &error);
if ((error != NULL) && (trustResult != kSecTrustResultRecoverableTrustFailure))
{
os_log_error(TSKLogHandle(), "Could not evaluate trust for the certificate: %{public}@", [error localizedDescription]);
CFRelease(trust);
return nil;
}

SecKeyRef publicKey = copyKey(trust);
CFRelease(trust);
return publicKey;
}

- (NSURL *)SPKICachePath
{
NSURL *cachesDirUrl = [NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory
Expand Down
8 changes: 0 additions & 8 deletions TrustKit/Pinning/pinning_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,4 @@ void evaluateCertificateChainTrust(SecTrustRef serverTrust, SecTrustResultType *
*/
SecCertificateRef getCertificateAtIndex(SecTrustRef serverTrust, CFIndex index);

/**
Returns the public key for a leaf certificate after it has been evaluated.

@param serverTrust The trust management object to evaluate
@return The leaf certificate's public key, or nil if the public key could not be extracted
*/
SecKeyRef copyKey(SecTrustRef serverTrust);

#endif
4 changes: 0 additions & 4 deletions TrustKit/Pinning/pinning_utils.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,3 @@ SecCertificateRef getCertificateAtIndex(SecTrustRef serverTrust, CFIndex index)
CFRelease(certs);
return certificate;
}

SecKeyRef copyKey(SecTrustRef serverTrust) {
return SecTrustCopyKey(serverTrust);
}
80 changes: 80 additions & 0 deletions TrustKit/Swift/SubjectPublicKeyInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*

SubjectPublicKeyInfo.swift
TrustKit

Copyright 2026 The TrustKit Project Authors
Licensed under the MIT license, see associated LICENSE file for terms.
See AUTHORS file for the list of project authors.

*/

import Foundation

@objc(TSKSubjectPublicKeyInfo)
public final class SubjectPublicKeyInfo: NSObject {
/// Returns the DER-encoded Subject Public Key Info of a DER-encoded certificate, exactly as it
/// is encoded in the certificate, or nil if the certificate could not be read.
@objc public static func data(fromCertificate certificate: Data) -> Data? {
// An Array keeps every read below bounds-checked, including in release builds
let der = [UInt8](certificate)
guard let subjectPublicKeyInfo = subjectPublicKeyInfoRange(in: der) else {
return nil
}
return Data(der[subjectPublicKeyInfo])
}
}

private struct DERElement {
let tag: UInt8
let content: Range<Int>
let element: Range<Int>
}

private func subjectPublicKeyInfoRange(in der: [UInt8]) -> Range<Int>? {
guard let certificate = readElement(der, in: der.indices), certificate.tag == 0x30,
let tbsCertificate = readElement(der, in: certificate.content), tbsCertificate.tag == 0x30 else {
return nil
}

// RFC 5280 4.1: an optional [0] version, then serialNumber, signature, issuer, validity and subject
var remaining = tbsCertificate.content
if let version = readElement(der, in: remaining), version.tag == 0xa0 {
remaining = version.element.upperBound..<remaining.upperBound
}
for expectedTag: UInt8 in [0x02, 0x30, 0x30, 0x30, 0x30] {
guard let field = readElement(der, in: remaining), field.tag == expectedTag else {
return nil
}
remaining = field.element.upperBound..<remaining.upperBound
}

guard let subjectPublicKeyInfo = readElement(der, in: remaining), subjectPublicKeyInfo.tag == 0x30 else {
return nil
}
return subjectPublicKeyInfo.element
}

private func readElement(_ der: [UInt8], in range: Range<Int>) -> DERElement? {
guard range.count >= 2 else {
return nil
}
let tag = der[range.lowerBound]
var contentStart = range.lowerBound + 2
var length = Int(der[range.lowerBound + 1])
if length & 0x80 != 0 {
// DER has no indefinite length (0x80), and no certificate needs more than 3 length bytes
let lengthBytes = length & 0x7f
guard (1...3).contains(lengthBytes), range.upperBound - contentStart >= lengthBytes else {
return nil
}
length = der[contentStart..<contentStart + lengthBytes].reduce(0) { $0 << 8 | Int($1) }
contentStart += lengthBytes
}
guard range.upperBound - contentStart >= length else {
return nil
}
return DERElement(tag: tag,
content: contentStart..<contentStart + length,
element: range.lowerBound..<contentStart + length)
}
Binary file not shown.
Loading
Loading