Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support to CRT HTTP Client for providing custom trust and key store #672

Merged
merged 35 commits into from
Apr 25, 2024

Conversation

dayaffe
Copy link
Contributor

@dayaffe dayaffe commented Feb 26, 2024

Issue #

#1110

Description of changes

Example Usage with STSClient on Apple devices:

let urlSessionTLSOptions = URLSessionTLSOptions(
    certificateFile: "localhost", // .cer
    keyStoreName: "certificate", // .p12
    keyStorePassword: "test"
)

let client = try await STSClient(
    config: .init(
        httpClientConfiguration: .init(
            tlsOptions: .init(urlSessionTLSOptions: urlSessionTLSOptions)
        )
    )
)

Example Usage with STSClient on Linux:

let crtTLSOptions = CRTClientTLSOptions(
    certificatePath: "/path/",
    certificateFilename: "trustStoreFile",
    keyStoreFilepath: "/path/file",
    keyStorePassword: "password"
)

let client = try await STSClient(
    config: .init(
        httpClientConfiguration: .init(
            tlsOptions: .init(crtTLSOptions: crtTLSOptions)
        )
    )
)

Directions for testing URLSession changes:

Step 1: Downgrade Openssl on mac from 3.x to 1.1. Mac's Keychain Access and XCode are not compatible with the latest openssl encryption algorithms

Step 2. Generate a key (.pem) and a certificate (.cer) and then include both in a keystore (.p12)

Step 3. Include both the certificate file (.cer) and keystore (.p12) in the project's bundle

Step 4. Create a local test server. You can use mine as an exmaple and run it using node server.js:

const express = require('express');
const https = require('https');
const fs = require('fs');

const app = express();

app.get('/', (req, res) => {
  res.send('Hello, Secure World!');
});

https.createServer({
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('certificate.pem'),
  passphrase: "test"
}, app)
.listen(3000, () => {
  console.log('Listening on https://localhost:3000');
});

Step 5. Run XCode proj that hits the server. Here is my example code:

let urlSessionTLSOptions = URLSessionTLSOptions(
    certificateFile: "localhost", // .cer
    keyStoreName: "certificate", // .p12
    keyStorePassword: "test"
)

// Create an instance of our URLSessionHTTPClient
let urlSessionClient = URLSessionHTTPClient(httpClientConfiguration: .init(urlSessionTLSOptions: urlSessionTLSOptions))

// Create an SdkHttpRequest
let sdkHttpRequest = SdkHttpRequest(
    method: .get,
    endpoint: Endpoint(host: "localhost", path: "/", port: 3000, protocolType: .https),
    body: .noStream
)
// Send the request
do {
    let response = try await urlSessionClient.send(request: sdkHttpRequest)
    print("Response received: \(response)")
} catch {
    print("Failed to make request: \(error)")
}

Scope

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@dayaffe dayaffe requested a review from sichanyoo March 26, 2024 16:32
Copy link
Contributor

@sichanyoo sichanyoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestions

Comment on lines 38 to 48
/// Custom TLS configuration for HTTPS connections.
///
/// Enables specifying client certificates and trust stores for secure communication.
/// Defaults to system's TLS settings if `nil`.
public var tlsContext: TLSContext?

/// Custom TLS configuration for HTTPS connections with URLSession client.
///
/// Enables specifying client certificates and trust stores for secure communication.
/// Defaults to system's TLS settings if `nil`.
public var urlSessionTLSOptions: URLSessionTLSOptions?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the public API, Is it possible to have one TLSContext class and create either a URLSessionTLSOptions or a TLSContext internally from it?

Copy link

@waahm7 waahm7 Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to just expose a wrapper to TLSContextOptions and create a TLSContext from that. No one should be passing server mode to this TLSContext.init function.

Comment on lines 19 to 23
/// Information is provided to use custom trust store
public var useSelfSignedCertificate: Bool

/// Information is provided to use custom key store
public var useProvidedKeystore: Bool
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are derived variables, but someone can modify them after the init function. Is that intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will change so they cant modify

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should any of these properties be modifiable? Generally configuration objects are immutable once created.

@dayaffe dayaffe requested a review from jbelkins March 29, 2024 16:09

