Skip to content

Commit 4ce1b9b

Browse files
josephnoirLukasa
andauthored
Add a PKCS#8 DER property to private keys (#372)
### Motivation PKCS#8 is pretty widely used. Currently getting a key in PKCS#8 DER representations requires going through a PKCS8 PEM document and then get its DER bytes. ### Modifications Add a computed property to RSA private keys that calls into BoringSSL or Security.framework to get the PKCS8 DER representation of the key. ECDH keys use the existing `derRepresentation` property to provide a property of the same name. A small ASN1 encoder adds the functionality to ed25519/x25519 keys. ### Result The representation can be accessed directly. *The identifiers for MLKEM are [still a draft](https://datatracker.ietf.org/doc/draft-ietf-lamps-kyber-certificates/). As such MLKEM is not included in the PR.* --------- Co-authored-by: Cory Benfield <lukasa@apple.com>
1 parent 7416764 commit 4ce1b9b

File tree

9 files changed

+509
-0
lines changed

9 files changed

+509
-0
lines changed

Sources/_CryptoExtras/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ add_library(_CryptoExtras
3232
"ARC/ARCServer.swift"
3333
"ChaCha20CTR/BoringSSL/ChaCha20CTR_boring.swift"
3434
"ChaCha20CTR/ChaCha20CTR.swift"
35+
"EC/ObjectIdentifier.swift"
36+
"EC/PKCS8DERRepresentation.swift"
37+
"EC/PKCS8PrivateKey.swift"
38+
"EC/RFC8410AlgorithmIdentifier.swift"
3539
"ECToolbox/BoringSSL/ECToolbox_boring.swift"
3640
"ECToolbox/ECToolbox.swift"
3741
"H2G/HashToField.swift"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftCrypto 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 SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftASN1
16+
17+
extension ASN1ObjectIdentifier.AlgorithmIdentifier {
18+
// Identifies the key agreement algorithm X25519.
19+
//
20+
// This identifier is defined in RFC 8410
21+
static let idX25519: ASN1ObjectIdentifier = [1, 3, 101, 110]
22+
23+
// Identifies the key agreement algorithm X448.
24+
//
25+
// This identifier is defined in RFC 8410
26+
static let idX448: ASN1ObjectIdentifier = [1, 3, 101, 111]
27+
28+
// Identifies the signature algorithm Ed25519.
29+
//
30+
// This identifier is defined in RFC 8410
31+
static let idEd25519: ASN1ObjectIdentifier = [1, 3, 101, 112]
32+
33+
// Identifies the signature algorithm Ed448.
34+
//
35+
// This identifier is defined in RFC 8410
36+
static let idEd448: ASN1ObjectIdentifier = [1, 3, 101, 113]
37+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftCrypto 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 SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Crypto
16+
import Foundation
17+
import SwiftASN1
18+
19+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
20+
extension Curve25519.Signing.PrivateKey {
21+
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
22+
public var pkcs8DERRepresentation: Data {
23+
let pkey = ASN1.PKCS8PrivateKey(algorithm: .ed25519, privateKey: Array(self.rawRepresentation))
24+
var serializer = DER.Serializer()
25+
26+
// Serializing this key can't throw
27+
try! serializer.serialize(pkey)
28+
return Data(serializer.serializedBytes)
29+
}
30+
}
31+
32+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
33+
extension Curve25519.KeyAgreement.PrivateKey {
34+
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
35+
public var pkcs8DERRepresentation: Data {
36+
let pkey = ASN1.PKCS8PrivateKey(algorithm: .x25519, privateKey: Array(self.rawRepresentation))
37+
var serializer = DER.Serializer()
38+
39+
// Serializing this key can't throw
40+
try! serializer.serialize(pkey)
41+
return Data(serializer.serializedBytes)
42+
}
43+
}
44+
45+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
46+
extension P256.Signing.PrivateKey {
47+
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
48+
///
49+
/// This property provides the same output as the existing `derRepresentation` property,
50+
/// which already conforms to the PKCS#8 standard.
51+
public var pkcs8DERRepresentation: Data {
52+
self.derRepresentation
53+
}
54+
}
55+
56+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
57+
extension P384.Signing.PrivateKey {
58+
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
59+
///
60+
/// This property provides the same output as the existing `derRepresentation` property,
61+
/// which already conforms to the PKCS#8 standard.
62+
public var pkcs8DERRepresentation: Data {
63+
self.derRepresentation
64+
}
65+
}
66+
67+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
68+
extension P521.Signing.PrivateKey {
69+
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
70+
///
71+
/// This property provides the same output as the existing `derRepresentation` property,
72+
/// which already conforms to the PKCS#8 standard.
73+
public var pkcs8DERRepresentation: Data {
74+
self.derRepresentation
75+
}
76+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftCrypto 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 SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Crypto
16+
import SwiftASN1
17+
18+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
19+
extension ASN1 {
20+
// A PKCS#8 private key is one of two formats, depending on the version:
21+
//
22+
// For PKCS#8 we need the following for the private key:
23+
//
24+
// PrivateKeyInfo ::= SEQUENCE {
25+
// version Version,
26+
// privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
27+
// privateKey PrivateKey,
28+
// attributes [0] IMPLICIT Attributes OPTIONAL }
29+
//
30+
// Version ::= INTEGER
31+
//
32+
// PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
33+
//
34+
// PrivateKey ::= OCTET STRING
35+
//
36+
// Attributes ::= SET OF Attribute
37+
//
38+
// We disregard the attributes because we don't support them anyway.
39+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
40+
struct PKCS8PrivateKey: DERImplicitlyTaggable {
41+
static var defaultIdentifier: ASN1Identifier {
42+
.sequence
43+
}
44+
45+
var algorithm: RFC8410AlgorithmIdentifier
46+
47+
var privateKey: ASN1OctetString
48+
49+
init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
50+
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
51+
let version = try Int(derEncoded: &nodes)
52+
guard version == 0 || version == 1 else {
53+
throw ASN1Error.invalidASN1Object(reason: "Version number mismatch")
54+
}
55+
56+
let algorithm = try ASN1.RFC8410AlgorithmIdentifier(derEncoded: &nodes)
57+
let privateKeyBytes = try ASN1OctetString(derEncoded: &nodes)
58+
59+
// We ignore the attributes
60+
_ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { _ in }
61+
62+
let privateKeyNode = try DER.parse(privateKeyBytes.bytes)
63+
let privateKey = try ASN1OctetString(derEncoded: privateKeyNode)
64+
65+
return try .init(algorithm: algorithm, privateKey: privateKey)
66+
}
67+
}
68+
69+
private init(algorithm: ASN1.RFC8410AlgorithmIdentifier, privateKey: ASN1OctetString) throws {
70+
self.privateKey = privateKey
71+
self.algorithm = algorithm
72+
}
73+
74+
init(algorithm: ASN1.RFC8410AlgorithmIdentifier, privateKey: [UInt8]) {
75+
self.algorithm = algorithm
76+
self.privateKey = ASN1OctetString(contentBytes: privateKey[...])
77+
}
78+
79+
func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
80+
try coder.appendConstructedNode(identifier: identifier) { coder in
81+
try coder.serialize(0)
82+
try coder.serialize(self.algorithm)
83+
84+
// Here's a weird one: we recursively serialize the private key, and then turn the bytes into an octet string.
85+
var subCoder = DER.Serializer()
86+
try subCoder.serialize(self.privateKey)
87+
let serializedKey = ASN1OctetString(contentBytes: subCoder.serializedBytes[...])
88+
89+
try coder.serialize(serializedKey)
90+
}
91+
}
92+
}
93+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftCrypto 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 SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftASN1
16+
17+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
18+
extension ASN1 {
19+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
20+
struct RFC8410AlgorithmIdentifier: DERImplicitlyTaggable, Hashable {
21+
static var defaultIdentifier: ASN1Identifier {
22+
.sequence
23+
}
24+
25+
var algorithm: ASN1ObjectIdentifier
26+
27+
// RFC 8410: For all of these OIDs, the parameters MUST be absent.
28+
// They are still part of the identifer block.
29+
var parameters: ASN1Any?
30+
31+
init(algorithm: ASN1ObjectIdentifier, parameters: ASN1Any?) {
32+
self.algorithm = algorithm
33+
self.parameters = parameters
34+
}
35+
36+
init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
37+
// The AlgorithmIdentifier block looks like this.
38+
//
39+
// AlgorithmIdentifier ::= SEQUENCE {
40+
// algorithm OBJECT IDENTIFIER,
41+
// parameters ANY DEFINED BY algorithm OPTIONAL
42+
// }
43+
//
44+
// We don't bother with helpers: we just try to decode it directly.
45+
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
46+
let algorithmOID = try ASN1ObjectIdentifier(berEncoded: &nodes)
47+
48+
let parameters = nodes.next().map { ASN1Any(berEncoded: $0) }
49+
50+
return .init(algorithm: algorithmOID, parameters: parameters)
51+
}
52+
}
53+
54+
func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
55+
try coder.appendConstructedNode(identifier: identifier) { coder in
56+
try coder.serialize(self.algorithm)
57+
if let parameters = self.parameters {
58+
try coder.serialize(parameters)
59+
}
60+
}
61+
}
62+
}
63+
}
64+
65+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
66+
extension ASN1.RFC8410AlgorithmIdentifier {
67+
static let x25519 = ASN1.RFC8410AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idX25519, parameters: nil)
68+
69+
static let x448 = ASN1.RFC8410AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idX448, parameters: nil)
70+
71+
static let ed25519 = ASN1.RFC8410AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idEd25519, parameters: nil)
72+
73+
static let ed448 = ASN1.RFC8410AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idEd448, parameters: nil)
74+
}

