c# CloudKit Server 2 Server API

I'm struggling getting the signing part of sending server 2 sever requests working from .Net/c#


I'm struggling to build the header X-Apple-CloudKit-Request-Signaturev1


Has anyone had any joy getting this working (I know announcement was only about 3 days ago)


:-)

Answered by PatrickSHM in 112693022

I have tried it a few Days with no success if using the Build-In Functions. Now a tried it with the Bouncy Castle Lib (https://www.bouncycastle.org) an then it worked... here is my code for testing:


using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;


namespace CloudKitTester
{
  public class CloudKitHelper
  {
  private string _keyID;
  private string _container;
  private string _environment;
  private AsymmetricCipherKeyPair _dsaKeyPair;
  private string _apiVersion = "1";




  public CloudKitHelper(string pemFilePath, string keyID, string container, string environment = "development")
  {
  _keyID = keyID;
  _container = container;
  _environment = environment;


  using (var reader = File.OpenText(pemFilePath))
  {
  var pemReader = new PemReader(reader);
  _dsaKeyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
  }
  }


  public string Request(string query, string jsonData = "")
  {
  SHA256Cng sha256 = new SHA256Cng();


  var serverPath = String.Format("/database/{0}/{1}/{2}/public/{3}", _apiVersion, _container, _environment, query);
  var requestBodyBinary = Encoding.UTF8.GetBytes(jsonData);
  var requestBodyHashBase64 = Convert.ToBase64String(sha256.ComputeHash(requestBodyBinary));
  var time = String.Format("{0}Z", DateTime.UtcNow.ToString("s"));


  var signatureString = String.Format("{0}:{1}:{2}", time, requestBodyHashBase64, serverPath);
  var signatureBinary = Encoding.UTF8.GetBytes(signatureString);


  var ecDSASigner = SignerUtilities.GetSigner("SHA256withECDSA");
  ecDSASigner.Init(true, _dsaKeyPair.Private);
  ecDSASigner.BlockUpdate(signatureBinary, 0, signatureBinary.Length);


  var signatureBase64 = Convert.ToBase64String(ecDSASigner.GenerateSignature());


  var request = HttpWebRequest.Create(String.Format("https://api.apple-cloudkit.com{0}", serverPath));
  request.Method = "POST";
  request.ContentType = "text/plain";


  request.Headers.Add("X-Apple-CloudKit-Request-SignatureV1", signatureBase64);
  request.Headers.Add("X-Apple-CloudKit-Request-KeyID", _keyID);
  request.Headers.Add("X-Apple-CloudKit-Request-ISO8601Date", time);


  using (var stream = request.GetRequestStream())
  {
  stream.Write(requestBodyBinary, 0, requestBodyBinary.Length);
  }


  try
  {
  var response = (HttpWebResponse) request.GetResponse();


  using (var output = new StreamReader(response.GetResponseStream()))
  {
  return output.ReadToEnd();
  }
  }
  catch
  {
  return null;
  }
  }
  }

  class Program
  {
  static void Main(string[] args)
  {
  var pathToPemFile = "eckey.pem";
  var keyIDfromCloudKitDashboard = "2c384ea...";
  var cloudKitContainer = "iCloud.com.....";


  var helper = new CloudKitHelper(pathToPemFile, keyIDfromCloudKitDashboard, cloudKitContainer);


  var recordsJSON = helper.Request("records/query", "{\"query\":{\"recordType\":\"News\"}}");


  Debug.Print(recordsJSON);


  }
  }
}

I was able to build and sign the signature for GET requests, but not POST.

Here's what I found out; the signature must consist of three elements and must be colon separated, so it should look like this:

[date]:[body]:[URL path]

The date should be any ISO8601 date without milliseconds.

The body must should be hashed with SHA256 and then base64-encoded.

The URL path must not contain 'https://api.apple-cloudkit.com' and only the following parameters.


When combined, the concatinated string is then signed using your private key and then base64-encoded.


As I mentioned, I only got GET requests to work, I cannot figure why, but at least this is my interpretation of the API.

So I've got

[date]:[body]:[URL path]


& managed to SHA256 hash, and base64 encode


Its the signing it bit in .Net that I can't get to work... :-(

Accepted Answer

I have tried it a few Days with no success if using the Build-In Functions. Now a tried it with the Bouncy Castle Lib (https://www.bouncycastle.org) an then it worked... here is my code for testing:


using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;


namespace CloudKitTester
{
  public class CloudKitHelper
  {
  private string _keyID;
  private string _container;
  private string _environment;
  private AsymmetricCipherKeyPair _dsaKeyPair;
  private string _apiVersion = "1";




  public CloudKitHelper(string pemFilePath, string keyID, string container, string environment = "development")
  {
  _keyID = keyID;
  _container = container;
  _environment = environment;


  using (var reader = File.OpenText(pemFilePath))
  {
  var pemReader = new PemReader(reader);
  _dsaKeyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
  }
  }


  public string Request(string query, string jsonData = "")
  {
  SHA256Cng sha256 = new SHA256Cng();


  var serverPath = String.Format("/database/{0}/{1}/{2}/public/{3}", _apiVersion, _container, _environment, query);
  var requestBodyBinary = Encoding.UTF8.GetBytes(jsonData);
  var requestBodyHashBase64 = Convert.ToBase64String(sha256.ComputeHash(requestBodyBinary));
  var time = String.Format("{0}Z", DateTime.UtcNow.ToString("s"));


  var signatureString = String.Format("{0}:{1}:{2}", time, requestBodyHashBase64, serverPath);
  var signatureBinary = Encoding.UTF8.GetBytes(signatureString);


  var ecDSASigner = SignerUtilities.GetSigner("SHA256withECDSA");
  ecDSASigner.Init(true, _dsaKeyPair.Private);
  ecDSASigner.BlockUpdate(signatureBinary, 0, signatureBinary.Length);


  var signatureBase64 = Convert.ToBase64String(ecDSASigner.GenerateSignature());


  var request = HttpWebRequest.Create(String.Format("https://api.apple-cloudkit.com{0}", serverPath));
  request.Method = "POST";
  request.ContentType = "text/plain";


  request.Headers.Add("X-Apple-CloudKit-Request-SignatureV1", signatureBase64);
  request.Headers.Add("X-Apple-CloudKit-Request-KeyID", _keyID);
  request.Headers.Add("X-Apple-CloudKit-Request-ISO8601Date", time);


  using (var stream = request.GetRequestStream())
  {
  stream.Write(requestBodyBinary, 0, requestBodyBinary.Length);
  }


  try
  {
  var response = (HttpWebResponse) request.GetResponse();


  using (var output = new StreamReader(response.GetResponseStream()))
  {
  return output.ReadToEnd();
  }
  }
  catch
  {
  return null;
  }
  }
  }

  class Program
  {
  static void Main(string[] args)
  {
  var pathToPemFile = "eckey.pem";
  var keyIDfromCloudKitDashboard = "2c384ea...";
  var cloudKitContainer = "iCloud.com.....";


  var helper = new CloudKitHelper(pathToPemFile, keyIDfromCloudKitDashboard, cloudKitContainer);


  var recordsJSON = helper.Request("records/query", "{\"query\":{\"recordType\":\"News\"}}");


  Debug.Print(recordsJSON);


  }
  }
}

Wow! Another day of pulling my hair out, think I was never going to solve this! You Have! Your sample worked first time. Many thanks.....

Three and a half days... But I still have not figure out why the build in ecdsa library didn't work. If you find something about this let me know

First of all Partick, great work. Your code looked a lot like mine.


But I wasn't sure it would work. It seemed like a convulated way to get the result. And I'd have much rather have used the built in System.Security.Cryptography.


In the year since you posted this, did you make any headway in moving away from Bouncy Castle?

Hello. Thank you very much for your example. It was very helpful for me!!!

c# CloudKit Server 2 Server API
 
 
Q