Skip to content

Commit 147d854

Browse files
dnadobaLukasa
andauthored
Policy Builder (#79)
* experiment with policy builder * convert tests to use new policy builder * Add AnyPolicy * deprecate PolicySet * revert swift tools version * code review * run ./scripts/update_cmakelists.sh * add tests * Fix Swift 5.7 * add documentation --------- Co-authored-by: Cory Benfield <lukasa@apple.com>
1 parent 922fdbd commit 147d854

12 files changed

+843
-270
lines changed

Sources/X509/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ add_library(X509
8181
"SEC1PrivateKey.swift"
8282
"Signature.swift"
8383
"SignatureAlgorithm.swift"
84+
"Verifier/AnyPolicy.swift"
8485
"Verifier/CertificateStore.swift"
86+
"Verifier/PolicyBuilder.swift"
8587
"Verifier/RFC5280/BasicConstraintsPolicy.swift"
8688
"Verifier/RFC5280/DNSNames.swift"
8789
"Verifier/RFC5280/DirectoryNames.swift"

Sources/X509/CryptographicMessageSyntax/CMSOperations.swift

+23-19
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,16 @@ public enum CMS {
9393
signatureBytes: SignatureBytes,
9494
additionalIntermediateCertificates: [Certificate] = [],
9595
trustRoots: CertificateStore,
96-
policy: PolicySet
97-
) async -> SignatureVerificationResult {
96+
@PolicyBuilder policy: () throws -> some VerifierPolicy
97+
) async rethrows -> SignatureVerificationResult {
98+
let signedData: CMSSignedData
99+
let signingCert: Certificate
98100
do {
99101
let parsedSignature = try CMSContentInfo(derEncoded: ArraySlice(signatureBytes))
100-
guard let signedData = try parsedSignature.signedData else {
102+
guard let _signedData = try parsedSignature.signedData else {
101103
return .failure(.init(invalidCMSBlockReason: "Unable to parse signed data"))
102104
}
105+
signedData = _signedData
103106

104107
// We have a bunch of very specific requirements here: in particular, we need to have only one signature. We also only want
105108
// to tolerate v1 signatures and detached signatures.
@@ -134,9 +137,10 @@ public enum CMS {
134137

135138
// Ok, now we need to find the signer. We expect to find them in the list of certificates provided
136139
// in the signature.
137-
guard let signingCert = try signedData.certificates?.certificate(signerInfo: signer) else {
140+
guard let _signingCert = try signedData.certificates?.certificate(signerInfo: signer) else {
138141
return .failure(.init(invalidCMSBlockReason: "Unable to locate signing certificate"))
139142
}
143+
signingCert = _signingCert
140144

141145
// Ok at this point we've done the cheap stuff and we're fairly confident we have the entity who should have
142146
// done the signing. Our next step is to confirm that they did in fact sign the data. For that we have to compute
@@ -145,24 +149,24 @@ public enum CMS {
145149
guard signingCert.publicKey.isValidSignature(signature, for: dataBytes, signatureAlgorithm: signatureAlgorithm) else {
146150
return .failure(.init(invalidCMSBlockReason: "Invalid signature from signing certificate: \(signingCert)"))
147151
}
148-
149-
// Ok, the signature was signed by the private key associated with this cert. Now we need to validate the certificate.
150-
// This force-unwrap is safe: we know there are certificates because we've located at least one certificate from this set!
151-
var untrustedIntermediates = CertificateStore(signedData.certificates!)
152-
untrustedIntermediates.insert(contentsOf: additionalIntermediateCertificates)
153-
154-
var verifier = Verifier(rootCertificates: trustRoots, policy: policy)
155-
let result = await verifier.validate(leafCertificate: signingCert, intermediates: untrustedIntermediates)
156-
157-
switch result {
158-
case .validCertificate:
159-
return .success(.init(signer: signingCert))
160-
case .couldNotValidate(let validationFailures):
161-
return .failure(.unableToValidateSigner(.init(validationFailures: validationFailures, signer: signingCert)))
162-
}
163152
} catch {
164153
return .failure(.invalidCMSBlock(.init(reason: String(describing: error))))
165154
}
155+
156+
// Ok, the signature was signed by the private key associated with this cert. Now we need to validate the certificate.
157+
// This force-unwrap is safe: we know there are certificates because we've located at least one certificate from this set!
158+
var untrustedIntermediates = CertificateStore(signedData.certificates!)
159+
untrustedIntermediates.insert(contentsOf: additionalIntermediateCertificates)
160+
161+
var verifier = try Verifier(rootCertificates: trustRoots, policy: policy)
162+
let result = await verifier.validate(leafCertificate: signingCert, intermediates: untrustedIntermediates)
163+
164+
switch result {
165+
case .validCertificate:
166+
return .success(.init(signer: signingCert))
167+
case .couldNotValidate(let validationFailures):
168+
return .failure(.unableToValidateSigner(.init(validationFailures: validationFailures, signer: signingCert)))
169+
}
166170
}
167171

168172
@_spi(CMS)

Sources/X509/OCSP/OCSPPolicy.swift

+5-6
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,11 @@ extension OCSPVerifierPolicy.Storage {
401401
}
402402

403403
var verifier = Verifier(
404-
rootCertificates: CertificateStore([issuer]),
405-
policy: PolicySet(policies: [
406-
OCSPResponderSigningPolicy(issuer: issuer),
407-
RFC5280Policy(validationTime: validationTime),
408-
])
409-
)
404+
rootCertificates: CertificateStore([issuer])
405+
) {
406+
OCSPResponderSigningPolicy(issuer: issuer)
407+
RFC5280Policy(validationTime: validationTime)
408+
}
410409

411410
let validationResult = await verifier.validate(leafCertificate: leafCertificate, intermediates: CertificateStore())
412411

Sources/X509/Verifier/AnyPolicy.swift

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCertificates open source project
4+
//
5+
// Copyright (c) 2022-2023 Apple Inc. and the SwiftCertificates project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import SwiftASN1
15+
16+
17+
/// ``AnyPolicy`` can be used to erase the concrete type of some ``VerifierPolicy``.
18+
/// Only use ``AnyPolicy`` if type erasure is necessary.
19+
/// Instead try to use conditional inclusion of different policies using ``PolicyBuilder``.
20+
///
21+
/// Use ``AnyPolicy`` at the top level during construction of a ``Verifier`` to get a ``Verifier`` of type `Verifier<AnyPolicy>` e.g.:
22+
/// ```swift
23+
/// let verifier = Verifier(rootCertificates: CertificateStore()) {
24+
/// AnyPolicy {
25+
/// RFC5280Policy(validationTime: Date())
26+
/// }
27+
/// }
28+
/// ```
29+
public struct AnyPolicy: VerifierPolicy {
30+
@usableFromInline
31+
var policy: any VerifierPolicy
32+
33+
34+
@inlinable
35+
/// Erases the type of some ``VerifierPolicy`` to ``AnyPolicy``.
36+
/// - Parameter policy: the concrete ``VerifierPolicy``
37+
public init(_ policy: some VerifierPolicy) {
38+
self.policy = policy
39+
}
40+
41+
/// Erases the type of some ``VerifierPolicy`` to ``AnyPolicy``.
42+
/// - Parameter policy: the ``VerifierPolicy`` constructed using the ``PolicyBuilder`` DSL.
43+
@inlinable
44+
public init(@PolicyBuilder makePolicy: () throws -> some VerifierPolicy) rethrows {
45+
self.init(try makePolicy())
46+
}
47+
48+
@inlinable
49+
public var verifyingCriticalExtensions: [SwiftASN1.ASN1ObjectIdentifier] {
50+
policy.verifyingCriticalExtensions
51+
}
52+
53+
@inlinable
54+
public mutating func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) async -> PolicyEvaluationResult {
55+
await policy.chainMeetsPolicyRequirements(chain: chain)
56+
}
57+
}
58+
59+
struct LegacyPolicySet: VerifierPolicy {
60+
let verifyingCriticalExtensions: [ASN1ObjectIdentifier]
61+
62+
var policies: [any VerifierPolicy]
63+
64+
init(policies: [any VerifierPolicy]) {
65+
self.policies = policies
66+
67+
var extensions: [ASN1ObjectIdentifier] = []
68+
extensions.reserveCapacity(policies.reduce(into: 0, { $0 += $1.verifyingCriticalExtensions.count }))
69+
70+
for policy in policies {
71+
extensions.append(contentsOf: policy.verifyingCriticalExtensions)
72+
}
73+
74+
self.verifyingCriticalExtensions = extensions
75+
}
76+
77+
mutating func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) async -> PolicyEvaluationResult {
78+
var policyIndex = self.policies.startIndex
79+
80+
while policyIndex < self.policies.endIndex {
81+
switch await self.policies[policyIndex].chainMeetsPolicyRequirements(chain: chain) {
82+
case .meetsPolicy:
83+
()
84+
case .failsToMeetPolicy(reason: let reason):
85+
return .failsToMeetPolicy(reason: reason)
86+
}
87+
88+
self.policies.formIndex(after: &policyIndex)
89+
}
90+
91+
return .meetsPolicy
92+
}
93+
}
94+
95+
@available(*, deprecated, message: "use PolicyBuilder to construct a custom policy")
96+
public typealias PolicySet = AnyPolicy
97+
98+
extension AnyPolicy {
99+
@available(*, deprecated, message: "use PolicyBuilder to construct a custom policy")
100+
public init(policies: [any VerifierPolicy]) {
101+
self.init(LegacyPolicySet(policies: policies))
102+
}
103+
}

0 commit comments

Comments
 (0)