Sources/_CryptoExtras/RSA/RSA.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ extension _RSA.Signing {
259259
self.backing.pemRepresentation
260260
}
261261

262+
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
263+
public var pkcs8DERRepresentation: Data {
264+
self.backing.pkcs8DERRepresentation
265+
}
266+
262267
public var pkcs8PEMRepresentation: String {
263268
self.backing.pkcs8PEMRepresentation
264269
}

Sources/_CryptoExtras/RSA/RSA_boring.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ internal struct BoringSSLRSAPrivateKey: Sendable {
106106
self.backing.pemRepresentation
107107
}
108108

109+
var pkcs8DERRepresentation: Data {
110+
self.backing.pkcs8DERRepresentation
111+
}
112+
109113
var pkcs8PEMRepresentation: String {
110114
self.backing.pkcs8PEMRepresentation
111115
}
@@ -737,6 +741,15 @@ extension BoringSSLRSAPrivateKey {
737741
}
738742
}
739743

744+
fileprivate var pkcs8DERRepresentation: Data {
745+
BIOHelper.withWritableMemoryBIO { bio in
746+
let rc = CCryptoBoringSSL_i2d_PKCS8PrivateKeyInfo_bio(bio, self.pointer)
747+
precondition(rc == 1, "Exporting PKCS8 DER key failed")
748+
749+
return try! Data(copyingMemoryBIO: bio)
750+
}
751+
}
752+
740753
fileprivate var pkcs8PEMRepresentation: String {
741754
BIOHelper.withWritableMemoryBIO { bio in
742755
let evp = CCryptoBoringSSL_EVP_PKEY_new()

Sources/_CryptoExtras/RSA/RSA_security.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ internal struct SecurityRSAPrivateKey: @unchecked Sendable {
169169
return pemString.appending("\n")
170170
}
171171

172+
var pkcs8DERRepresentation: Data {
173+
let pkcs1Bytes = self.derRepresentation
174+
let pkcs8Bytes = Data(privateKeyPKCS8BytesForPKCS1Bytes: pkcs1Bytes)
175+
return pkcs8Bytes
176+
}
177+
172178
var keySizeInBits: Int {
173179
SecKeyGetBlockSize(self.backing) * 8
174180
}

0 commit comments

Comments
 (0)