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