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