在 iOS App 中使用自签名证书

大多数App都需和Server通信来提供服务,这中间就牵涉到网络通信安全。网络通信安全是一个很大的话题,本文不打算全面覆盖,而是来理理HTTPS。

移动设备可能会处于不安全的网络环境中,比如连接了某个公共热点,攻击者不需要访问设备,只需访问设备所在的网络,就能获取到用户信息,所以,当应用中用户的信息需要保护时,开发者需要保证通信的安全性。

最简单直接的解决办法是采用HTTPS,在web服务器上安装一个自签名证书,启用HTTPS,然后对NSURLSession进行配置以接受该自签名证书。

HTTPS是如何做到通信安全的呢?答案是TLS/SSL协议。TLS(Transport Layer Security)/SSL(Secure Socket Layer)协议是专门为解决网络通信安全设计的。它的基石是非对称加密。

TLS/SSL链路中的数据是加密的,客户端给服务器发送的数据是用服务器的公钥加密的,由于非对称加密的数学特性,只有拥有私钥的服务器才能正确解密数据。服务器给客户端发送的数据则是用自己的私钥加密的,客户端用公钥解密。

那么我们如何判断服务器发给我们的公钥是值得信任的呢?通常商业网站的数字证书都是由中级证书或根证书来签名,而根证书是一开始就内置在设备中,不是通过网络交换的,这样当某个服务器声明说我是某某,我们可以通过证书链来判断真伪。

根证书其实是一个自签名证书,我们的应用也可以用自签名证书来确保网络通信安全,还可以省掉很大一笔证书费用,只要私钥足够安全,它甚至比商业证书更安全。

创建自签名证书

为了方便创建自签名证书来测试 TLS, Apple 为我们提供一个工具 Certificate Assitant,它内置在 OS X 中,我们可以通过 KeyChain 打开它;我们也可以使用 openssl。新手的话还是建议使用 Certificate Assitant. 详细步骤参考Creating Certificates for TLS Testing.

为服务端配置证书

我使用的是 Apache,配置如下:

1
2
3
4
5
6
7
#/etc/apache2/httpd.conf
<VirtualHost *:443>
#ServerName www.example.com
SSLEngine on
SSLCertificateFile "/etc/apache2/server.crt"
SSLCertificateKeyFile "/etc/apache2/server.key"
</VirtualHost>

由于 Apache 是使用 PEM 格式的证书和私钥,所以我们需要格式转换下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#Extracting a digital identity for use with Apache

$ # First extract the server certificate.
$
$ openssl pkcs12 -in "Deep Thought.p12" -nokeys -out server.crt
Enter Import Password: ****
MAC verified OK
$
$ # Next extract the server private key.
$
$ openssl pkcs12 -in "Deep Thought.p12" -nocerts -nodes -out server.key
Enter Import Password: ****
MAC verified OK

重启 Apache, 我们可以使用 openssl 的 s_client 子命令来测试下。

1
2
3
4
5
// Failed
$ openssl s_client -connect myserver.com:443

// Success
$ openssl s_client -connect myserver.com:443 -CAfile ./MyCACertificate.pem

接受自签名证书

URLSession

我们需要介入到 TLS 的授权过程,基本做法是判断是与我们指定的服务器通信需要授权,然后把自签名证书加入锚中。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Authentication Challenges and TLS Chain Validation
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

    print("authentication method \(challenge.protectionSpace.authenticationMethod)\n host: \(challenge.protectionSpace.host)")

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust && challenge.protectionSpace.host == "dongmeiliangsmacbook-pro.local" {

            // Custom evaluating a trust object
            let serverTrust = challenge.protectionSpace.serverTrust!
                let policy = SecPolicyCreateSSL(true, "dongmeiliangsmacbook-pro.local" as CFString)

                SecTrustSetPolicies(serverTrust, [policy] as CFArray)

                let path = Bundle.init(for: ViewController.self).path(forResource: "ServerCertificates", ofType: "cer")

                do {
                    let certData = try NSData(contentsOfFile: path!, options: NSData.ReadingOptions(rawValue: 0))
                        if let certificate = SecCertificateCreateWithData(nil, certData as CFData) {
                            SecTrustSetAnchorCertificates(serverTrust, [certificate] as CFArray)

                                var allowConnection = false

                                var trustResult: SecTrustResultType = .invalid

                                let err = SecTrustEvaluate(serverTrust, &trustResult)

                                if err == noErr {
                                    allowConnection = (trustResult == .unspecified) || (trustResult == .proceed)
                                }

                            print("err:\(err)\nallowConnection:\(allowConnection)")

                                    if
                                        allowConnection
                                        {
                                            completionHandler(.useCredential, URLCredential(trust:serverTrust))
                                        }
                                    else
                                    {
                                        completionHandler(.cancelAuthenticationChallenge, nil)
                                    }

                        }
                        else
                        {
                            print("certificate create with data failed")
                            completionHandler(.cancelAuthenticationChallenge, nil)
                        }

                }
            catch
            {
                print("read certificate data failed:\(error)")
                completionHandler(.cancelAuthenticationChallenge, nil)
            }

        }
        else
        {
            completionHandler(.performDefaultHandling, nil)
        }
}