let options = [kSecImportExportPassphrase as String: password] as CFDictionary
var items: CFArray?
let status = SecPKCS12Import(p12Data as CFData, options, &items)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecPKCS12Import: The latest aws-crt-swift disabled the API for iOS, tvOS, and watchOS, as we dont really have a way to test the API on those platforms. Would you have a test plan to the feature with pkcs12 key on the platforms? Maybe I could add the tests to CRT as well and enable the API there.

awslabs/aws-crt-swift#246 <= PR that disabled SecPKCS12Import

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree there's no easy way to test on those platforms natively (would work via simulator for dev testing), I manually generated the p12 file using a combination of keychain access and openssl 1.1

Copy link
Contributor

@jbelkins jbelkins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See individual comments. Also it seems that we have some HTTP client config that is specific to only one client. Short of clearly documenting what affects what, is there a way that we can unify settings so that one setting affects both clients?

if try serverTrust.evaluateAllowing(rootCertificates: [customRoot]) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
logger.debug("Trust evaluation failed, cancelling authentication challenge.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be error log level?

Comment on lines 19 to 23
/// Information is provided to use custom trust store
public var useSelfSignedCertificate: Bool

/// Information is provided to use custom key store
public var useProvidedKeystore: Bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should any of these properties be modifiable? Generally configuration objects are immutable once created.

var error: CFError?
let evaluationSucceeded = SecTrustEvaluateWithError(self, &error)
guard evaluationSucceeded else {
print(error.debugDescription)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eliminate the print statement, or change it to a log statement

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should error be unwrapped here? Seems odd to log & throw an error you're not sure is there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eliminating print statement, the error has to be there as part of description of SecTrustEvaluateWithError. If result is false, error will be populated with reason, if true error will be null

// Set the custom root certificates as trusted anchors.
let status = SecTrustSetAnchorCertificates(self, rootCertificates as CFArray)
guard status == errSecSuccess else {
throw TrustEvaluationError.evaluationFailed(error: nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual error here is that setting anchor certificates failed? Should the error we throw say that more explicitly?

@@ -330,4 +425,71 @@ public enum URLSessionHTTPClientError: Error {
case unresumedConnection
}

extension Bundle {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move these extensions to a separate source file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URLSessionHTTPClient is getting big

self.crtTLSContext = nil

// Set CRT client tls options with .client mode
if let _crtTLSOptions = crtTLSOptions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why unwrap to a local var with an underscore?

if let crtTLSOptions {

will unwrap the optional then "shadow" it inside the if.

@@ -39,13 +39,15 @@ public class CRTClientEngine: HTTPClient {
private var http2ConnectionPools: [ConnectionPoolID: HTTP2StreamManager] = [:]
private let sharedDefaultIO = SDKDefaultIO.shared
private let connectTimeoutMs: UInt32?
private let tlsContext: TLSContext?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is TLSContext a CRT type? If so, let's not expose it directly to customers. CRT is an "implementation detail" of the AWS SDK for Swift that should remain concealed from customers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will change this to CRTClientTLSOptions type that I created and provide a resolveContext() method on it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also rename that if we want to get rid of CRT from it

// - The `as? SecIdentity` casting causes a compiler error on Apple platforms as the cast is guaranteed.
// - Directly returning `identity` works on Linux but not on macOS due to strict type expectations.
// SwiftLint is temporarily disabled for the next line to allow a force cast, acknowledging the platform-specific behavior.
// swiftlint:disable:next force_cast
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually Swift Foundation will eliminate these differences, but for now we're stuck with swift-corelibs-foundation on Linux which is not always faithful to the original.

self.keyStorePassword = keyStorePassword

self.useSelfSignedCertificate = certificateFile != nil
self.useProvidedKeystore = keyStoreName != nil && keyStorePassword != nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can this be a computed property instead of stored?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with the property above

@dayaffe dayaffe requested a review from jbelkins April 17, 2024 21:26
Copy link
Contributor

@jbelkins jbelkins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the swiftlint issue before merging

@dayaffe dayaffe merged commit 84f036d into main Apr 25, 2024
12 checks passed
@dayaffe dayaffe deleted the day/tls-key-store-config branch April 25, 2024 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants