CertificateChain.swift 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /*
  2. * Copyright 2025, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Crypto
  17. import Foundation
  18. import SwiftASN1
  19. import X509
  20. /// Create a certificate chain with a root and intermediate certificate stored in a trust roots file and
  21. /// key / certificate pairs for a client and a server. These can be used to establish mTLS connections
  22. /// with local trust.
  23. ///
  24. /// Usage example:
  25. /// ```
  26. /// // Create a new certificate chain
  27. /// let certificateChain = try CertificateChain()
  28. /// // Tag our certificate files with the function name
  29. /// let filePaths = try certificateChain.writeToTemp(fileTag: #function)
  30. /// // Access the file paths of the certificate files.
  31. /// let clientCertPath = filePaths.clientCert
  32. /// ...
  33. /// ```
  34. struct CertificateChain {
  35. /// Each node in the chain has a certificate and a key
  36. struct CertificateKeyPair {
  37. let certificate: Certificate
  38. let key: Certificate.PrivateKey
  39. }
  40. /// Leaf certificates can authenticate either a client or a server
  41. enum Authenticating {
  42. case client
  43. case server
  44. }
  45. /// Writing the files to disk returns the paths to all written files
  46. struct FilePaths {
  47. var clientCert: String
  48. var clientKey: String
  49. var serverCert: String
  50. var serverKey: String
  51. var trustRoots: String
  52. }
  53. /// The domains names for the leaf certificates
  54. static let serverName = "my.server"
  55. static let clientName = "my.client"
  56. /// Our certificate chain
  57. let root: CertificateKeyPair
  58. let intermediate: CertificateKeyPair
  59. let server: CertificateKeyPair
  60. let client: CertificateKeyPair
  61. /// On initialization create a chain of certificates: root signes intermediate, intermediate signs both leaf certificates
  62. init() throws {
  63. let root = try Self.makeRootCertificate(commonName: "root")
  64. let intermediate = try Self.makeIntermediateCertificate(
  65. commonName: "intermediate",
  66. signedBy: root
  67. )
  68. let server = try Self.makeLeafCertificate(
  69. commonName: "server",
  70. domainName: CertificateChain.serverName,
  71. authenticating: .server,
  72. signedBy: intermediate
  73. )
  74. let client = try Self.makeLeafCertificate(
  75. commonName: "client",
  76. domainName: CertificateChain.clientName,
  77. authenticating: .client,
  78. signedBy: intermediate
  79. )
  80. self.root = root
  81. self.intermediate = intermediate
  82. self.server = server
  83. self.client = client
  84. }
  85. /// Create a new root certificate.
  86. ///
  87. /// - Parameter commonName: CN of the certificate
  88. /// - Returns: A certificate and a private key.
  89. private static func makeRootCertificate(commonName cn: String) throws -> CertificateKeyPair {
  90. let privateKey = P256.Signing.PrivateKey()
  91. let key = Certificate.PrivateKey(privateKey)
  92. let subjectName = try DistinguishedName {
  93. CommonName(cn)
  94. }
  95. let issuerName = subjectName
  96. let now = Date()
  97. let extensions = try Certificate.Extensions {
  98. Critical(
  99. BasicConstraints.isCertificateAuthority(maxPathLength: nil)
  100. )
  101. Critical(
  102. KeyUsage(keyCertSign: true)
  103. )
  104. }
  105. let certificate = try Certificate(
  106. version: .v3,
  107. serialNumber: Certificate.SerialNumber(),
  108. publicKey: key.publicKey,
  109. notValidBefore: now.addingTimeInterval(-1),
  110. notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365 * 10), // 10 years
  111. issuer: issuerName,
  112. subject: subjectName,
  113. signatureAlgorithm: .ecdsaWithSHA256,
  114. extensions: extensions,
  115. issuerPrivateKey: key
  116. )
  117. return CertificateKeyPair(certificate: certificate, key: key)
  118. }
  119. /// Create a new intermediate certificate.
  120. ///
  121. /// - Parameters:
  122. /// - commonName: CN for the certificate
  123. /// - signedBy: Certificate that signs this
  124. /// - Returns: A certificate and a private key.
  125. private static func makeIntermediateCertificate(
  126. commonName cn: String,
  127. signedBy issuer: CertificateKeyPair
  128. ) throws -> CertificateKeyPair {
  129. // Generate a new private key for the intermediate certificate
  130. let privateKey = P256.Signing.PrivateKey()
  131. let key = Certificate.PrivateKey(privateKey)
  132. // Create subject name for the intermediate certificate
  133. let subjectName = try DistinguishedName {
  134. CommonName(cn)
  135. }
  136. // Parse the root certificate to get the issuer information
  137. let issuerCert = issuer.certificate
  138. let issuerName = issuerCert.subject
  139. // Parse the root certificate's private key for signing
  140. let issuerKey = issuer.key
  141. let now = Date()
  142. // Configure extensions for intermediate CA
  143. let extensions = try Certificate.Extensions {
  144. Critical(
  145. BasicConstraints.isCertificateAuthority(
  146. maxPathLength: nil
  147. )
  148. )
  149. Critical(
  150. KeyUsage(keyCertSign: true, cRLSign: true)
  151. )
  152. // Add Authority Key Identifier linking to the root certificate
  153. try AuthorityKeyIdentifier(
  154. keyIdentifier: issuerCert.extensions.subjectKeyIdentifier?
  155. .keyIdentifier
  156. )
  157. }
  158. // Create the intermediate certificate
  159. let certificate = try Certificate(
  160. version: .v3,
  161. serialNumber: Certificate.SerialNumber(),
  162. publicKey: key.publicKey,
  163. notValidBefore: now.addingTimeInterval(-1),
  164. notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365), // 1 year
  165. issuer: issuerName,
  166. subject: subjectName,
  167. signatureAlgorithm: .ecdsaWithSHA256,
  168. extensions: extensions,
  169. issuerPrivateKey: issuerKey
  170. )
  171. return CertificateKeyPair(
  172. certificate: certificate,
  173. key: key
  174. )
  175. }
  176. /// Create a new leaf certificate.
  177. ///
  178. /// - Parameters:
  179. /// - commonName: CN for the certificate
  180. /// - domainName: Domain name added as a SAN to the cert
  181. /// - authenticating: Whether the certificate authenticates a client or a server
  182. /// - signedBy: Certificate that signs this
  183. /// - Returns: A certificate and a private key.
  184. private static func makeLeafCertificate(
  185. commonName cn: String,
  186. domainName: String,
  187. authenticating side: Authenticating,
  188. signedBy issuer: CertificateKeyPair
  189. ) throws -> CertificateKeyPair {
  190. // Generate a new private key for the Leaf certificate
  191. let privateKey = P256.Signing.PrivateKey()
  192. let key = Certificate.PrivateKey(privateKey)
  193. // Create subject name for the Leaf certificate
  194. let subjectName = try DistinguishedName {
  195. CommonName(cn)
  196. }
  197. // Parse the root certificate to get the issuer information
  198. let issuerCert = issuer.certificate
  199. let issuerName = issuerCert.subject
  200. // Parse the root certificate's private key for signing
  201. let issuerKey = issuer.key
  202. let now = Date()
  203. // Configure extensions for Leaf CA
  204. let extensions = try Certificate.Extensions {
  205. BasicConstraints.notCertificateAuthority
  206. try ExtendedKeyUsage(
  207. side == .server ? [.serverAuth] : [.clientAuth]
  208. )
  209. SubjectAlternativeNames([.dnsName(domainName)])
  210. }
  211. // Create the Leaf certificate
  212. let certificate = try Certificate(
  213. version: .v3,
  214. serialNumber: Certificate.SerialNumber(),
  215. publicKey: key.publicKey,
  216. notValidBefore: now.addingTimeInterval(-1),
  217. notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 90), // 90 days
  218. issuer: issuerName,
  219. subject: subjectName,
  220. signatureAlgorithm: .ecdsaWithSHA256,
  221. extensions: extensions,
  222. issuerPrivateKey: issuerKey
  223. )
  224. return CertificateKeyPair(
  225. certificate: certificate,
  226. key: key
  227. )
  228. }
  229. /// Write the certificate chain to a temporary directory.
  230. ///
  231. /// - Parameters:
  232. /// - fileTag: A prefix added to all certificates files
  233. /// - Returns: A struct that contains paths of the written file
  234. public func writeToTemp(fileTag: String = #function) throws -> FilePaths {
  235. let fm = FileManager.default
  236. let directory = fm.temporaryDirectory
  237. // Store file paths
  238. let trustRootsURL = directory.appendingPathComponent("\(fileTag).ca-chain.cert.pem")
  239. let clientCertURL = directory.appendingPathComponent("\(fileTag).client.cert.pem")
  240. let clientKeyURL = directory.appendingPathComponent("\(fileTag).client.key.pem")
  241. let serverCertURL = directory.appendingPathComponent("\(fileTag).server.cert.pem")
  242. let serverKeyURL = directory.appendingPathComponent("\(fileTag).server.key.pem")
  243. // Write chain: certificates of the root and intermediate in one file
  244. let rootPEM = try self.root.certificate.serializeAsPEM().pemString
  245. let intermediatePEM = try self.intermediate.certificate.serializeAsPEM().pemString
  246. try intermediatePEM.appending("\n").appending(rootPEM).write(
  247. to: trustRootsURL,
  248. atomically: true,
  249. encoding: .utf8
  250. )
  251. // Write leaf certificates and keys
  252. try self.client.writeKeyPair(certPath: clientCertURL, keyPath: clientKeyURL)
  253. try self.server.writeKeyPair(certPath: serverCertURL, keyPath: serverKeyURL)
  254. return FilePaths(
  255. clientCert: clientCertURL.path(),
  256. clientKey: clientKeyURL.path(),
  257. serverCert: serverCertURL.path(),
  258. serverKey: serverKeyURL.path(),
  259. trustRoots: trustRootsURL.path()
  260. )
  261. }
  262. }
  263. extension CertificateChain.CertificateKeyPair {
  264. fileprivate func writeKeyPair(certPath: URL, keyPath: URL) throws {
  265. try self.certificate.serializeAsPEM().pemString.write(
  266. to: certPath,
  267. atomically: true,
  268. encoding: .utf8
  269. )
  270. try self.key.serializeAsPEM().pemString.write(to: keyPath, atomically: true, encoding: .utf8)
  271. }
  272. }