AFNetworking

AFNetworking 只需要我们配置下 securityPolicy,代码如下:

1
2
3
4
5
6
7
8
9
lazy var apiClient: AFHTTPSessionManager = {
    let client = AFHTTPSessionManager(baseURL: URL(string: "https://dongmeiliangsmacbook-pro.local/"))
        let selfSignedCertificates = AFSecurityPolicy.certificates(in: Bundle.init(for: ViewController.self))

        client.securityPolicy = AFSecurityPolicy(pinningMode: .certificate, withPinnedCertificates: selfSignedCertificates)
        client.securityPolicy.allowInvalidCertificates = true

        return client
}()

注意点

客户端是把服务端的证书加入锚中。

完整示例

SelfSignedCertificate

Reference

AFNetworking SSL Pinning With Self-Signed Certificates
Creating Certificates for TLS Testing
HTTPS Server Trust Evaluation
URL Session Programming Guide

如何创建自定义的Xcode 6 工程模板

随着时间的推移,这篇文章的实践部分需要更新了,我找到一个更好的工具来做这件事,这就是 liftoff。相比与手动来制作工程模板,自定义 liftoff 的配置文件要容易很多,更重要的是它提供了文档。

liftoff 支持配置工程的组,目录结构和模板。配置工程的组和目录结构是通过 .liftoffrc 文件,它查找顺序是 ./.liftoffrc > ~/.liftoffrc > 默认配置文件,可以 man liftoffrc 来查看详细介绍。

litfoff 还可以在新建工程时通过在 .liftoffrc 中指定包含哪些文件,这些文件的来源顺序是 ./.liftoff/templates > ~/.liftoff/templates > 默认的文件。

通过 .liftoffrc 来配置我们想要的工程结构,然后自定义Podfile 来包含工程常用的 pod,可以为我们在新建工程省些事,减轻些负担,总之还是我们之前想办法把重复的事情自动化的思想。

下面是我的 ~/.liftoffrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
############################################################################
# The following keys can be used to configure defaults for project creation
# project_name:
# company:
# author:
# prefix:
# company_identifier:
############################################################################

test_target_name: UnitTests
configure_git: true
warnings_as_errors: true
enable_static_analyzer: true
indentation_level: 4
use_tabs: false
dependency_managers: cocoapods
enable_settings: false
strict_prompts: false
deployment_target: 8.0

run_script_phases:
  - file: todo.sh
    name: Warn for TODO and FIXME comments
  - file: bundle_version.sh
    name: Set version number

templates:
  - test.sh: bin/test
  - setup.sh: bin/setup
  - README.md: README.md

warnings:
  - GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED
  - GCC_WARN_MISSING_PARENTHESES
  - GCC_WARN_ABOUT_RETURN_TYPE
  - GCC_WARN_SIGN_COMPARE
  - GCC_WARN_CHECK_SWITCH_STATEMENTS
  - GCC_WARN_UNUSED_FUNCTION
  - GCC_WARN_UNUSED_LABEL
  - GCC_WARN_UNUSED_VALUE
  - GCC_WARN_UNUSED_VARIABLE
  - GCC_WARN_SHADOW
  - GCC_WARN_64_TO_32_BIT_CONVERSION
  - GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS
  - GCC_WARN_ABOUT_MISSING_NEWLINE
  - GCC_WARN_UNDECLARED_SELECTOR
  - GCC_WARN_TYPECHECK_CALLS_TO_PRINTF
  - GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS
  - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS
  - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF
  - CLANG_WARN_IMPLICIT_SIGN_CONVERSION
  - CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION
  - CLANG_WARN_EMPTY_BODY
  - CLANG_WARN_ENUM_CONVERSION
  - CLANG_WARN_INT_CONVERSION
  - CLANG_WARN_CONSTANT_CONVERSION

