iOS 14的新功能:App Attest

95 min read

When jailbreaking of iOS devices first became popular, it was common for iOS developers to try to defend their apps from users that altered their devices to enable piracy. There were many ways of doing that, which included checking for the existence of Cydia, checking if the app could read files outside its sandbox, crashing the app if it detects a debugger, and more.

当越狱的iOS设备首次流行时,iOS开发人员通常会试图保护自己的应用程序,使其免受用户改动而导致盗版。 有很多方法可以做到这一点,包括检查Cydia的存在,检查应用程序是否可以读取其沙箱之外的文件,在检测到调试器时使应用程序崩溃等。

As time has shown, these defensive measures were and still are a really bad idea. If an attacker has physical access to the device, there’s no way you can trust your app’s logic. It was and still is trivial for hackers to pretend their devices are not jailbroken and effectively bypass these measures. Besides, having a jailbroken device doesn’t mean the user wants to pirate content — some people just want cooler-looking home screens.

正如时间所表明的那样,这些防御措施曾经是而且仍然是一个非常糟糕的主意。 如果攻击者可以物理访问设备,则您无法相信应用程序的逻辑。 对于黑客来说,假装他们的设备没有越狱并有效地绕过这些措施,过去是,现在仍然是微不足道的。 此外,拥有越狱设备并不意味着用户要盗版内容-有些人只想要外观更酷的主屏幕。

As (possibly) a response to jailbreaking becomes popular again in recent times, Apple has released their own measure to this problem. In iOS 14, the new App Attest APIs provide you a way to sign server requests as an attempt to prove to your server that they came from an uncompromised version of your app.

随着近来对(可能)越狱的React再次流行,苹果公司针对此问题发布了自己的解决方案。 在iOS 14中,新的App Attest API为您提供了一种对服务器请求进行签名的方法,以尝试向服务器证明它们来自应用程序的未破坏版本。

It’s important to know that App Attest is not a “is this device jailbroken?” check, as that has been proven over and over to be impossible to perform. Instead, it aims to protect server requests in order to make it harder for hackers to create compromised versions of your app that unlock premium features or inserts features like cheats. Note the word harder: as jailbreakers have physical access to their devices, nothing will completely safeguard you from fraud in this case.

重要的是要知道App Attest不是“此设备越狱了吗?” 检查 ,因为已被反复证明无法执行。 相反,它旨在保护服务器请求 ,以使黑客更难创建盗用的应用程序版本,以解锁高级功能或插入作弊功能。 请注意更难的词:由于越狱者可以物理访问其设备,因此在这种情况下,没有什么可以完全保护您免受欺诈。

As you can’t trust your app to protect itself, App Attest requires work on your backend to be fully implemented. I won’t go through the back end part of it as this is a Swift blog, but will at least mention how it works to show how it wraps things together.

由于您不信任自己的应用程序来保护自己,因此App Attest要求后端上的工作得到完全实施。 我将不介绍它的后端部分,因为这是一个Swift博客,但至少会提到它是如何工作的以展示它如何将事物包装在一起。

生成一对密钥以签署请求 (Generating a Pair of Keys to Sign Requests)

App Attest works through the use of an asymmetric public/secret pair of encryption keys. The intention, in the end, is for your app to sign server requests with the secret key, send the data to the backend and have it confirmed it to be true with the public one. If a hacker intercepts the request, it will not be able to alter its contents without making the backend’s subsequent validation fail.

App Attest通过使用非对称的公共/秘密对加密密钥来工作。 最终,目的是让您的应用使用密钥对服务器请求进行签名,将数据发送到后端,并确认它对公共端是正确的。 如果黑客拦截了该请求,它将无法更改其内容,而不会导致后端的后续验证失败。

To generate the keys, import the DeviceCheck framework and call the generateKey method from the DCAppAttestService singleton:

要生成密钥,请导入DeviceCheck框架并从DCAppAttestService单例调用generateKey方法:

import DeviceChecklet service = DCAppAttestService.sharedservice.generateKey { (keyIdentifier, error) in    guard error == nil else {        return    }}

