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