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]
- [keyID], no problem. You can find this in the CloudKit dashboard.
- [Date], I think this should work: 2016-02-06T20:41:00Z
- [signature], here is the problem...
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
I made an working code example in PHP: https://gist.github.com/Mauricevb/87c144cec514c5ce73bd (based on @Jessedc's JavaScript example)
By the way, make sure you set the date time in UTC timezone. My code didn't work because of this.
I had the same problem and ended up writing a library that works with python-requests to interface with the CloudKit API in Python.
After it's installed, just import the authentication handler (
CloudKitAuth
) and use it directly with requests. It will transparently authenticate any request you make to the CloudKit API.The GitHub project is available at https://github.com/lionheart/requests-cloudkit if you'd like to contribute or report an issue.
The last part of the message
must not include the domain (it must include any query parameters):
With newlines for better readability:
The following shows how to compute the header value in pseudocode
The exact API calls depend on the concrete language and crypto library you use.
Extracting Apple's cloudkit.js implementation and using the first call from the Apple sample code node-client-s2s/index.js you can construct the following:
You hash the request body request with
sha256
:The sign the
[Current date]:[Request body]:[Web Service URL]
payload with the private key provided in the config.Another note is the
[Web Service URL]
payload component must not include the domain but it does need any query parameters.Make sure the date value is the same in
X-Apple-CloudKit-Request-ISO8601Date
as it is in the signature. (These details are not documented completely, but is observed by looking through the CloudKit.js implementation).A more complete nodejs example looks like this:
This also exists as a gist: https://gist.github.com/jessedc/a3161186b450317a9cb5
On the command line with openssl (Updated)
The first hashing can be done with this command:
To sign the second part of the request you need a more modern version of openSSL than what OSX 10.11 comes with and use the following command:
Thanks to @maurice_vB below and on twitter for this info
In case someone else is trying to do this via Ruby, there's a key method alias required to monkey patch the OpenSSL lib to work:
Note that in the above example, url is the path excluding the domain component (starting with /database...) and CK_PEM_STRING is simply a File.read of the pem generated when setting up your private/public key pair.
The iso8601_date is most easily generated using:
Of course, you want to store that in a variable to include in your final request. Construction of the final request can be done with the following pattern:
Works like a charm now for me.
Distilled this from a project I'm working on in Node. Maybe you will find it useful. Replace the
X-Apple-CloudKit-Request-KeyID
and the container identifier inrequestOptions.path
to make it work.The private key/ pem is generated with:
openssl ecparam -name prime256v1 -genkey -noout -out eckey.pem
and generate the public key to register at the CloudKit dashboardopenssl ec -in eckey.pem -pubout
.To sign the request:
And now you can send the request:
So given the following:
Using the request:
For your copy pasting pleasure: https://gist.github.com/spllr/4bf3fadb7f6168f67698 (edited)