aws-amplify / aws-sdk-ios

AWS SDK for iOS. For more information, see our web site:
https://aws-amplify.github.io/docs
Other
1.68k stars 879 forks source link

IoTDataManager Connection with x509 and Private Key provided at run-time #5300

Closed michael-aiphone closed 3 months ago

michael-aiphone commented 4 months ago

State your question I am new to the AWS ecosystem, this framework and IoT communication in general so please forgive me if I confuse some terminology here.

After following the IoT tutorials, I was able to establish communication with a test AWS IoT Thing using a bundled p12 cert via AWSIoTManager.importIdentity(...) and AWSIoTDataManager.connect(withClientId: cleanSession: certificateId).

However, due to some quirky legacy APIs that my client is working with I will be getting the certificate(x509 format) information and private key at runtime from the server. Is there some convenient way I can configure my AWSIoTManager/AWSIoTDataManager to handle this need without having to resort to something like finding a good copy of openSSL and converting the format on the fly or...?

Which AWS Services are you utilizing? AWS IoT Provide code snippets (if applicable)

Environment(please complete the following information):

lawmicha commented 4 months ago

Hi @michael-aiphone, thanks for opening this question. I'm not too familiar with the IoT APIs myself but if you could provide us more details as to what you'd expect it to look like when using the AWSIoTManager/AWSIoTDataManager, such as what inputs you are looking to provide it, what it should do, code examples, we may be able to provide more guidance on the topic.

michael-aiphone commented 4 months ago

Thank you for getting back to me on this so swiftly 😁.

This is the crux of what I have currently:

App Delegate

// fake info so that .defaultServiceConfiguration is not nil when accessed later
let credentials = AWSStaticCredentialsProvider(accessKey: "fake1", secretKey: "fake2")
let configuration = AWSServiceConfiguration(region: .USEast2, credentialsProvider: credentials)
AWSServiceManager.default().defaultServiceConfiguration = configuration

View Controller

// configure IoTDataManager
let iotEndpoint = AWSEndpoint(urlString: "https://abcdef12345-ats.iot.us-east-2.amazonaws.com")!
let credentialsProvider = AWSServiceManager.default().defaultServiceConfiguration.credentialsProvider
let iotDataConfiguration = AWSServiceConfiguration(region: .USEast2, endpoint: iotEndpoint, credentialsProvider: credentialsProvider)!

AWSIoTDataManager.register(with: iotDataConfiguration, forKey: "ios-sdk")
self.iotDataManager = AWSIoTDataManager(forKey: "ios-sdk")
// connect with bundled cert
let paths = Bundle.main.paths(forResourcesOfType: "p12", inDirectory: nil)
let uuid = UUID().uuidString
guard let certId = paths.first else { fatalError("missing p12 cert in bundle") }
let data = try! Data(contentsOf: URL(filePath: certId))

DispatchQueue.global(qos: .background).async {
      if AWSIoTManager.importIdentity(fromPKCS12Data: data, passPhrase: "abcdef", certificateId: certId) {
             self.iotDataManager.connect(withClientId: uuid, cleanSession: true, certificateId: certId, statusCallback: self.mqttStatusEventCallback(_:))
       }
}

A nice to have for me would be some function on the AWSIoTManager that imports an identity via a x509 cert and private key. This would still allow me to use the same .connect method on the iotDataManager. Something like:

AWSIoTManager.importIdentity(fromX509Cert: String/Data, privateKey: String, certId: String)...

But maybe there is already a way to do this that I cannot seem to find in the documentation, I'm just not sure. 😅

lawmicha commented 4 months ago

Hi @michael-aiphone, thanks for the additional details. I found this sample https://github.com/awslabs/aws-sdk-ios-samples/blob/main/IoT-Sample/Swift/README.md seems to reference some pre-build time steps to add a cert

Add the IoT certificate (IoT identity) to the Xcode project

Follow the instructions below to create the certificate
Place the PKCS #12 archive (.p12) in the same directory as Info.plist
Add it to the same group in Xcode as Info.plist
Select the build targets which will use this IoT identity

I'm not too sure all the details at the moment, but this does seem to imply that AWSIoTManager may be reading the cert from the main Bundle.

