Skip to content

Commit

Permalink
Add support for Ed25519 certificates
Browse files Browse the repository at this point in the history
Motivation

While Ed25519 is not available in the WebPKI, there
are a number of contexts where it's an appropriate
signature algorithm for X.509. In those contexts,
we should allow the signature algorithm.

Modifications

Add support for Ed25519 keys and signatures.
Add unit tests

Result

Ed25519 support in the X509 certs
  • Loading branch information
Lukasa committed Oct 29, 2024
1 parent c0b6a47 commit 9ee1fe6
Show file tree
Hide file tree
Showing 10 changed files with 352 additions and 14 deletions.
35 changes: 34 additions & 1 deletion Sources/X509/CertificatePrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ extension Certificate {
self.backing = .rsa(rsa)
}

/// Construct a private key wrapping an Ed25519 private key.
/// - Parameter ed25519: The Ed25519 private key to wrap.
@inlinable
public init(_ ed25519: Curve25519.Signing.PrivateKey) {
self.backing = .ed25519(ed25519)
}

#if canImport(Darwin)
/// Construct a private key wrapping a SecureEnclave.P256 private key.
/// - Parameter secureEnclaveP256: The SecureEnclave.P256 private key to wrap.
Expand All @@ -85,24 +92,31 @@ extension Certificate {
signatureAlgorithm: SignatureAlgorithm
) throws -> Signature {
try self.validateAlgorithmForKey(algorithm: signatureAlgorithm)
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)

switch self.backing {
case .p256(let p256):
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
return try p256.signature(for: bytes, digestAlgorithm: digestAlgorithm)
case .p384(let p384):
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
return try p384.signature(for: bytes, digestAlgorithm: digestAlgorithm)
case .p521(let p521):
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
return try p521.signature(for: bytes, digestAlgorithm: digestAlgorithm)
case .rsa(let rsa):
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
let padding = try _RSA.Signing.Padding(forSignatureAlgorithm: signatureAlgorithm)
return try rsa.signature(for: bytes, digestAlgorithm: digestAlgorithm, padding: padding)
#if canImport(Darwin)
case .secureEnclaveP256(let secureEnclaveP256):
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
return try secureEnclaveP256.signature(for: bytes, digestAlgorithm: digestAlgorithm)
case .secKey(let secKeyWrapper):
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
return try secKeyWrapper.signature(for: bytes, digestAlgorithm: digestAlgorithm)
#endif
case .ed25519(let ed25519):
return try ed25519.signature(for: bytes)
}
}

Expand All @@ -125,6 +139,8 @@ extension Certificate {
case .secKey(let secKeyWrapper):
return secKeyWrapper.publicKey
#endif
case .ed25519(let ed25519):
return PublicKey(ed25519.publicKey)
}
}

