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 */ 51 private static func escapeStructuredHeaderStringItem(_ str: String) -> String { 52 return str.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") 53 } 54 55 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 131 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 145 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 { 181 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 { 193 func firstIndex(of: String, startingAt: String.Index) -> String.Index? { 194 return self[startingAt...].range(of: of)?.lowerBound 195 } 196 } 197