Closed jdanthinne closed 2 years ago
AWSIoTManager is only designed to have a single certificate loaded at a time. Therefore, AWSIoTManager.importIdentity(...) does not store the certificate ID as part of the tag. AWSIoTManager.deleteCertificate() deletes the current certificate. The reason for this is that the purpose of this certificate is to authenticate the client (in this case the iOS device) to the AWS IoT Core device gateway.
The certificate registered with this API authenticates the iOS device with the AWS IoT Core device gateway, which in turn sends and receives published MQTT messages via its message broker to/from other devices (e.g. any IoT devices to be controlled by this app). Because the messages are brokered via the AWS IoT backend, the iOS device does not require certificates for the devices it's controlling. There should only be one AWS IoT certificate per iOS device and user registered with this API, used to authenticate to the backend, regardless of the number of IoT devices with which it interacts. Each of those AWS IoT devices would similarly need their own certificate which they would use to authenticate to the IoT backend.
This sample app has a method which deletes certificate for your reference.
Does that apply to my particular case?
I'm building an app that handle communications between AWS IoT and BLE devices that are NOT able by themselves to send MQTT messages (no Wifi), the iPhone acting here as a relay. These devices need to be enrolled (RegisterThing) individually to be identified on the platform, so the iPhone enrolls each one of them, and receive a certificate per device.
Are you suggesting that only the iPhone needs to be enrolled then? That sounds strange to me.
Moreover, I see that the Android SDK can delete individual certificate, so why is it different on iOS?
Hello, Thank you for posting this. We're discussing this issue within our team and will post here with updates.
Hi @jdanthinne,
If you have specific functionality you'd like us to add please create a feature request.
In the meantime, if you query your keychain items where the key is kSecClass
with kSecClassCertificate
as the value your can get a list of the certificates which are stored. You can look at AWSIoTKeychain.m to see how certificates are managed internally. This functionality is not on the public API so any changes directly to keychain items are not supported. Reading this code may help you with your use case.
Below is some code you could use as well. You can either include a single certificate with your app which all instances of the app would use. You can also create a certificate on the device for the app to use. The SDK gives you access to managing certificates. You will need to associate a policy with these certificates to control access.
private func findCertificateId() -> String? {
let defaults = UserDefaults.standard
// No certificate ID has been stored in the user defaults; check to see if any .p12 files
// exist in the bundle.
let certificates = Bundle.main.paths(forResourcesOfType: "p12" as String, inDirectory:nil)
guard let certificateId = certificates.first else {
let result = defaults.string(forKey: "certificateId")
return result
}
// A PKCS12 file may exist in the bundle. Attempt to load the first one
// into the keychain (the others are ignored), and set the certificate ID in the
// user defaults as the filename. If the PKCS12 file requires a passphrase,
// you'll need to provide that here; this code is written to expect that the
// PKCS12 file will not have a passphrase.
guard let data = try? Data(contentsOf: URL(fileURLWithPath: certificateId)) else {
print("[ERROR] Found PKCS12 File in bundle, but unable to use it")
let certificateId = defaults.string( forKey: "certificateId")
return certificateId
}
log("Found identity, importing: \(certificateId)")
if AWSIoTManager.importIdentity(fromPKCS12Data: data, passPhrase: "", certificateId: certificateId) {
// Set the certificate ID and ARN values to indicate that we have imported
// our identity from the PKCS12 file in the bundle.
defaults.set(certificateId, forKey: "certificateId")
defaults.set("from-bundle", forKey: "certificateArn")
self.log("Using certificate: \(certificateId))")
}
let result = defaults.string( forKey: "certificateId")
return result
}
private func createCertificateId(resultHandler: @escaping (Result<String, Error>) -> Void) {
// Create and store the certificate ID in UserDefaults
let csrDictionary = [ "commonName": CertificateSigningRequestCommonName,
"countryName": CertificateSigningRequestCountryName,
"organizationName": CertificateSigningRequestOrganizationName,
"organizationalUnitName": CertificateSigningRequestOrganizationalUnitName]
let iot = AWSIoT.default()
// CreateKeysAndCertificate
// https://docs.aws.amazon.com/iot/latest/apireference/API_CreateKeysAndCertificate.html
iotManager.createKeysAndCertificate(fromCsr: csrDictionary) { response in
guard let response = response,
let certificateId = response.certificateId,
let certificateArn = response.certificateArn else {
self.log("Unable to create keys and/or certificate, check CSR values. \(csrDictionary)")
resultHandler(.failure(Failure.failedToCreateCertificate))
return
}
let defaults = UserDefaults.standard
defaults.set(response.certificateId, forKey:"certificateId")
defaults.set(response.certificateArn, forKey:"certificateArn")
self.log("Response: \(response)")
guard let attachPolicyRequest = AWSIoTAttachPolicyRequest() else {
fatalError()
}
attachPolicyRequest.policyName = PolicyName
attachPolicyRequest.target = certificateArn
// Attach the policy to the certificate
iot.attachPolicy(attachPolicyRequest).continueWith (block: { (task) -> AnyObject? in
if let error = task.error {
self.log("Error: \(error)")
resultHandler(.failure(error))
} else if let result = task.result {
self.log("Result: \(result)")
resultHandler(.success(certificateId))
}
return nil
})
}
}
private func loadIdentityCertificate(readyToConnectHandler: (Result<String, Error>) -> Void) {
dispatchPrecondition(condition: .notOnQueue(.main))
let defaults = UserDefaults.standard
if let certificateId = defaults.string(forKey: "certificateId") {
if !AWSIoTManager.isValidCertificate(certificateId) {
log("Invalid certificate found and deleted")
AWSIoTManager.deleteCertificate()
} else {
log("Using imported identity")
readyToConnectHandler(.success(certificateId))
}
} else {
guard let certificateId = Bundle.main.path(forResource: "awsiot-identity", ofType: "p12") else {
readyToConnectHandler(.failure(Failure.die("Failed to find certificate in bundle")))
return
}
let certificateURL = URL(fileURLWithPath: certificateId)
guard let certificateData = try? Data(contentsOf: certificateURL) else {
readyToConnectHandler(.failure(Failure.die("Failed to load certificate from bundle")))
return
}
if AWSIoTManager.importIdentity(fromPKCS12Data: certificateData,
passPhrase: "",
certificateId: certificateId) {
defaults.set(certificateId, forKey:"certificateId")
defaults.set("from-bundle", forKey:"certificateArn")
log("Imported identity")
readyToConnectHandler(.success(certificateId))
} else {
readyToConnectHandler(.failure(Failure.die("Failed to import identity")))
}
}
}
I've already tried to query the Keychain to retrieve the certificate, but unfortunately, it isn't tagged with the certificateId, so how can I find it back?
You're appending the certificateId to the public and private keys tags, but not to the certificate tag. Is there a reason for that?
NSString *publicTag = [AWSIoTKeychain.publicKeyTag stringByAppendingString:certificateId];
NSString *privateTag = [AWSIoTKeychain.privateKeyTag stringByAppendingString:certificateId];
@jdanthinne I've been going through the code and running a dev app to gather the details about what the SDK does when creating an identity or importing one from a .p12
included in the app bundle. To support your use case there would need to be changes to the SDK to support more than 1 certificate. But the code base currently does not include the certificateId
with the keychain item for the certificate while it does for the public/private key pair which go with the certificate.
Normally an iOS app only uses a single identity with AWS IoT. You can create a feature request to add this support.
To support your use case there would need to be a way to match BLE devices with the identities managed by the iOS app. An identity can be created by the app and associated with a BLE device. An instance of AWSIoTDataManager
also only supports a single connection at a time so you would need to create multiple data managers. Running multiple connections concurrently would require time to develop and test. It would also use more resources to operate which may not perform well on a mobile device.
What you may want to consider is how you can use a single identity with multiple BLE devices to pass through messages. If you were to use a topics which includes the BLE device id you could have each BLE device publish and subscribe using topics which are unique to them while using a single identity and IoT connection. When BLE devices connect you can start subscribing for topics for that device and alter unsubscribe when it is disconnected. This would be compatible with the SDK.
Please let us know what you'd like to do.
@jdanthinne I just created a branch which has the features needed to tag the certificate with the certificateId like the public and private keys have been. You can see it linked below.
https://github.com/aws-amplify/aws-sdk-ios/tree/stehlib.iot-4084
Since this is different behavior than the SDK has done before I added a property which will allow you to opt into this behavior. You'd need to add this line when initializing IoT.
AWSIoTManager.tagCertificateEnabled = true
In my development code I have these properties defined.
var bundleIdentifier: String {
Bundle(for: AWSIoTDataManager.self).bundleIdentifier ?? ""
}
var publicKeyTag: String {
"\(bundleIdentifier).RSAPublicTag."
}
var privateKeyTag: String {
"\(bundleIdentifier).RSAPrivateTag."
}
var certTag: String {
"\(bundleIdentifier).RSACertTag."
}
They allow for querying keychain items and extracting the certificateId
from keychain items.
private func getCertificates() -> [String] {
let query: [CFString: Any] = [
kSecClass: kSecClassCertificate,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnAttributes: kCFBooleanTrue as Any
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == noErr {
guard let items = result as? [[String: Any]] else {
fatalError("Failed to get certificates from keychain")
}
let certificates = items.compactMap {
$0[kSecAttrLabel as String] as? String
}.filter {
$0.hasPrefix(bundleIdentifier)
}
return certificates
} else if status == errSecItemNotFound {
log("No certificates found")
return []
} else {
log("Failed to query for certificates")
return []
}
}
This query gets the certificates which have been added. It is used by this function.
private func resetKeychainItems() {
let certificates = getCertificates()
let prefix = certTag
certificates
.filter {
$0.hasPrefix(prefix)
}
.forEach { tag in
let certificateId = String(tag.dropFirst(prefix.count))
AWSIoTManager.deleteCertificate(certificateId: certificateId)
}
clearCertificateDefaults()
}
For this dev app, the values are stored in UserDefaults with only a single identity stored in the keychain at a time. You'd need to use a different system to persist these values, perhaps a .json file supported by Codable for each BLE device to store these values. And if you connect these BLE devices to multiple iPhones each would have a copy of unique keychain items. They should not be copied across devices as the would compromise them.
private func storeCertificateDefaults(certificateId: String, certificateArn: String) {
let defaults = UserDefaults.standard
defaults.set(certificateId, forKey:"certificateId")
defaults.set(certificateArn, forKey:"certificateArn")
}
private func clearCertificateDefaults() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: "certificateId")
defaults.removeObject(forKey: "certificateArn")
}
You'd also want to get the public and private keys to log out their labels to see what is currently stored in the keychain.
private func getKeys(publicKey: Bool) -> [String] {
let keyClass = publicKey ? kSecAttrKeyClassPublic : kSecAttrKeyClassPrivate
let query: [CFString: Any] = [
kSecClass: kSecClassKey,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: keyClass,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnAttributes: kCFBooleanTrue as Any
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == noErr {
guard let items = result as? [[String: Any]] else {
fatalError("Failed to get keys from keychain")
}
let keys = items.compactMap {
$0[kSecAttrApplicationTag as String] as? String
}.filter {
$0.hasPrefix(bundleIdentifier)
}
return keys
} else if status == errSecItemNotFound {
log("No \(publicKey ? "public" : "private") keys found")
return []
} else {
log("Failed to query for \(publicKey ? "public" : "private") keys")
return []
}
}
private func getPrivateKeys() -> [String] {
getKeys(publicKey: false)
}
private func getPublicKeys() -> [String] {
getKeys(publicKey: true)
}
This would give you visibility into what your app is doing at runtime. I would wrap any logging for these values in a DEBUG block so these values are not logged a Release build.
#if DEBUG
logKeychainItems()
#endif
You will want to manage the value for each certificateId
related to each BLE device you are handling. This is just a branch and not a PR at this point as this is very much a rough draft. I am not sure running multiple connections will run well with this current code base. I would test this out with many devices before committing to this approach.
Let me know if this is what will get closer to working for you and we can consider this as a feature request and move forward with a PR.
Thanks for the branch, I will check that as soon as possible. It would also be a fix to some of the problems I got recently (as I needed to re-enroll some device, and storing new certificate, when I was trying to connect to MQTT, the SDK always took the initial certificate to connect, not the new one)…
Hi again. I had a second check at your code, and it would definitely be what I need. If it's ok fro you, I'd love to see this committed in the SDK. Thanks again!
@jdanthinne Glad it works for you. I will start the process to have it reviewed so it can go into a release. This may not happen quickly since this is sensitive code used by many developers. In the meantime you could fork the repo so you can point to that version and rebase from upstream as needed.
@jdanthinne It has been approved and merged to main. A coming release will include it.
State your question
How do I remove a certificate with a specified
certificateId
added withAWSIoTManager.importIdentity(fromPKCS12Data: p12Data, passPhrase: "myPassword", certificateId: "the-certificate-id")
? There is aAWSIoTManager.deleteCertificate()
method, but I can't pass nocertificateId
…Which AWS Services are you utilizing?
AWS IoT
Environment(please complete the following information):
Device Information (please complete the following information):