Expand Down Expand Up @@ -166,6 +182,12 @@ extension Certificate {
}
}
#endif
case .ed25519:
if algorithm != .ed25519 {
throw CertificateError.unsupportedSignatureAlgorithm(
reason: "Cannot use \(algorithm) with Ed25519 key \(self)"
)
}
}

}
Expand Down Expand Up @@ -193,6 +215,8 @@ extension Certificate.PrivateKey: CustomStringConvertible {
case .secKey:
return "SecKey"
#endif
case .ed25519:
return "Ed25519.PrivateKey"
}
}
}
Expand All @@ -208,6 +232,7 @@ extension Certificate.PrivateKey {
case secureEnclaveP256(SecureEnclave.P256.Signing.PrivateKey)
case secKey(SecKeyWrapper)
#endif
case ed25519(Crypto.Curve25519.Signing.PrivateKey)

@inlinable
static func == (lhs: BackingPrivateKey, rhs: BackingPrivateKey) -> Bool {
Expand All @@ -226,6 +251,8 @@ extension Certificate.PrivateKey {
case (.secKey(let l), .secKey(let r)):
return l.publicKey.backing == r.publicKey.backing
#endif
case (.ed25519(let l), .ed25519(let r)):
return l.rawRepresentation == r.rawRepresentation
default:
return false
}
Expand Down Expand Up @@ -255,6 +282,9 @@ extension Certificate.PrivateKey {
hasher.combine(secKeyWrapper.privateKey.hashValue)
hasher.combine(secKeyWrapper.publicKey.hashValue)
#endif
case .ed25519(let digest):
hasher.combine(6)
hasher.combine(digest.rawRepresentation)
}
}
}
Expand Down Expand Up @@ -300,6 +330,8 @@ extension Certificate.PrivateKey {

case .rsaKey:
self = try .init(_CryptoExtras._RSA.Signing.PrivateKey(derRepresentation: pkcs8.privateKey.bytes))
case .ed25519:
self = try .init(Curve25519.Signing.PrivateKey(pkcs8Key: pkcs8))
default:
throw CertificateError.unsupportedPrivateKey(reason: "unknown algorithm \(pkcs8.algorithm)")
}
Expand Down Expand Up @@ -342,6 +374,7 @@ extension Certificate.PrivateKey {
)
case .secKey(let key): return try key.pemDocument()
#endif
case .ed25519(let key): return key.pemRepresentation
}
}
}
60 changes: 49 additions & 11 deletions Sources/X509/CertificatePublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ extension Certificate {
_ = try RSAPKCS1PublicKey(derEncoded: spki.key.bytes)
let key = try _RSA.Signing.PublicKey(derRepresentation: spki.key.bytes)
self.backing = .rsa(key)
case .ed25519:
let key = try Curve25519.Signing.PublicKey(rawRepresentation: spki.key.bytes)
self.backing = .ed25519(key)
default:
throw CertificateError.unsupportedPublicKeyAlgorithm(reason: "\(spki.algorithmIdentifier)")
}
Expand Down Expand Up @@ -83,6 +86,13 @@ extension Certificate {
public init(_ rsa: _RSA.Signing.PublicKey) {
self.backing = .rsa(rsa)
}

/// Construct a public key wrapping an Ed25519 public key.
/// - Parameter ed25519: The Ed25519 public key to wrap.
@inlinable
public init(_ ed25519: Curve25519.Signing.PublicKey) {
self.backing = .ed25519(ed25519)
}
}
}

Expand Down Expand Up @@ -127,22 +137,20 @@ extension Certificate.PublicKey {
for bytes: Bytes,
signatureAlgorithm: Certificate.SignatureAlgorithm
) -> Bool {
let digest: Digest
do {
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
digest = try Digest.computeDigest(for: bytes, using: digestAlgorithm)
} catch {
return false
var digest: Digest?

if let digestAlgorithm = try? AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) {
digest = try? Digest.computeDigest(for: bytes, using: digestAlgorithm)
}

switch self.backing {
case .p256(let p256):
switch (self.backing, digest) {
case (.p256(let p256), .some(let digest)):
return p256.isValidSignature(signature, for: digest)
case .p384(let p384):
case (.p384(let p384), .some(let digest)):
return p384.isValidSignature(signature, for: digest)
case .p521(let p521):
case (.p521(let p521), .some(let digest)):
return p521.isValidSignature(signature, for: digest)
case .rsa(let rsa):
case (.rsa(let rsa), .some(let digest)):
// For now we don't support RSA PSS, as it's not deployed in the WebPKI.
// We could, if there are sufficient user needs.
do {
Expand All @@ -151,6 +159,10 @@ extension Certificate.PublicKey {
} catch {
return false
}
case (.ed25519(let ed25519), .none):
return ed25519.isValidSignature(signature, for: bytes)
default:
return false
}
}
}
Expand All @@ -170,6 +182,8 @@ extension Certificate.PublicKey: CustomStringConvertible {
return "P521.PublicKey"
case .rsa(let publicKey):
return "RSA\(publicKey.keySizeInBits).PublicKey"
case .ed25519:
return "Ed25519.PublicKey"
}
}
}
Expand All @@ -181,6 +195,7 @@ extension Certificate.PublicKey {
case p384(Crypto.P384.Signing.PublicKey)
case p521(Crypto.P521.Signing.PublicKey)
case rsa(_CryptoExtras._RSA.Signing.PublicKey)
case ed25519(Curve25519.Signing.PublicKey)

@inlinable
static func == (lhs: BackingPublicKey, rhs: BackingPublicKey) -> Bool {
Expand All @@ -193,6 +208,8 @@ extension Certificate.PublicKey {
return l.rawRepresentation == r.rawRepresentation
case (.rsa(let l), .rsa(let r)):
return l.derRepresentation == r.derRepresentation
case (.ed25519(let l), .ed25519(let r)):
return l.rawRepresentation == r.rawRepresentation
default:
return false
}
Expand All @@ -213,6 +230,9 @@ extension Certificate.PublicKey {
case .rsa(let digest):
hasher.combine(3)
hasher.combine(digest.derRepresentation)
case .ed25519(let digest):
hasher.combine(4)
hasher.combine(digest.rawRepresentation)
}
}
}
Expand All @@ -237,6 +257,9 @@ extension SubjectPublicKeyInfo {
case .rsa(let rsa):
algorithmIdentifier = .rsaKey
key = .init(bytes: ArraySlice(rsa.pkcs1DERRepresentation))
case .ed25519(let ed25519):
algorithmIdentifier = .ed25519
key = .init(bytes: ArraySlice(ed25519.rawRepresentation))
}

