Переопределяющая проверка цепочки TLS правильно
Эта статья описывает, как переопределить цепочечное поведение проверки сетевых соединений, защищенных с Transport Layer Security (TLS).
Когда сертификат TLS проверяется, операционная система проверяет свою цепочку доверия. Если та цепочка доверия содержит только допустимые сертификаты, и концы в известном (доверяли) сертификату привязки, то сертификат считают допустимым. Если это не делает, это считают недопустимым. При использовании коммерчески подписанного сертификата от крупного поставщика сертификат должен “просто работать”.
Однако, если Вы делаете что-то, что выходит за пределы нормы — создание клиентских сертификатов для Ваших пользователей, предоставление услуги для многократных доменов с единственным сертификатом, которому не доверяет для тех доменов, с помощью самоподписанного сертификата, соединяясь с узлом IP-адрес (где сетевой стек не может определить имя хоста сервера), и т.д. — необходимо предпринять дополнительные шаги, чтобы убедить операционную систему принимать сертификат.
На высоком уровне проверка цепочки TLS выполняется доверительным объектом (SecTrustRef
). Этот объект содержит много флагов, управляющих тем, какие типы проверки выполняются. Как правило Вы не должны касаться этих флагов, но необходимо знать об их существовании. Кроме того, доверительный объект содержит политику (SecPolicyRef
) это позволяет Вам обеспечивать имя хоста, которое должно использоваться при оценке сертификата TLS. Наконец, доверительный объект содержит список доверяемых сертификатов привязки, которые может изменить Ваше приложение.
Эта статья разделяется на многократные части. Первая часть, Управляя Доверительными Объектами, описывает распространенные способы управлять доверительным объектом изменить поведение проверки. Остающиеся разделы, Доверительные Объекты и NSURLConnection и Доверительные Объекты и NSStream, показывают, как интегрировать те изменения с различными сетевыми технологиями.
Управление доверительными объектами
Подробные данные управления доверительным объектом зависят в значительной степени от того, что Вы пытаетесь переопределить. Двумя наиболее распространенными вещами переопределить является имя хоста (который должен соответствовать или листовое общее название сертификата или одно из имен в его Подчиненном расширении Альтернативного названия), и набор привязок (которые определяют ряд доверенных центров сертификации).
Для добавления сертификата списку доверяемых сертификатов привязки необходимо скопировать существующие сертификаты привязки в массив, создать непостоянную версию того массива, добавить новый сертификат привязки непостоянному массиву и сказать доверительному объекту использовать тот недавно обновленный массив для будущей оценки доверия. Простая функция, чтобы сделать это перечислено в Перечислении 1.
Перечисление 1 , Добавляющее привязку к a SecTrustRef
объект
SecTrustRef addAnchorToTrust(SecTrustRef trust, SecCertificateRef trustedCert) |
{ |
#ifdef PRE_10_6_COMPAT |
CFArrayRef oldAnchorArray = NULL; |
/* In OS X prior to 10.6, copy the built-in |
anchors into a new array. */ |
if (SecTrustCopyAnchorCertificates(&oldAnchorArray) != errSecSuccess) { |
/* Something went wrong. */ |
return NULL; |
} |
CFMutableArrayRef newAnchorArray = CFArrayCreateMutableCopy( |
kCFAllocatorDefault, 0, oldAnchorArray); |
CFRelease(oldAnchorArray); |
#else |
/* In iOS and OS X v10.6 and later, just create an empty |
array. */ |
CFMutableArrayRef newAnchorArray = CFArrayCreateMutable ( |
kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); |
#endif |
CFArrayAppendValue(newAnchorArray, trustedCert); |
SecTrustSetAnchorCertificates(trust, newAnchorArray); |
#ifndef PRE_10_6_COMPAT |
/* In iOS or OS X v10.6 and later, reenable the |
built-in anchors after adding your own. |
*/ |
SecTrustSetAnchorCertificatesOnly(trust, false); |
#endif |
return trust; |
Для переопределения имени хоста (чтобы позволить сертификату для одного определенного сайта работать на другой определенный сайт или позволять сертификату работать, когда Вы соединились с узлом его IP-адресом) необходимо заменить объект политики, что доверительное использование политики, чтобы определить, как интерпретировать сертификат. Чтобы сделать это, сначала создайте новый объект политики TLS для желаемого имени хоста. Тогда создайте массив, содержащий ту политику. Наконец, скажите доверительному объекту использовать тот массив для будущей оценки доверия. Перечисление 2 показывает функцию, делающую это.
Перечисление 2 , Изменяющее удаленное имя хоста для a SecTrustRef
объект
SecTrustRef changeHostForTrust(SecTrustRef trust) |
{ |
CFMutableArrayRef newTrustPolicies = CFArrayCreateMutable( |
kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); |
SecPolicyRef sslPolicy = SecPolicyCreateSSL(true, CFSTR("www.example.com")); |
CFArrayAppendValue(newTrustPolicies, sslPolicy); |
#ifdef MAC_BACKWARDS_COMPATIBILITY |
/* This technique works in OS X (v10.5 and later) */ |
SecTrustSetPolicies(trust, newTrustPolicies); |
CFRelease(oldTrustPolicies); |
return trust; |
#else |
/* This technique works in iOS 2 and later, or |
OS X v10.7 and later */ |
CFMutableArrayRef certificates = CFArrayCreateMutable( |
kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); |
/* Copy the certificates from the original trust object */ |
CFIndex count = SecTrustGetCertificateCount(trust); |
CFIndex i=0; |
for (i = 0; i < count; i++) { |
SecCertificateRef item = SecTrustGetCertificateAtIndex(trust, i); |
CFArrayAppendValue(certificates, item); |
} |
/* Create a new trust object */ |
SecTrustRef newtrust = NULL; |
if (SecTrustCreateWithCertificates(certificates, newTrustPolicies, &newtrust) != errSecSuccess) { |
/* Probably a good spot to log something. */ |
return NULL; |
} |
return newtrust; |
#endif |
} |
Доверительные объекты и NSURLConnection
Переопределять цепочечное поведение проверки NSURLConnection
, необходимо переопределить два метода:
connection:canAuthenticateAgainstProtectionSpace:
Этот метод говорит
NSURLConnection
то, что это знает, как обработать аутентификацию определенного типа. Когда Ваше приложение решает, доверять ли сертификату сервера, это считают формой аутентификации — Ваше приложение, аутентифицирующее сервер.connection:didReceiveAuthenticationChallenge:
В этом методе Ваш код должен изменить доверительные политики, ключи или имена хоста, предоставленные сервером или клиентом так, чтобы доверительная политика оценила успешно.
Перечисление 3 показывает пример этих двух методов.
Перечисление 3 , Переопределяющее доверительный объект, используемый NSURLConnection
объект
// If you are building for OS X 10.7 and later or iOS 5 and later, |
// leave out the first method and use the second method as the |
// connection:willSendRequestForAuthenticationChallenge: method. |
// For earlier operating systems, include the first method, and |
// use the second method as the connection:didReceiveAuthenticationChallenge: |
// method. |
#ifndef NEW_STYLE |
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { |
#pragma unused(connection) |
NSString *method = [protectionSpace authenticationMethod]; |
if (method == NSURLAuthenticationMethodServerTrust) { |
return YES; |
} |
return NO; |
} |
-(void)connection:(NSURLConnection *)connection |
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge |
#else |
-(void)connection:(NSURLConnection *)connection |
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge |
#endif |
{ |
NSURLProtectionSpace *protectionSpace = [challenge protectionSpace]; |
if ([protectionSpace authenticationMethod] == NSURLAuthenticationMethodServerTrust) { |
SecTrustRef trust = [protectionSpace serverTrust]; |
/***** Make specific changes to the trust policy here. *****/ |
/* Re-evaluate the trust policy. */ |
SecTrustResultType secresult = kSecTrustResultInvalid; |
if (SecTrustEvaluate(trust, &secresult) != errSecSuccess) { |
/* Trust evaluation failed. */ |
[connection cancel]; |
// Perform other cleanup here, as needed. |
return; |
} |
switch (secresult) { |
case kSecTrustResultUnspecified: // The OS trusts this certificate implicitly. |
case kSecTrustResultProceed: // The user explicitly told the OS to trust it. |
{ |
NSURLCredential *credential = |
[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; |
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; |
return; |
} |
default: |
/* It's somebody else's key. Fall through. */ |
} |
/* The server sent a key other than the trusted key. */ |
[connection cancel]; |
// Perform other cleanup here, as needed. |
} |
} |
Доверительные объекты и NSStream
Путем Вы переопределяете доверие для NSStream
зависит от того, что Вы пытаетесь сделать.
Если все, что необходимо сделать, указывают различное имя хоста TLS, можно сделать это тривиально путем выполнения трех строк кода перед открытием потоков:
Перечисление 4 , Переопределяющее имя хоста TLS с NSStream
NSDictionary *sslSettings = |
[NSDictionary dictionaryWithObjectsAndKeys: |
@"www.gatwood.net", |
(__bridge id)kCFStreamSSLPeerName, nil]; |
if (![myInputStream setProperty: sslSettings |
forKey: (__bridge NSString *)kCFStreamPropertySSLSettings]) { |
// Handle the error here. |
} |
Это изменяет понятие потока его имени хоста так, чтобы, когда потоковый объект позже создает доверительный объект, это обеспечило новое имя.
Если необходимо фактически изменить список доверяемых привязок, процесс несколько более сложен. Как только потоковый объект создает доверительный объект, он оценивает его. Если та доверительная оценка перестала работать, поток закрывается, прежде чем Ваш код имеет возможность изменить доверительный объект. Таким образом, для переопределения доверительной оценки Вы должны:
Отключите проверку цепочки TLS. Поскольку поток никогда не оценивает цепочку TLS, оценка не перестала работать, и поток не закрывается.
Выполните цепочечную проверку сами в делегате потока (после того, как, изменив доверительный объект соответственно).
К тому времени, когда Ваш потоковый обработчик событий делегата вызывают, чтобы указать, что существует пространство, доступное на сокете, операционная система уже создала канал TLS, получила цепочку сертификата из другого конца соединения и создала доверительный объект оценить его. В этой точке у Вас есть открыть поток TLS, но Вы понятия не имеете, можно ли доверять узлу в другом конце. Путем отключения цепочечной проверки это становится ответственностью проверить, что можно доверять узлу в другом конце. Среди прочего это означает:
Не отключайте проверяющий имени хоста, создающий политику не-TLS или передающий в a
NULL
указатель для имени хоста. Если Вы преднамеренно соединяетесь с узлом с помощью имени хоста кроме одного из имен, перечисленных на его сертификате, необходимо позволить работу, только если сертификат, который Вы получаете от того узла, допустим для некоторого другого домена, которым Вы управляете.Не слепо доверяйте самоподписанным сертификатам как привязкам (
kSecTrustOptionImplicitAnchors
). Вместо этого добавьте свой собственный (самоподписанный) сертификат CA списку доверяемых привязок.Произвольно не отключайте другие параметры безопасности, такие как проверка сертификаты с истекшим сроком или корни. Существуют определенные ситуации, где выполнение так могло бы быть целесообразным (такие как проверка, что документ, со знаком назад в 2001, был подписан сертификатом, который был допустимой спиной в 2001), но в сетевых целях, должны обычно оставляться в покое опции по умолчанию.
С теми правилами в памяти, Перечисление 5 показывает, как использовать пользовательские привязки TLS с NSStream
. Это перечисление также использует функцию addAnchorToTrust
от Перечисления 1.
Перечисление 5 Используя пользовательские привязки TLS с NSStream
/* Code executed after creating the socket: */ |
[inStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL |
forKey:NSStreamSocketSecurityLevelKey]; |
NSDictionary *sslSettings = |
[NSDictionary dictionaryWithObjectsAndKeys: |
(id)kCFBooleanFalse, (id)kCFStreamSSLValidatesCertificateChain, |
nil]; |
[inStream setProperty: sslSettings forKey: (__bridge NSString *)kCFStreamPropertySSLSettings]; |
... |
/* Methods in your stream delegate class */ |
NSString *kAnchorAlreadyAdded = @"AnchorAlreadyAdded"; |
- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent |
{ |
if (streamEvent == NSStreamEventHasBytesAvailable || streamEvent == NSStreamEventHasSpaceAvailable) { |
/* Check it. */ |
NSArray *certs = [theStream propertyForKey: (__bridge NSString *)kCFStreamPropertySSLPeerCertificates]; |
SecTrustRef trust = (SecTrustRef)[theStream propertyForKey: (__bridge NSString *)kCFStreamPropertySSLPeerTrust]; |
/* Because you don't want the array of certificates to keep |
growing, you should add the anchor to the trust list only |
upon the initial receipt of data (rather than every time). |
*/ |
NSNumber *alreadyAdded = [theStream propertyForKey: kAnchorAlreadyAdded]; |
if (!alreadyAdded || ![alreadyAdded boolValue]) { |
trust = addAnchorToTrust(trust, self.trustedCert); // defined earlier. |
[theStream setProperty: [NSNumber numberWithBool: YES] forKey: kAnchorAlreadyAdded]; |
} |
SecTrustResultType res = kSecTrustResultInvalid; |
if (SecTrustEvaluate(trust, &res)) { |
/* The trust evaluation failed for some reason. |
This probably means your certificate was broken |
in some way or your code is otherwise wrong. */ |
/* Tear down the input stream. */ |
[theStream removeFromRunLoop: ... forMode: ...]; |
[theStream setDelegate: nil]; |
[theStream close]; |
/* Tear down the output stream. */ |
... |
return; |
} |
if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) { |
/* The host is not trusted. */ |
/* Tear down the input stream. */ |
[theStream removeFromRunLoop: ... forMode: ...]; |
[theStream setDelegate: nil]; |
[theStream close]; |
/* Tear down the output stream. */ |
... |
} else { |
// Host is trusted. Handle the data callback normally. |
} |
} |
} |