Muy buenas, me llamo Miguel y esta vez les traigo un post.
El propósito principal de este artículo es proporcionar un ejemplo de implementación de la fijación de hash de clave pública en iOS.
Los conceptos y teorías subyacentes no se explican a fondo en este artículo, aunque proporcionaré las fuentes que utilicé para aprender sobre el tema.
Índice
¿Qué es Pinning?
La fijación es el proceso de asociar un host con su certificado X509 o clave pública esperados. Una vez que un certificado o clave pública se conoce o se ve para un host, el certificado o la clave pública se asocia o «fija» al host.
de OWASP – Fijación de certificados y claves públicas
En un contexto de desarrollo iOS, esto significa que queremos asociar un certificado o una clave pública al servidor con el que nos comunicamos y solo permitir la comunicación con dicho servidor.
El problema
Una aplicación en la que estaba trabajando mi empresa utilizaba la fijación de certificados para garantizar la seguridad de nuestro canal de comunicación.
Esto significó que se incluyó un certificado X509 con la aplicación y, durante todas y cada una de las solicitudes, comparamos el certificado del servidor con el incluido y, si coincidían, consideramos que el canal era seguro.
Este método se considera seguro, pero conlleva un par de riesgos:
- Debido a que agrupamos el certificado en nuestro .ipa, básicamente lo exponemos en caso de descompilación o ingeniería inversa. Esto ya es bastante malo en sí mismo, pero
- Si nuestro servidor cambia su certificado, nuestra aplicación se rompe.
Estos riesgos nos han motivado a encontrar e implementar un método más a prueba de balas para asegurar nuestros canales de comunicación.
Otra motivación fue que el equipo de Android que trabajaba en la misma aplicación utilizó una metodología diferente, que demostró solucionar los riesgos mencionados anteriormente: este método consistía en pinchar el hash de la Clave Pública del certificado de nuestro servidor.
Solución
Un certificado se puede cambiar de una manera que deje intacta su clave pública (y su valor hash).
Esto nos da la oportunidad de agrupar solo el hash de la clave pública de nuestro certificado y de compararlo con el hash de la clave pública del certificado recibida durante una solicitud de red.
Nota: el hash es complejo y complicado, por lo que le aconsejo que utilice algoritmos proporcionados por fuentes confiables, por ejemplo. OpenSSL )
Este método es más complejo que simplemente anclar los certificados, pero creemos que vale la pena.
A continuación, puede encontrar una lista de pasos para obtener el hash de clave pública del certificado de su servidor utilizando algunos comandos de OpenSSL (comandos comunes de OpenSSL ):
Adquiera el certificado de su servidor. Esto se puede hacer solicitándolo a sus colegas desarrolladores de backend o simplemente descargándolo de un navegador (por ejemplo, si su servidor tiene un sitio web público).
openssl s_client -connect your-server.com:443 -showcerts </ dev / null | openssl x509 -outform der> server_cert.der
Cuando tenga el certificado, debe extraer y, opcionalmente, guardar su clave pública en formato PEM.
openssl x509 -inform der -in server_cert.der -pubkey -noout> server_cert_public_key.pem
Después de tener el certificado, puede usar el algoritmo hash que prefiera (solo asegúrese de que sea un algoritmo seguro). Usé SHA256 para codificar nuestra clave. Después de calcular el hash, simplemente lo codifiqué con codificación Base64, para que sea más fácil de almacenar y leer.
cat server_cert_public_key.pem | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
La salida de los comandos enumerados anteriormente es el hash de la clave pública de su servidor, que ahora se puede agregar a su aplicación.
Implementación
Ahora que tenemos nuestro hash, es hora de hacer un buen uso de él.
Como sabrá, hay muchas bibliotecas de redes que se utilizan en el desarrollo de iOS, por lo que hay muchas formas de integrar cualquier tipo de fijación en su aplicación. Debido a esto, solo te mostraré el núcleo de una posible implementación:
- extraer la clave pública del certificado recibido,
- hash,
- y emparejarlo con su hash almacenado.
A tener en cuenta: en iOS, durante la comunicación de red, recibe una cadena de certificados en un objeto utilizado para evaluar la confianza (SecTrust). Esta implementación proporcionada asume que almacena múltiples hash, un hash que pertenece a la clave pública de un certificado en esta cadena.
Interfaz pública
Comencemos con la estructura básica de nuestra clase PublicKeyPinner.
Necesitamos una propiedad para almacenar los hash que necesitamos hacer coincidir y un método que valide nuestro objeto de confianza. Opcionalmente podemos especificar el dominio desde donde planeamos recibir nuestro objeto de confianza.
public final class PublicKeyPinner { /// Stored public key hashes private let hashes: [String] public init(hashes: [String]) { self.hashes = hashes } /// Validates an object used to evaluate trust's certificates by comparing their public key hashes /// to the known, trused key hashes stored in the app. /// - Parameter serverTrust: The object used to evaluate trust. /// - Parameter domain: The domain from where we expect our trust object to come from. public func validate(serverTrust: SecTrust, domain: String?) -> Bool { return false } }
Validación
Ahora comencemos a implementar el cuerpo de nuestro validar método. Primero, si se proporciona un dominio, debemos configurarlo como SecPolicy:
if let domain = domain { let policies = NSMutableArray() policies.add(SecPolicyCreateSSL(true, domain as CFString)) SecTrustSetPolicies(serverTrust, policies) }
A continuación, debemos comprobar la validez de nuestro SecTrust objeto:
// Check if the trust is valid var secResult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secResult) guard status == errSecSuccess else { return false }
Ahora que tenemos un objeto de confianza válido, es hora de evaluar su confiabilidad.
Para hacer esto, tenemos que iterar a través de la cadena de certificados contenidos en el objeto de confianza. En cada iteración, tenemos que recuperar los datos de la clave pública del certificado actual, hash y comparar este hash con nuestros hash almacenados:
// For each certificate in the valid trust: for index in 0..<SecTrustGetCertificateCount(serverTrust) { // Get the public key data for the certificate at the current index of the loop. guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, index), let publicKey = SecCertificateCopyPublicKey(certificate), let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) else { return false } // Hash the key, and check it's validity. let keyHash = hash(data: (publicKeyData as NSData) as Data) if hashes.contains(keyHash) { // Success! This is our server! return true } } // If none of the calculated hashes match any of our stored hashes, the connection we tried to establish is untrusted. return false
Esto concluye la parte de validación de nuestra implementación. Puede haber muchos métodos desconocidos en los fragmentos de código anteriores, para referencia, consulte el documentación.
Puede notar una referencia al hash (datos: datos) método y lo has adivinado, esta es la siguiente y última parte de nuestra implementación de PublicKeyPinner.
Hashing
En primer lugar, el publicKeyData que vimos arriba falta información de clave (😉): el encabezado ASN1 para claves públicas para recrear la información de clave pública del sujeto (más información sobre esto aquí). Básicamente, necesitamos una matriz de enteros sin signo que contengan una indicación del algoritmo y cualquier parámetro del algoritmo con el que se utilizará la clave pública.
Si recuerda de antes en el artículo, usamos OpenSSL-s dgst Funcionamos con el hash sha256 para crear nuestros hashes. Para recrear los mismos hashes en nuestro código, se necesitan los siguientes bytes:
/// ASN1 header for our public key to re-create the subject public key info private let rsa2048Asn1Header: [UInt8] = [ 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00 ]
Ahora que tenemos nuestro encabezado, podemos implementar nuestro picadillo método. Hay muchas bibliotecas que proporcionan funciones criptográficas, para fines de demostración usaré una biblioteca de terceros (CryptoSwift) y dos marcos proporcionados por Apple: CryptoKit (iOS 13+) y CommonCrypto.
Primero, veamos cómo usar el nuevo marco CryptoKit de Apple, más precisamente es el hasher SHA256. Creamos una variable llamada `keyWithHeader` para almacenar el encabezado y los datos de clave pública recuperados del certificado. Si iOS 13 está disponible, creamos un resumen de nuestros datos y devolvemos su cadena codificada en base64.
import CryptoSwift import CommonCrypto #if canImport(CryptoKit) import CryptoKit #endif public final class PublicKeyPinner { ... /// Creates a hash from the received data using the `sha256` algorithm. /// `Returns` the `base64` encoded representation of the hash. /// /// To replicate the output of the `openssl dgst -sha256` command, an array of specific bytes need to be appended to /// the beginning of the data to be hashed. /// - Parameter data: The data to be hashed. private func hash(data: Data) -> String { // Add the missing ASN1 header for public keys to re-create the subject public key info var keyWithHeader = Data(rsa2048Asn1Header) keyWithHeader.append(data) // Check if iOS 13 is available, and use CryptoKit's hasher if #available(iOS 13, *) { return Data(SHA256.hash(data: keyWithHeader)).base64EncodedString() } else { ... } } }
Ahora veamos la rama else. Aquí, podemos ver dos ejemplos más de hash: uno que usa CommonCrypto y otro que usa CryptoSwift. Si no le agradó lo suficiente la gran API proporcionada por Seguridad framework, CommonCrypto-s API será un verdadero bocadillo para ti.
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) _ = keyWithHeader.withUnsafeBytes { CC_SHA256($0.baseAddress!, CC_LONG(keyWithHeader.count), &hash) } return Data(hash).base64EncodedString()
CryptoSwift, al igual que CryptoKit, proporciona una forma más elegante de hacer las cosas, con toda la magia oculta:
return keyWithHeader.sha256().base64EncodedString()
Y con eso, nuestro PublicKeyPinner está listo para alguna acción (segura). Para ver todo el código fuente en un solo lugar, consulte esta esencia.
Resumen
Hemos recorrido un largo camino en este artículo y hemos tocado algunos temas complejos, como el hash criptográfico y la fijación de hash de certificado / clave pública.
He aprendido mucho mientras investigaba y resolvía los problemas enumerados anteriormente, y espero que usted también lo haya leído al leer este artículo.
A tener en cuenta: si prefiere utilizar bibliotecas de terceros, la funcionalidad de la implementación anterior (y muchas más funciones) se puede encontrar en una biblioteca de código abierto llamada TrustKit.
Gracias por leer.
Añadir comentario