Проверка подлинности от сервера к серверу 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 с помощью метода server-to-server? Как это работает правильно?
Изменить: Исправить версию 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
Ответы
Ответ 1
Последняя часть сообщения
[Current date]:[Request body]:[Web Service URL]
не должен включать домен (он должен включать любые параметры запроса):
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)
Ответ 2
Извлечение 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 для этой информации
Ответ 3
Я сделал пример рабочего кода в PHP: https://gist.github.com/Mauricevb/87c144cec514c5ce73bd
(на основе примера JavaScript Jessedc)
Кстати, убедитесь, что вы установили дату в часовой пояс UTC. Из-за этого мой код не работал.
Ответ 4
Дистиллируйте это из проекта, над которым я работаю в 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 (отредактировано)
Ответ 5
В случае, если кто-то пытается это сделать через 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 - это просто File.read из пэма, созданного при настройке вашей пары private/public key.
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
Работает как очарование для меня.
Ответ 6
У меня была такая же проблема, и в итоге я написал библиотеку, которая работает с python-requests для взаимодействия с CloudKit API в 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, если вы хотите внести свой вклад или сообщить о проблеме.