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