Skip to content

Commit c49480c

Browse files
authored
Update to swift-certificates 0.4.0 (#6316)
* Update to swift-certificates 0.3.0 - Adjust code - Re-enable `SigningTests.testCMSCheckCertificateRevocationStatus` - Implement `SignatureProvider.extractSigningEntity` - Rework disabling of certificate expiry check - Update `OCSPPolicy` usage * Bump swift-crypto to 2.5.0; address review feedback * Bump swift-certificates to 0.4.0
1 parent bd506b7 commit c49480c

15 files changed

+451
-161
lines changed
Binary file not shown.
57 Bytes
Binary file not shown.
95 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
Binary file not shown.
Binary file not shown.
95 Bytes
Binary file not shown.
-1 Bytes
Binary file not shown.
Binary file not shown.
Binary file not shown.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,10 +751,10 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
751751
// dependency version changes here with those projects.
752752
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.2")),
753753
.package(url: "https://github.com/apple/swift-driver.git", branch: relatedDependenciesBranch),
754-
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "2.4.1")),
754+
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "2.5.0")),
755755
.package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "1.1.1")),
756756
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMinor(from: "1.0.1")),
757-
.package(url: "https://github.com/apple/swift-certificates.git", .upToNextMinor(from: "0.1.0")),
757+
.package(url: "https://github.com/apple/swift-certificates.git", .upToNextMinor(from: "0.4.0")),
758758
]
759759
} else {
760760
package.dependencies += [

Sources/PackageSigning/SignatureProvider.swift

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public enum SignatureProvider {
6464
let provider = format.provider
6565
return try await provider.extractSigningEntity(
6666
signature: signature,
67+
format: format,
6768
verifierConfiguration: verifierConfiguration
6869
)
6970
}
@@ -119,6 +120,9 @@ public enum SigningError: Error {
119120
case keyDoesNotSupportSignatureAlgorithm
120121
case signingIdentityNotSupported
121122
case unableToValidateSignature(String)
123+
case invalidSignature(String)
124+
case certificateInvalid(String)
125+
case certificateNotTrusted(SigningEntity)
122126
}
123127

124128
// MARK: - Signature formats and providers
@@ -177,6 +181,7 @@ protocol SignatureProviderProtocol {
177181

178182
func extractSigningEntity(
179183
signature: [UInt8],
184+
format: SignatureFormat,
180185
verifierConfiguration: VerifierConfiguration
181186
) async throws -> SigningEntity
182187
}
@@ -249,13 +254,6 @@ struct CMSSignatureProvider: SignatureProviderProtocol {
249254
}
250255
}
251256

252-
func extractSigningEntity(
253-
signature: [UInt8],
254-
verifierConfiguration: VerifierConfiguration
255-
) async throws -> SigningEntity {
256-
throw StringError("not implemented")
257-
}
258-
259257
func status(
260258
signature: [UInt8],
261259
content: [UInt8],
@@ -310,6 +308,66 @@ struct CMSSignatureProvider: SignatureProviderProtocol {
310308
throw SigningError.unableToValidateSignature("\(error)")
311309
}
312310
}
311+
312+
func extractSigningEntity(
313+
signature: [UInt8],
314+
format: SignatureFormat,
315+
verifierConfiguration: VerifierConfiguration
316+
) async throws -> SigningEntity {
317+
switch format {
318+
case .cms_1_0_0:
319+
do {
320+
let cmsSignature = try CMSSignature(derEncoded: signature)
321+
let signers = try cmsSignature.signers
322+
guard signers.count == 1, let signer = signers.first else {
323+
throw SigningError.invalidSignature("expected 1 signer but got \(signers.count)")
324+
}
325+
326+
let signingCertificate = signer.certificate
327+
328+
var trustRoots: [Certificate] = []
329+
if verifierConfiguration.includeDefaultTrustStore {
330+
trustRoots.append(contentsOf: CertificateStores.defaultTrustRoots)
331+
}
332+
trustRoots.append(contentsOf: try verifierConfiguration.trustedRoots.map { try Certificate($0) })
333+
334+
// Verifier uses these to build cert chain for validation
335+
// (see also notes in `status` method)
336+
var untrustedIntermediates: [Certificate] = []
337+
// WWDR intermediates are not required when signing with ADP certs,
338+
// (i.e., these intermediates may not be in the signature), hence
339+
// we include them here to ensure Verifier can build cert chain.
340+
untrustedIntermediates.append(contentsOf: Certificates.wwdrIntermediates)
341+
// For self-signed certificate, the signature should include intermediate(s).
342+
untrustedIntermediates.append(contentsOf: cmsSignature.certificates)
343+
344+
let policySet = self.buildPolicySet(configuration: verifierConfiguration, httpClient: self.httpClient)
345+
346+
var verifier = Verifier(rootCertificates: CertificateStore(trustRoots), policy: policySet)
347+
let result = await verifier.validate(
348+
leafCertificate: signingCertificate,
349+
intermediates: CertificateStore(untrustedIntermediates)
350+
)
351+
352+
switch result {
353+
case .validCertificate:
354+
return SigningEntity.from(certificate: signingCertificate)
355+
case .couldNotValidate(let validationFailures):
356+
if validationFailures.isEmpty {
357+
let signingEntity = SigningEntity.from(certificate: signingCertificate)
358+
throw SigningError.certificateNotTrusted(signingEntity)
359+
} else {
360+
throw SigningError
361+
.certificateInvalid("failures: \(validationFailures.map(\.policyFailureReason))")
362+
}
363+
}
364+
} catch let error as SigningError {
365+
throw error
366+
} catch {
367+
throw SigningError.invalidSignature("\(error)")
368+
}
369+
}
370+
}
313371
}
314372

