Основными шагами являются:
- Создать систему для генерации сертификатов (простой, но нетривиальной, если это производственная система)
- Передача сертификатов на IPad (не встраивается в комплекте с магазином приложений!)
- Сохраните все полученные учетные данные в цепочке ключей приложения (где Apple говорит, что они принадлежат)
- Получить сохраненные учетные данные f ром брелок для использования в NSURLConnections
- На самом деле подлинность сертификата сервера и вернуть учетные данные клиента
Шаг 1. Сформировать Сертификаты
Ref: http://developer-should-know.tumblr.com/post/127063737582/how-to-create-your-own-pki-with-openssl
Вы можете использовать другие методы, но OpenSSL для Windows [http://slproweb.com/products.html] является довольно удивительным, за исключением того, что стандартным интерфейсом является cmdline, и документация трудно выполнить.
Что я хочу, чтобы кто-то мне объяснил, это очевидно, но нет: [a] Приложение устанавливается в корневой каталог уровня и включает файлы конфигурации, которые используются по умолчанию для параметров, которые не указаны в команде строка [b] местоположения промежуточных и выходных файлов должны быть указаны в файлах конфигурации [c] определенные файлы должны быть созданы вручную перед запуском команд [d] вы должны создать структуру файлов/папок, которая подходит что вы пытаетесь сделать, а затем соответствующим образом настроить файлы cfg.
В моем случае это означало, что для моей компании был один RootCA, промежуточный сертификат для каждого клиента (настроенный на то, чтобы делать только клиентские сертификаты), сертификат сервера для каждого клиента и клиентские сертификаты по мере необходимости.(Это минимальная конфигурация, никогда не используйте пар CA/клиента, держать корень в сейфе) Вот моя структура файла:
c:\sslcert
root
certs
YourCompany (duplicate this structure as required)
intermediate
server
client
crl (optional)
В папке sslcert верхнего уровня
.rnd (empty file)
certindex.txt (empty file)
serial.txt (Text file seeded with the text “01”, hold the quotes)
В корневой папке
RootCA.cfg
В сертификаты \ папка шаблона
IntermediateCA.cfg
Установить рабочий каталог и запустить OpenSSL кд \ sslcert C: \ OpenSSL-Win32 \ Bin \ openssl.exe
Создание корневой ключ и сертификат в одном шаге
req -config ./root/RootCA.cfg -new -x509 -days 7300 -extensions v3_ca -keyout root/YourCompanyRootCAkey.pem -out root/YourCompanyRootCAcert.cer
Примечание для начинающих: -extensions позволяет вам выбрать применение одного из нескольких подразделов в одном и том же файле cfg.
Проверить ключ и сертификат (опционально)
x509 -noout -text -in root/YourCompanyRootCAcert.cer
Запрос новый промежуточный сертификат
req -config certs/YourCompany/IntermediateCA.cfg -new -keyout certs/YourCompany/intermediate/intermediateCAkey.pem -out certs/YourCompany/intermediate/intermediateCAreq.pem
Вход промежуточный сертификат с использованием корневого сертификата находится в корневой конфигурации
ca -config root/RootCA.cfg -extensions v3_intermediate_ca -days 3650 -notext -in certs/YourCompany/intermediate/intermediateCAreq.pem -out certs/YourCompany/intermediate/YourCompanyIntermediateCAcert.cer
Check ключ и сертификат (необязательно)
x509 -noout -text -in certs/YourCompany/intermediate/YourCompanyIntermediateCAcert.cer
Создайте файл сертификата цепи путем конкатенации промежуточные и корневые сертификаты (это только простой Append из командной строки - новая цепь будет добавлена в окончательный пакет p12)
c:\sslcert> type c:\sslcert\certs\YourCompany\intermediate\YourCompanyIntermediateCAcert.cer c:\sslcert\root\YourCompanyRootCAcert.cer > c:\sslcert\certs\YourCompany\intermediate\YourCompanyCAchain.cer
Запросить новый клиентский ключ и сертификат
genrsa -aes256 -out certs/YourCompany/client/YourCompanyClientkey.pem 2048
req -config certs/YourCompany/IntermediateCA.cfg -key
certs/YourCompany/client/YourCompanyClientkey.pem -new -sha256 -out certs/YourCompany/client/YourCompanyClientreq.pem
Подписать и тестовый клиент сертификат с промежуточным органом
ca -config certs/YourCompany/IntermediateCA.cfg -extensions usr_cert -days 1095 -notext -md sha256 -in certs/YourCompany/client/YourCompanyClientreq.pem -out certs/YourCompany/client/YourCompanyClientcert.cer
x509 -noout -text -in certs/YourCompany/client/YourCompanyClientcert.cer
verify -CAfile certs/YourCompany/intermediate/YourCompanyCAchain.cer certs/YourCompany/client/YourCompanyClientcert.cer
сертификат клиента Пакета
pkcs12 -export -in certs/YourCompany/client/YourCompanyClientcert.cer -name “YourCompany Smips Client” -inkey certs/YourCompany/client/YourCompanyClientkey.pem -certfile certs/YourCompany/intermediate/YourCompanyCAchain.cer -out certs/YourCompany/client/YourCompanyClientWithName.p12
Rename PKCS для импорта в прошивку из электронной почты/ITunes
c:\sslcert> copy c:\sslcert\certs\YourCompany\client\YourCompanyClient.p12 c:\sslcert\certs\YourCompany\client\YourCompanyClient.yourext12
Запрос нового ключа сервера и сертификата
genrsa -aes256 -out certs/YourCompany/server/YourCompanyServerkey.pem 2048
req -config certs/YourCompany/IntermediateCA.cfg -key certs/YourCompany/server/YourCompanyServerkey.pem -new -sha256 -out certs/YourCompany/server/YourCompanyServerreq.pem
Войдите и контрольная работа сертификат сервера с промежуточным органом
ca -config certs/YourCompany/IntermediateCA.cfg -extensions server_cert -days 1095 -notext -md sha256 -in certs/YourCompany/server/YourCompanyServerreq.pem -out certs/YourCompany/server/YourCompanyServercert.cer
x509 -noout -text -in certs/YourCompany/server/YourCompanyServercert.cer
verify -CAfile certs/YourCompany/intermediate/YourCompanyCAchain.cer certs/YourCompany/server/YourCompanyServercert.cer
сертификат сервера Пакет
pkcs12 -export -in certs/YourCompany/server/YourCompanyServercert.cer -name “YourCompany Smips Server” -inkey certs/YourCompany/server/YourCompanyServerkey.pem -certfile certs/YourCompany/intermediate/YourCompanyCAchain.cer -out certs/YourCompany/server/YourCompanyServer.p12
Вот CFG файлы: Root
dir = .
[ ca ]
default_ca = CA_default
[ CA_default ]
serial = $dir/serial.txt
database = $dir/certindex.txt
new_certs_dir = $dir/certs
certs = $dir/certs
private_key = $dir/root/yourcompanyRootCAkey.pem
certificate = $dir/root/yourcompanyRootCAcert.cer
default_days = 7300
default_md = sha256
preserve = no
email_in_dn = no
nameopt = default_ca
certopt = default_ca
policy = policy_strict
[ policy_strict ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 4096 # Size of keys
default_keyfile = key.pem # name of generated keys
default_md = sha256 # message digest algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
[ req_distinguished_name ]
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
0.organizationName_default = yourcompany
organizationalUnitName_default = yourcompanyRoot Certification
emailAddress_default = [email protected]
localityName_default = Okeefenokee
stateOrProvinceName_default = Wisconsin
countryName_default = US
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ crl_ext ]
authorityKeyIdentifier=keyid:always
Intermediate
dir = .
# [For non-command-line folks, everything is keyed to the working directory here (.) so if your working prompt says c:\sslcerts> then the cfg will look for serial.txt at c:\sslcerts\serial.txt and bomb if it doesn’t find things laid out accordingly. Thats why you set up a directory structure to match these entries]
[ ca ]
default_ca = CA_default
[ CA_default ]
serial = $dir/serial.txt
database = $dir/certindex.txt
crl_dir = $dir/certs/yourcompany/crl
new_certs_dir = $dir/certs
certs = $dir/certs
private_key = $dir/certs/yourcompany/intermediate/IntermediateCAkey.pem
certificate = $dir/certs/yourcompany/intermediate/yourcompanyIntermediateCAcert.cer
default_days = 3650
default_md = sha256
preserve = no
email_in_dn = no
nameopt = default_ca
certopt = default_ca
crlnumber = $dir/certs/yourcompany/crl/crlnumber
crl = $dir/certs/yourcompany/crl/crl.pem
crl_extensions = crl_ext
default_crl_days = 365
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 4096 # Size of keys
default_keyfile = $dir/certs/yourcompany/intermediate/IntermediateCAkey.pem
default_md = sha256 # message digest
# the old default was md1 - change this]
algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name
x509_extensions = v3_intermediate_ca
[ req_distinguished_name ]
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
0.organizationName_default = yourcompany
organizationalUnitName_default = yourcompany Intermediate Certification
emailAddress_default = [email protected]
localityName_default = Okeefenokee
stateOrProvinceName_default = Wisconsin [should be spelled out]
countryName_default = US
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
# Important - the pathlen parameter prevents this cert from being used to create new intermediate certs. The subsequent subsections for server and client certs allows you to specify their type and intended usage, as distinct from the intermediate cert, in the same cfg file
[ usr_cert ]
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[ crl_ext ]
authorityKeyIdentifier = keyid:always
2. Передача сертификатов на IPad
Ref: how to register the app to open the pdf file in my app in ipad
компании Apple рекомендует регистрирующую новый тип файл обрабатывается вашим приложением и передача файлов p12 переименованных с новым расширением пользовательского к устройству (вручную или электронной почты) для установки клиентских сертификатов. Файл p12 должен включать в себя общедоступную цепочку сертификатов, а также информацию о сертификате клиента, как определено в шаге 1 выше. Когда вы пытаетесь открыть такой файл, устройство отправляет запуск/пробуждение в ваш делегат приложения, который вам нужно обрабатывать (не в didload, потому что это может быть след).
Это немного изменилось с помощью v8 или 9, но мне нужно поддерживать 7, поэтому это относится к устаревшему обработчику. Это же решение, хотя и начинается с добавления в файл plist приложения, как показано на скриншотах ниже.
Обратите внимание, что вам потребуется две новые иконки и расширение файла, который, вероятно, будет востребовано другим приложением
Далее вам нужно делегат/обработчик, который должен быть само не -explanatory. Поскольку эта часть не имеет никакого отношения к нормальному потоку управления, я обрабатываю обработку всех делегатов в AppDelegate.m. (Это так неправильно?) Установить методы/переменные по мере необходимости и игнорируйте параноидальную дополнительную проверку на наличие файла ...
Ref: https://www.raywenderlich.com/6475/basic-security-in-ios-5-tutorial-part-1
- (BOOL) application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
if (url) {
self.p12Data = [NSData dataWithContentsOfFile:[url path]];
if (!p12Data) {
[self messageBox:@"Warning" : @"Failed to read data file, cancelling certificate import"];
}
else {
[self presentAlertViewForPassPhrase];
}
NSFileManager * fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:[url path]]) {
[fileManager removeItemAtPath:[url path] error:NULL];
}
}
return YES;
}
- (void)presentAlertViewForPassPhrase {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Certificate Credentials"
message:@"Please enter the passphrase for your certificate"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Done", nil];
[alert setAlertViewStyle:UIAlertViewStyleSecureTextInput];
[alert show];
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
if (buttonIndex == 1) { // User selected "Done"
UITextField *ppField = [alertView textFieldAtIndex:0];
if ([ppField.text length] > 0) {
[self loadCertificates:ppField.text];
}
//Handle Else
}
else
{ // User selected "Cancel"
[self messageBox:@"Information" : @"Certificate import cancelled"];
}
}
3. Сохранить полученный учетные данные в app keychain
Теперь, когда у вас есть необработанные данные p12, должно быть просто выяснить, что делать дальше ... НЕ. Вся документация, по-видимому, относится к хранилищу имен/pwd, а страшное количество плакатов предполагает сохранение сертификата сервера в файловой системе, что нормально, но совершенно не имеет смысла, когда у вас есть брелок, и Apple говорит, что для этого и есть. И последнее, но не менее важное: как вы различаете хранимые сертификаты и как их обновлять?
Короче говоря, я решил сделать полное удаление/переустановку после того, как попробовал всевозможные вещи, которые не работают, чтобы проверить, должно ли оно быть обновление или начальная загрузка. Кроме того, это то, что я хотел сделать в первом потому что это моя сеть приложений. Все это материал CF, и я не использую ARC, потому что я отказываюсь переносить все, что мне не нужно. Насколько я могу судить, до тех пор, пока вы назначаете CF, бросаете NS и CFRelease после использования, никаких предупреждений нет.
Это ключевые ссылки:
Enumerate all Keychain items in my iOS application
[важно, чтобы помочь себе, что ваш брелок выглядит]
How to delete all keychain items accessible to an app?
What makes a keychain item unique (in iOS)?
http://help.sap.com/saphelp_smp307sdk/helpdata/en/7c/03830b70061014a937d8267bb3f358/content.htm
[https://developer.apple.com/library/ios/samplecode/AdvancedURLConnections/Listings/Credentials_m.html, который говорит:
// IMPORTANT: SecCertificateRef's are not uniqued (that is, you can get two
// different SecCertificateRef values that described the same fundamental
// certificate in the keychain), nor can they be compared with CFEqual. So
// we match up certificates based on their data values.
резюме в том, что (Дух), проще всего сделать, это присвоить метку к серт, так что вы можете посмотреть его однозначно и понимать, что если вы сохраните идентичность, он будет автоматически разделен на ключ и сертификат, что может быть - не обязательно - привело к некоторым трудностям с заменой.
Код (объяснение следует):
- (void) loadCertificates:(NSString *)passPhrase {
BOOL lastError = false;
NSMutableDictionary * p12Options = [[NSMutableDictionary alloc] init];
[p12Options setObject:passPhrase forKey:(id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus err = SecPKCS12Import((CFDataRef)p12Data, (CFDictionaryRef)p12Options, &items);
if (err != noErr) {
[self messageBox:@"Error" : @"Unable to extract security information with the supplied credentials. Please retry"];
lastError = true;
}
if (!lastError && err == noErr && CFArrayGetCount(items) > 0) {
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
//Clean-up
NSArray *secItemClasses = [NSArray arrayWithObjects:
(id)kSecClassCertificate,
(id)kSecClassKey,
(id)kSecClassIdentity,
nil];
for (id secItemClass in secItemClasses) {
NSDictionary *spec = @{(id)kSecClass: secItemClass};
err = SecItemDelete((CFDictionaryRef)spec);
}
//Client Identity & Certificate
SecIdentityRef clientIdentity = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
NSDictionary *addIdentityQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kClientIdentityLabel, kSecAttrLabel,
(id)clientIdentity, kSecValueRef,
nil];
err = SecItemAdd((CFDictionaryRef)addIdentityQuery, NULL);
if (err == errSecDuplicateItem) {
NSLog(@"Duplicate identity");
}
if (err != noErr) {
[self messageBox:@"Warning" : @"Failed to save the new identity"];
lastError = true;
}
//Server Certificate
CFArrayRef chain = CFDictionaryGetValue(identityDict, kSecImportItemCertChain);
CFIndex N = CFArrayGetCount(chain);
BOOL brk = false;
for (CFIndex i=0; (i < N) && (brk == false); i++) {
SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(chain, i);
CFStringRef summary = SecCertificateCopySubjectSummary(cert);
NSString* strSummary = [[NSString alloc] initWithString:(NSString *)summary];
if ([strSummary containsString:@"Root"] || (i == N)) {
NSDictionary *addCertQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kServerCertificateLabel, kSecAttrLabel,
(id)cert, kSecValueRef,
nil];
err = SecItemAdd((CFDictionaryRef)addCertQuery, NULL);
if (err == errSecDuplicateItem) {
NSLog(@"Duplicate root certificate");
}
if (err != noErr) {
[self messageBox:@"Warning" : @"Failed to save the new server certificate"];
lastError = true;
}
brk = true;
}
[strSummary release];
CFRelease(summary);
}
}
else {
[self messageBox:@"Error" : @"Unable to extract security information with the supplied credentials. Please retry"];
lastError = true;
}
[p12Options release];
CFRelease(items);
if (!lastError) [self messageBox:@"Information" : @"Certificate import succeeded"];
}
где kClientIdentityLabel и kServerCertificateLabel произвольные метки.
Функции kSec слишком много/сложны, чтобы объяснить здесь подробно. Достаточно сказать, что все очищено, затем сохраняется извлеченная идентификация клиента, а затем извлечение корневого ЦС, который затем сохраняется отдельно. Почему цикл? потому что я не знал, является ли технически правильным предположить, что корень находится в конце цепи, но будет, если я сгенерирую p12, поэтому код пока что есть.
Обратите внимание, что ошибки из kSec кодируются так этот сайт незаменим: https://www.osstatus.com
4. Получить сохраненные учетные данные из брелка
После того, как учетные данные в связке ключей вы можете извлечь их константы выглядит (режимы отказа оставляют желать лучшего): сертификат сервера
- (void) reloadCredentials {
self.clientCredential = nil;
self.serverCertificateData = nil;
if (self.useClientCertificateIfPresent) {
NSDictionary* idQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kClientIdentityLabel, kSecAttrLabel,
(id)kSecClassIdentity, kSecClass,
kCFBooleanTrue, kSecReturnRef,
kSecMatchLimitAll, kSecMatchLimit,
nil];
CFArrayRef result = nil;
OSStatus err = SecItemCopyMatching((CFDictionaryRef)idQuery, (CFTypeRef*)&result);
if (err == errSecItemNotFound) {
[self messageBox:@"Warning" : @"Client credentials not found. Server connection may fail"];
}
else if (err == noErr && result != nil) {
SecIdentityRef clientIdentity = (SecIdentityRef)CFArrayGetValueAtIndex(result, 0);
SecCertificateRef clientCertificate;
SecIdentityCopyCertificate(clientIdentity, &clientCertificate);
const void *certs[] = { clientCertificate };
CFArrayRef certsArray = CFArrayCreate(NULL, certs, 1, NULL);
self.clientCredential = [NSURLCredential credentialWithIdentity:clientIdentity certificates:(NSArray*)certsArray
persistence:NSURLCredentialPersistenceNone];
CFRelease(certsArray);
CFRelease(clientCertificate);
CFRelease(result);
}
else {
[self messageBox:@"Warning" : @"Client or Server credentials not found. Server connection may fail"];
}
NSDictionary* serverCertQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kServerCertificateLabel, kSecAttrLabel,
(id)kSecClassCertificate, kSecClass,
kCFBooleanTrue, kSecReturnRef,
kSecMatchLimitAll, kSecMatchLimit,
nil];
CFArrayRef result1 = nil;
err = SecItemCopyMatching((CFDictionaryRef)serverCertQuery, (CFTypeRef*)&result1);
if (err == errSecItemNotFound) {
[self messageBox:@"Warning" : @"Server certificate not found. Server connection may fail"];
}
else if (err == noErr && result1 != nil) {
SecCertificateRef certRef = (SecCertificateRef)CFArrayGetValueAtIndex(result1, 0);
CFDataRef certRefData = SecCertificateCopyData(certRef);
self.serverCertificateData = (NSData *)certRefData;
CFRelease(certRefData);
CFRelease(result1);
}
else {
[self messageBox:@"Warning" : @"Client or Server credentials not found. Server connection may fail"];
}
}
}
5. Authenticate и вернуть клиенту верительные грамоты
Hoo boy. Это редактирование, объясняющее, как на самом деле использовать извлеченные сертификаты (это должно было быть легко очевидной частью ...)
Во-первых, уже сомнительная документация Apple уже омрачена новой инфраструктурой безопасности приложений (см., Например, : http://useyourloaf.com/blog/app-transport-security/). Я не собираюсь заниматься этим, но идея состоит в том, чтобы заставить всех всегда использовать https и доверенные сертификаты по умолчанию. Для моего сценария, с сертификатом пиннингом и взаимной аутентификацией между выделенными клиентами и частным сервером вы можете безопасно отключить эту функцию, добавив словарь на PLIST следующим образом:
Затем на шаге- уже имел учетные данные клиента для немедленного реагирования на эту проблему, но серверный сертификат плавает вокруг как NSData в формате DER, созданный SecCertificateCopyData, и неясно, что должно произойти, когда это вызовет.
Оказывается, что вы должны выполнить алгоритм в разделе 6 «Стандарт X.509» (https://tools.ietf.org/html/rfc5280). К счастью, это реализовано за кулисами с помощью функции iOS SecTrustEvaluate, но есть строительные леса для создания и странные вещи для понимания.
[Незначительная проблема - закончился из космоса !! Добавлен новый вопрос, в том числе и в конце этого шага.]
https://stackoverflow.com/questions/35964632/correctly-use-a-pinned-self-signed-certificate-in-ios-9-2
[Продолжение с другого поста]
Так вот это. Извините за не совсем качество производства, но я хотел похлопать его вместе, пока он был еще свеж в моем сознании. Я буду обновлять сообщение, если найду ошибки.
Надеется, что это помогает, и вот последнее звено в очень хорошую книгу, что, помимо всего прочего, даст вам мурашку о доверчивых коммерческих центрах ...
https://www.cs.auckland.ac.nz/~pgut001/pubs/book.pdf
Эй люди удивительного пост, Ваше наблюдение однако сообщение было удалено. Как вы могли бы предоставить отдых в чате? – murphguy
К сожалению, кто-то отказался от этого: «Я голосую, чтобы закрыть этот вопрос как не относящийся к теме, потому что это сообщение в блоге, а не вопрос - Paulw11 13 мар в 16:14». Может быть, у кого-то есть возможность его реактивировать, он все еще там? – saminpa
Спасибо, кстати. Я был бы рад отправить остальных, но на этом форуме нет возможности добраться до PM – saminpa