Apple published a new method to authenticate against CloudKit, server-to-server. https://developer.apple.com/library/content/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/SettingUpWebServices.html#//apple_ref/doc/uid/TP40015240-CH24-SW6
I tried to authenticate against CloudKit and this method. At first I generated the key pair and gave the public key to CloudKit, no problem so far.
I started to build the request header. According to the documentation it should look like this:
X-Apple-CloudKit-Request-KeyID: [keyID]
X-Apple-CloudKit-Request-ISO8601Date: [date]
X-Apple-CloudKit-Request-SignatureV1: [signature]
The documentation says:
The signature created in Step 1.
Step 1 says:
Concatenate the following parameters and separate them with colons.
[Current date]:[Request body]:[Web Service URL]
I asked myself "Why do I have to generate the key pair?".
But step 2 says:
Compute the ECDSA signature of this message with your private key.
Maybe they mean to sign the concatenated signature with the private key and put this into the header? Anyway I tried both...
My sample for this (unsigned) signature value looks like:
2016-02-06T20:41:00Z:YTdkNzAwYTllNjI1M2EyZTllNDNiZjVmYjg0MWFhMGRiMTE2MjI1NTYwNTA2YzQyODc4MjUwNTQ0YTE5YTg4Yw==:https://api.apple-cloudkit.com/database/1/[iCloud Container]/development/public/records/lookup
The request body value is SHA256 hashed and after that base64 encoded. My question is, I should concatenate with a ":" but the url and the date also contains ":". Is it correct? (I also tried to URL-Encode the URL and delete the ":" in the date).
At next I signed this signature string with ECDSA, put it into the header and send it. But I always get 401 "Authentication failed" back. To sign it, I used the ecdsa python module, with following commands:
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')
Maybe the python module doesn't work correctly. But it can generate the right public key from the private key. So I hope the other functions also work.
Has anybody managed to authenticate against CloudKit with the server-to-server method? How does it work correctly?
Edit: Correct python version that works
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
The last part of the message
[Current date]:[Request body]:[Web Service URL]
must not include the domain (it must include any query parameters):
2016-02-06T20:41:00Z:YTdkNzAwYTllNjI1M2EyZTllNDNiZjVmYjg0MWFhMGRiMTE2MjI1NTYwNTA2YzQyODc4MjUwNTQ0YTE5YTg4Yw==:/database/1/[iCloud Container]/development/public/records/lookup
With newlines for better readability:
2016-02-06T20:41:00Z
:YTdkNzAwYTllNjI1M2EyZTllNDNiZjVmYjg0MWFhMGRiMTE2MjI1NTYwNTA2YzQyODc4MjUwNTQ0YTE5YTg4Yw==
:/database/1/[iCloud Container]/development/public/records/lookup
The exact API calls depend on the concrete language and crypto library you use.
//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)