authentication cloudkit server-to-server

authentication - Autentificación de servidor a servidor de CloudKit



server-to-server (6)

Al extraer la implementación de cloudkit.js de Apple y usar la primera llamada del código de ejemplo de Apple node-client-s2s/index.js , puede construir lo siguiente:

Hash la solicitud del cuerpo de solicitud con sha256 :

var crypto = require(''crypto''); var bodyHasher = crypto.createHash(''sha256''); bodyHasher.update(requestBody); var hashedBody = bodyHasher.digest("base64");

El signo de la [Current date]:[Request body]:[Web Service URL] carga útil con la clave privada proporcionada en la configuración.

var c = crypto.createSign("RSA-SHA256"); c.update(rawPayload); var requestSignature = c.sign(key, "base64");

Otra nota es que el componente de carga útil [Web Service URL] no debe incluir el dominio, pero sí necesita parámetros de consulta.

Asegúrese de que el valor de la fecha sea el mismo en X-Apple-CloudKit-Request-ISO8601Date como en la firma. (Estos detalles no están documentados completamente, pero se observan al observar la implementación de CloudKit.js).

Un ejemplo más completo de nodejs se ve así:

(function() { const https = require(''https''); var fs = require(''fs''); var crypto = require(''crypto''); var key = fs.readFileSync(__dirname + ''/eckey.pem'', "utf8"); var authKeyID = ''auth-key-id''; // path of our request (domain not included) var requestPath = "/database/1/iCloud.containerIdentifier/development/public/users/current"; // request body (GET request is blank) var requestBody = ''''; // date string without milliseconds var requestDate = (new Date).toISOString().replace(/(/./d/d/d)Z/, "Z"); var bodyHasher = crypto.createHash(''sha256''); bodyHasher.update(requestBody); var hashedBody = bodyHasher.digest("base64"); var rawPayload = requestDate + ":" + hashedBody + ":" + requestPath; // sign payload var c = crypto.createSign("sha256"); c.update(rawPayload); var requestSignature = c.sign(key, "base64"); // put headers together var headers = { ''X-Apple-CloudKit-Request-KeyID'': authKeyID, ''X-Apple-CloudKit-Request-ISO8601Date'': requestDate, ''X-Apple-CloudKit-Request-SignatureV1'': requestSignature }; var options = { hostname: ''api.apple-cloudkit.com'', port: 443, path: requestPath, method: ''GET'', headers: headers }; var req = https.request(options, (res) => { //... handle nodejs response }); req.end(); })();

Esto también existe como una esencia: https://gist.github.com/jessedc/a3161186b450317a9cb5

En la línea de comandos con openssl (Actualizado)

El primer hashing se puede hacer con este comando:

openssl sha -sha256 -binary < body.txt | base64

Para firmar la segunda parte de la solicitud, necesita una versión más moderna de openSSL que OSX 10.11 y use el siguiente comando:

/usr/local/bin/openssl dgst -sha256WithRSAEncryption -binary -sign ck-server-key.pem raw_signature.txt | base64

Gracias a @maurice_vB a continuación y en twitter por esta información

Apple publicó un nuevo método para autenticarse en CloudKit, de servidor a servidor. https://developer.apple.com/library/content/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/SettingUpWebServices.html#//apple_ref/doc/uid/TP40015240-CH24-SW6

Intenté autenticar contra CloudKit y este método. Al principio, generé el par de claves y le di la clave pública a CloudKit, no hay problema hasta ahora.

Comencé a construir el encabezado de solicitud. Según la documentación debería verse así:

X-Apple-CloudKit-Request-KeyID: [keyID] X-Apple-CloudKit-Request-ISO8601Date: [date] X-Apple-CloudKit-Request-SignatureV1: [signature]

  • [KeyID], no hay problema. Puedes encontrar esto en el panel de CloudKit.
  • [Fecha], creo que esto debería funcionar: 2016-02-06T20: 41: 00Z
  • [firma], aquí está el problema ...

La documentación dice:

La firma creada en el paso 1.

El paso 1 dice:

Concatene los siguientes parámetros y sepárelos con dos puntos.
[Current date]:[Request body]:[Web Service URL]

Me pregunté "¿Por qué tengo que generar el par de claves?".
Pero el paso 2 dice:

Calcule la firma ECDSA de este mensaje con su clave privada.

¿Tal vez quieren firmar la firma concatenada con la clave privada y ponerla en el encabezado? De todos modos probé los dos ...

Mi muestra para este valor de firma (sin firmar) se ve así:

2016-02-06T20:41:00Z:YTdkNzAwYTllNjI1M2EyZTllNDNiZjVmYjg0MWFhMGRiMTE2MjI1NTYwNTA2YzQyODc4MjUwNTQ0YTE5YTg4Yw==:https://api.apple-cloudkit.com/database/1/[iCloud Container]/development/public/records/lookup

El valor del cuerpo de la solicitud es hash SHA256 y, a continuación, codificado en base64. Mi pregunta es, debo concatenar con un ":" pero la url y la fecha también contienen ":". ¿Es correcto? (También traté de codificar la URL con URL y eliminar el ":" en la fecha).
A continuación firmé esta cadena de firma con ECDSA, la puse en el encabezado y la envié. Pero siempre me devuelven 401 "Autenticación fallida". Para firmarlo, usé el módulo ecdsa python, con los siguientes comandos:

from ecdsa import SigningKey a = SigningKey.from_pem(open("path_to_pem_file").read()) b = "[date]:[base64(request_body)]:/database/1/iCloud....." print a.sign(b).encode(''hex'')

Tal vez el módulo de python no funciona correctamente. Pero puede generar la clave pública correcta a partir de la clave privada. Así que espero que las otras funciones también funcionen.

¿Alguien ha logrado autenticarse contra CloudKit con el método de servidor a servidor? ¿Cómo funciona correctamente?

Edición: Corregir la versión de python que funciona

from ecdsa import SigningKey import ecdsa, base64, hashlib a = SigningKey.from_pem(open("path_to_pem_file").read()) b = "[date]:[base64(sha256(request_body))]:/database/1/iCloud....." signature = a.sign(b, hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_der) signature = base64.b64encode(signature) print signature #include this into the header


Destilado de un proyecto en el que estoy trabajando en Nodo. Tal vez te resulte útil. Reemplace el X-Apple-CloudKit-Request-KeyID y el identificador de contenedor en requestOptions.path para que funcione.

La clave privada / pem se genera con: openssl ecparam -name prime256v1 -genkey -noout -out eckey.pem y genera la clave pública para registrarse en el panel de control de CloudKit openssl ec -in eckey.pem -pubout .

var crypto = require("crypto"), https = require("https"), fs = require("fs") var CloudKitRequest = function(payload) { this.payload = payload this.requestOptions = { // Used with `https.request` hostname: "api.apple-cloudkit.com", port: 443, path: ''/database/1/iCloud.com.your.container/development/public/records/modify'', method: ''POST'', headers: { // We will add more headers in the sign methods "X-Apple-CloudKit-Request-KeyID": "your-ck-request-keyID" } } }

Para firmar la solicitud:

CloudKitRequest.prototype.sign = function(privateKey) { var dateString = new Date().toISOString().replace(//.[0-9]+?Z/, "Z"), // NOTE: No milliseconds hash = crypto.createHash("sha256"), sign = crypto.createSign("RSA-SHA256") // Create the hash of the payload hash.update(this.payload, "utf8") var payloadSignature = hash.digest("base64") // Create the signature string to sign var signatureData = [ dateString, payloadSignature, this.requestOptions.path ].join(":") // [Date]:[Request body]:[Web Service URL] // Construct the signature sign.update(signatureData) var signature = sign.sign(privateKey, "base64") // Update the request headers this.requestOptions.headers["X-Apple-CloudKit-Request-ISO8601Date"] = dateString this.requestOptions.headers["X-Apple-CloudKit-Request-SignatureV1"] = signature return signature // This might be useful to keep around }

Y ahora puedes enviar la solicitud:

CloudKitRequest.prototype.send = function(cb) { var request = https.request(this.requestOptions, function(response) { var responseBody = "" response.on("data", function(chunk) { responseBody += chunk.toString("utf8") }) response.on("end", function() { cb(null, JSON.parse(responseBody)) }) }) request.on("error", function(err) { cb(err, null) }) request.end(this.payload) }

Así que dado lo siguiente:

var privateKey = fs.readFileSync("./eckey.pem"), creationPayload = JSON.stringify({ "operations": [{ "operationType" : "create", "record" : { "recordType" : "Post", "fields" : { "title" : { "value" : "A Post From The Server" } } } }] })

Utilizando la solicitud:

var creationRequest = new CloudKitRequest(creationPayload) creationRequest.sign(privateKey) creationRequest.send(function(err, response) { console.log("Created a new entry with error", err, "and respone", response) })

Para su placer de pegar copias: https://gist.github.com/spllr/4bf3fadb7f6168f67698 (editado)


En caso de que alguien más esté tratando de hacer esto a través de Ruby, hay un alias de método clave requerido para parchear el lib de OpenSSL para que funcione:

def signature_for_request(body_json, url, iso8601_date) body_sha_hash = Digest::SHA256.digest(body_json) payload_for_signature = [iso8601_date, Base64.strict_encode64(body_sha_hash), url].join(":") OpenSSL::PKey::EC.send(:alias_method, :private?, :private_key?) ec = OpenSSL::PKey::EC.new(CK_PEM_STRING) digest = OpenSSL::Digest::SHA256.new signature = ec.sign(digest, payload_for_signature) base64_signature = Base64.strict_encode64(signature) return base64_signature end

Tenga en cuenta que en el ejemplo anterior, url es la ruta que excluye el componente del dominio (que comienza con / database ...) y CK_PEM_STRING es simplemente un archivo.read de la pem generada al configurar su par de claves privada / pública.

El iso8601_date se genera más fácilmente usando:

Time.now.utc.iso8601

Por supuesto, usted quiere almacenar eso en una variable para incluir en su solicitud final. La construcción de la solicitud final se puede hacer con el siguiente patrón:

def perform_request(url, body, iso8601_date) signature = self.signature_for_request(body, url, iso8601_date) uri = URI.parse(CK_SERVICE_BASE + url) header = { "Content-Type" => "text/plain", "X-Apple-CloudKit-Request-KeyID" => CK_KEY_ID, "X-Apple-CloudKit-Request-ISO8601Date" => iso8601_date, "X-Apple-CloudKit-Request-SignatureV1" => signature } # Create the HTTP objects http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri.request_uri, header) request.body = body # Send the request response = http.request(request) return response end

Funciona como un encanto ahora para mí.



La última parte del mensaje.

[Current date]:[Request body]:[Web Service URL]

No debe incluir el dominio ( debe incluir cualquier parámetro de consulta):

2016-02-06T20:41:00Z:YTdkNzAwYTllNjI1M2EyZTllNDNiZjVmYjg0MWFhMGRiMTE2MjI1NTYwNTA2YzQyODc4MjUwNTQ0YTE5YTg4Yw==:/database/1/[iCloud Container]/development/public/records/lookup

Con nuevas líneas para una mejor legibilidad:

2016-02-06T20:41:00Z :YTdkNzAwYTllNjI1M2EyZTllNDNiZjVmYjg0MWFhMGRiMTE2MjI1NTYwNTA2YzQyODc4MjUwNTQ0YTE5YTg4Yw== :/database/1/[iCloud Container]/development/public/records/lookup

A continuación se muestra cómo calcular el valor del encabezado en pseudocódigo.

Las llamadas a API exactas dependen del lenguaje concreto y la biblioteca criptográfica que utilice.

//1. Date //Example: 2016-02-07T18:58:24Z //Pitfall: make sure to not include milliseconds date = isoDateWithoutMilliseconds() //2. Payload //Example (empty string base64 encoded; GET requests): //47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= //Pitfall: make sure the output is base64 encoded (not hex) payload = base64encode(sha256(body)) //3. Path //Example: /database/1/[containerIdentifier]/development/public/records/lookup //Pitfall: Don''t include the domain; do include any query parameter path = stripDomainKeepQueryParams(url) //4. Message //Join date, payload, and path with colons message = date + '':'' + payload + '':'' + path //5. Compute a signature for the message using your private key. //This step looks very different for every language/crypto lib. //Pitfall: make sure the output is base64 encoded. //Hint: the key itself contains information about the signature algorithm // (on NodeJS you can use the signature name ''RSA-SHA256'' to compute a // the correct ECDSA signature with an ECDSA key). signature = base64encode(sign(message, key)) //6. Set headers X-Apple-CloudKit-Request-KeyID = keyID X-Apple-CloudKit-Request-ISO8601Date = date X-Apple-CloudKit-Request-SignatureV1 = signature //7. For POST requests, don''t forget to actually send the unsigned request body // (not just the headers)


Tuve el mismo problema y terminé escribiendo una biblioteca que funciona con python-requests para interactuar con la API de CloudKit en Python.

pip install requests-cloudkit

Después de que esté instalado, simplemente importe el controlador de autenticación ( CloudKitAuth ) y utilícelo directamente con las solicitudes. Autentificará de forma transparente cualquier solicitud que realice a la API de CloudKit.

>>> import requests >>> from requests_cloudkit import CloudKitAuth >>> auth = CloudKitAuth(key_id=YOUR_KEY_ID, key_file_name=YOUR_PRIVATE_KEY_PATH) >>> requests.get("https://api.apple-cloudkit.com/database/[version]/[container]/[environment]/public/zones/list", auth=auth)

El proyecto GitHub está disponible en https://github.com/lionheart/requests-cloudkit si desea contribuir o informar un problema.