Skip to content

Commit 87a9e06

Browse files
authored
Add PEM/DER APIs for Curve25519 (#419)
Add PEM/DER APIs for Curve25519 ### Checklist - [x] I've run tests to see all new and existing tests pass - [x] I've followed the code style of the rest of the project - [x] I've read the [Contribution Guidelines](CONTRIBUTING.md) - [x] I've updated the documentation if necessary #### If you've made changes to `gyb` files - [ ] I've run `./scripts/generate_boilerplate_files_with_gyb.sh` and included updated generated files in a commit of this pull request ### Motivation: Closes #82 Creating Ed25519/X25519 keys using PEM/DER and also generating their PEM/DER representations can be handy ### Modifications: Added - `init(pemRepresentation:)` and the `pemRepresentation` computed property - `init(derRepresentation:)` and the `derRepresentation` computed property to `Curve25519.{Signing,KeyAgreement}.{Private,Public}Key` ### Result: Curve25519 can now be created with and can produce PEM/DER representations
1 parent 781478b commit 87a9e06

File tree

6 files changed

+421
-12
lines changed

6 files changed

+421
-12
lines changed

Sources/CryptoExtras/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ add_library(CryptoExtras
3333
"ARC/ARCServer.swift"
3434
"ChaCha20CTR/BoringSSL/ChaCha20CTR_boring.swift"
3535
"ChaCha20CTR/ChaCha20CTR.swift"
36+
"EC/Curve25519+PEM.swift"
3637
"EC/ObjectIdentifier.swift"
3738
"EC/PKCS8DERRepresentation.swift"
3839
"EC/PKCS8PrivateKey.swift"
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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.
22+
public var derRepresentation: Data {
23+
let pkey = ASN1.PKCS8PrivateKey(algorithm: .ed25519, privateKey: Array(self.rawRepresentation))
24+
var serializer = DER.Serializer()
25+
26+
try! serializer.serialize(pkey)
27+
return Data(serializer.serializedBytes)
28+
}
29+
30+
/// A Privacy-Enhanced Mail (PEM) representation of the private key.
31+
public var pemRepresentation: String {
32+
let pemDocument = ASN1.PEMDocument(type: "PRIVATE KEY", derBytes: self.derRepresentation)
33+
return pemDocument.pemString
34+
}
35+
36+
/// Creates a Curve25519 private key for signing from a Privacy-Enhanced Mail
37+
/// (PEM) representation.
38+
///
39+
/// - Parameters:
40+
/// - pemRepresentation: A PEM representation of the key.
41+
public init(pemRepresentation: String) throws {
42+
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
43+
self = try .init(derRepresentation: document.derBytes)
44+
}
45+
46+
/// Creates a Curve25519 private key for signing from a Distinguished Encoding
47+
/// Rules (DER) encoded representation.
48+
///
49+
/// - Parameters:
50+
/// - derRepresentation: A DER-encoded representation of the key.
51+
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
52+
let bytes = Array(derRepresentation)
53+
let key = try ASN1.PKCS8PrivateKey(derEncoded: bytes)
54+
self = try .init(rawRepresentation: key.privateKey.bytes)
55+
}
56+
}
57+
58+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
59+
extension Curve25519.Signing.PublicKey {
60+
/// A Distinguished Encoding Rules (DER) encoded representation of the public key.
61+
public var derRepresentation: Data {
62+
let spki = SubjectPublicKeyInfo(algorithmIdentifier: .ed25519, key: Array(self.rawRepresentation))
63+
var serializer = DER.Serializer()
64+
65+
try! serializer.serialize(spki)
66+
return Data(serializer.serializedBytes)
67+
}
68+
69+
/// A Privacy-Enhanced Mail (PEM) representation of the public key.
70+
public var pemRepresentation: String {
71+
let pemDocument = ASN1.PEMDocument(type: "PUBLIC KEY", derBytes: self.derRepresentation)
72+
return pemDocument.pemString
73+
}
74+
75+
/// Creates a Curve25519 public key for signing from a Privacy-Enhanced Mail
76+
/// (PEM) representation.
77+
///
78+
/// - Parameters:
79+
/// - pemRepresentation: A PEM representation of the key.
80+
public init(pemRepresentation: String) throws {
81+
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
82+
self = try .init(derRepresentation: document.derBytes)
83+
}
84+
85+
/// Creates a Curve25519 public key for signing from a Distinguished Encoding
86+
/// Rules (DER) encoded representation.
87+
///
88+
/// - Parameters:
89+
/// - derRepresentation: A DER-encoded representation of the key.
90+
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
91+
let bytes = Array(derRepresentation)
92+
let spki = try SubjectPublicKeyInfo(derEncoded: bytes)
93+
guard spki.algorithmIdentifier == .ed25519 else {
94+
throw CryptoKitASN1Error.invalidASN1Object
95+
}
96+
self = try .init(rawRepresentation: spki.key.bytes)
97+
}
98+
}
99+
100+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
101+
extension Curve25519.KeyAgreement.PrivateKey {
102+
/// A Distinguished Encoding Rules (DER) encoded representation of the private key.
103+
public var derRepresentation: Data {
104+
let pkey = ASN1.PKCS8PrivateKey(algorithm: .x25519, privateKey: Array(self.rawRepresentation))
105+
var serializer = DER.Serializer()
106+
107+
// Serializing this key can't throw
108+
try! serializer.serialize(pkey)
109+
return Data(serializer.serializedBytes)
110+
}
111+
112+
/// A Privacy-Enhanced Mail (PEM) representation of the private key.
113+
public var pemRepresentation: String {
114+
let pemDocument = ASN1.PEMDocument(type: "PRIVATE KEY", derBytes: self.derRepresentation)
115+
return pemDocument.pemString
116+
}
117+
118+
/// Creates a Curve25519 private key for key agreement from a Privacy-Enhanced Mail
119+
/// (PEM) representation.
120+
///
121+
/// - Parameters:
122+
/// - pemRepresentation: A PEM representation of the key.
123+
public init(pemRepresentation: String) throws {
124+
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
125+
self = try .init(derRepresentation: document.derBytes)
126+
}
127+
128+
/// Creates a Curve25519 private key for key agreement from a Distinguished Encoding
129+
/// Rules (DER) encoded representation.
130+
///
131+
/// - Parameters:
132+
/// - derRepresentation: A DER-encoded representation of the key.
133+
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
134+
let bytes = Array(derRepresentation)
135+
let key = try ASN1.PKCS8PrivateKey(derEncoded: bytes)
136+
self = try .init(rawRepresentation: key.privateKey.bytes)
137+
}
138+
}
139+
140+
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
141+
extension Curve25519.KeyAgreement.PublicKey {
142+
/// A Distinguished Encoding Rules (DER) encoded representation of the public key.
143+
public var derRepresentation: Data {
144+
let spki = SubjectPublicKeyInfo(algorithmIdentifier: .x25519, key: Array(self.rawRepresentation))
145+
var serializer = DER.Serializer()
146+
147+
try! serializer.serialize(spki)
148+
return Data(serializer.serializedBytes)
149+
}
150+
151+
/// A Privacy-Enhanced Mail (PEM) representation of the public key.
152+
public var pemRepresentation: String {
153+
let pemDocument = ASN1.PEMDocument(type: "PUBLIC KEY", derBytes: self.derRepresentation)
154+
return pemDocument.pemString
155+
}
156+
157+
/// Creates a Curve25519 public key for key agreement from a Privacy-Enhanced Mail
158+
/// (PEM) representation.
159+
///
160+
/// - Parameters:
161+
/// - pemRepresentation: A PEM representation of the key.
162+
public init(pemRepresentation: String) throws {
163+
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
164+
self = try .init(derRepresentation: document.derBytes)
165+
}
166+
167+
/// Creates a Curve25519 public key for key agreement from a Distinguished Encoding
168+
/// Rules (DER) encoded representation.
169+
///
170+
/// - Parameters:
171+
/// - derRepresentation: A DER-encoded representation of the key.
172+
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
173+
let bytes = Array(derRepresentation)
174+
let spki = try SubjectPublicKeyInfo(derEncoded: bytes)
175+
guard spki.algorithmIdentifier == .x25519 else {
176+
throw CryptoKitASN1Error.invalidASN1Object
177+
}
178+
self = try .init(rawRepresentation: spki.key.bytes)
179+
}
180+
}

