Skip to content

Add SPKI certificate pinning support#910

Open
o-nnerb wants to merge 33 commits into
swift-server:mainfrom
o-nnerb:main
Open

Add SPKI certificate pinning support#910
o-nnerb wants to merge 33 commits into
swift-server:mainfrom
o-nnerb:main

Conversation

@o-nnerb

@o-nnerb o-nnerb commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

This PR introduces SPKI-based certificate pinning to AsyncHTTPClient to mitigate MITM attacks and compromised CAs.

Highlights:

  • Implements .strict and .audit modes with multi-algorithm hashing (SHA-256/384/512) and constant-time comparison.
  • Aligned with OWASP/NIST security standards, featuring rotation safety warnings and explicit error handling.
  • Requirement: Requires OpenSSL/BoringSSL backend (Network.framework connections ignore pinning configuration).

o-nnerb and others added 30 commits February 1, 2026 18:37
@o-nnerb

o-nnerb commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

@czechboy0 @FranzBusch can you review this?

@FranzBusch FranzBusch requested review from Lukasa and glbrntt June 23, 2026 12:41

@glbrntt glbrntt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks! This mostly looks good but I left some feedback inline which needs addressing.

//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's 2026

Suggested change
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Copyright (c) 2026 Apple Inc. and the AsyncHTTPClient project authors

public var tlsConfiguration: TLSConfiguration?

/// Optional SPKI pinning configuration for TLS certificate validation.
public var tlsPinning: SPKIPinningConfiguration?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I noted in the description that the Network.framework backend ignores this. I think it'd be better to validate the configuration and throw an error if pinning is enabled and Network.framework is being used, otherwise a user may not realize that they aren't actually using pinning. This behavior should also be documented. Same goes for using NIOSSL with unsupported platforms (i.e. macOS < 10.15, however unlikely that is)

/// are rejected early using public knowledge (algorithm-determined digest size),
/// which cannot leak secret information.
internal func constantTimeAnyMatch(_ target: Data, _ candidates: [SPKIHash]) -> Bool {
guard !candidates.isEmpty else { return false }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: if X { ... } is typically more readable than guard !X else { ... }

Suggested change
guard !candidates.isEmpty else { return false }
if candidates.isEmpty { return false }

Comment on lines +208 to +214
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.rawValue == rhs.rawValue
}

public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are these necessary? The compiler should be able to synthesize them.

/// Use in production environments where security is paramount.
public static let strict = SPKIPinningPolicy(rawValue: .strict)

public var description: String {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did you want this type to conform to CustomStringConvertible?

}

func testSPKIPinning_ValidPin_AllowsConnection() {
XCTAsyncTest {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think XCTAsyncTest is required anymore, XCTest has had solid async support for long enough now.

Comment on lines +51 to +55
/// - Throws: `HTTPClientError.invalidDigestLength` if length doesn't match algorithm.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public init<Algorithm: HashFunction>(algorithm: Algorithm.Type, base64: String) throws {
guard let data = Data(base64Encoded: base64) else {
throw HTTPClientError.invalidDigestLength

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be a separate error case?

//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

2026

}
}

func testSPKIPinning_ValidPin_AllowsConnection() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you add tests for http/1.1 as well?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There's also a lot of duplicated code across these four tests, can you refactor to have a test function which varies the pin kind (valid/invalid) and policy?

//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

2026

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.

2 participants