Skip to content
Open
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
180 changes: 180 additions & 0 deletions Sources/CryptoExtras/EC/Curve25519+PEM.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftCrypto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Crypto
import Foundation
import SwiftASN1

@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
extension Curve25519.Signing.PrivateKey {
/// A Distinguished Encoding Rules (DER) encoded representation of the private key.
public var derRepresentation: Data {
let pkey = ASN1.PKCS8PrivateKey(algorithm: .ed25519, privateKey: Array(self.rawRepresentation))
var serializer = DER.Serializer()

try! serializer.serialize(pkey)
return Data(serializer.serializedBytes)
}

/// A Privacy-Enhanced Mail (PEM) representation of the private key.
public var pemRepresentation: String {
let pemDocument = ASN1.PEMDocument(type: "PRIVATE KEY", derBytes: self.derRepresentation)
return pemDocument.pemString
}

/// Creates a Curve25519 private key for signing from a Privacy-Enhanced Mail
/// (PEM) representation.
///
/// - Parameters:
/// - pemRepresentation: A PEM representation of the key.
public init(pemRepresentation: String) throws {
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
self = try .init(derRepresentation: document.derBytes)
}

/// Creates a Curve25519 private key for signing from a Distinguished Encoding
/// Rules (DER) encoded representation.
///
/// - Parameters:
/// - derRepresentation: A DER-encoded representation of the key.
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
let bytes = Array(derRepresentation)
let key = try ASN1.PKCS8PrivateKey(derEncoded: bytes)
self = try .init(rawRepresentation: key.privateKey.bytes)
}
}

@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
extension Curve25519.Signing.PublicKey {
/// A Distinguished Encoding Rules (DER) encoded representation of the public key.
public var derRepresentation: Data {
let spki = SubjectPublicKeyInfo(algorithmIdentifier: .ed25519, key: Array(self.rawRepresentation))
var serializer = DER.Serializer()

try! serializer.serialize(spki)
return Data(serializer.serializedBytes)
}

/// A Privacy-Enhanced Mail (PEM) representation of the public key.
public var pemRepresentation: String {
let pemDocument = ASN1.PEMDocument(type: "PUBLIC KEY", derBytes: self.derRepresentation)
return pemDocument.pemString
}

/// Creates a Curve25519 public key for signing from a Privacy-Enhanced Mail
/// (PEM) representation.
///
/// - Parameters:
/// - pemRepresentation: A PEM representation of the key.
public init(pemRepresentation: String) throws {
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
self = try .init(derRepresentation: document.derBytes)
}

/// Creates a Curve25519 public key for signing from a Distinguished Encoding
/// Rules (DER) encoded representation.
///
/// - Parameters:
/// - derRepresentation: A DER-encoded representation of the key.
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
let bytes = Array(derRepresentation)
let spki = try SubjectPublicKeyInfo(derEncoded: bytes)
guard spki.algorithmIdentifier == .ed25519 else {
throw CryptoKitASN1Error.invalidASN1Object
}
self = try .init(rawRepresentation: spki.key.bytes)
}
}

@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
extension Curve25519.KeyAgreement.PrivateKey {
/// A Distinguished Encoding Rules (DER) encoded representation of the private key.
public var derRepresentation: Data {
let pkey = ASN1.PKCS8PrivateKey(algorithm: .x25519, privateKey: Array(self.rawRepresentation))
var serializer = DER.Serializer()

// Serializing this key can't throw
try! serializer.serialize(pkey)
return Data(serializer.serializedBytes)
}

/// A Privacy-Enhanced Mail (PEM) representation of the private key.
public var pemRepresentation: String {
let pemDocument = ASN1.PEMDocument(type: "PRIVATE KEY", derBytes: self.derRepresentation)
return pemDocument.pemString
}

/// Creates a Curve25519 private key for key agreement from a Privacy-Enhanced Mail
/// (PEM) representation.
///
/// - Parameters:
/// - pemRepresentation: A PEM representation of the key.
public init(pemRepresentation: String) throws {
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
self = try .init(derRepresentation: document.derBytes)
}

/// Creates a Curve25519 private key for key agreement from a Distinguished Encoding
/// Rules (DER) encoded representation.
///
/// - Parameters:
/// - derRepresentation: A DER-encoded representation of the key.
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
let bytes = Array(derRepresentation)
let key = try ASN1.PKCS8PrivateKey(derEncoded: bytes)
self = try .init(rawRepresentation: key.privateKey.bytes)
}
}

@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
extension Curve25519.KeyAgreement.PublicKey {
/// A Distinguished Encoding Rules (DER) encoded representation of the public key.
public var derRepresentation: Data {
let spki = SubjectPublicKeyInfo(algorithmIdentifier: .x25519, key: Array(self.rawRepresentation))
var serializer = DER.Serializer()

try! serializer.serialize(spki)
return Data(serializer.serializedBytes)
}

/// A Privacy-Enhanced Mail (PEM) representation of the public key.
public var pemRepresentation: String {
let pemDocument = ASN1.PEMDocument(type: "PUBLIC KEY", derBytes: self.derRepresentation)
return pemDocument.pemString
}

/// Creates a Curve25519 public key for key agreement from a Privacy-Enhanced Mail
/// (PEM) representation.
///
/// - Parameters:
/// - pemRepresentation: A PEM representation of the key.
public init(pemRepresentation: String) throws {
let document = try ASN1.PEMDocument(pemString: pemRepresentation)
self = try .init(derRepresentation: document.derBytes)
}

/// Creates a Curve25519 public key for key agreement from a Distinguished Encoding
/// Rules (DER) encoded representation.
///
/// - Parameters:
/// - derRepresentation: A DER-encoded representation of the key.
public init<Bytes: RandomAccessCollection>(derRepresentation: Bytes) throws where Bytes.Element == UInt8 {
let bytes = Array(derRepresentation)
let spki = try SubjectPublicKeyInfo(derEncoded: bytes)
guard spki.algorithmIdentifier == .x25519 else {
throw CryptoKitASN1Error.invalidASN1Object
}
self = try .init(rawRepresentation: spki.key.bytes)
}
}
14 changes: 2 additions & 12 deletions Sources/CryptoExtras/EC/PKCS8DERRepresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,15 @@ import SwiftASN1
extension Curve25519.Signing.PrivateKey {
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
public var pkcs8DERRepresentation: Data {
let pkey = ASN1.PKCS8PrivateKey(algorithm: .ed25519, privateKey: Array(self.rawRepresentation))
var serializer = DER.Serializer()

// Serializing this key can't throw
try! serializer.serialize(pkey)
return Data(serializer.serializedBytes)
self.derRepresentation
}
}