self.algorithmIdentifier = algorithmIdentifier
Expand Down Expand Up @@ -329,6 +352,21 @@ extension _RSA.Signing.PublicKey {
}
}

extension Curve25519.Signing.PublicKey {
/// Create a Curve25519 Public Key from a given ``Certificate/PublicKey-swift.struct``.
///
/// Fails if the key is not a Curve25519 key.
///
/// - parameters:
/// - key: The key to unwrap.
public init?(_ key: Certificate.PublicKey) {
guard case .ed25519(let inner) = key.backing else {
return nil
}
self = inner
}
}

extension Certificate.PublicKey: PEMParseable, PEMSerializable {
@inlinable
public static var defaultPEMDiscriminator: String {
Expand Down
44 changes: 44 additions & 0 deletions Sources/X509/Curve25519+DER.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCertificates open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftCertificates project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import SwiftASN1
import Crypto

extension Curve25519.Signing.PrivateKey {
@inlinable
init(pkcs8Key: PKCS8PrivateKey) throws {
// Annoyingly, the PKCS8 key has the raw bytes wrapped inside an octet string.
let rawRepresentation = try ASN1OctetString(derEncoded: pkcs8Key.privateKey.bytes)
self = try .init(rawRepresentation: rawRepresentation.bytes)
}

@inlinable
var derRepresentation: [UInt8] {
// The DER representation we want is a PKCS8 private key. Somewhat annoyingly
// for us, we have to wrap the key bytes in an extra layer of ASN1OctetString
// which we encode separately.
let pkcs8Key = PKCS8PrivateKey(
algorithm: .ed25519, privateKey: ASN1OctetString(contentBytes: ArraySlice(self.rawRepresentation))
)
var serializer = DER.Serializer()
try! serializer.serialize(pkcs8Key)
return serializer.serializedBytes
}

@inlinable
var pemRepresentation: PEMDocument {
return PEMDocument(type: "PRIVATE KEY", derBytes: self.derRepresentation)
}
}
22 changes: 22 additions & 0 deletions Sources/X509/Digests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ extension _RSA.Signing.PublicKey {
}
}

extension Curve25519.Signing.PublicKey {
@inlinable
func isValidSignature<Bytes: DataProtocol>(_ signature: Certificate.Signature, for bytes: Bytes) -> Bool {
guard case .ed25519(let rawInnerSignature) = signature.backing else {
// Signature mismatch
return false
}

return self.isValidSignature(rawInnerSignature, for: bytes)
}
}

// MARK: Private key operations

extension P256.Signing.PrivateKey {
Expand Down Expand Up @@ -255,3 +267,13 @@ extension _RSA.Signing.PrivateKey {
return Certificate.Signature(backing: .rsa(signature))
}
}

extension Curve25519.Signing.PrivateKey {
@inlinable
func signature<Bytes: DataProtocol>(
for bytes: Bytes
) throws -> Certificate.Signature {
let signature: Data = try self.signature(for: bytes)
return Certificate.Signature(backing: .ed25519(.init(signature)))
}
}
4 changes: 2 additions & 2 deletions Sources/X509/PKCS8PrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ struct PKCS8PrivateKey: DERImplicitlyTaggable {
// We ignore the attributes
_ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { _ in }

return try .init(algorithm: algorithm, privateKey: privateKeyBytes)
return .init(algorithm: algorithm, privateKey: privateKeyBytes)
}
}

@inlinable
init(algorithm: AlgorithmIdentifier, privateKey: ASN1OctetString) throws {
init(algorithm: AlgorithmIdentifier, privateKey: ASN1OctetString) {
self.privateKey = privateKey
self.algorithm = algorithm
}
Expand Down
Loading

0 comments on commit 9ee1fe6

Please sign in to comment.