xcode_command: open -a 'Xcode' .

project_template: objc

app_target_templates:
  objc:
    - <%= project_name %>:
      - Categories:
      - Protocols:
      - Headers:
      - Models:
      - Sections:
      - Classes:
      - AppDelegate:
        - <%= prefix %>AppDelegate.h
        - <%= prefix %>AppDelegate.m
      - Network:
      - DataPersistence:
      - Docs:
      - Vendors:
      - Resources:
        - Images.xcassets
        - Nibs:
          - LaunchScreen.xib
        - Other-Sources:
          - Info.plist
          - <%= project_name %>-Prefix.pch
          - main.m
  swift:
    - <%= project_name %>:
      - Extensions:
      - Protocols:
      - Models:
      - ViewModels:
      - Controllers:
        - AppDelegate.swift
      - Views:
      - Resources:
        - Images.xcassets
        - Storyboards:
          - Main.storyboard
        - Nibs:
          - LaunchScreen.xib
        - Other-Sources:
          - Info.plist

test_target_templates:
  objc:
    - <%= test_target_name %>:
      - Resources:
        - <%= test_target_name %>-Info.plist
        - <%= test_target_name %>-Prefix.pch
      - Helpers:
      - Tests:
  swift:
    - <%= test_target_name %>:
      - Resources:
        - <%= test_target_name %>-Info.plist
      - Helpers:
      - Tests:

使用Xcode 6新建工程时,Apple准备了好些模板,这些模板写个Demo还是没有问题的,但是用来组织项目文件还是太弱了,所以情况经常是不得不每次去新建各种目录,这种重复性的劳动一来乏味,二来浪费时间。那么我们能不像创建自己的模板呢?这样新建的工程就能按自己的想法包含各种目录和文件。好消息是可以,坏消息是Apple没有提供相应的文档。虽然没有文档,还是试着来创建一个模板,每次都重复实在太烦(就是这么任性)。

既然没有文档,我们就把Apple的模板复制一份,在它的基础上修改成我们需要的样子。/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/有iOS所有工程模板。用户自定义的模板建议放到~/Library/Developer/Xcode/Templates/,目录如果不存在就创建。模板至少要包含两部分:一是扩展名为.xctemplate的文件夹;二是名称为TemplateInfo.plist的属性列表文件。好了,我们来创建一个自定义模板:

1
2
3
4
5
// Step 1:
$ mkdir ~/Library/Developer/Xcode/Templates/CocoaBite.xctemplate/

// Step 2:
$ cp /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/Single\ View\ Application.xctemplate/* ~/Library/Developer/Xcode/Templates/CocoaBite.xctemplate/
继续阅读

英语句子结构

英语的基本语序是SVO(Subject-Verbe-Object),且基本上不能任意变换语序,除了在少数诗词以外;另一方面,有时英语会使用OSV(Object-Subject-Verbe)的语序。它有6大句型:

  1. SV (S:subject,V: verbal phrase)主语+谓语
  2. SVP (P:predicative)主语+谓语+表语
  3. SVO (O:object)主语+谓语+宾语
  4. SVOiOd 主语+谓语+直接宾语+间接宾语
  5. SVOC(C:complement) 主语+谓语+宾语+补语
  6. There+系动词+主语

SV (S:subject,V: verbal phrase)主语+谓语

John sleeps.
Jill is eating.
Jack will arrive next week.
The tlephone rang.
His father might have died.
Sandy walks in the playground evening.
He never gives up easily.
The children listened carefully.
Our Manager has arrived in pairs.
The water in the jar has ran out.
My brother will talk on the phone.
Mummy won’t agree.

继续阅读