Are you currently implementing or have implemented a .importIdentity(fromPKCS12Data:passPhrase:certificateId:) method in your app? We can evaluate consolidating your logic with the existing (if we confirm AWSIoTManager does infact read from the Bundle), and moving that logic over to the AWSIoTManager class as a new API that explicitly takes Data type

michael-aiphone commented 4 months ago

To clarify, the code above is the solution where the identity is included in bundle. The steps I did are outlined as follows.

What I can currently do and confirmed that it works: (and what the tutorials show)

  1. Create a test environment for AWS IoT
  2. Create an IoT thing
  3. Create and Attach a certificate to the IoT thing.
  4. Create and Attach a policy to the certificate.
  5. Download the certificate (comes in X509 format)
  6. In Terminal use open SSL to convert that X509 to p12.
  7. Add the created p12 to app bundle.
  8. Use the provided .importIdentity(fromPKCS12Data: passPhrase: certificateId:) to get the identity into KeyChain with a recognizable ID.
  9. Use the provided .connect(withClientId: cleanSession: certificateId: statusCallback:) to pass authentication.

The issue I am trying to solve:

  1. The real AWS IoT environment I need to connect to is already established.
  2. An IoT thing in that environment is created when the mobile app reaches the end of a sign up flow.
  3. A backend server will send the certificate and private key information (as Strings) to the mobile app after sign up (while the app is still running)
  4. Therefore I cannot use the .importIdentity(fromPKCS12Data: passPhrase: certificateId:) function because those credentials are not p12, nor are they included in the bundle at compile/build time. Additionally, they are the default format that AWS provides -- X509 (PEM RSA)

Summary

I am wondering if there is a way that I can manually create an identity from those X509 credentials and add it to KeyChain and then call the .connect() method (since the connect method requires cert be in KeyChain). If this is possible, what would I use for the certId? --(in the provided p12 import the cert id was relative path) -- Or would it be possible to add a new method to the library that will create and add an identity to the keychain from that format?

Another alternative would be a different .connect() method that allows me to provide those credentials at runtime.

For further context, my Android colleague is using the SDK for Java and says they include a method for Android to provide the credentials like the following. In this snippet we can see they are using a builder pattern so admittedly it a bit different but it does allow for providing the credentials from X509 string data. In essence, I am looking to replicate this functionality on the iOS side in whatever way is possible/best with the stipulation that I cannot include the certificate information in the app bundle.

Android doing what I want my iOS to do

override fun connect(clientEndpoint: String, certificateData: String, keyData: String): Result<Nothing?> {
    val builder = AwsIotMqtt5ClientBuilder.newDirectMqttBuilderWithMtlsFromMemory(clientEndpoint, certificateData, keyData)
        .withLifeCycleEvents(AWSLifecycleEvents())
        .withPublishEvents(AWSPublishEvents())

    try { client = builder.build() }
    catch (e: Exception) {
        Log.d("AWSClientManager","Failed to build client, likely due to authentication issues.")
        return Result.failure(Exception("Failed to build client, likely due to authentication issues."))
    }
    client?.start()

    return Result.success(null)
}

Thanks for reading this short novel :)

michael-aiphone commented 3 months ago

@lawmicha I just wanted to follow up on this and see if you might be able to put me in contact with someone at AWS on the IoT Team. Would that be possible? Thank you for your time.

5d commented 3 months ago

Hi @michael-aiphone ,

I am not familiar with AWS IoT, but upon reviewing the delaration of AWSIoTManager, I noticed there is a method called createKeysAndCertificateFromCsr that dynamically creates certificates. I also found documentation on this method. Have you tried this solution?

jdanthinne commented 3 months ago

@michael-aiphone I had the same issue two years ago, and without any solution, I ended up importing OpenSSL in my project, and using the code below to convert the certificate and the private key into PKCS12 data, which I can import into AWSIoTManager.importIdentity(fromPKCS12Data:passPhrase:certificateId:).

With OpenSSL 1.x, everything was working fine, but unfortunately, I need to use OpenSSL 3.x now (min required by Apple now), and the generated PKCS12 is not accepted by the importIdentity method anymore, related to the new algorithms used in this new version of OpenSSL… and I'm stuck 😬.

