Skip to content

Commit 36dc7e6

Browse files
authored
Add PackageSigning module (swiftlang#6129)
* Add PackageSigning module Motivation: Define API for package signing and signature validation in order to make progress on registry publication. Changes: - New `PackageSigning` module - Add API stubs for package signature - Add model stubs related to package signing * async/await instead of callback * Add SigningEntityType * Restructure code * Add test for reading signing identity from Keychain * CMS using Security framework * CMS decoding using Security framework * Signing entity * Refactor * User can provide chain instead of single signing cert * Certificate only. No chain. * Add verifier config
1 parent a5936ba commit 36dc7e6

File tree

5 files changed

+596
-0
lines changed

5 files changed

+596
-0
lines changed

Package.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,13 @@ let package = Package(
289289
],
290290
exclude: ["CMakeLists.txt"]
291291
),
292+
293+
.target(
294+
name: "PackageSigning",
295+
dependencies: [
296+
"Basics",
297+
]
298+
),
292299

293300
// MARK: Package Manager Functionality
294301

@@ -643,6 +650,10 @@ let package = Package(
643650
name: "PackageRegistryTests",
644651
dependencies: ["SPMTestSupport", "PackageRegistry"]
645652
),
653+
.testTarget(
654+
name: "PackageSigningTests",
655+
dependencies: ["SPMTestSupport", "PackageSigning"]
656+
),
646657
.testTarget(
647658
name: "SourceControlTests",
648659
dependencies: ["SourceControl", "SPMTestSupport"],
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct Foundation.Data
14+
15+
#if os(macOS)
16+
import Security
17+
#endif
18+
19+
import Basics
20+
21+
public struct SignatureProvider {
22+
public init() {}
23+
24+
public func sign(
25+
_ content: Data,
26+
with identity: SigningIdentity,
27+
in format: SignatureFormat,
28+
observabilityScope: ObservabilityScope
29+
) async throws -> Data {
30+
let provider = format.provider
31+
return try await provider.sign(content, with: identity, observabilityScope: observabilityScope)
32+
}
33+
34+
public func status(
35+
of signature: Data,
36+
for content: Data,
37+
in format: SignatureFormat,
38+
verifierConfiguration: VerifierConfiguration,
39+
observabilityScope: ObservabilityScope
40+
) async throws -> SignatureStatus {
41+
let provider = format.provider
42+
return try await provider.status(of: signature, for: content, observabilityScope: observabilityScope)
43+
}
44+
}
45+
46+
public struct VerifierConfiguration {
47+
public var trustedRoots: [Certificate]
48+
public var certificateExpiration: CertificateExpiration
49+
public var certificateRevocation: CertificateRevocation
50+
51+
public init() {
52+
self.trustedRoots = []
53+
self.certificateExpiration = .disabled
54+
self.certificateRevocation = .disabled
55+
}
56+
57+
public enum CertificateExpiration {
58+
case enabled
59+
case disabled
60+
}
61+
62+
public enum CertificateRevocation {
63+
case strict
64+
case allowSoftFail
65+
case disabled
66+
}
67+
}
68+
69+
public enum SignatureStatus: Equatable {
70+
case valid
71+
case invalid(String)
72+
case certificateInvalid(String)
73+
case certificateNotTrusted
74+
}
75+
76+
extension Certificate {
77+
public enum RevocationStatus {
78+
case valid
79+
case revoked
80+
case unknown
81+
}
82+
}
83+
84+
public enum SigningError: Error {
85+
case encodeInitializationFailed(String)
86+
case decodeInitializationFailed(String)
87+
case signingFailed(String)
88+
case signatureInvalid(String)
89+
}
90+
91+
protocol SignatureProviderProtocol {
92+
func sign(
93+
_ content: Data,
94+
with identity: SigningIdentity,
95+
observabilityScope: ObservabilityScope
96+
) async throws -> Data
97+
98+
func status(
99+
of signature: Data,
100+
for content: Data,
101+
observabilityScope: ObservabilityScope
102+
) async throws -> SignatureStatus
103+
104+
func signingEntity(of signature: Data) throws -> SigningEntity
105+
}
106+
107+
public enum SignatureFormat: String {
108+
case cms_1_0_0 = "cms-1.0.0"
109+
110+
var provider: SignatureProviderProtocol {
111+
switch self {
112+
case .cms_1_0_0:
113+
return CMSSignatureProvider(format: self)
114+
}
115+
}
116+
}
117+
118+
struct CMSSignatureProvider: SignatureProviderProtocol {
119+
let format: SignatureFormat
120+
121+
init(format: SignatureFormat) {
122+
precondition(format.rawValue.hasPrefix("cms-"), "Unsupported signature format '\(format)'")
123+
self.format = format
124+
}
125+
126+
func sign(
127+
_ content: Data,
128+
with identity: SigningIdentity,
129+
observabilityScope: ObservabilityScope
130+
) async throws -> Data {
131+
try self.validate(identity: identity)
132+
133+
#if os(macOS)
134+
if CFGetTypeID(identity as CFTypeRef) == SecIdentityGetTypeID() {
135+
var cmsEncoder: CMSEncoder?
136+
var status = CMSEncoderCreate(&cmsEncoder)
137+
guard status == errSecSuccess, let cmsEncoder = cmsEncoder else {
138+
throw SigningError.encodeInitializationFailed("Unable to create CMSEncoder. Error: \(status)")
139+
}
140+
141+
CMSEncoderAddSigners(cmsEncoder, identity as! SecIdentity)
142+
CMSEncoderSetHasDetachedContent(cmsEncoder, true) // Detached signature
143+
CMSEncoderSetSignerAlgorithm(cmsEncoder, kCMSEncoderDigestAlgorithmSHA256)
144+
CMSEncoderAddSignedAttributes(cmsEncoder, CMSSignedAttributes.attrSigningTime)
145+
CMSEncoderSetCertificateChainMode(cmsEncoder, .signerOnly)
146+
147+
var contentArray = Array(content)
148+
CMSEncoderUpdateContent(cmsEncoder, &contentArray, content.count)
149+
150+
var signature: CFData?
151+
status = CMSEncoderCopyEncodedContent(cmsEncoder, &signature)
152+
guard status == errSecSuccess, let signature = signature else {
153+
throw SigningError.signingFailed("Signing failed. Error: \(status)")
154+
}
155+
156+
return signature as Data
157+
} else {
158+
fatalError("TO BE IMPLEMENTED")
159+
}
160+
#else
161+
fatalError("TO BE IMPLEMENTED")
162+
#endif
163+
}
164+
165+
func status(
166+
of signature: Data,
167+
for content: Data,
168+
observabilityScope: ObservabilityScope
169+
) async throws -> SignatureStatus {
170+
#if os(macOS)
171+
var cmsDecoder: CMSDecoder?
172+
var status = CMSDecoderCreate(&cmsDecoder)
173+
guard status == errSecSuccess, let cmsDecoder = cmsDecoder else {
174+
throw SigningError.decodeInitializationFailed("Unable to create CMSDecoder. Error: \(status)")
175+
}
176+
177+
CMSDecoderSetDetachedContent(cmsDecoder, content as CFData)
178+
179+
status = CMSDecoderUpdateMessage(cmsDecoder, [UInt8](signature), signature.count)
180+
guard status == errSecSuccess else {
181+
return .invalid("Unable to update CMSDecoder with signature. Error: \(status)")
182+
}
183+
status = CMSDecoderFinalizeMessage(cmsDecoder)
184+
guard status == errSecSuccess else {
185+
return .invalid("Failed to set up CMSDecoder. Error: \(status)")
186+
}
187+
188+
var signerStatus = CMSSignerStatus.needsDetachedContent
189+
var certificateVerifyResult: OSStatus = errSecSuccess
190+
var trust: SecTrust?
191+
192+
// TODO: build policy based on user config
193+
let basicPolicy = SecPolicyCreateBasicX509()
194+
let revocationPolicy = SecPolicyCreateRevocation(kSecRevocationOCSPMethod)
195+
CMSDecoderCopySignerStatus(
196+
cmsDecoder,
197+
0,
198+
[basicPolicy, revocationPolicy] as CFArray,
199+
true,
200+
&signerStatus,
201+
&trust,
202+
&certificateVerifyResult
203+
)
204+
205+
guard certificateVerifyResult == errSecSuccess else {
206+
return .certificateInvalid("Certificate verify result: \(certificateVerifyResult)")
207+
}
208+
guard signerStatus == .valid else {
209+
return .invalid("Signer status: \(signerStatus)")
210+
}
211+
212+
guard let trust = trust else {
213+
return .certificateNotTrusted
214+
}
215+
216+
// TODO: user configured trusted roots
217+
SecTrustSetNetworkFetchAllowed(trust, true)
218+
// SecTrustSetAnchorCertificates(trust, trustedCAs as CFArray)
219+
// SecTrustSetAnchorCertificatesOnly(trust, true)
220+
221+
guard SecTrustEvaluateWithError(trust, nil) else {
222+
return .certificateNotTrusted
223+
}
224+
225+
var revocationStatus: Certificate.RevocationStatus?
226+
if let trustResult = SecTrustCopyResult(trust) as? [String: Any],
227+
let trustRevocationChecked = trustResult[kSecTrustRevocationChecked as String] as? Bool
228+
{
229+
revocationStatus = trustRevocationChecked ? .valid : .revoked
230+
}
231+
observabilityScope.emit(debug: "Certificate revocation status: \(String(describing: revocationStatus))")
232+
233+
return .valid
234+
#else
235+
fatalError("TO BE IMPLEMENTED")
236+
#endif
237+
}
238+
239+
func signingEntity(of signature: Data) throws -> SigningEntity {
240+
#if os(macOS)
241+
var cmsDecoder: CMSDecoder?
242+
var status = CMSDecoderCreate(&cmsDecoder)
243+
guard status == errSecSuccess, let cmsDecoder = cmsDecoder else {
244+
throw SigningError.decodeInitializationFailed("Unable to create CMSDecoder. Error: \(status)")
245+
}
246+
247+
status = CMSDecoderUpdateMessage(cmsDecoder, [UInt8](signature), signature.count)
248+
guard status == errSecSuccess else {
249+
throw SigningError
250+
.decodeInitializationFailed("Unable to update CMSDecoder with signature. Error: \(status)")
251+
}
252+
status = CMSDecoderFinalizeMessage(cmsDecoder)
253+
guard status == errSecSuccess else {
254+
throw SigningError.decodeInitializationFailed("Failed to set up CMSDecoder. Error: \(status)")
255+
}
256+
257+
var certificate: SecCertificate?
258+
status = CMSDecoderCopySignerCert(cmsDecoder, 0, &certificate)
259+
guard status == errSecSuccess, let certificate = certificate else {
260+
throw SigningError.signatureInvalid("Unable to extract signing certificate. Error: \(status)")
261+
}
262+
263+
return SigningEntity(certificate: certificate)
264+
#else
265+
// TODO: decode `data` by `format`, then construct `signedBy` from signing cert
266+
fatalError("TO BE IMPLEMENTED")
267+
#endif
268+
}
269+
270+
private func validate(identity: SigningIdentity) throws {
271+
switch self.format {
272+
case .cms_1_0_0:
273+
// TODO: key must be EC
274+
()
275+
}
276+
}
277+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct Foundation.Data
14+
15+
#if os(macOS)
16+
import Security
17+
#endif
18+
19+
// MARK: - SigningEntity is the entity that generated the signature
20+
21+
public struct SigningEntity {
22+
public let type: SigningEntityType?
23+
public let name: String?
24+
public let organizationalUnit: String?
25+
public let organization: String?
26+
27+
public var isRecognized: Bool {
28+
self.type != nil
29+
}
30+
31+
public init(of signature: Data, signatureFormat: SignatureFormat) throws {
32+
let provider = signatureFormat.provider
33+
self = try provider.signingEntity(of: signature)
34+
}
35+
36+
#if os(macOS)
37+
init(certificate: SecCertificate) {
38+
self.type = certificate.signingEntityType
39+
self.name = certificate.commonName
40+
41+
guard let dict = SecCertificateCopyValues(certificate, nil, nil) as? [CFString: Any],
42+
let subjectDict = dict[kSecOIDX509V1SubjectName] as? [CFString: Any],
43+
let propValueList = subjectDict[kSecPropertyKeyValue] as? [[String: Any]]
44+
else {
45+
self.organizationalUnit = nil
46+
self.organization = nil
47+
return
48+
}
49+
50+
let props = propValueList.reduce(into: [String: String]()) { result, item in
51+
if let label = item["label"] as? String, let value = item["value"] as? String {
52+
result[label] = value
53+
}
54+
}
55+
56+
self.organizationalUnit = props[kSecOIDOrganizationalUnitName as String]
57+
self.organization = props[kSecOIDOrganizationName as String]
58+
}
59+
#endif
60+
61+
init(certificate: Certificate) {
62+
// TODO: extract id, name, organization, etc. from cert
63+
fatalError("TO BE IMPLEMENTED")
64+
}
65+
}
66+
67+
// MARK: - SigningEntity types that SwiftPM recognizes
68+
69+
public enum SigningEntityType {
70+
case adp // Apple Developer Program
71+
72+
static let oid_adpSwiftPackageMarker = "1.2.840.113635.100.6.1.35"
73+
}
74+
75+
#if os(macOS)
76+
extension SecCertificate {
77+
var signingEntityType: SigningEntityType? {
78+
guard let dict = SecCertificateCopyValues(
79+
self,
80+
[SigningEntityType.oid_adpSwiftPackageMarker as CFString] as CFArray,
81+
nil
82+
) as? [CFString: Any] else {
83+
return nil
84+
}
85+
return dict.isEmpty ? nil : .adp
86+
}
87+
}
88+
#endif

0 commit comments

Comments
 (0)