1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import Foundation
4 import CommonCrypto
5 import ASN1Decoder
6 
7 struct CodeSigningMetadataFields {
8   static let KeyIdFieldKey = "keyid"
9   static let AlgorithmFieldKey = "alg"
10 }
11 
12 @objc(EXUpdatesCodeSigningValidationResult)
13 public enum CodeSigningValidationResult : Int {
14   case Valid
15   case Invalid
16   case Skipped
17 }
18 
19 @objc(EXUpdatesCodeSigningSignatureValidationResult)
20 public class CodeSigningSignatureValidationResult : NSObject {
21   @objc private(set) public var validationResult: CodeSigningValidationResult
22   @objc private(set) public var expoProjectInformation: CodeSigningProjectInformation?
23 
24   required init(validationResult: CodeSigningValidationResult, expoProjectInformation: CodeSigningProjectInformation?) {
25     self.validationResult = validationResult
26     self.expoProjectInformation = expoProjectInformation
27   }
28 }
29 
30 @objc(EXUpdatesCodeSigningConfiguration)
31 public class CodeSigningConfiguration : NSObject {
32   private var embeddedCertificateString: String
33   private var keyIdFromMetadata: String
34   private var algorithmFromMetadata: CodeSigningAlgorithm
35   private var includeManifestResponseCertificateChain: Bool
36   private var allowUnsignedManifests: Bool
37 
38   @objc
39   public required init(embeddedCertificateString: String,
40                        metadata: [String: String],
41                        includeManifestResponseCertificateChain: Bool,
42                        allowUnsignedManifests: Bool) throws {
43     self.embeddedCertificateString = embeddedCertificateString
44     self.keyIdFromMetadata = metadata[CodeSigningMetadataFields.KeyIdFieldKey] ?? CodeSigningSignatureHeaderInfo.DefaultKeyId
45     self.algorithmFromMetadata = try CodeSigningAlgorithm.parseFromString(metadata[CodeSigningMetadataFields.AlgorithmFieldKey])
46     self.includeManifestResponseCertificateChain = includeManifestResponseCertificateChain
47     self.allowUnsignedManifests = allowUnsignedManifests
48   }
49 
50   /**
51    * String escaping is defined by https://www.rfc-editor.org/rfc/rfc8941.html#section-3.3.3
52    */
53   private static func escapeStructuredHeaderStringItem(_ str: String) -> String {
54     return str.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
55   }
56 
57   @objc
58   public func createAcceptSignatureHeader() -> String {
59     return "sig, keyid=\"\(CodeSigningConfiguration.escapeStructuredHeaderStringItem(keyIdFromMetadata))\", alg=\"\(CodeSigningConfiguration.escapeStructuredHeaderStringItem(algorithmFromMetadata.rawValue))\""
60   }
61 
62   @objc
63   public func validateSignature(signature: String?,
64                                 signedData: Data,
65                                 manifestResponseCertificateChain: String?) throws -> CodeSigningSignatureValidationResult {
66     guard let signature = signature else {
67       if !self.allowUnsignedManifests {
68         throw CodeSigningError.SignatureHeaderMissing
69       } else {
70         // no-op
71         return CodeSigningSignatureValidationResult(validationResult: CodeSigningValidationResult.Skipped, expoProjectInformation: nil)
72       }
73     }
74 
75     return try validateSignatureInternal(
76       signatureHeaderInfo: try CodeSigningSignatureHeaderInfo.parseSignatureHeader(signatureHeader: signature),
77       signedData: signedData,
78       manifestResponseCertificateChain: manifestResponseCertificateChain
79     )
80   }
81 
82   private func validateSignatureInternal(signatureHeaderInfo: CodeSigningSignatureHeaderInfo,
83                                          signedData: Data,
84                                          manifestResponseCertificateChain: String?) throws -> CodeSigningSignatureValidationResult {
85     let certificateChain: CodeSigningCertificateChain
86     if (self.includeManifestResponseCertificateChain) {
87       certificateChain = try CodeSigningCertificateChain(
88         certificateStrings: CodeSigningConfiguration.separateCertificateChain(certificateChainInManifestResponse: manifestResponseCertificateChain ?? "") + [self.embeddedCertificateString]
89       )
90     } else {
91       // check that the key used to sign the response is the same as the key in the code signing certificate
92       if (signatureHeaderInfo.keyId != self.keyIdFromMetadata) {
93         throw CodeSigningError.KeyIdMismatchError
94       }
95 
96       // note that a mismatched algorithm doesn't fail early. it still tries to verify the signature with the
97       // algorithm specified in the configuration
98       if (signatureHeaderInfo.algorithm != self.algorithmFromMetadata) {
99         NSLog("Key with alg=\(signatureHeaderInfo.algorithm) from signature does not match client configuration algorithm, continuing")
100       }
101 
102       certificateChain = try CodeSigningCertificateChain(certificateStrings: [embeddedCertificateString])
103     }
104 
105     // For now only SHA256withRSA is supported. This technically should be `metadata.algorithm` but
106     // it breaks down when metadata is for a different key than the signing key (the case where intermediate
107     // certs are served alongside the manifest and the metadata is for the root embedded cert).
108     // In the future if more methods are added we will need to be sure that we think about how to
109     // specify what algorithm should be used in the chain case. One approach may be that in the case of
110     // chains served alongside the manifest we fork the behavior to trust the `info.algorithm` while keeping
111     // `metadata.algorithm` for the embedded case.
112     let (secCertificate, _) = try certificateChain.codeSigningCertificate()
113 
114     guard let publicKey = secCertificate.publicKey else {
115       throw CodeSigningError.CertificateMissingPublicKeyError
116     }
117 
118     guard let signatureData = Data(base64Encoded: signatureHeaderInfo.signature) else {
119       throw CodeSigningError.SignatureEncodingError
120     }
121 
122     let isValid = try self.verifyRSASHA256SignedData(signedData: signedData, signatureData: signatureData, publicKey: publicKey)
123     return CodeSigningSignatureValidationResult(validationResult: isValid ? CodeSigningValidationResult.Valid : CodeSigningValidationResult.Invalid,
124                                               expoProjectInformation: try certificateChain.codeSigningCertificate().1.expoProjectInformation())
125   }
126 
127   private func verifyRSASHA256SignedData(signedData: Data, signatureData: Data, publicKey: SecKey) throws -> Bool {
128     let hashBytes = signedData.sha256()
129     var error: Unmanaged<CFError>?
130     if SecKeyVerifySignature(publicKey, .rsaSignatureDigestPKCS1v15SHA256, hashBytes as CFData, signatureData as CFData, &error) {
131       return true
132     } else {
133       if let error = error, (error.takeRetainedValue() as Error as NSError).code != errSecVerifyFailed {
134         NSLog("Sec key signature verification error: %@", error.takeRetainedValue().localizedDescription)
135         throw CodeSigningError.SecurityFrameworkError
136       }
137       return false
138     }
139   }
140 
141   public static func separateCertificateChain(certificateChainInManifestResponse: String) -> [String] {
142     let startDelimiter = "-----BEGIN CERTIFICATE-----"
143     let endDelimiter = "-----END CERTIFICATE-----"
144     var certificateStringList = [] as [String]
145 
146     var currStartIndex = certificateChainInManifestResponse.startIndex
147     while (true) {
148       let startIndex = certificateChainInManifestResponse.firstIndex(of: startDelimiter, startingAt: currStartIndex)
149       let endIndex = certificateChainInManifestResponse.firstIndex(of: endDelimiter, startingAt: currStartIndex)
150 
151       if let startIndex = startIndex, let endIndex = endIndex {
152         let newEndIndex = certificateChainInManifestResponse.index(endIndex, offsetBy: endDelimiter.count)
153         certificateStringList.append(String(certificateChainInManifestResponse[startIndex..<newEndIndex]))
154         currStartIndex = newEndIndex
155       } else {
156         break
157       }
158     }
159 
160     return certificateStringList
161   }
162 }
163 
164 extension SecCertificate {
165   public var publicKey: SecKey? {
166     SecCertificateCopyKey(self)
167   }
168 }
169 
170 extension OSStatus {
171   public var isSuccess: Bool {
172     self == errSecSuccess
173   }
174 }
175 
176 extension Data {
177   func sha256() -> Data {
178     var digest = Data(count: Int(CC_SHA256_DIGEST_LENGTH))
179     withUnsafeBytes { bytes in
180       digest.withUnsafeMutableBytes { mutableBytes in
181         _ = CC_SHA256(bytes.baseAddress, CC_LONG(count), mutableBytes.bindMemory(to: UInt8.self).baseAddress)
182       }
183     }
184     return digest
185   }
186 }
187 
188 extension String {
189   func firstIndex(of: String, startingAt: String.Index) -> String.Index? {
190     return self[startingAt...].range(of: of)?.lowerBound
191   }
192 }
193