315373
#if canImport(Security)

Sources/PackageSigning/VerifierPolicies.swift

Lines changed: 112 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,99 +16,149 @@ import struct Foundation.Date
1616
import struct Foundation.URL
1717

1818
import Basics
19-
@_implementationOnly import X509
19+
@_implementationOnly import SwiftASN1
20+
@_implementationOnly @_spi(DisableValidityCheck) import X509
2021

2122
extension SignatureProviderProtocol {
2223
func buildPolicySet(configuration: VerifierConfiguration, httpClient: HTTPClient) -> PolicySet {
23-
var policies = [VerifierPolicy]()
24-
25-
if case .enabled(let validationTime) = configuration.certificateExpiration {
26-
policies.append(RFC5280Policy(validationTime: validationTime ?? Date()))
27-
}
28-
29-
switch configuration.certificateRevocation {
30-
case .strict(let validationTime):
31-
policies.append(_OCSPVerifierPolicy(httpClient: httpClient, mode: .strict, validationTime: validationTime))
32-
case .allowSoftFail(let validationTime):
24+
var policies: [VerifierPolicy] = [
25+
_CodeSigningPolicy(),
26+
_ADPCertificatePolicy(),
27+
]
28+
29+
let now = Date()
30+
switch (configuration.certificateExpiration, configuration.certificateRevocation) {
31+
case (.enabled(let expiryValidationTime), .strict(let revocationValidationTime)):
32+
policies.append(RFC5280Policy(validationTime: expiryValidationTime ?? now))
33+
policies
34+
.append(_OCSPVerifierPolicy(
35+
failureMode: .hard,
36+
httpClient: httpClient,
37+
validationTime: revocationValidationTime ?? now
38+
))
39+
case (.enabled(let expiryValidationTime), .allowSoftFail(let revocationValidationTime)):
40+
policies.append(RFC5280Policy(validationTime: expiryValidationTime ?? now))
41+
policies
42+
.append(_OCSPVerifierPolicy(
43+
failureMode: .soft,
44+
httpClient: httpClient,
45+
validationTime: revocationValidationTime ?? now
46+
))
47+
case (.enabled(let expiryValidationTime), .disabled):
48+
policies.append(RFC5280Policy(validationTime: expiryValidationTime ?? now))
49+
case (.disabled, .strict(let revocationValidationTime)):
50+
// Always do expiry check (and before) if revocation check is enabled
51+
policies.append(RFC5280Policy(validationTime: revocationValidationTime ?? now))
3352
policies
3453
.append(_OCSPVerifierPolicy(
54+
failureMode: .hard,
3555
httpClient: httpClient,
36-
mode: .allowSoftFail,
37-
validationTime: validationTime
56+
validationTime: revocationValidationTime ?? now
3857
))
39-
case .disabled:
40-
()
58+
case (.disabled, .allowSoftFail(let revocationValidationTime)):
59+
// Always do expiry check (and before) if revocation check is enabled
60+
policies.append(RFC5280Policy(validationTime: revocationValidationTime ?? now))
61+
policies
62+
.append(_OCSPVerifierPolicy(
63+
failureMode: .soft,
64+
httpClient: httpClient,
65+
validationTime: revocationValidationTime ?? now
66+
))
67+
case (.disabled, .disabled):
68+
// We should still do basic certificate validations even if expiry check is disabled
69+
policies.append(RFC5280Policy.withValidityCheckDisabled())
4170
}
4271

4372
return PolicySet(policies: policies)
4473
}
4574
}
4675

76+
/// Policy for code signing certificates.
77+
struct _CodeSigningPolicy: VerifierPolicy {
78+
let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [
79+
ASN1ObjectIdentifier.X509ExtensionID.keyUsage,
80+
ASN1ObjectIdentifier.X509ExtensionID.extendedKeyUsage,
81+
]
82+
83+
func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) async -> PolicyEvaluationResult {
84+
let isCodeSigning = (
85+
try? chain.leaf.extensions.extendedKeyUsage?.contains(ExtendedKeyUsage.Usage.codeSigning)
86+
) ??
87+
false
88+
guard isCodeSigning else {
89+
return .failsToMeetPolicy(reason: "Certificate \(chain.leaf) does not have code signing extended key usage")
90+
}
91+
return .meetsPolicy
92+
}
93+
}
94+
95+
/// Policy for ADP certificates.
96+
struct _ADPCertificatePolicy: VerifierPolicy {
97+
/// Include custom marker extensions (which can be critical) so they would not
98+
/// be considered unhandled and cause certificate chain validation to fail.
99+
let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = Self.swiftPackageMarkers
100+
+ Self.developmentMarkers
101+
102+
// Marker extensions for Swift Package certificate
103+
private static let swiftPackageMarkers: [ASN1ObjectIdentifier] = [
104+
// This is not a critical extension but including it just in case
105+
ASN1ObjectIdentifier.NameAttributes.adpSwiftPackageMarker,
106+
]
107+
108+
// Marker extensions for Development certificate (included for testing)
109+
private static let developmentMarkers: [ASN1ObjectIdentifier] = [
110+
[1, 2, 840, 113_635, 100, 6, 1, 2],
111+
[1, 2, 840, 113_635, 100, 6, 1, 12],
112+
]
113+
114+
func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) async -> PolicyEvaluationResult {
115+
// Not policing anything here. This policy is mainly for
116+
// listing marker extensions to prevent chain validation
117+
// from failing prematurely.
118+
.meetsPolicy
119+
}
120+
}
121+
47122
struct _OCSPVerifierPolicy: VerifierPolicy {
48123
private static let cacheTTL: DispatchTimeInterval = .seconds(5 * 60)
49124
private let cache = ThreadSafeKeyValueStore<
50125
UnverifiedCertificateChain,
51126
(result: PolicyEvaluationResult, expires: DispatchTime)
52127
>()
53128

54-
private let underlying: OCSPVerifierPolicy<_OCSPRequester>
55-
private let mode: Mode
56-
private let validationTime: Date
57-
58-
init(httpClient: HTTPClient, mode: Mode, validationTime: Date?) {
59-
self.underlying = OCSPVerifierPolicy(requester: _OCSPRequester(httpClient: httpClient))
60-
self.mode = mode
61-
self.validationTime = validationTime ?? Date()
129+
private var underlying: OCSPVerifierPolicy<_OCSPRequester>
130+
131+
let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = []
132+
133+
/// Initializes an `_OCSPVerifierPolicy` that caches its results.
134+
///
135+
/// - Parameters:
136+
/// - failureMode: `OCSPFailureMode` that defines policy failure in event of failure.
137+
/// Possible values are `hard` (OCSP request failure and unknown status
138+
/// not allowed) or `soft` (OCSP request failure and unknown status allowed).
139+
/// - httpClient: `HTTPClient` that backs`_OCSPRequester` for making OCSP requests.
140+
/// - validationTime: The time used to decide if the OCSP request is relatively recent. It is
141+
/// considered a failure if the request is too old.
142+
init(failureMode: OCSPFailureMode, httpClient: HTTPClient, validationTime: Date) {
143+
self.underlying = OCSPVerifierPolicy(
144+
failureMode: failureMode,
145+
requester: _OCSPRequester(httpClient: httpClient),
146+
validationTime: validationTime
147+
)
62148
}
63149

64-
func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) async -> PolicyEvaluationResult {
65-
// Check for expiration of the leaf before revocation
66-
let leaf = chain.leaf
67-
if leaf.notValidBefore > leaf.notValidAfter {
68-
return .failsToMeetPolicy(
69-
reason: "OCSPVerifierPolicy: leaf certificate \(leaf) has invalid expiry, notValidAfter is earlier than notValidBefore"
70-
)
71-
}
72-
if self.validationTime < leaf.notValidBefore {
73-
return .failsToMeetPolicy(reason: "OCSPVerifierPolicy: leaf certificate \(leaf) is not yet valid")
74-
}
75-
if self.validationTime > leaf.notValidAfter {
76-
return .failsToMeetPolicy(reason: "OCSPVerifierPolicy: leaf certificate \(leaf) has expired")
77-
}
78-
150+
mutating func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) async -> PolicyEvaluationResult {
79151
// Look for cached result
80152
if let cached = self.cache[chain], cached.expires < .now() {
81153
return cached.result
82154
}
83155

84156
// This makes HTTP requests
85157
let result = await self.underlying.chainMeetsPolicyRequirements(chain: chain)
86-
let actualResult: PolicyEvaluationResult
87-
switch result {
88-
case .meetsPolicy:
89-
actualResult = result
90-
case .failsToMeetPolicy(let reason):
91-
switch self.mode {
92-
case .strict:
93-
actualResult = result
94-
case .allowSoftFail:
95-
// Allow 'unknown' status and failed OCSP request in this mode
96-
if reason.lowercased().contains("revoked through ocsp") {
97-
actualResult = result
98-
} else {
99-
actualResult = .meetsPolicy
100-
}
101-
}
102-
}
103158

104159
// Save result to cache
105-
self.cache[chain] = (result: actualResult, expires: .now() + Self.cacheTTL)
106-
return actualResult
107-
}
108-
109-
enum Mode {
110-
case strict
111-
case allowSoftFail
160+
self.cache[chain] = (result: result, expires: .now() + Self.cacheTTL)
161+
return result
112162
}
113163
}
114164