Sources/CryptoExtras/EC/PKCS8DERRepresentation.swift

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,15 @@ import SwiftASN1
2020
extension Curve25519.Signing.PrivateKey {
2121
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
2222
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)
23+
self.derRepresentation
2924
}
3025
}
3126

3227
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
3328
extension Curve25519.KeyAgreement.PrivateKey {
3429
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
3530
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)
31+
self.derRepresentation
4232
}
4333
}
4434

Sources/CryptoExtras/Util/SubjectPublicKeyInfo.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,10 @@ extension SubjectPublicKeyInfo {
125125
return serializer.serializedBytes
126126
}
127127
}
128+
129+
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
130+
extension RFC5480AlgorithmIdentifier {
131+
static let ed25519 = RFC5480AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idEd25519, parameters: nil)
132+
133+
static let x25519 = RFC5480AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idX25519, parameters: nil)
134+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 CryptoExtras
16+
import XCTest
17+
18+
final class Curve25519DERTests: XCTestCase {
19+
func testSigningPrivateKeyDERRoundTrip() throws {
20+
let privateKey = Curve25519.Signing.PrivateKey()
21+
22+
let der = privateKey.derRepresentation
23+
let imported = try Curve25519.Signing.PrivateKey(derRepresentation: der)
24+
25+
XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation)
26+
}
27+
28+
func testSigningPublicKeyDERRoundTrip() throws {
29+
let privateKey = Curve25519.Signing.PrivateKey()
30+
let publicKey = privateKey.publicKey
31+
32+
let der = publicKey.derRepresentation
33+
let imported = try Curve25519.Signing.PublicKey(derRepresentation: der)
34+
35+
XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation)
36+
}
37+
38+
func testKeyAgreementPrivateKeyDERRoundTrip() throws {
39+
let privateKey = Curve25519.KeyAgreement.PrivateKey()
40+
41+
let der = privateKey.derRepresentation
42+
let imported = try Curve25519.KeyAgreement.PrivateKey(derRepresentation: der)
43+
44+
XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation)
45+
}
46+
47+
func testKeyAgreementPublicKeyDERRoundTrip() throws {
48+
let privateKey = Curve25519.KeyAgreement.PrivateKey()
49+
let publicKey = privateKey.publicKey
50+
51+
let der = publicKey.derRepresentation
52+
let imported = try Curve25519.KeyAgreement.PublicKey(derRepresentation: der)
53+
54+
XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation)
55+
}
56+
57+
func testInvalidDERThrows() throws {
58+
let invalidDER: [UInt8] = [0x01, 0x02, 0x03]
59+
60+
XCTAssertThrowsError(try Curve25519.Signing.PrivateKey(derRepresentation: invalidDER))
61+
XCTAssertThrowsError(try Curve25519.Signing.PublicKey(derRepresentation: invalidDER))
62+
XCTAssertThrowsError(try Curve25519.KeyAgreement.PrivateKey(derRepresentation: invalidDER))
63+
XCTAssertThrowsError(try Curve25519.KeyAgreement.PublicKey(derRepresentation: invalidDER))
64+
}
65+
66+
func testImportOpenSSLSigningPrivateKeyDER() throws {
67+
// DER extracted from an OpenSSL-generated Ed25519 PKCS#8 PEM
68+
let derBytes: [UInt8] = [
69+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70,
70+
0x04, 0x22, 0x04, 0x20, 0x3f, 0x10, 0x52, 0x03, 0xb4, 0x06, 0x87, 0x0c,
71+
0x51, 0x71, 0xfa, 0xdc, 0x8f, 0x96, 0xbd, 0x2a, 0x5f, 0x42, 0xac, 0x5c,
72+
0xb9, 0x5b, 0x27, 0x4e, 0xf0, 0x06, 0xe5, 0x61, 0x6a, 0x12, 0x00, 0xa5,
73+
]
74+
75+
let key = try Curve25519.Signing.PrivateKey(derRepresentation: derBytes)
76+
XCTAssertEqual(key.rawRepresentation.count, 32)
77+
}
78+
79+
func testImportOpenSSLKeyAgreementPrivateKeyDER() throws {
80+
// DER extracted from an OpenSSL-generated X25519 PKCS#8 PEM
81+
let derBytes: [UInt8] = [
82+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e,
83+
0x04, 0x22, 0x04, 0x20, 0xb8, 0x38, 0x3f, 0x28, 0xea, 0x8f, 0x1d, 0x71,
84+
0x49, 0xa2, 0xa3, 0x91, 0x37, 0x00, 0xa1, 0x0c, 0x7c, 0x9d, 0xa9, 0x59,
85+
0x28, 0x2d, 0x14, 0x7e, 0x9b, 0x1e, 0x1b, 0x8c, 0x04, 0xa5, 0xd8, 0x47,
86+
]
87+
88+
let key = try Curve25519.KeyAgreement.PrivateKey(derRepresentation: derBytes)
89+
XCTAssertEqual(key.rawRepresentation.count, 32)
90+
}
91+
}

0 commit comments

Comments
 (0)