Skip to content

[DNM][Collections] Signing (all, 1): certificate and key types #3260

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Fixtures/Collections/Signing/Test_ec.cer
Binary file not shown.
Binary file added Fixtures/Collections/Signing/Test_rsa.cer
Binary file not shown.
12 changes: 10 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,16 @@ let package = Package(
/** Package collections models */
name: "PackageCollectionsModel",
dependencies: []),

.target(
/** Package collections signing */
name: "PackageCollectionsSigning",
dependencies: ["Crypto"]),

.target(
/** Data structures and support for package collections */
name: "PackageCollections",
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageModel", "SourceControl", "PackageCollectionsModel", "Crypto"]),
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageModel", "SourceControl", "PackageCollectionsModel"]),

// MARK: Package Manager Functionality

Expand Down Expand Up @@ -265,9 +270,12 @@ let package = Package(
.testTarget(
name: "PackageCollectionsModelTests",
dependencies: ["PackageCollectionsModel"]),
.testTarget(
name: "PackageCollectionsSigningTests",
dependencies: ["PackageCollectionsSigning", "SPMTestSupport"]),
.testTarget(
name: "PackageCollectionsTests",
dependencies: ["SPMTestSupport", "PackageCollections"]),
dependencies: ["PackageCollections", "SPMTestSupport"]),
.testTarget(
name: "SourceControlTests",
dependencies: ["SourceControl", "SPMTestSupport"]),
Expand Down
3 changes: 2 additions & 1 deletion Sources/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
Expand All @@ -12,6 +12,7 @@ add_subdirectory(Commands)
add_subdirectory(LLBuildManifest)
add_subdirectory(PackageCollections)
add_subdirectory(PackageCollectionsModel)
add_subdirectory(PackageCollectionsSigning)
add_subdirectory(PackageDescription)
add_subdirectory(PackageGraph)
add_subdirectory(PackageLoading)
Expand Down
4 changes: 0 additions & 4 deletions Sources/PackageCollections/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,11 @@ target_link_libraries(PackageCollections PUBLIC
TSCBasic
TSCUtility
Basics
Crypto
CCryptoBoringSSL
PackageModel
SourceControl)
# NOTE(compnerd) workaround for CMake not setting up include flags yet
set_target_properties(PackageCollections PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
target_link_options(PackageCollections PRIVATE
"$<$<PLATFORM_ID:Darwin>:SHELL:-Xlinker -framework -Xlinker Security>")

if(USE_CMAKE_INSTALL)
install(TARGETS PackageCollections
Expand Down
44 changes: 44 additions & 0 deletions Sources/PackageCollectionsSigning/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2021 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_library(PackageCollectionsSigning
Certificate/Certificate.swift
Key/ASN1/ASN1.swift
Key/ASN1/ASN1Error.swift
Key/ASN1/PEMDocument.swift
Key/ASN1/SEC1PrivateKey.swift
Key/ASN1/SubjectPublicKeyInfo.swift
Key/ASN1/Types/ASN1BitString.swift
Key/ASN1/Types/ASN1Identifier.swift
Key/ASN1/Types/ASN1Integer.swift
Key/ASN1/Types/ASN1ObjectIdentifier.swift
Key/ASN1/Types/ASN1OctetString.swift
Key/BoringSSLKey.swift
Key/Key.swift
Key/Key+EC.swift
Key/Key+RSA.swift
Utilities/Utilities.swift)
target_link_libraries(PackageCollectionsSigning PUBLIC
$<$<NOT:$<PLATFORM_ID:Darwin>>:dispatch>
$<$<NOT:$<PLATFORM_ID:Darwin>>:Foundation>)
target_link_libraries(PackageCollectionsSigning PRIVATE
Crypto
CCryptoBoringSSL)
# NOTE(compnerd) workaround for CMake not setting up include flags yet
set_target_properties(PackageCollectionsSigning PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
target_link_options(PackageCollectionsSigning PRIVATE
"$<$<PLATFORM_ID:Darwin>:SHELL:-Xlinker -framework -Xlinker Security>")

if(USE_CMAKE_INSTALL)
install(TARGETS PackageCollectionsSigning
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
endif()
set_property(GLOBAL APPEND PROPERTY SwiftPM_EXPORTS PackageCollectionsSigning)
237 changes: 237 additions & 0 deletions Sources/PackageCollectionsSigning/Certificate/Certificate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import struct Foundation.Data

#if canImport(Security)
import Security
#else
@_implementationOnly import CCryptoBoringSSL
#endif

#if canImport(Security)
typealias Certificate = CoreCertificate
#else
typealias Certificate = BoringSSLCertificate
#endif

// MARK: - Certificate implementation using the Security framework

#if canImport(Security)
struct CoreCertificate {
let underlying: SecCertificate

init(derEncoded data: Data) throws {
guard let certificate = SecCertificateCreateWithData(nil, data as CFData) else {
throw CertificateError.initializationFailure
}
self.underlying = certificate
}

func subject() throws -> CertificateName {
try self.extractName(kSecOIDX509V1SubjectName)
}

func issuer() throws -> CertificateName {
try self.extractName(kSecOIDX509V1IssuerName)
}

private func extractName(_ name: CFString) throws -> CertificateName {
guard let dict = SecCertificateCopyValues(self.underlying, [name] as CFArray, nil) as? [CFString: Any],
let nameDict = dict[name] as? [CFString: Any],
let propValueList = nameDict[kSecPropertyKeyValue] as? [[String: Any]] else {
throw CertificateError.nameExtractionFailure
}

let props = propValueList.reduce(into: [String: String]()) { result, item in
if let label = item["label"] as? String, let value = item["value"] as? String {
result[label] = value
}
}

return CertificateName(
userID: props["0.9.2342.19200300.100.1.1"], // FIXME: don't hardcode?
commonName: props[kSecOIDCommonName as String],
organization: props[kSecOIDOrganizationName as String],
organizationalUnit: props[kSecOIDOrganizationalUnitName as String]
)
}

func publicKey() throws -> PublicKey {
guard let key = SecCertificateCopyKey(self.underlying) else {
throw CertificateError.keyExtractionFailure
}

var error: Unmanaged<CFError>?
guard let data = SecKeyCopyExternalRepresentation(key, &error) as Data? else {
throw error.map { $0.takeRetainedValue() as Error } ?? CertificateError.keyExtractionFailure
}

switch try self.keyType(of: key) {
case .RSA:
return try CoreRSAPublicKey(data: data)
case .EC:
return try ECPublicKey(data: data)
}
}

func keyType() throws -> KeyType {
guard let key = SecCertificateCopyKey(self.underlying) else {
throw CertificateError.keyExtractionFailure
}
return try self.keyType(of: key)
}

private func keyType(of key: SecKey) throws -> KeyType {
guard let attributes = SecKeyCopyAttributes(key) as? [CFString: Any],
let keyType = attributes[kSecAttrKeyType] as? String else {
throw CertificateError.indeterminateKeyType
}

if keyType == (kSecAttrKeyTypeRSA as String) {
return .RSA
} else if keyType == (kSecAttrKeyTypeEC as String) {
return .EC
} else {
throw CertificateError.unsupportedKeyType
}
}
}

// MARK: - Certificate implementation using BoringSSL

#else
final class BoringSSLCertificate {
let underlying: UnsafeMutablePointer<X509>

deinit {
CCryptoBoringSSL_X509_free(self.underlying)
}

init(derEncoded data: Data) throws {
let bytes = data.copyBytes()
let x509 = try bytes.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer<UInt8>) throws -> UnsafeMutablePointer<X509> in
var pointer = ptr.baseAddress
guard let x509 = CCryptoBoringSSL_d2i_X509(nil, &pointer, numericCast(bytes.count)) else {
throw CertificateError.initializationFailure
}
return x509
}
self.underlying = x509
}

func subject() throws -> CertificateName {
guard let subject = CCryptoBoringSSL_X509_get_subject_name(self.underlying) else {
throw CertificateError.nameExtractionFailure
}
return CertificateName(x509Name: subject)
}

func issuer() throws -> CertificateName {
guard let issuer = CCryptoBoringSSL_X509_get_issuer_name(self.underlying) else {
throw CertificateError.nameExtractionFailure
}
return CertificateName(x509Name: issuer)
}

func publicKey() throws -> PublicKey {
guard let key = CCryptoBoringSSL_X509_get_pubkey(self.underlying) else {
throw CertificateError.keyExtractionFailure
}
defer { CCryptoBoringSSL_EVP_PKEY_free(key) }

var buffer: UnsafeMutablePointer<CUnsignedChar>?
defer { CCryptoBoringSSL_OPENSSL_free(buffer) }

let length = CCryptoBoringSSL_i2d_PublicKey(key, &buffer)
guard length > 0 else {
throw CertificateError.keyExtractionFailure
}

let data = Data(UnsafeBufferPointer(start: buffer, count: Int(length)))

switch try self.keyType(of: key) {
case .RSA:
return try BoringSSLRSAPublicKey(data: data)
case .EC:
return try ECPublicKey(data: data)
}
}

func keyType() throws -> KeyType {
guard let key = CCryptoBoringSSL_X509_get_pubkey(self.underlying) else {
throw CertificateError.keyExtractionFailure
}
defer { CCryptoBoringSSL_EVP_PKEY_free(key) }

return try self.keyType(of: key)
}

private func keyType(of key: UnsafeMutablePointer<EVP_PKEY>) throws -> KeyType {
let algorithm = CCryptoBoringSSL_OBJ_obj2nid(self.underlying.pointee.cert_info.pointee.key.pointee.algor.pointee.algorithm)

switch algorithm {
case NID_rsaEncryption:
return .RSA
case NID_X9_62_id_ecPublicKey:
return .EC
default:
throw CertificateError.unsupportedKeyType
}
}
}

private extension CertificateName {
init(x509Name: UnsafeMutablePointer<X509_NAME>) {
self.userID = x509Name.getStringValue(of: NID_userId)
self.commonName = x509Name.getStringValue(of: NID_commonName)
self.organization = x509Name.getStringValue(of: NID_organizationName)
self.organizationalUnit = x509Name.getStringValue(of: NID_organizationalUnitName)
}
}

private extension UnsafeMutablePointer where Pointee == X509_NAME {
func getStringValue(of nid: CInt) -> String? {
let index = CCryptoBoringSSL_X509_NAME_get_index_by_NID(self, nid, -1)
guard index >= 0 else {
return nil
}

let entry = CCryptoBoringSSL_X509_NAME_get_entry(self, index)
guard let data = CCryptoBoringSSL_X509_NAME_ENTRY_get_data(entry) else {
return nil
}

var value: UnsafeMutablePointer<CUnsignedChar>?
defer { CCryptoBoringSSL_OPENSSL_free(value) }

guard CCryptoBoringSSL_ASN1_STRING_to_UTF8(&value, data) >= 0 else {
return nil
}

return String.decodeCString(value, as: UTF8.self, repairingInvalidCodeUnits: true)?.result
}
}
#endif

struct CertificateName {
let userID: String?
let commonName: String?
let organization: String?
let organizationalUnit: String?
}

enum CertificateError: Error {
case initializationFailure
case nameExtractionFailure
case keyExtractionFailure
case indeterminateKeyType
case unsupportedKeyType
}
Loading