我是just reading关于在移动设备上使用iCloud ID令牌进行应用识别的问题。
如果我的服务器通过互联网收到带有iCloud ID令牌的请求,是否有办法验证它是否由Apple发出并且不是由发送方组成的?
答案 0 :(得分:0)
查看Device Check Framework.“访问关联的服务器可以在其业务逻辑中使用的每个设备,每个开发人员的数据。”在最近对this SO thread中的答案的评论中提出了该建议。
这是将设备检查与iCloud用户ID哈希一起使用以确保对您的API的请求合法的方法。以下许多代码都是根据this改编而成的。
在您的iOS应用中从Apple获取临时设备检查令牌,然后将其与您的请求以及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应用-例如某人的终端。接下来,使用设备检查查询端点获取当前设备的两个设备检查位。如果bit0为true,则假定此设备在您的请求表中至少已经有一行记录在给定的iCloud用户名哈希值上。如果存在这样的行,则这可能是合法请求,因为很难猜测其他键。如果没有这样的行,则用户可能生成了伪造的iCloud用户哈希。但是,如果bit0为false,则此设备尚未在请求表中放置一行。在给定的iCloud用户名哈希上键入一行,并使用Device Check更新端点将此设备的bit0设置为true。这是一个AWS Lambda节点8.10中的示例,其中请求表位于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;