@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
extension Curve25519.KeyAgreement.PrivateKey {
/// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format.
public var pkcs8DERRepresentation: Data {
let pkey = ASN1.PKCS8PrivateKey(algorithm: .x25519, privateKey: Array(self.rawRepresentation))
var serializer = DER.Serializer()

// Serializing this key can't throw
try! serializer.serialize(pkey)
return Data(serializer.serializedBytes)
self.derRepresentation
}
}

Expand Down
7 changes: 7 additions & 0 deletions Sources/CryptoExtras/Util/SubjectPublicKeyInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,10 @@ extension SubjectPublicKeyInfo {
return serializer.serializedBytes
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension RFC5480AlgorithmIdentifier {
static let ed25519 = RFC5480AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idEd25519, parameters: nil)

static let x25519 = RFC5480AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idX25519, parameters: nil)
}
91 changes: 91 additions & 0 deletions Tests/CryptoExtrasTests/Curve25519DERTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftCrypto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import CryptoExtras
import XCTest

final class Curve25519DERTests: XCTestCase {
func testSigningPrivateKeyDERRoundTrip() throws {
let privateKey = Curve25519.Signing.PrivateKey()

let der = privateKey.derRepresentation
let imported = try Curve25519.Signing.PrivateKey(derRepresentation: der)

XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation)
}

func testSigningPublicKeyDERRoundTrip() throws {
let privateKey = Curve25519.Signing.PrivateKey()
let publicKey = privateKey.publicKey

let der = publicKey.derRepresentation
let imported = try Curve25519.Signing.PublicKey(derRepresentation: der)

XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation)
}

func testKeyAgreementPrivateKeyDERRoundTrip() throws {
let privateKey = Curve25519.KeyAgreement.PrivateKey()

let der = privateKey.derRepresentation
let imported = try Curve25519.KeyAgreement.PrivateKey(derRepresentation: der)

XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation)
}

func testKeyAgreementPublicKeyDERRoundTrip() throws {
let privateKey = Curve25519.KeyAgreement.PrivateKey()
let publicKey = privateKey.publicKey

let der = publicKey.derRepresentation
let imported = try Curve25519.KeyAgreement.PublicKey(derRepresentation: der)

XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation)
}

func testInvalidDERThrows() throws {
let invalidDER: [UInt8] = [0x01, 0x02, 0x03]

XCTAssertThrowsError(try Curve25519.Signing.PrivateKey(derRepresentation: invalidDER))
XCTAssertThrowsError(try Curve25519.Signing.PublicKey(derRepresentation: invalidDER))
XCTAssertThrowsError(try Curve25519.KeyAgreement.PrivateKey(derRepresentation: invalidDER))
XCTAssertThrowsError(try Curve25519.KeyAgreement.PublicKey(derRepresentation: invalidDER))
}

func testImportOpenSSLSigningPrivateKeyDER() throws {
// DER extracted from an OpenSSL-generated Ed25519 PKCS#8 PEM
let derBytes: [UInt8] = [
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70,
0x04, 0x22, 0x04, 0x20, 0x3f, 0x10, 0x52, 0x03, 0xb4, 0x06, 0x87, 0x0c,
0x51, 0x71, 0xfa, 0xdc, 0x8f, 0x96, 0xbd, 0x2a, 0x5f, 0x42, 0xac, 0x5c,
0xb9, 0x5b, 0x27, 0x4e, 0xf0, 0x06, 0xe5, 0x61, 0x6a, 0x12, 0x00, 0xa5,
]

let key = try Curve25519.Signing.PrivateKey(derRepresentation: derBytes)
XCTAssertEqual(key.rawRepresentation.count, 32)
}

func testImportOpenSSLKeyAgreementPrivateKeyDER() throws {
// DER extracted from an OpenSSL-generated X25519 PKCS#8 PEM
let derBytes: [UInt8] = [
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e,
0x04, 0x22, 0x04, 0x20, 0xb8, 0x38, 0x3f, 0x28, 0xea, 0x8f, 0x1d, 0x71,
0x49, 0xa2, 0xa3, 0x91, 0x37, 0x00, 0xa1, 0x0c, 0x7c, 0x9d, 0xa9, 0x59,
0x28, 0x2d, 0x14, 0x7e, 0x9b, 0x1e, 0x1b, 0x8c, 0x04, 0xa5, 0xd8, 0x47,
]

let key = try Curve25519.KeyAgreement.PrivateKey(derRepresentation: derBytes)
XCTAssertEqual(key.rawRepresentation.count, 32)
}
}
Loading