Tests/PackageSigningTests/SigningIdentityTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ final class SigningIdentityTests: XCTestCase {
3030
let certificate = try Certificate(certificateBytes)
3131

3232
let subject = certificate.subject
33-
XCTAssertEqual("Test (EC)", subject.commonName)
34-
XCTAssertEqual("Test (EC)", subject.organizationalUnitName)
35-
XCTAssertEqual("Test (EC)", subject.organizationName)
33+
XCTAssertEqual("Test (EC) leaf", subject.commonName)
34+
XCTAssertEqual("Test (EC) org unit", subject.organizationalUnitName)
35+
XCTAssertEqual("Test (EC) org", subject.organizationName)
3636

3737
let privateKeyBytes = try readFileContents(
3838
in: fixturePath,
@@ -63,9 +63,9 @@ final class SigningIdentityTests: XCTestCase {
6363
let certificate = try Certificate(certificateBytes)
6464

6565
let subject = certificate.subject
66-
XCTAssertEqual("Test (RSA)", subject.commonName)
67-
XCTAssertEqual("Test (RSA)", subject.organizationalUnitName)
68-
XCTAssertEqual("Test (RSA)", subject.organizationName)
66+
XCTAssertEqual("Test (RSA) leaf", subject.commonName)
67+
XCTAssertEqual("Test (RSA) org unit", subject.organizationalUnitName)
68+
XCTAssertEqual("Test (RSA) org", subject.organizationName)
6969

7070
let privateKeyBytes = try readFileContents(
7171
in: fixturePath,

0 commit comments

Comments
 (0)