如何针对 iCloud 验证 iCloud ID 令牌?

Posted

技术标签:

【中文标题】如何针对 iCloud 验证 iCloud ID 令牌?【英文标题】:how to verify an iCloud ID token against iCloud? 【发布时间】:2017-10-18 23:58:55 【问题描述】:

我是just reading,关于在移动设备上使用 iCloud ID 令牌进行应用识别。

如果我的服务器通过 Internet 接收到带有 iCloud ID 令牌的请求,它有没有办法验证它是由 Apple 发出的,而不是由发送方编造的?

【问题讨论】:

【参考方案1】:

查看Device Check Framework.“访问每个设备、每个开发人员的数据,您的关联服务器可以在其业务逻辑中使用这些数据。”这是在最近对this SO thread 的答案的评论中提出的。

这是如何使用带有 iCloud 用户 ID 哈希的设备检查来确保对您的 API 的请求是合法的。以下很多代码都改编自this。

    在您的 ios 应用中从 Apple 获取一个临时的 Device Check 令牌,然后将其与您的请求以及 iCloud 用户名哈希一起发送到您的后端。

    在 Swift 4 中:

    import DeviceCheck
    
    let currDevice = DCDevice.current
    
    if ViewController.currDevice.isSupported 
        ViewController.currDevice.generateToken  (data, error) in
            if let data = data 
                let url = "your-url"
                let sesh = URLSession(configuration: .default)
                var req = URLRequest(url: url)
                req.addValue("application/json", forHTTPHeaderField: "Content-Type")
                req.httpMethod = "POST"
                DispatchQueue.main.sync 
                    var jsonObj = [
                        "deviceCheckToken" : data.base64EncodedString(), 
                        "iCloudUserNameHash": self.iCloudUserID,
                        "moreParams": "moreParamsHere"
                    ]
                    let data = try! JSONSerialization.data(withJSONObject: jsonObj, options: [])
                    req.httpBody = data
                    let task = sesh.dataTask(with: req, completionHandler:  (data, response, error) in
                        if let data = data, let jsonData = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers), let jsonDictionary = jsonData as? [String: Any]  
                            DispatchQueue.main.async 
                                // Process response here
                            
                        
                    )
                    task.resume()
                
             else if let error = error 
                print("Error when generating a token:", error.localizedDescription)
            
        
     else 
        print("Platform is not supported. Make sure you aren't running in an emulator.")
    
    

    您可以在设备检查框架中为每个设备的每个应用存储两个位。使用 bit0 记住您已经向当前设备提供了请求。首先调用 Device Check 验证端点以查看请求是否来自 iOS 应用程序——而不是例如某人的终端。接下来,使用 Device Check 查询端点获取当前设备的两个 Device Check 位。如果 bit0 为真,则假设此设备在您的请求表中已经至少有一行键入了给定的 iCloud 用户名哈希。如果有这样的一行,这可能是一个合法的请求,因为很难猜出其他键。如果没有这样的行,用户可能生成了一个虚假的 iCloud 用户哈希。但是,如果 bit0 为 false,则该设备尚未在 requests 表中放入一行。在给定的 iCloud 用户名哈希上键入一行,并使用 Device Check 更新端点将此设备的 bit0 设置为 true。这是 AWS Lambda 中节点 8.10 中的一个示例,其中 requests 表位于 DynamoDB 中。

    endpoint.js

    const AWS = require('aws-sdk');
    const utf8 = require('utf8');
    
    const asyncAWS = require('./lib/awsPromiseWrappers');
    const deviceCheck = require('./lib/deviceCheck');
    const util = require('./lib/util');
    
    // AWS globals
    const lambda = new AWS.Lambda(
        region: process.env.AWS_REGION,
    );
    const dynamodb = new AWS.DynamoDB.DocumentClient();
    
    // Apple Device Check keys
    const cert = utf8.encode([
        process.env.BEGIN_PRIVATE_KEY,
        process.env.APPLE_DEVICE_CHECK_CERT,
        process.env.END_PRIVATE_KEY,
    ].join('\n'));  // utf8 encoding and newlines are necessary for jwt to do job
    const keyId = process.env.APPLE_DEVICE_CHECK_KEY_ID;
    const teamId = process.env.APPLE_ITUNES_CONNECT_TEAM_ID;
    
    // Return true if device check succeeds
    const isLegitDevice = async (deviceCheckToken, iCloudUserNameHash) => 
    
        // Pick the correct (dev or prod) Device Check API URL
        var deviceCheckHost;
        if (process.env.STAGE === 'dev') 
            deviceCheckHost = process.env.DEV_DEVICE_CHECK_API_URL;
         else if (stage === 'prod') 
            deviceCheckHost = process.env.PROD_DEVICE_CHECK_API_URL;
         else 
            util.cloudwatchLog(`--> Unrecognized stage $stage. Aborting DC`);
            return;
        
    
        // Make sure device is valid. If not, return false
        try 
            await deviceCheck.validateDevice(
                cert, keyId, teamId, deviceCheckToken, deviceCheckHost);
         catch (err) 
            util.cloudwatchLog(`--> DC validation failed. $err`);
            return false;
        
    
        // Query for Device Check bits
        var dcQueryResults;
        try 
            dcQueryResults = await deviceCheck.queryTwoBits(
                cert, keyId, teamId, deviceCheckToken, deviceCheckHost);
         catch (err) 
            dcQueryResults = null;
        
    
        // If bit0 is true, then this device already has at least one row in the
        // search counts table
        if (dcQueryResults && dcQueryResults.bit0) 
    
            // Try to get the counts row keyed on given user name
            const getParams = 
                TableName: process.env.SEARCH_COUNTS_TABLE,
                Key:  u: iCloudUserNameHash ,
            ;
            var countsRow;
            try 
                countsRow = await asyncAWS.invokeDynamoDBGet(dynamodb, getParams);
             catch (err) 
                const msg = `--> Couldn't get counts row during DC call: $err`;
                util.cloudwatchLog(msg);
                return false;
            
    
            // If it doesn't exist, return false
            if (!countsRow) 
                return false;
             else   // if it DOES exist, this is a legit request
                return true;
            
         else 
    
            // Initialize the row in memory
            const secsSinceEpoch = (new Date()).getTime() / 1000;
            const countsRow = 
                h: [0, secsSinceEpoch],
                d: [0, secsSinceEpoch],
                w: [0, secsSinceEpoch],
                m: [0, secsSinceEpoch],
                y: [0, secsSinceEpoch],
                a: 0,
                u: iCloudUserNameHash,
            ;
    
            // Put it in the search counts table
            const putParams = 
                Item: countsRow,
                TableName: process.env.SEARCH_COUNTS_TABLE,
            ;
            try 
                await asyncAWS.invokeDynamoDBPut(dynamodb, putParams);
             catch (err) 
                const msg = `--> Couldn't set counts row in DC call: $err`
                util.cloudwatchLog(msg);
                return false;
            
    
            // Set the device check bit
            try 
                await deviceCheck.updateTwoBits(true, false,
                    cert, keyId, teamId, deviceCheckToken, deviceCheckHost);
             catch (err) 
                const msg = `--> DC update failed. $iCloudUserNameHash $err`;
                util.cloudwatchLog(msg);
                return false;
            
    
            // If we got here, the request was legit
            return true;
        
    ;
    
    exports.main = async (event, context, callback) => 
    
        // Handle inputs
        const body = JSON.parse(event.body);
        const iCloudUserNameHash = body.iCloudUserNameHash;
        const deviceCheckToken = body.deviceCheckToken;
        const otherParams = body.otherParams;
    
        // If allowed to search, increment search counts then search
        var deviceCheckSucceeded;
        try 
            deviceCheckSucceeded =
                await isLegitDevice(deviceCheckToken, iCloudUserNameHash);
         catch (err) 
            util.cloudwatchLog(`--> Error checking device: $err`);
            return callback(null, resp.failure());
        
    
        if (deviceCheckSucceeded) 
    
            // Do your stuff here
    
            return callback(null, resp.success());
         else 
            return callback(null, resp.failure());
        
    ;
    

    deviceCheck.js

    const https = require('https');
    const jwt = require('jsonwebtoken');
    const uuidv4 = require('uuid/v4');
    
    const util = require('../lib/util');
    
    // Set the two Device Check bits for this device.
    // Params:
    //   bit0 (boolean) - true if never seen given iCloud user ID
    //   bit1 (boolean) - TODO not used yet
    //   cert (string) - Device Check certificate. Get from developer.apple.com)
    //   keyId (string) - Part of metadata for Device Check certificate)
    //   teamId (string) - My developer team ID. Can be found in iTunes Connect
    //   dcToken (string) - Ephemeral Device Check token passed from frontend
    //   deviceCheckHost (string) - API URL, which is either for dev or prod env
    const updateTwoBits = async (
        bit0, bit1, cert, keyId, teamId, dcToken, deviceCheckHost) => 
    
        return new Promise((resolve, reject) => 
            var jwToken = jwt.sign(, cert, 
                algorithm: 'ES256',
                keyid: keyId,
                issuer: teamId,
            );
    
            var postData = 
                'device_token' : dcToken,
                'transaction_id': uuidv4(),
                'timestamp': Date.now(),
                'bit0': bit0,
                'bit1': bit1,
            
    
            var postOptions = 
                host: deviceCheckHost,
                port: '443',
                path: '/v1/update_two_bits',
                method: 'POST',
                headers: 
                    'Authorization': 'Bearer ' + jwToken,
                ,
            ;
    
            var postReq = https.request(postOptions, function(res) 
                res.setEncoding('utf8');
    
                var data = '';
                res.on('data', function (chunk) 
                    data += chunk;
                );
    
                res.on('end', function() 
                    util.cloudwatchLog(
                        `--> Update bits done with status code $res.statusCode`);
                    resolve();
                );
    
                res.on('error', function(data) 
                    util.cloudwatchLog(
                        `--> Error $res.statusCode in update bits: $data`);
                    reject();
                );
            );
    
            postReq.write(new Buffer.from(JSON.stringify(postData)));
            postReq.end();
        );
    ;
    
    // Query the two Device Check bits for this device.
    // Params:
    //     cert (string) - Device Check certificate. Get from developer.apple.com)
    //     keyId (string) - Part of metadata for Device Check certificate)
    //     teamId (string) - My developer team ID. Can be found in iTunes Connect
    //     dcToken (string) - Ephemeral Device Check token passed from frontend
    //     deviceCheckHost (string) - API URL, which is either for dev or prod env
    // Return:
    //      bit0 (boolean), bit1 (boolean), lastUpdated (String) 
    const queryTwoBits = async (cert, keyId, teamId, dcToken, deviceCheckHost) => 
    
        return new Promise((resolve, reject) => 
    
            var jwToken = jwt.sign(, cert, 
                algorithm: 'ES256',
                keyid: keyId,
                issuer: teamId,
            );
    
            var postData = 
                'device_token' : dcToken,
                'transaction_id': uuidv4(),
                'timestamp': Date.now(),
            
    
            var postOptions = 
                host: deviceCheckHost,
                port: '443',
                path: '/v1/query_two_bits',
                method: 'POST',
                headers: 
                    'Authorization': 'Bearer ' + jwToken,
                ,
            ;
    
            var postReq = https.request(postOptions, function(res) 
                res.setEncoding('utf8');
    
                var data = '';
                res.on('data', function (chunk) 
                    data += chunk;
                );
    
                res.on('end', function() 
                    try 
                        var json = JSON.parse(data);
                        resolve(
                            bit0: json.bit0,
                            bit1: json.bit1,
                            lastUpdated: json.last_update_time,
                        );
                     catch (e) 
                        const rc = res.statusCode;
                        util.cloudwatchLog(
                            `--> DC query call failed. $e, $data, $rc`);
                        reject();
                    
                );
    
                res.on('error', function(data) 
                    const code = res.statusCode;
                    util.cloudwatchLog(
                        `--> Error $code with query bits call: $data`);
                    reject();
                );
            );
    
            postReq.write(new Buffer.from(JSON.stringify(postData)));
            postReq.end();
        );
    ;
    
    // Make sure devie is valid.
    // Params:
    //   cert (string) - Device Check certificate. Get from developer.apple.com)
    //   keyId (string) - Part of metadata for Device Check certificate)
    //   teamId (string) - My developer team ID. Can be found in iTunes Connect
    //   dcToken (string) - Ephemeral Device Check token passed from frontend
    //   deviceCheckHost (string) - API URL, which is either for dev or prod env
    const validateDevice = async (
        cert, keyId, teamId, dcToken, deviceCheckHost) => 
    
        return new Promise((resolve, reject) => 
            var jwToken = jwt.sign(, cert, 
                algorithm: 'ES256',
                keyid: keyId,
                issuer: teamId,
            );
    
            var postData = 
                'device_token' : dcToken,
                'transaction_id': uuidv4(),
                'timestamp': Date.now(),
            
    
            var postOptions = 
                host: deviceCheckHost,
                port: '443',
                path: '/v1/validate_device_token',
                method: 'POST',
                headers: 
                    'Authorization': 'Bearer ' + jwToken,
                ,
            ;
    
            var postReq = https.request(postOptions, function(res) 
                res.setEncoding('utf8');
    
                var data = '';
                res.on('data', function (chunk) 
                    data += chunk;
                );
    
                res.on('end', function() 
                    util.cloudwatchLog(
                        `--> DC validation done w/ status code $res.statusCode`);
                    if (res.statusCode === 200) 
                        resolve();
                     else 
                        reject();
                    
                );
    
                res.on('error', function(data) 
                    util.cloudwatchLog(
                        `--> Error $res.statusCode in DC validate: $data`);
                    reject();
                );
            );
    
            postReq.write(new Buffer.from(JSON.stringify(postData)));
            postReq.end();
        );
    ;
    
    exports.updateTwoBits = updateTwoBits;
    exports.queryTwoBits = queryTwoBits;
    exports.validateDevice = validateDevice;
    

【讨论】:

以上是关于如何针对 iCloud 验证 iCloud ID 令牌?的主要内容,如果未能解决你的问题,请参考以下文章

苹果平板电脑忘记icloud密码如何退出账号

怎么注销ipad icloud账号

苹果icloud登陆不了怎么办

苹果手机为啥登陆不了icloud?

iCloud:应用程序在 Xamarin Studio 中验证,但不在 XCode 中

iOS 5.0.1:如何验证文件夹是不是标记为 iCloud 的“不备份”?