The keys generated by App Attest are safely stored in your device’s Security Enclave. As you can’t directly access it, the result of this method will be a keyIdentifier property that allows iOS to find the keys when needed. You need to store it so you can later validate your app's requests.

App Attest生成的密钥安全地存储在设备的安全区域中。 由于您无法直接访问它,因此此方法的结果将是keyIdentifier属性,该属性允许iOS在需要时查找密钥。 您需要存储它,以便以后可以验证应用程序的请求。

It’s important to mention that App Attest is not supported by all types of devices, and if you look at Apple’s own documentation, they will ask you to first check if it’s supported and have your server support a fallback in case it’s not:

值得一提的是,并非所有类型的设备支持App Attest,如果您查看Apple自己的文档,他们会要求您首先检查它是否受支持,并在不支持的情况下让您的服务器支持回退:

if service.isSupported { ... }

Do not do this! As said before, it’s trivial for a jailbreak user to “pretend” their device doesn’t support it. Apple doesn’t expand on this topic, but the reasons for this check to exist appears to be that there are some Macbooks that don’t have the necessary chip to support it. However, as investigated by Guilherme Rambo, it appears that every single iOS device supports it. For an iOS app, you do not need to do a compatibility check.

不要这样做! 如前所述,越狱用户“假装”他们的设备不支持它是微不足道的。 Apple并未对此主题进行扩展,但是存在此检查的原因似乎是,有些Macbook没有支持它的必要芯片。 但是, 根据Guilherme Rambo的调查,似乎每个iOS设备都支持它。 对于iOS应用,您无需进行兼容性检查。

证明:将公钥发送到后端 (Attesting: Sending the Public Key to The Backend)

In order to sign server requests, you need to provide your backend with a way to confirm that signature. This is done by giving the backend access to the public key we previously generated, but we can’t simply create a request and add it as a parameter because it would be pretty easy for a hacker to intercept it and send their own public key instead, giving them full control of what your app sends to the backend.

为了对服务器请求进行签名,您需要为后端提供一种确认该签名的方法。 这是通过为后端提供对我们先前生成的公钥的访问权限来完成的,但是我们不能简单地创建一个请求并将其添加为参数,因为黑客很容易拦截该请求并发送自己的公钥,让他们可以完全控制您的应用程序发送到后端的内容。

The solution to this problem is to ask Apple to attest that what the key we’re sending originated from an uncompromised version of your app. This is done by calling the attestKey method, which receives the key's identifier as a parameter:

解决此问题的方法是让Apple证明我们发送的密钥来自您应用程序的安全版本。 这是通过调用attestKey方法来完成的,该方法接收密钥的标识符作为参数:

