From 58ba842c248b29ab60c6bf8af00a7322b8b3a0d4 Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Thu, 22 Oct 2026 14:03:46 -0500 Subject: [PATCH 1/5] Hash each certificate's SPKI as encoded instead of rebuilding it (#236) Read the Subject Public Key Info straight from the certificate's DER and hash it as is. This removes the five hardcoded ASN.1 headers and the extra trust evaluation per certificate on a cache miss, so any key type can be pinned. Adds a P-521 test certificate and malformed-input tests for the DER reader. --- TrustKit.xcodeproj/project.pbxproj | 12 ++ TrustKit/Pinning/TSKSPKIHashCache.m | 197 +----------------- TrustKit/Pinning/pinning_utils.h | 8 +- TrustKit/Pinning/pinning_utils.m | 59 +++++- .../ECDSA_sec521r1/Secp521r1TestCA.der | Bin 0 -> 603 bytes TrustKitTests/TSKPublicKeyAlgorithmTests.m | 35 ++++ 6 files changed, 112 insertions(+), 199 deletions(-) create mode 100644 TrustKitTests/Certificates/ECDSA_sec521r1/Secp521r1TestCA.der diff --git a/TrustKit.xcodeproj/project.pbxproj b/TrustKit.xcodeproj/project.pbxproj index 7caf3c57..d2dce0fa 100644 --- a/TrustKit.xcodeproj/project.pbxproj +++ b/TrustKit.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 66CF699E1BD05B45270DFD2E /* TSKEndToEndSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CBADAA81F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m */; }; 67731757DF2837DC809F1135 /* GoodRootCA.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05D51E28AAA40045D8B3 /* GoodRootCA.der */; }; 67C02585489EE40A186A5A35 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05E41E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der */; }; + DFA90B767A90476814B30C29 /* Secp521r1TestCA.der in Resources */ = {isa = PBXBuildFile; fileRef = D2E379C6EA9EB04E33F5ED0F /* Secp521r1TestCA.der */; }; 6947D4B221FF8E10DE1E76B1 /* anchor-ca.cert.pem in Resources */ = {isa = PBXBuildFile; fileRef = FCC1DD051EECD19E00AB3D81 /* anchor-ca.cert.pem */; }; 6B032D401AF1AEC200EAFA69 /* TSKReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B032D3F1AF1AEB600EAFA69 /* TSKReporterTests.m */; }; 6B2B06AD1B05154A00FC749E /* TSKBackgroundReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B2B06AC1B05154A00FC749E /* TSKBackgroundReporter.h */; }; @@ -194,8 +195,11 @@ 8CBA05E21E28AAA40045D8B3 /* www.good.com.selfsigned.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05D71E28AAA40045D8B3 /* www.good.com.selfsigned.der */; }; 8CBA05E31E28AAA40045D8B3 /* www.good.com.selfsigned.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05D71E28AAA40045D8B3 /* www.good.com.selfsigned.der */; }; 8CBA05E51E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05E41E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der */; }; + 32D48D404CE5B9DCB8012B9E /* Secp521r1TestCA.der in Resources */ = {isa = PBXBuildFile; fileRef = D2E379C6EA9EB04E33F5ED0F /* Secp521r1TestCA.der */; }; 8CBA05E61E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05E41E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der */; }; + 2391BA800486FCAECBDF4B3C /* Secp521r1TestCA.der in Resources */ = {isa = PBXBuildFile; fileRef = D2E379C6EA9EB04E33F5ED0F /* Secp521r1TestCA.der */; }; 8CBA05E71E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05E41E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der */; }; + D055C49F2F445E93B9B7ACD9 /* Secp521r1TestCA.der in Resources */ = {isa = PBXBuildFile; fileRef = D2E379C6EA9EB04E33F5ED0F /* Secp521r1TestCA.der */; }; 8CBADAA91F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CBADAA81F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m */; }; 8CBADAAA1F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CBADAA81F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m */; }; 8CBADAAB1F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CBADAA81F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m */; }; @@ -257,6 +261,7 @@ 983CC0C818D2101B84C82776 /* www.good.com.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05D61E28AAA40045D8B3 /* www.good.com.der */; }; 9A5120235297DD14E140CABE /* Corporation Service Company RSA OV SSL CA.der in Resources */ = {isa = PBXBuildFile; fileRef = 91B276432B9A463E004B41A7 /* Corporation Service Company RSA OV SSL CA.der */; }; A1545F2242505B24A34908CE /* GeoTrust_Primary_CA_G2_ECC.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05E41E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der */; }; + A5848AD19F06AE3B82E2A61D /* Secp521r1TestCA.der in Resources */ = {isa = PBXBuildFile; fileRef = D2E379C6EA9EB04E33F5ED0F /* Secp521r1TestCA.der */; }; A24BC90C183D5AE61637717A /* OCMock in Frameworks */ = {isa = PBXBuildFile; productRef = 10E47E25291C2EAB4E7C79C8 /* OCMock */; }; A53B7497AB0709DE758E15F3 /* TSKNSURLConnectionDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 8CD5F72F1BC5ED4A005801D8 /* TSKNSURLConnectionDelegateProxy.h */; }; A5A3331EF9F962289F145FD8 /* www.good.com.selfsigned.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05D71E28AAA40045D8B3 /* www.good.com.selfsigned.der */; }; @@ -412,6 +417,7 @@ 8CBA05D61E28AAA40045D8B3 /* www.good.com.der */ = {isa = PBXFileReference; lastKnownFileType = file; name = www.good.com.der; path = Certificates/RSA_4096/www.good.com.der; sourceTree = ""; }; 8CBA05D71E28AAA40045D8B3 /* www.good.com.selfsigned.der */ = {isa = PBXFileReference; lastKnownFileType = file; name = www.good.com.selfsigned.der; path = Certificates/RSA_4096/www.good.com.selfsigned.der; sourceTree = ""; }; 8CBA05E41E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der */ = {isa = PBXFileReference; lastKnownFileType = file; name = GeoTrust_Primary_CA_G2_ECC.der; path = Certificates/ECDSA_sec384r1/GeoTrust_Primary_CA_G2_ECC.der; sourceTree = ""; }; + D2E379C6EA9EB04E33F5ED0F /* Secp521r1TestCA.der */ = {isa = PBXFileReference; lastKnownFileType = file; name = Secp521r1TestCA.der; path = Certificates/ECDSA_sec521r1/Secp521r1TestCA.der; sourceTree = ""; }; 8CBADAA81F2A850F00FCD7FB /* TSKEndToEndSwizzlingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSKEndToEndSwizzlingTests.m; sourceTree = ""; }; 8CC5D24E1D6E64D10074F515 /* TrustKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TrustKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8CC78B1E1B1B586F00523A25 /* TSKCertificateUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSKCertificateUtils.h; sourceTree = ""; }; @@ -665,6 +671,7 @@ isa = PBXGroup; children = ( 8CBA05E41E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der */, + D2E379C6EA9EB04E33F5ED0F /* Secp521r1TestCA.der */, ); name = "ECDSA sec384r1"; sourceTree = ""; @@ -1205,6 +1212,7 @@ files = ( CAB30AE3F25878920301AFB7 /* anchor-fake.yahoo.com.cert.pem in Resources */, 67C02585489EE40A186A5A35 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */, + DFA90B767A90476814B30C29 /* Secp521r1TestCA.der in Resources */, A7E4C7FEF227A79892290A1B /* anchor-intermediate.cert.pem in Resources */, 72B6AEAF956C7653D6A0A567 /* www.good.com.selfsigned.der in Resources */, 6257932D020646705F9CFC8F /* GlobalSignDomainValidationCA-SHA256-G2.der in Resources */, @@ -1234,6 +1242,7 @@ files = ( 03655B98EBD2E5FA7E022C95 /* anchor-fake.yahoo.com.cert.pem in Resources */, A1545F2242505B24A34908CE /* GeoTrust_Primary_CA_G2_ECC.der in Resources */, + A5848AD19F06AE3B82E2A61D /* Secp521r1TestCA.der in Resources */, 1D82C78917FF8BFF2048EA13 /* anchor-intermediate.cert.pem in Resources */, A5A3331EF9F962289F145FD8 /* www.good.com.selfsigned.der in Resources */, 7D9DD27EB4A45E73F274D7F4 /* GlobalSignDomainValidationCA-SHA256-G2.der in Resources */, @@ -1263,6 +1272,7 @@ files = ( FCC1DD091EECD19E00AB3D81 /* anchor-fake.yahoo.com.cert.pem in Resources */, 8CBA05E51E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */, + 32D48D404CE5B9DCB8012B9E /* Secp521r1TestCA.der in Resources */, FCC1DD0A1EECD19E00AB3D81 /* anchor-intermediate.cert.pem in Resources */, 8CBA05E11E28AAA40045D8B3 /* www.good.com.selfsigned.der in Resources */, 8CBA05C71E28AA6C0045D8B3 /* GlobalSignDomainValidationCA-SHA256-G2.der in Resources */, @@ -1292,6 +1302,7 @@ files = ( FC049B3E1EECD1B100FDC5F4 /* anchor-fake.yahoo.com.cert.pem in Resources */, 8CBA05E71E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */, + D055C49F2F445E93B9B7ACD9 /* Secp521r1TestCA.der in Resources */, FC049B3F1EECD1B100FDC5F4 /* anchor-intermediate.cert.pem in Resources */, 8CBA05E31E28AAA40045D8B3 /* www.good.com.selfsigned.der in Resources */, 8CBA05C91E28AA6C0045D8B3 /* GlobalSignDomainValidationCA-SHA256-G2.der in Resources */, @@ -1321,6 +1332,7 @@ files = ( FC049B3B1EECD1B000FDC5F4 /* anchor-fake.yahoo.com.cert.pem in Resources */, 8CBA05E61E294CC30045D8B3 /* GeoTrust_Primary_CA_G2_ECC.der in Resources */, + 2391BA800486FCAECBDF4B3C /* Secp521r1TestCA.der in Resources */, FC049B3C1EECD1B000FDC5F4 /* anchor-intermediate.cert.pem in Resources */, 8CBA05E21E28AAA40045D8B3 /* www.good.com.selfsigned.der in Resources */, 8CBA05C81E28AA6C0045D8B3 /* GlobalSignDomainValidationCA-SHA256-G2.der in Resources */, diff --git a/TrustKit/Pinning/TSKSPKIHashCache.m b/TrustKit/Pinning/TSKSPKIHashCache.m index 5ee28ddc..685a3910 100644 --- a/TrustKit/Pinning/TSKSPKIHashCache.m +++ b/TrustKit/Pinning/TSKSPKIHashCache.m @@ -15,121 +15,6 @@ #import #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]); -} - - @interface TSKSPKIHashCache () // Dictionnary to cache SPKI hashes instead of having to compute them on every connection @@ -234,54 +119,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 = getSubjectPublicKeyInfo(certificateData); + 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; @@ -339,41 +185,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 diff --git a/TrustKit/Pinning/pinning_utils.h b/TrustKit/Pinning/pinning_utils.h index 9b11ab24..e0b48edb 100644 --- a/TrustKit/Pinning/pinning_utils.h +++ b/TrustKit/Pinning/pinning_utils.h @@ -45,11 +45,11 @@ void evaluateCertificateChainTrust(SecTrustRef serverTrust, SecTrustResultType * SecCertificateRef getCertificateAtIndex(SecTrustRef serverTrust, CFIndex index); /** - Returns the public key for a leaf certificate after it has been evaluated. + Returns the Subject Public Key Info of a certificate, exactly as it is encoded in the certificate. - @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 + @param certificateData The DER-encoded certificate + @return The DER-encoded Subject Public Key Info, or nil if the certificate could not be read */ -SecKeyRef copyKey(SecTrustRef serverTrust); +NSData *getSubjectPublicKeyInfo(NSData *certificateData); #endif diff --git a/TrustKit/Pinning/pinning_utils.m b/TrustKit/Pinning/pinning_utils.m index f977a774..bbf7b47b 100644 --- a/TrustKit/Pinning/pinning_utils.m +++ b/TrustKit/Pinning/pinning_utils.m @@ -35,6 +35,61 @@ SecCertificateRef getCertificateAtIndex(SecTrustRef serverTrust, CFIndex index) return certificate; } -SecKeyRef copyKey(SecTrustRef serverTrust) { - return SecTrustCopyKey(serverTrust); +static BOOL readDERElement(const uint8_t *element, const uint8_t *end, uint8_t *tag, const uint8_t **content, const uint8_t **next) { + if (end - element < 2) { + return NO; + } + *tag = element[0]; + const uint8_t *cursor = element + 2; + size_t length = element[1]; + if (length & 0x80) { + // DER has no indefinite length (0x80), and certificate lengths fit in 4 bytes + size_t lengthBytes = length & 0x7f; + if (lengthBytes == 0 || lengthBytes > 4 || (size_t)(end - cursor) < lengthBytes) { + return NO; + } + length = 0; + for (size_t i = 0; i < lengthBytes; i++) { + length = (length << 8) | *cursor++; + } + } + if ((size_t)(end - cursor) < length) { + return NO; + } + *content = cursor; + *next = cursor + length; + return YES; +} + +NSData *getSubjectPublicKeyInfo(NSData *certificateData) { + const uint8_t *cursor = certificateData.bytes; + const uint8_t *end = cursor + certificateData.length; + uint8_t tag; + const uint8_t *content, *next; + + // Step into the Certificate and tbsCertificate SEQUENCEs + for (int i = 0; i < 2; i++) { + if (!readDERElement(cursor, end, &tag, &content, &next) || tag != 0x30) { + return nil; + } + cursor = content; + end = next; + } + + // RFC 5280 4.1: an optional [0] version, then serialNumber, signature, issuer, validity and subject + if (readDERElement(cursor, end, &tag, &content, &next) && tag == 0xa0) { + cursor = next; + } + const uint8_t skippedTags[] = {0x02, 0x30, 0x30, 0x30, 0x30}; + for (size_t i = 0; i < sizeof(skippedTags); i++) { + if (!readDERElement(cursor, end, &tag, &content, &next) || tag != skippedTags[i]) { + return nil; + } + cursor = next; + } + + if (!readDERElement(cursor, end, &tag, &content, &next) || tag != 0x30) { + return nil; + } + return [NSData dataWithBytes:cursor length:(NSUInteger)(next - cursor)]; } diff --git a/TrustKitTests/Certificates/ECDSA_sec521r1/Secp521r1TestCA.der b/TrustKitTests/Certificates/ECDSA_sec521r1/Secp521r1TestCA.der new file mode 100644 index 0000000000000000000000000000000000000000..d55e6bb4cd7b9e2ebd80b9479bcf2c8a1e384f0c GIT binary patch literal 603 zcmXqLVhT5CV%)KSnTe5!Nu*de_1>C!`xdTlDG&_`JoPk7-+qw+7aNCGo5wj@7G@R$ zQ$ukBQ8wmK7A_vXkfPG!67S3sg^<+Zl43(C0|}4>Gml_!YI1?8kztV`SVqCw(Lhd| z*TBHQ5D38_N}Si&0*Onmv5m701lZWYj$>kEW7TeCVNhmnY-3WMwp%F7iRXS zv9c-+3{`d>Y74Z&UuW(M6yw;sJyf{Tiu2|xZ&ok2j;Rs#$s%>{dxM)?}wSp(J{?C~GK6A^@BObSZ*7mLq-p9PB{@&Cr z{x(uR6SdyoWYO8@|114x$8Mk*ZKvD~&DGZ~yLL^h<=q6PsrGM`qIvQ&OAh_*@#Nja p->B8^9k~8gsq(do4M9A@Qg5mjnX(jIy7{TV{Dt_PHB*nI0|2sb(C7dF literal 0 HcmV?d00001 diff --git a/TrustKitTests/TSKPublicKeyAlgorithmTests.m b/TrustKitTests/TSKPublicKeyAlgorithmTests.m index 7bfb4932..1150178e 100644 --- a/TrustKitTests/TSKPublicKeyAlgorithmTests.m +++ b/TrustKitTests/TSKPublicKeyAlgorithmTests.m @@ -15,6 +15,7 @@ #import "../TrustKit/Pinning/ssl_pin_verifier.h" #import "../TrustKit/Pinning/TSKSPKIHashCache.h" +#import "../TrustKit/Pinning/pinning_utils.h" #import "../TrustKit/Reporting/reporting_utils.h" #import "TSKCertificateUtils.h" @@ -113,6 +114,40 @@ - (void)testExtractEcDsaSecp384r1 } +- (void)testExtractEcDsaSecp521r1 +{ + // Ensure a secp521r1 key is properly extracted from its certificate + SecCertificateRef certificate = [TSKCertificateUtils createCertificateFromDer:@"Secp521r1TestCA"]; + + NSData *spkiHash = [spkiCache hashSubjectPublicKeyInfoFromCertificate:certificate]; + NSString *spkiPin = [spkiHash base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; + + XCTAssertEqualObjects(spkiPin, @"JDbjp5T1MJ6GUN/fY/voVivkg/5a+kiWCTF7yBFL4DM="); + CFRelease(certificate); +} + + +- (void)testSubjectPublicKeyInfoFromMalformedCertificate +{ + SecCertificateRef certificate = [TSKCertificateUtils createCertificateFromDer:@"www.good.com"]; + NSData *certificateData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); + CFRelease(certificate); + + XCTAssertNotNil(getSubjectPublicKeyInfo(certificateData)); + for (NSUInteger length = 0; length < [certificateData length]; length++) + { + XCTAssertNil(getSubjectPublicKeyInfo([certificateData subdataWithRange:NSMakeRange(0, length)]), @"Truncated to %lu bytes", (unsigned long)length); + } + + const uint8_t indefiniteLength[] = {0x30, 0x80, 0x30, 0x80, 0x00, 0x00, 0x00, 0x00}; + const uint8_t fiveByteLength[] = {0x30, 0x85, 0x00, 0x00, 0x00, 0x00, 0x02, 0x30, 0x00}; + const uint8_t notASequence[] = {0x31, 0x02, 0x30, 0x00}; + XCTAssertNil(getSubjectPublicKeyInfo([NSData dataWithBytes:indefiniteLength length:sizeof(indefiniteLength)])); + XCTAssertNil(getSubjectPublicKeyInfo([NSData dataWithBytes:fiveByteLength length:sizeof(fiveByteLength)])); + XCTAssertNil(getSubjectPublicKeyInfo([NSData dataWithBytes:notASequence length:sizeof(notASequence)])); +} + + - (void)testSPKICacheMissOffMainThreadDoesNotWaitForMainThread { SecCertificateRef certificate = [TSKCertificateUtils createCertificateFromDer:@"www.globalsign.com"]; From 8e90712bbd546cbe2364e966541daff9415965c8 Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Thu, 22 Oct 2026 14:12:48 -0500 Subject: [PATCH 2/5] Stop building arm64e and arm64e.x1 simulator slices No simulator runtime can run them (all are arm64-only), and Xcode 27 ships no Swift standard library for them. Device, macOS and Mac Catalyst slices keep arm64e and arm64e.x1. --- TrustKit.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TrustKit.xcodeproj/project.pbxproj b/TrustKit.xcodeproj/project.pbxproj index d2dce0fa..b2a9b95b 100644 --- a/TrustKit.xcodeproj/project.pbxproj +++ b/TrustKit.xcodeproj/project.pbxproj @@ -1759,6 +1759,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; + "EXCLUDED_ARCHS[sdk=*simulator*]" = "arm64e arm64e.x1"; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; @@ -1847,6 +1848,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; + "EXCLUDED_ARCHS[sdk=*simulator*]" = "arm64e arm64e.x1"; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_TREAT_WARNINGS_AS_ERRORS = YES; From 207b589f1247cd3248a7f295ab0d2d7cbd9f14a8 Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Thu, 22 Oct 2026 15:08:41 -0500 Subject: [PATCH 3/5] Read the certificate's SPKI in Swift (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First Swift code in TrustKit. The DER reader moves to TrustKit/Swift/SubjectPublicKeyInfo.swift, exposed to Objective-C as TSKSubjectPublicKeyInfo. Xcode compiles it into the framework targets; SwiftPM builds it as a TrustKitSwift target. Uses Swift 6 language mode and swift-tools-version 6.0. Removes the recursive HEADER_SEARCH_PATHS, which resolved to source headers and broke the framework module. Documents the empty Swift file that Objective-C-only apps using the static XCFramework need below iOS 16 / macOS 13. --- Package.swift | 8 ++- TrustKit.xcodeproj/project.pbxproj | 16 ++++- TrustKit/Pinning/TSKSPKIHashCache.m | 8 ++- TrustKit/Pinning/pinning_utils.h | 8 --- TrustKit/Pinning/pinning_utils.m | 59 ---------------- TrustKit/Swift/SubjectPublicKeyInfo.swift | 80 ++++++++++++++++++++++ TrustKitTests/TSKPublicKeyAlgorithmTests.m | 12 ++-- docs/getting-started.md | 10 +++ 8 files changed, 123 insertions(+), 78 deletions(-) create mode 100644 TrustKit/Swift/SubjectPublicKeyInfo.swift diff --git a/Package.swift b/Package.swift index 9b61fedb..ff64ad3d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -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" + ), ] ) diff --git a/TrustKit.xcodeproj/project.pbxproj b/TrustKit.xcodeproj/project.pbxproj index b2a9b95b..c5d36371 100644 --- a/TrustKit.xcodeproj/project.pbxproj +++ b/TrustKit.xcodeproj/project.pbxproj @@ -95,6 +95,7 @@ 7C9060CC7B837D9C4A227DF7 /* TSKPinFailureReport.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C15F99F1B16094D00F06C0E /* TSKPinFailureReport.m */; }; 7D9DD27EB4A45E73F274D7F4 /* GlobalSignDomainValidationCA-SHA256-G2.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05C41E28AA6C0045D8B3 /* GlobalSignDomainValidationCA-SHA256-G2.der */; }; 7E0DCB5F043EC6726C2936B5 /* pinning_utils.m in Sources */ = {isa = PBXBuildFile; fileRef = B005E3E729B85EBA007C3D84 /* pinning_utils.m */; }; + 45B5FB2AF8D093EDE71C825D /* SubjectPublicKeyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E84F4FFC49B4AC9B23F1EF4 /* SubjectPublicKeyInfo.swift */; }; 805B476B7164A3EDD62F3F24 /* TSKPinningValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 7033D35C248FE84100BDFF50 /* TSKPinningValidator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 82B218B6BA21627354BF9F46 /* ssl_pin_verifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CE919211AEA077F002B29AE /* ssl_pin_verifier.m */; }; 8418436F3F12D407F7D14221 /* TSKSPKIHashCache.h in Headers */ = {isa = PBXBuildFile; fileRef = FC1A09081E57AC450055B12C /* TSKSPKIHashCache.h */; }; @@ -270,9 +271,13 @@ AE59E8A5C3487566AADBDFB2 /* www.good.com.der in Resources */ = {isa = PBXBuildFile; fileRef = 8CBA05D61E28AAA40045D8B3 /* www.good.com.der */; }; AED4268613023DF853EB22EC /* Corporation Service Company RSA OV SSL CA.der in Resources */ = {isa = PBXBuildFile; fileRef = 91B276432B9A463E004B41A7 /* Corporation Service Company RSA OV SSL CA.der */; }; B005E3E829B85EBA007C3D84 /* pinning_utils.m in Sources */ = {isa = PBXBuildFile; fileRef = B005E3E729B85EBA007C3D84 /* pinning_utils.m */; }; + BDA43C6C288EEF13E3A3D6B3 /* SubjectPublicKeyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E84F4FFC49B4AC9B23F1EF4 /* SubjectPublicKeyInfo.swift */; }; B005E3EB29B85EBA007C3D84 /* pinning_utils.m in Sources */ = {isa = PBXBuildFile; fileRef = B005E3E729B85EBA007C3D84 /* pinning_utils.m */; }; + 83D3CE8E6A8308E00B957A40 /* SubjectPublicKeyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E84F4FFC49B4AC9B23F1EF4 /* SubjectPublicKeyInfo.swift */; }; B005E3ED29B85EBA007C3D84 /* pinning_utils.m in Sources */ = {isa = PBXBuildFile; fileRef = B005E3E729B85EBA007C3D84 /* pinning_utils.m */; }; + F2BFBF8001DFD807C061A666 /* SubjectPublicKeyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E84F4FFC49B4AC9B23F1EF4 /* SubjectPublicKeyInfo.swift */; }; B005E3EF29B85EBA007C3D84 /* pinning_utils.m in Sources */ = {isa = PBXBuildFile; fileRef = B005E3E729B85EBA007C3D84 /* pinning_utils.m */; }; + B3A8FCA3E62F7CD5FDEA0B92 /* SubjectPublicKeyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E84F4FFC49B4AC9B23F1EF4 /* SubjectPublicKeyInfo.swift */; }; B005E3F129B8C2F8007C3D84 /* pinning_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = B005E3F029B85ED0007C3D84 /* pinning_utils.h */; }; B005E3F229B8C2F8007C3D84 /* pinning_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = B005E3F029B85ED0007C3D84 /* pinning_utils.h */; }; B005E3F329B8C2F9007C3D84 /* pinning_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = B005E3F029B85ED0007C3D84 /* pinning_utils.h */; }; @@ -440,6 +445,7 @@ 8CF27AA11F01BB7B009369B0 /* TSKLoggerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSKLoggerTests.m; sourceTree = ""; }; 91B276432B9A463E004B41A7 /* Corporation Service Company RSA OV SSL CA.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Corporation Service Company RSA OV SSL CA.der"; sourceTree = ""; }; B005E3E729B85EBA007C3D84 /* pinning_utils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = pinning_utils.m; path = Pinning/pinning_utils.m; sourceTree = ""; }; + 5E84F4FFC49B4AC9B23F1EF4 /* SubjectPublicKeyInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SubjectPublicKeyInfo.swift; path = Swift/SubjectPublicKeyInfo.swift; sourceTree = ""; }; B005E3F029B85ED0007C3D84 /* pinning_utils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = pinning_utils.h; path = Pinning/pinning_utils.h; sourceTree = ""; }; B0C703E4281D53491D783E5F /* TrustKit visionOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "TrustKit visionOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; DC6F28762BAB30A8001B604A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -724,6 +730,7 @@ FC1A09091E57AC450055B12C /* TSKSPKIHashCache.m */, FC1A09121E57C6820055B12C /* TSKPublicKeyAlgorithm.h */, B005E3E729B85EBA007C3D84 /* pinning_utils.m */, + 5E84F4FFC49B4AC9B23F1EF4 /* SubjectPublicKeyInfo.swift */, B005E3F029B85ED0007C3D84 /* pinning_utils.h */, ); name = Pinning; @@ -1389,6 +1396,7 @@ 6BEE9B751CA48C25F2D78954 /* TSKReportsRateLimiter.m in Sources */, BA4033A9744CF186E7E38480 /* parse_configuration.m in Sources */, 7E0DCB5F043EC6726C2936B5 /* pinning_utils.m in Sources */, + 45B5FB2AF8D093EDE71C825D /* SubjectPublicKeyInfo.swift in Sources */, 2C3C580D8BBA830D9CE101FD /* TSKBackgroundReporter.m in Sources */, 6C7FCCD6E6E97546DF3EEFCE /* TSKNSURLSessionDelegateProxy.m in Sources */, 26024026147AF4313E2B54BB /* configuration_utils.m in Sources */, @@ -1413,6 +1421,7 @@ 8C9EBE031B619BBE00CA7EE0 /* TSKReportsRateLimiter.m in Sources */, 8C5D98B31CEFF079008E654B /* parse_configuration.m in Sources */, B005E3E829B85EBA007C3D84 /* pinning_utils.m in Sources */, + BDA43C6C288EEF13E3A3D6B3 /* SubjectPublicKeyInfo.swift in Sources */, 6B2B06AF1B05157400FC749E /* TSKBackgroundReporter.m in Sources */, 8CD5F74B1BCB535E005801D8 /* TSKNSURLSessionDelegateProxy.m in Sources */, 8C4346DA1E5B894A008023F9 /* configuration_utils.m in Sources */, @@ -1456,6 +1465,7 @@ 8C84CB921D6E0981009B3E7D /* TSKReportsRateLimiter.m in Sources */, 8C84CB931D6E0981009B3E7D /* parse_configuration.m in Sources */, B005E3ED29B85EBA007C3D84 /* pinning_utils.m in Sources */, + F2BFBF8001DFD807C061A666 /* SubjectPublicKeyInfo.swift in Sources */, 8C84CB941D6E0981009B3E7D /* TSKBackgroundReporter.m in Sources */, 8C84CB951D6E0981009B3E7D /* TSKNSURLSessionDelegateProxy.m in Sources */, 8C4346DD1E5B894A008023F9 /* configuration_utils.m in Sources */, @@ -1498,6 +1508,7 @@ 8CA6CC1A1BAE2B6600BDA419 /* TSKBackgroundReporter.m in Sources */, 8CA6CC1C1BAE2B6600BDA419 /* TSKPinFailureReport.m in Sources */, B005E3EB29B85EBA007C3D84 /* pinning_utils.m in Sources */, + 83D3CE8E6A8308E00B957A40 /* SubjectPublicKeyInfo.swift in Sources */, 8CD5F74D1BCB535E005801D8 /* TSKNSURLSessionDelegateProxy.m in Sources */, 8C5D98B51CEFF079008E654B /* parse_configuration.m in Sources */, 8C4346DC1E5B894A008023F9 /* configuration_utils.m in Sources */, @@ -1542,6 +1553,7 @@ 8CC5D2271D6E64D10074F515 /* TSKReportsRateLimiter.m in Sources */, 8CC5D2281D6E64D10074F515 /* parse_configuration.m in Sources */, B005E3EF29B85EBA007C3D84 /* pinning_utils.m in Sources */, + B3A8FCA3E62F7CD5FDEA0B92 /* SubjectPublicKeyInfo.swift in Sources */, 8CC5D22A1D6E64D10074F515 /* TSKBackgroundReporter.m in Sources */, 8CC5D22B1D6E64D10074F515 /* TSKNSURLSessionDelegateProxy.m in Sources */, 8C4346DE1E5B894A008023F9 /* configuration_utils.m in Sources */, @@ -1782,7 +1794,6 @@ GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = "$(SRCROOT)/**"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 3.0.8; @@ -1793,6 +1804,7 @@ SDKROOT = macosx; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 15.0; VALID_ARCHS = "$(ARCHS_STANDARD)"; @@ -1866,7 +1878,6 @@ GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = "$(SRCROOT)/**"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 3.0.8; @@ -1877,6 +1888,7 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 15.0; VALIDATE_PRODUCT = YES; diff --git a/TrustKit/Pinning/TSKSPKIHashCache.m b/TrustKit/Pinning/TSKSPKIHashCache.m index 685a3910..4b9f2440 100644 --- a/TrustKit/Pinning/TSKSPKIHashCache.m +++ b/TrustKit/Pinning/TSKSPKIHashCache.m @@ -13,7 +13,11 @@ #import "TSKSPKIHashCache.h" #import "../TSKLog.h" #import -#import "pinning_utils.h" +#if SWIFT_PACKAGE +@import TrustKitSwift; +#else +#import +#endif @interface TSKSPKIHashCache () @@ -119,7 +123,7 @@ - (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..."); - NSData *subjectPublicKeyInfo = getSubjectPublicKeyInfo(certificateData); + NSData *subjectPublicKeyInfo = [TSKSubjectPublicKeyInfo dataFromCertificate:certificateData]; if (subjectPublicKeyInfo == nil) { os_log_error(TSKLogHandle(), "Could not read the Subject Public Key Info from the certificate"); diff --git a/TrustKit/Pinning/pinning_utils.h b/TrustKit/Pinning/pinning_utils.h index e0b48edb..64b426cc 100644 --- a/TrustKit/Pinning/pinning_utils.h +++ b/TrustKit/Pinning/pinning_utils.h @@ -44,12 +44,4 @@ void evaluateCertificateChainTrust(SecTrustRef serverTrust, SecTrustResultType * */ SecCertificateRef getCertificateAtIndex(SecTrustRef serverTrust, CFIndex index); -/** - Returns the Subject Public Key Info of a certificate, exactly as it is encoded in the certificate. - - @param certificateData The DER-encoded certificate - @return The DER-encoded Subject Public Key Info, or nil if the certificate could not be read - */ -NSData *getSubjectPublicKeyInfo(NSData *certificateData); - #endif diff --git a/TrustKit/Pinning/pinning_utils.m b/TrustKit/Pinning/pinning_utils.m index bbf7b47b..65ab4b19 100644 --- a/TrustKit/Pinning/pinning_utils.m +++ b/TrustKit/Pinning/pinning_utils.m @@ -34,62 +34,3 @@ SecCertificateRef getCertificateAtIndex(SecTrustRef serverTrust, CFIndex index) CFRelease(certs); return certificate; } - -static BOOL readDERElement(const uint8_t *element, const uint8_t *end, uint8_t *tag, const uint8_t **content, const uint8_t **next) { - if (end - element < 2) { - return NO; - } - *tag = element[0]; - const uint8_t *cursor = element + 2; - size_t length = element[1]; - if (length & 0x80) { - // DER has no indefinite length (0x80), and certificate lengths fit in 4 bytes - size_t lengthBytes = length & 0x7f; - if (lengthBytes == 0 || lengthBytes > 4 || (size_t)(end - cursor) < lengthBytes) { - return NO; - } - length = 0; - for (size_t i = 0; i < lengthBytes; i++) { - length = (length << 8) | *cursor++; - } - } - if ((size_t)(end - cursor) < length) { - return NO; - } - *content = cursor; - *next = cursor + length; - return YES; -} - -NSData *getSubjectPublicKeyInfo(NSData *certificateData) { - const uint8_t *cursor = certificateData.bytes; - const uint8_t *end = cursor + certificateData.length; - uint8_t tag; - const uint8_t *content, *next; - - // Step into the Certificate and tbsCertificate SEQUENCEs - for (int i = 0; i < 2; i++) { - if (!readDERElement(cursor, end, &tag, &content, &next) || tag != 0x30) { - return nil; - } - cursor = content; - end = next; - } - - // RFC 5280 4.1: an optional [0] version, then serialNumber, signature, issuer, validity and subject - if (readDERElement(cursor, end, &tag, &content, &next) && tag == 0xa0) { - cursor = next; - } - const uint8_t skippedTags[] = {0x02, 0x30, 0x30, 0x30, 0x30}; - for (size_t i = 0; i < sizeof(skippedTags); i++) { - if (!readDERElement(cursor, end, &tag, &content, &next) || tag != skippedTags[i]) { - return nil; - } - cursor = next; - } - - if (!readDERElement(cursor, end, &tag, &content, &next) || tag != 0x30) { - return nil; - } - return [NSData dataWithBytes:cursor length:(NSUInteger)(next - cursor)]; -} diff --git a/TrustKit/Swift/SubjectPublicKeyInfo.swift b/TrustKit/Swift/SubjectPublicKeyInfo.swift new file mode 100644 index 00000000..2529f8a9 --- /dev/null +++ b/TrustKit/Swift/SubjectPublicKeyInfo.swift @@ -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 + let element: Range +} + +private func subjectPublicKeyInfoRange(in der: [UInt8]) -> Range? { + 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..) -> 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..= length else { + return nil + } + return DERElement(tag: tag, + content: contentStart.. #import "../TrustKit/Reporting/reporting_utils.h" #import "TSKCertificateUtils.h" @@ -133,18 +133,18 @@ - (void)testSubjectPublicKeyInfoFromMalformedCertificate NSData *certificateData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); CFRelease(certificate); - XCTAssertNotNil(getSubjectPublicKeyInfo(certificateData)); + XCTAssertNotNil([TSKSubjectPublicKeyInfo dataFromCertificate:certificateData]); for (NSUInteger length = 0; length < [certificateData length]; length++) { - XCTAssertNil(getSubjectPublicKeyInfo([certificateData subdataWithRange:NSMakeRange(0, length)]), @"Truncated to %lu bytes", (unsigned long)length); + XCTAssertNil([TSKSubjectPublicKeyInfo dataFromCertificate:[certificateData subdataWithRange:NSMakeRange(0, length)]], @"Truncated to %lu bytes", (unsigned long)length); } const uint8_t indefiniteLength[] = {0x30, 0x80, 0x30, 0x80, 0x00, 0x00, 0x00, 0x00}; const uint8_t fiveByteLength[] = {0x30, 0x85, 0x00, 0x00, 0x00, 0x00, 0x02, 0x30, 0x00}; const uint8_t notASequence[] = {0x31, 0x02, 0x30, 0x00}; - XCTAssertNil(getSubjectPublicKeyInfo([NSData dataWithBytes:indefiniteLength length:sizeof(indefiniteLength)])); - XCTAssertNil(getSubjectPublicKeyInfo([NSData dataWithBytes:fiveByteLength length:sizeof(fiveByteLength)])); - XCTAssertNil(getSubjectPublicKeyInfo([NSData dataWithBytes:notASequence length:sizeof(notASequence)])); + XCTAssertNil([TSKSubjectPublicKeyInfo dataFromCertificate:[NSData dataWithBytes:indefiniteLength length:sizeof(indefiniteLength)]]); + XCTAssertNil([TSKSubjectPublicKeyInfo dataFromCertificate:[NSData dataWithBytes:fiveByteLength length:sizeof(fiveByteLength)]]); + XCTAssertNil([TSKSubjectPublicKeyInfo dataFromCertificate:[NSData dataWithBytes:notASequence length:sizeof(notASequence)]]); } diff --git a/docs/getting-started.md b/docs/getting-started.md index ca54504b..5486947e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -70,6 +70,16 @@ Add the following line to your Cartfile: Then, follow the instructions from the [Carthage README](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). +#### Prebuilt static XCFramework + +Each [GitHub release](https://github.com/datatheorem/TrustKit/releases) includes `TrustKit-static.zip`, +which contains a static `TrustKit.xcframework`. + +Part of TrustKit is written in Swift. If your app has no Swift code of its own and its deployment +target is earlier than iOS 16, tvOS 16, Mac Catalyst 16 or macOS 13, linking fails with +`Undefined symbols … __swift_FORCE_LOAD_$_swiftCompatibility56`. To fix this, add an empty Swift +file to your app target; Xcode then links the Swift libraries that TrustKit needs on those OS versions. + #### CocoaPods TrustKit 4.0 and later are not published to CocoaPods. Use the Swift Package From 7227e22cd29247418809bb2c0fb8cba8c9ee7bfe Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Thu, 22 Oct 2026 15:15:13 -0500 Subject: [PATCH 4/5] Replace the Python helper scripts get_pin_from_certificate.sh replaces get_pin_from_certificate.py. It hashes any key type and reads PEM or DER, including with macOS's built-in LibreSSL. Removes generate_test_certificates.py; the test certificates are committed and nothing regenerates them. --- docs/getting-started.md | 16 ++++-- generate_test_certificates.py | 11 ---- get_pin_from_certificate.py | 97 ----------------------------------- get_pin_from_certificate.sh | 20 ++++++++ 4 files changed, 31 insertions(+), 113 deletions(-) delete mode 100644 generate_test_certificates.py delete mode 100644 get_pin_from_certificate.py create mode 100755 get_pin_from_certificate.sh diff --git a/docs/getting-started.md b/docs/getting-started.md index 5486947e..78a5a474 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -38,12 +38,18 @@ certificate's Subject Public Key Info; this is the same as what is described in the [HTTP Public Key Pinning specification](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning). -To generate such values, a Python helper script is available within the project's -repository; it can be used to generate the pin configuration from a PEM or DER -certificate: +To generate such values, run the `get_pin_from_certificate.sh` script from the project's +repository on a PEM or DER certificate: - $ python get_pin_from_certificate.py ca.pem - $ python get_pin_from_certificate.py --type DER ca.der + $ ./get_pin_from_certificate.sh ca.pem + $ ./get_pin_from_certificate.sh ca.der + +It runs the following `openssl` commands: + + $ openssl x509 -in ca.pem -pubkey -noout \ + | openssl pkey -pubin -outform der \ + | openssl dgst -sha256 -binary \ + | openssl enc -base64 ## Deploying TrustKit diff --git a/generate_test_certificates.py b/generate_test_certificates.py deleted file mode 100644 index cb4d3a32..00000000 --- a/generate_test_certificates.py +++ /dev/null @@ -1,11 +0,0 @@ -import trustme - -# Create a CA -ca = trustme.CA() - -# Issue a cert signed by this CA -server_cert = ca.issue_cert(u"www.good.com") - -# Save the PEM-encoded data to a file -ca.cert_pem.write_to_path("GoodRootCA.pem") -server_cert.private_key_and_cert_chain_pem.write_to_path("www.good.com.pem") diff --git a/get_pin_from_certificate.py b/get_pin_from_certificate.py deleted file mode 100644 index 4050a7f7..00000000 --- a/get_pin_from_certificate.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python -"""Helper script to generate a TrustKit or HPKP pin from a PEM/DER certificate file. -""" -from __future__ import print_function -from subprocess import Popen, PIPE -from sys import stdin -import os.path -import argparse -import hashlib -import base64 -import platform -import io - - -class SupportedKeyAlgorithmsEnum(object): - RSA_2048 = 1 - RSA_4096 = 2 - ECDSA_SECP256R1 = 3 - ECDSA_SECP384R1 = 4 - - -if __name__ == '__main__': - # Parse the command line - parser = argparse.ArgumentParser(description='Generate HPKP / TrustKit SSL Pins.') - parser.add_argument('certificate', metavar='FILE', nargs='?', help='certificate file to read, if empty, stdin is used') - parser.add_argument('--type', dest='type', action='store', default='PEM', - help='Certificate file type; "PEM" (default) or "DER".') - args = parser.parse_args() - - if not args.certificate: - certificate = stdin.read() - elif os.path.isfile(args.certificate): - with io.open(args.certificate, 'rb') as certFile: - certificate = certFile.read() - else: - raise ValueError('Could not open certificate file {}'.format(args.certificate)) - - if args.type not in ['DER', 'PEM']: - raise ValueError('Invalid certificate type {}; expected DER or PEM'.format(args.type)) - - # Parse the certificate and print its information - p1 = Popen('openssl x509 -inform {} -text -noout'.format(args.type), shell=True, stdin=PIPE, stdout=PIPE) - certificate_txt = p1.communicate(input=certificate)[0].decode('utf-8') - - print('\nCERTIFICATE INFO\n----------------') - p1 = Popen('openssl x509 -subject -issuer -fingerprint -sha1 -noout -inform {}'.format( - args.type), shell=True, stdin=PIPE, stdout=PIPE) - print(p1.communicate(input=certificate)[0]) - - # Extract the certificate key's algorithm - # Tested on the output of OpenSSL 0.9.8zh and OpenSSL 1.0.2i - alg_txt = certificate_txt.split('Public Key Algorithm:')[1].split('\n')[0].strip() - key_algorithm = None - if alg_txt == 'id-ecPublicKey': - if 'prime256v1' in certificate_txt: - key_algorithm = SupportedKeyAlgorithmsEnum.ECDSA_SECP256R1 - if 'secp384r1' in certificate_txt: - key_algorithm = SupportedKeyAlgorithmsEnum.ECDSA_SECP384R1 - elif alg_txt == 'rsaEncryption': - if 'Key: (2048 bit)' in certificate_txt: - key_algorithm = SupportedKeyAlgorithmsEnum.RSA_2048 - elif 'Key: (4096 bit)' in certificate_txt: - key_algorithm = SupportedKeyAlgorithmsEnum.RSA_4096 - - if key_algorithm is None: - raise ValueError('Error: Certificate key algorithm not supported: {}.'.format(alg_txt)) - - - # Generate the Subject Public Key Info hash - if key_algorithm == SupportedKeyAlgorithmsEnum.ECDSA_SECP256R1: - # The OpenSSL command is different for ECDSA secp256 - openssl_alg = 'ec' - elif key_algorithm == SupportedKeyAlgorithmsEnum.ECDSA_SECP384R1: - # The OpenSSL command is different for ECDSA secp384 - openssl_alg = 'ec' - elif key_algorithm == SupportedKeyAlgorithmsEnum.RSA_2048: - openssl_alg = 'rsa' - elif key_algorithm == SupportedKeyAlgorithmsEnum.RSA_4096: - openssl_alg = 'rsa' - else: - raise ValueError('Unexpected key algorithm') - - if platform.system() == 'Windows': - cmd_redirects = '2>nul' - else: - cmd_redirects = '-in /dev/stdin 2>/dev/null' - - p1 = Popen('openssl x509 -pubkey -noout -inform {} ' - '| openssl {} -outform DER -pubin {}'.format(args.type, openssl_alg, cmd_redirects), - shell=True, stdin=PIPE, stdout=PIPE) - spki = p1.communicate(input=certificate)[0] - - spki_hash = hashlib.sha256(spki).digest() - hpkp_pin = base64.b64encode(spki_hash) - - print('\nTRUSTKIT CONFIGURATION\n----------------------') - print('kTSKPublicKeyHashes: @[@"{}"] // You will also need to configure a backup pin'.format(hpkp_pin)) diff --git a/get_pin_from_certificate.sh b/get_pin_from_certificate.sh new file mode 100755 index 00000000..58c20393 --- /dev/null +++ b/get_pin_from_certificate.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# The LibreSSL that ships with macOS doesn't detect DER input on its own +FORMAT=PEM +openssl x509 -inform PEM -in "$1" -noout 2>/dev/null || FORMAT=DER + +PIN="$(openssl x509 -inform "$FORMAT" -in "$1" -pubkey -noout \ + | openssl pkey -pubin -outform der \ + | openssl dgst -sha256 -binary \ + | openssl enc -base64)" +echo "$PIN" From 113795f46f2084833fc2d9a64aa869930b81ed5b Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Thu, 22 Oct 2026 15:32:18 -0500 Subject: [PATCH 5/5] Hash the SPKI as encoded in the pin script; version the SPKI cache openssl pkey re-encodes the key, so for some certificates the script's pin never matched what TrustKit computes. The script now hashes the SPKI bytes straight from the certificate. The shared SPKI cache file is renamed so that hashes cached by TrustKit 3.x are recomputed. --- TrustKit/Pinning/TSKSPKIHashCache.h | 3 ++- docs/getting-started.md | 9 +++------ get_pin_from_certificate.sh | 13 +++++++++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/TrustKit/Pinning/TSKSPKIHashCache.h b/TrustKit/Pinning/TSKSPKIHashCache.h index ff6d4f7d..9ae33a3d 100644 --- a/TrustKit/Pinning/TSKSPKIHashCache.h +++ b/TrustKit/Pinning/TSKSPKIHashCache.h @@ -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 SPKICacheDictionnary; diff --git a/docs/getting-started.md b/docs/getting-started.md index 78a5a474..d58b8689 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -44,12 +44,9 @@ repository on a PEM or DER certificate: $ ./get_pin_from_certificate.sh ca.pem $ ./get_pin_from_certificate.sh ca.der -It runs the following `openssl` commands: - - $ openssl x509 -in ca.pem -pubkey -noout \ - | openssl pkey -pubin -outform der \ - | openssl dgst -sha256 -binary \ - | openssl enc -base64 +The script hashes the Subject Public Key Info exactly as it is encoded in the certificate, as +TrustKit does. Avoid re-encoding the key first, for example with `openssl pkey`: for some +certificates that changes the bytes, and the resulting pin won't match. ## Deploying TrustKit diff --git a/get_pin_from_certificate.sh b/get_pin_from_certificate.sh index 58c20393..4439ab11 100755 --- a/get_pin_from_certificate.sh +++ b/get_pin_from_certificate.sh @@ -13,8 +13,17 @@ fi FORMAT=PEM openssl x509 -inform PEM -in "$1" -noout 2>/dev/null || FORMAT=DER -PIN="$(openssl x509 -inform "$FORMAT" -in "$1" -pubkey -noout \ - | openssl pkey -pubin -outform der \ +DER="$(mktemp)" +trap 'rm -f "$DER"' EXIT +openssl x509 -inform "$FORMAT" -in "$1" -outform DER -out "$DER" + +# TrustKit hashes the Subject Public Key Info exactly as the certificate encodes it; re-encoding the key with +# `openssl pkey` can change those bytes. It is tbsCertificate's 7th field, or its 6th without the optional version. +read -r OFFSET LENGTH < <(openssl asn1parse -inform DER -in "$DER" \ + | sed -E 's/^ *([0-9]+):d=([0-9]+) +hl= *([0-9]+) +l= *([0-9]+) +(.*)$/\1 \2 \3 \4 \5/' \ + | awk '$2 == 2 { n++; if (n == 1 && /cont \[ 0 \]/) skip = 1; if (n == 6 + skip) { if (/SEQUENCE/) print $1, $3 + $4; exit } }') + +PIN="$(tail -c +$((OFFSET + 1)) "$DER" | head -c "$LENGTH" \ | openssl dgst -sha256 -binary \ | openssl enc -base64)" echo "$PIN"