swiftcloudkitvaporecdsa

Server side Swift - help connecting to CloudKit Web Services


I'm trying to connect to ck web services using Vapor.

I keep getting code 401 (authentication failed). I read and reread the docs(https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/SettingUpWebServices.html#//apple_ref/doc/uid/TP40015240-CH24-SW1) a hundred times but still no luck

Here is my code:

let body = [
        "records":
            [
                ["recordName": "email@gmail.com"]
            ]
    ]
    let bodyData = try? JSONSerialization.data(withJSONObject: body)

    let requestBody = bodyData!
    let body64 = requestBody.base64EncodedString()

    let calendar = Calendar(identifier: .gregorian)
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
    dateFormatter.timeZone = calendar.timeZone
    let date = dateFormatter.string(from: Date())
    
    let webServiceURLPath = "/database/1/MYCONTAINERID/development/public/records/lookup"
    let message = date + ":" + "\(body64)" + ":" + webServiceURLPath
   
    let privateKeyPem =
        """
        -----BEGIN EC PRIVATE KEY-----
        MY PRIVATE HIDDEN PRIVATE KEY
        -----END EC PRIVATE KEY-----
        """
    
    let privateKey = try? P256.Signing.PrivateKey(pemRepresentation: privateKeyPem)
    let sign = try? privateKey?.signature(for: SHA256.hash(data: message.data(using: .utf8)!))
    let signatureBase64 = sign!.derRepresentation.base64EncodedString()

    let keyID = "MYKEYID"
    
    let url = URI(string: "https://api.apple-cloudkit.com/database/1/MYCONTAINERID/development/public/records/lookup")
    
    let headers = HTTPHeaders([
        ("X-Apple-CloudKit-Request-KeyID", keyID),
        ("X-Apple-CloudKit-Request-ISO8601Date", date),
        ("X-Apple-CloudKit-Request-SignatureV1", signatureBase64)
    ])
   
    let response = try app.client.post(url, headers: headers) { request in
        try request.content.encode(body)
    }
    response.flatMapThrowing({ response in
        print(response)
    })

Solution

  • Here is one way to sign a CloudKit web services request

    // Swift example for Apple CloudKit server-to-server token authentication
    // See: https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/SettingUpWebServices.html#//apple_ref/doc/uid/TP40015240-CH24-SW6
    
    
    import Foundation
    import CryptoKit
    import Network
    
    
    // Set up your body JSON
    let body = ["": ""]
    let bodyData = try! JSONSerialization.data(withJSONObject: body)
    
    // hash then base64-encode the body
    let bodyHash = SHA256.hash(data: bodyData)
    let body64 = Data(bodyHash).base64EncodedString()
    
    // set up ISO8601 date string, at UTC
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
    dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
    let date = dateFormatter.string(from: Date())
    
    // endpoint path you want to query
    let path = "/database/1/iCloud.SceneMapper/development/public/zones/list"
    
    // create the concatenated date, encoded body and subpath
    let message = date + ":" + body64 + ":" + path
    
    // Your private key (as generated following the Apple docs above)
    let keyPem =
    """
    -----BEGIN EC PRIVATE KEY-----
    YOUR KEY HERE
    -----END EC PRIVATE KEY-----
    """
    
    // Set up the key and get ECDSA signature
    let privateKey = try? P256.Signing.PrivateKey(pemRepresentation: keyPem)
    let sign = try? privateKey?.signature(for: SHA256.hash(data: message.data(using: .utf8)!))
    //let sign = try? privateKey?.signature(for: message.data(using: .utf8)!)
    let signatureBase64 = sign!.derRepresentation.base64EncodedString()
    
    // Your server-to-server key from the CloudKit dashboard
    let keyID = "your_key_here"
    
    // Set up the full URI
    let url = URL(string: "https://api.apple-cloudkit.com" + path)!
    var request = URLRequest(url: url)
    
    // Set CloudKit-required headers
    request.setValue(keyID, forHTTPHeaderField: "X-Apple-CloudKit-Request-KeyID")
    request.setValue(date, forHTTPHeaderField: "X-Apple-CloudKit-Request-ISO8601Date")
    request.setValue(signatureBase64, forHTTPHeaderField: "X-Apple-CloudKit-Request-SignatureV1")
    
    // Request method
    request.httpMethod = "POST"
    
    // Our original body data for the request
    request.httpBody = bodyData
    
    // Send the request
    let (data, _) = try await URLSession.shared.data(for: request)
    let response = try JSONSerialization.jsonObject(with: data
    
    // Yada yada, do stuff with response