service.attestKey(keyIdentifier, clientDataHash: hash) { attestation, error in    guard error == nil else { return }    let attestationString = attestation?.base64EncodedString()    // Send the attestation to the server. It now has access to the public key!    // If it fails, throw the identifier away and start over.}

This method accesses a remote Apple server, and the result is an “attestation” object that contains not only your public key but a ton of information about your app that serves as a statement from Apple that the previously generated keys are not fake. When you receive this object, you must send it to your back end and have it perform several validations on it that allows it to confirm that it was unaltered. If the attestation object was successfully validated, the backend will be able to safely extract the app’s public key from it.

此方法访问远程Apple服务器,结果是一个“证明”对象,该对象不仅包含您的公共密钥,而且还包含有关您的应用程序的大量信息,这是Apple声明的先前生成的密钥不是伪造的。 收到该对象时,必须将其发送到后端,并对其执行几次验证,以使其确认未更改。 如果成功验证了证明对象,则后端将能够从中安全地提取应用程序的公钥。

It’s unclear if Apple attempts or not to check if the user’s device if jailbroken during this process. It’s never mentioned that this is the case, but they do say “App Attest can’t definitively pinpoint a device with a compromised operating system.” which could imply that they at least try something. It’s probably safe to assume that this is not the case, and the word attest here simply means that your request (probably) wasn’t intercepted and modified.

目前尚不清楚Apple是否在此过程中试图检查用户的设备是否越狱。 从来没有提到过这种情况,但是他们确实说“ App Attest不能确切地指出操作系统受损的设备。” 这可能意味着他们至少要尝试一些东西。 可以肯定地说不是这种情况,并且attest这个词仅表示您的请求(可能)未被拦截和修改。

The additional clientDataHash parameter of the attestation request is not related to the attestation process itself, but extremely important for it to make it safe. As it is, this request is susceptible to a replay attack where a hacker could intercept the validation request and steal the attestation object sent from Apple so that later they can "replay" the same validation request at a fake version of your app to make the server believe it came from the real one.

证明请求的附加clientDataHash参数与证明过程本身无关,但是对于使其变得安全至关重要。 实际上,此请求很容易受到重播攻击,黑客可以拦截验证请求并窃取从Apple发送的证明对象,以便以后他们可以在您的应用程序的伪造版本中“重播”相同的验证请求以使服务器认为它来自真实的服务器。

A solution to this problem is to simply not allow the validation request to be executed freely. Instead, the client can provide a one-time use token (or session ID) that the server will expect to accompany the request to ensure its validity. If the same token is used twice, the request will fail. That’s what clientDataHash is for: By providing a hashed version of that expected token to the attestation request, Apple will embed it into the final object and provide your server a way to extract it. With this, it's pretty hard for a hacker to create a compromised version of your app by simply intercepting requests.

该问题的解决方案是根本不允许自由地执行验证请求。 相反,客户端可以提供服务器希望与请求一起使用的一次性使用令牌(或会话ID),以确保其有效性。 如果两次使用相同的令牌,则请求将失败。 这就是clientDataHash目的:通过为证明请求提供该预期令牌的哈希版本,Apple会将其嵌入到最终对象中,并为您的服务器提供一种提取它的方法。 有了这个,对于黑客来说,仅通过拦截请求就很难创建您应用程序的受感染版本。

let challenge = getSessionId().data(using: .utf8)!let hash = Data(SHA256.hash(data: challenge))service.attestKey(keyIdentifier, clientDataHash: hash) { ... }

As mentioned earlier, Apple suggests you do not reuse keys. You should do this entire process for each user account in a device.

如前所述,Apple建议您不要重用密钥。 您应该对设备中的每个用户帐户执行整个过程。

Because this request relies on a remote Apple server, it’s possible for it to fail. If the error is that the server was unavailable, Apple says that you can simply try again, but if it’s anything else, you should discard the key identifier and start over. This can happen for example when a user reinstalls your app — The keys that you generate remain valid through regular app updates, but don’t survive app reinstallation, device migration, or restoration of a device from a backup. For these cases, your app needs to be able to restart the key generation process.

由于此请求依赖于远程Apple服务器,因此可能会失败。 如果错误是服务器不可用,Apple表示您可以重试,但是如果还有其他错误,则应丢弃密钥标识符并重新开始。 例如,当用户重新安装您的应用程序时,可能会发生这种情况-您生成的密钥在常规应用程序更新中仍然有效,但是在重新安装应用程序,设备迁移或从备份还原设备后仍然无法幸免。 对于这些情况,您的应用程序需要能够重新启动密钥生成过程。

From the server-side of things, it’s also interesting to mention that the statement object also contains a receipt that your server can use to request fraud assessment metrics from Apple. This allows you to check the number of generated keys and the devices that they have been associated to detect possible cases of fraud. Apple specifically mentions the possibility of an attack where a user could use one device to provide valid assertions to compromised devices, which can be detected by this fraud assessment by locating users with unusually high amounts of assertion requests.

从服务器方面来说,还值得一提的是,语句对象还包含一张收据,您的服务器可以使用该收据来向Apple请求欺诈评估指标。 这使您可以检查生成的密钥的数量以及与它们关联的设备,以检测可能的欺诈情况。 苹果公司特别提到了攻击的可能性,即用户可以使用一个设备向受感染设备提供有效的断言,这种欺诈评估可以通过定位具有异常高数量的断言请求的用户来检测到。

总结:加密服务器请求 (Wrapping It Up: Encrypting Server Requests)

After attesting the validity of the key, your backend will have access to the public key. From now on, every time you’re dealing with sensitive content, you have the ability to safely sign that request. The generateAssertion method used for this works very similarly to the attestation of the keys, except this time you're attesting the request itself:

在证明了密钥的有效性之后,您的后端将可以访问公共密钥。 从现在开始,每次您处理敏感内容时,您都可以安全地签署该请求。 用于此目的的generateAssertion方法的工作原理与密钥的证明非常相似,只是这次您要证明请求本身:

let challenge = getSessionId().data(using: .utf8)!let requestJSON = "{ 'requestedPremiumLevel': 300, 'sessionId': '\(challenge)' }".data(using: .utf8)!let hash = Data(SHA256.hash(data: challenge))service.generateAssertion(keyIdentifier, clientDataHash: hash) { assertion, error in    guard error == nil else { return }    let assertionString = assertion?.base64EncodedString()    // Send the signed assertion to your server.    // The server will validate it, grab your request and process it.}

Just as before, your back end must support the use of a one-time token to prevent replay attacks. This time, since the request itself is our clientDataHash, we're adding the token inside the JSON. There's no restriction on the number of assertions that you can make with a given key. But still, you should typically reserve them for requests made at sensitive moments of your app, such as the download of premium content.

与以前一样,您的后端必须支持使用一次性令牌来防止重放攻击。 这次,由于请求本身就是我们的clientDataHash ,因此我们将令牌添加到JSON中。 对于给定键可以进行的断言数量没有限制。 但是,尽管如此,您通常仍应保留它们,以在应用程序敏感时刻发出请求,例如下载高级内容。

In this case, your additional protection comes from the fact that the request is hashed and usable only once. Because the entire request is signed by your private key, a hacker can’t simply intercept your requests and use them to craft their own. They must figure out where the parameters of your request are coming from and manually attempt to sign it, something that will take slightly more skill than simply attaching a proxy. As mentioned at the beginning, it’s not impossible to break this protection, just harder.

在这种情况下,您的额外保护来自以下事实:该请求被散列并且只能使用一次。 因为整个请求都是由您的私钥签名的,所以黑客不能简单地拦截您的请求并使用它们来编写自己的请求。 他们必须弄清楚您的请求的参数来自何处,并手动尝试对其进行签名,这比简单附加代理要花费更多的技巧。 正如开头提到的,要打破这种保护并不是没有可能,只是要更加努力。

测试并推出您的实施 (Testing and Rolling Out Your Implementation)

The App Attest service records metrics that you can’t reset. To prevent that, apps not in a production environment will use a sandboxed version of it. If you instead want to test in the production environment, you should add the com.apple.developer.devicecheck.appattest-environment entitlement to your app and set its value to production.

App Attest服务记录了您无法重置的指标。 为防止这种情况,不在生产环境中的应用程序将使用其沙盒版本。 如果您想在生产环境中进行测试,则应将com.apple.developer.devicecheck.appattest-environment授权添加到您的应用中,并将其值设置为production

If you have a large user base, Apple recommends you to gradually roll this feature as requests to attestKey are rate limited. After carefully rolling it out for existing users, you can guarantee that it will only be called for new users.

如果您的用户群很大,Apple建议您逐步attestKey此功能,因为对attestKey请求attestKey速率限制。 在为现有用户精心推出后,您可以保证只为新用户调用它。

结论 (Conclusion)

By implementing this in your client and in your backend, it should become harder for hackers to create compromised versions of your app. However, be aware of the word harder — it doesn’t mean impossible! As mentioned before, there’s no sure way for you to detect if a user has a jailbroken device and even fewer ways to prevent them from attacking your app. As with most security measures, the intention of App Attest is instead to make this process difficult enough so that only a very skilled and dedicated hacker would be able to find a way to break into your app — someone much harder to come by.

通过在客户端和后端中实现此功能,黑客应更难创建应用程序的受感染版本。 但是,请加倍注意这个词-这并不意味着不可能! 如前所述,没有确定的方法可以让您检测用户是否有越狱设备,甚至无法阻止用户攻击您的应用程序。 与大多数安全措施一样,App Attest的目的是使此过程足够困难,以使只有非常熟练和专业的黑客才能找到闯入您的应用程序的途径-很难找到。