func pkcs12(fromPem pemCertificate: String,
                       withPrivateKey pemPrivateKey: String,
                       p12Password: String = "") throws -> NSData {
        // Set OpenSSL parameters
        OpenSSL_add_all_algorithms()

        // Read certificate and private key
        let x509CertificateBuffer = BIO_new_mem_buf(pemCertificate, Int32(pemCertificate.count))
        let x509Certificate = PEM_read_bio_X509(x509CertificateBuffer, nil, nil, nil)

        let privateKeyBuffer = BIO_new_mem_buf(pemPrivateKey, Int32(pemPrivateKey.count))
        let privateKey = PEM_read_bio_PrivateKey(privateKeyBuffer, nil, nil, nil)

        defer {
            BIO_free(x509CertificateBuffer)
            BIO_free(privateKeyBuffer)
            X509_free(x509Certificate)
        }

        // Check if private key matches certificate
        guard X509_check_private_key(x509Certificate, privateKey) == 1 else {
            throw X509Error.privateKeyDoesNotMatchCertificate
        }

        // Generate PKCS12
        guard let p12 = PKCS12_create(
            p12Password,
            "SSL Certificate",
            privateKey,
            x509Certificate,
            nil,
            0, // nid_key Key encryption algorithm (triple DES encryption in OpenSSL 1.1, AES in OpenSSL 3.1)
            0, // nid_cert Certicate encryption algorithm (40 bit RC2 in OpenSSL 1.1, AES in OpenSSL 3.1)
            0, // iter Encryption algorithm iteration count (PKCS12_DEFAULT_ITER in Open SSL 1.1, ? in OpenSSL 3.1)
            PKCS12_DEFAULT_ITER, // mac_iter MAC iteration count (1 in OpenSSL 1.1, PKCS12_DEFAULT_ITER in OpenSSL 3.1)
            0  // keytype Type of key
        ) else {
            ERR_print_errors_fp(stderr)
            throw X509Error.cannotCreateP12Keystore
        }

        defer {
            PKCS12_free(p12)
        }

        // Save P12 keystore
        let fileManager = FileManager.default
        let tempFilePath = fileManager
            .temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
            .path
        fileManager.createFile(atPath: tempFilePath, contents: nil, attributes: nil)
        guard let fileHandle = FileHandle(forWritingAtPath: tempFilePath) else {
            NSLog("Cannot open file handle: \(tempFilePath)")
            throw X509Error.cannotOpenFileHandles
        }
        let p12File = fdopen(fileHandle.fileDescriptor, "w")
        i2d_PKCS12_fp(p12File, p12)
        fclose(p12File)
        fileHandle.closeFile()

        // Read P12 Data
        guard let p12Data = NSData(contentsOfFile: tempFilePath) else {
            throw X509Error.cannotReadP12Certificate
        }

        // Remove temporary file
        try? FileManager.default.removeItem(atPath: tempFilePath)

        return p12Data
    }
michael-aiphone commented 3 months ago

@5d Thanks for offering the help. Unfortunately that method is not quite what I need.

@jdanthinne Thank you for your answer. Unfortunate to hear that it is no longer an option with openSSL 3.x. I suppose I will have to try out some other libraries to meet my use case.

jdanthinne commented 3 months ago

@michael-aiphone FYI. I'm still in active process of finding a solution with OpenSLL 1.x that could be signed to please Apple, or enabling the "legacy" mode with OpenSSL 3.x. I'll keep you informed of my findings.

michael-aiphone commented 3 months ago

@jdanthinne I appreciate that, thank you! And best of luck on discovering the workaround! 🤞

jdanthinne commented 3 months ago

@michael-aiphone I ended up cloning the OpenSSL SDK, and reverting to the latest 1.x version, signing the xcframework locally, and my code above is now working fine, importIdentity don't reject my certificate anymore and MQTT connection seems fine with that as well. I'm talking with the OpenSSL SDK maintainer to see if an alternate 1.x branch could be made official…

michael-aiphone commented 3 months ago

@jdanthinne Which OpenSSL SDK did you end up using? I tried going down the rabbit hole of creating an identity from the X509 but got stuck turning the private key into an acceptable format for use as a SecKey, so I think I better just go with the OpenSSL approach.

jdanthinne commented 3 months ago

The best one 😀: https://github.com/krzyzanowskim/OpenSSL. Go for the latest 1.x release, which my code relies on.