Аутентификация между серверами CloudKit

Apple опубликовала новый метод аутентификации против CloudKit, сервер к серверу. https://developer.apple.com/library/content/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/SettingUpWebServices.html#//apple_ref/doc/uid/TP40015240-CH24-SW6

Я попытался аутентифицироваться против CloudKit и этого метода. Сначала я сгенерировал пару ключей и дал открытый ключ CloudKit, до сих пор никаких проблем.

я начал строить заголовок запроса. Согласно документации, он должен выглядеть следующим образом:

X-Apple-CloudKit-Request-KeyID: [keyID]  
X-Apple-CloudKit-Request-ISO8601Date: [date]  
X-Apple-CloudKit-Request-SignatureV1: [signature]
  • [keyID], нет проблем. Вы можете найти это в панели CloudKit.
  • [Дата], я думаю, что это должно работать: 2016-02-06T20:41: 00Z
  • [подпись], вот в чем проблема...

в документации сказано, что:

подпись, созданная на Шаге 1.

Шаг 1 говорит:

объедините следующие параметры и разделите их двоеточиями.
[Current date]:[Request body]:[Web Service URL]

Я спросил себя: "Почему я должен сгенерировать пару ключей?".
Но Шаг 2 говорит:

вычислите подпись ECDSA этого сообщения с вашим закрытым ключом.

может быть, они хотят подписать объединенную подпись с закрытым ключом и поместить это в заголовок? Во всяком случае, я попробовал и то и другое...

мой образец для этого (без знака) значение подписи выглядит следующим образом:

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

значение тела запроса SHA256 хэшируется и после этого кодируется base64. Мой вопрос в том, что я должен объединить с":", но url и дата также содержат ":". Правильно ли это? (Я также попытался URL-кодировать URL-адрес и удалить ":" в дате).
Затем я подписал эту строку подписи с ECDSA, поместил ее в заголовок и отправил ее. Но я всегда получаю 401 "ошибка аутентификации". Чтобы подписать его, я использовал алгоритма ECDSA модуль python, со следующими командами:

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')

возможно, модуль python работает неправильно. Но он может генерировать правильный открытый ключ из закрытого ключа. Поэтому я надеюсь, что другие функции также работают.

кто-нибудь сумел пройти на CloudKit с серверами способ? Как это работает правильно?

Edit: правильная версия python, которая работает

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

6 ответов


последняя часть сообщения

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

не должен включить домен (it должны включить любые параметры запроса):

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

С новыми строками для лучшей читаемости:

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

далее показано, как вычислить значение заголовка в псевдокоде

точные вызовы API зависят от конкретного языка и криптографической библиотеки, которую вы используете.

//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)

Я сделал пример рабочего кода в PHP: https://gist.github.com/Mauricevb/87c144cec514c5ce73bd (на основе примера JavaScript @Jessedc)

кстати, убедитесь, что вы установили дату в часовом поясе UTC. Мой код не работал из-за этого.


извлечение Apple cloudkit.js реализация и использование первого вызова из образца кода Apple node-client-S2S / index.js вы можете построить следующим образом:

вы хэш-запрос текст запроса с sha256:

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

подписать [Current date]:[Request body]:[Web Service URL] полезная нагрузка с закрытым ключом, предоставленным в конфигурации.

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

еще одна заметка -[Web Service URL] полезная нагрузка компонент не должен включать домен но ему нужны любые параметры запроса.

убедитесь, что значение даты совпадает в X-Apple-CloudKit-Request-ISO8601Date как это в подписи. (Эти детали не документированы полностью, но наблюдаются, просматривая CloudKit.реализация JS).

более полный пример nodejs выглядит так:

(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();

})();

это также существует как суть:https://gist.github.com/jessedc/a3161186b450317a9cb5

в командной строке с openssl (Обновлено)

первое хеширование можно сделать с помощью этой команды:

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

чтобы подписать вторую часть запроса, вам нужна более современная версия openSSL, чем OSX 10.11, и используйте следующую команду:

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

спасибо @maurice_vB ниже и в twitter для этой информации


дистиллировал это из проекта, над которым я работаю в Node. Может быть, вам это пригодится. Заменить X-Apple-CloudKit-Request-KeyID и идентификатор контейнера в requestOptions.path чтобы заставить его работать.

закрытый ключ / pem генерируется с помощью:openssl ecparam -name prime256v1 -genkey -noout -out eckey.pem и создать открытый ключ для регистрации на панели 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"
    }
  }
}

подписать запрос:

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
}

и теперь вы можете отправить запрос:

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)
}

так дали следующий:

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

используя запрос:

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)
})

для вашего удовольствия вставки копии:https://gist.github.com/spllr/4bf3fadb7f6168f67698 (отредактировано)


в случае, если кто-то еще пытается сделать это через Ruby, есть псевдоним ключевого метода, необходимый для исправления OpenSSL lib для работы:

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

обратите внимание, что в приведенном выше примере url-адрес-это путь, исключающий компонент домена (начиная с /database...) и CK_PEM_STRING просто файл.прочтите pem, созданный при настройке пары закрытый/открытый ключ.

iso8601_date наиболее легко генерируется с помощью:

Time.now.utc.iso8601

конечно, вы хотите сохраните это в переменной для включения в окончательный запрос. Построение окончательного запроса может быть выполнено по следующей схеме:

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

работает как шарм для меня.


у меня была та же проблема, и в итоге я написал библиотеку, которая работает с python-запросы для взаимодействия с API CloudKit в Python.

pip install requests-cloudkit

после его установки просто импортируйте обработчик аутентификации (CloudKitAuth) и использовать его сразу с просьбами. Это будет прозрачно аутентифицировать запрос к API 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)

проект GitHub доступен по адресу https://github.com/lionheart/requests-cloudkit если вы хотите внести свой вклад или сообщить о проблеме.