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