Skip to content

Commit

Permalink
feat: Key import and export in raw, SPKI, PKCS8 and JWK formats
Browse files Browse the repository at this point in the history
fix: JSONWebKey validation for OKP type
chore: Privacy info for Appstore
  • Loading branch information
amosavian committed Feb 11, 2024
1 parent d9c1332 commit e510573
Show file tree
Hide file tree
Showing 20 changed files with 896 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: swift-actions/setup-swift@v1
with:
swift-version: ${{ matrix.swift }}
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get swift version
run: swift --version
- name: Build
Expand Down
5 changes: 5 additions & 0 deletions Sources/JWSETKit/Base/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public enum JSONWebKeyError: JSONWebError, Sendable {
/// Operation is not allowed with given class/struct or key.
case operationNotAllowed

/// Key format is invalid.
case invalidKeyFormat

/// A localized message describing what error occurred.
public func localizedError(for locale: Locale) -> String {
switch self {
Expand All @@ -60,6 +63,8 @@ public enum JSONWebKeyError: JSONWebError, Sendable {
return .init(localizingKey: "errorKeyNotFound", locale: locale)
case .operationNotAllowed:
return .init(localizingKey: "errorOperationNotAllowed", locale: locale)
case .invalidKeyFormat:
return .init(localizingKey: "errorInvalidKeyFormat", locale: locale)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/JWSETKit/Cryptography/Algorithms/Algorithms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public struct AnyJSONWebAlgorithm: JSONWebAlgorithm {
}

var keyLength: Int? {
if let result = JSONWebSignatureAlgorithm(rawValue: rawValue).curve?.keySize {
if let result = JSONWebSignatureAlgorithm(rawValue: rawValue).curve?.coordinateSize {
return result * 8
} else if let result = JSONWebKeyEncryptionAlgorithm(rawValue: rawValue).keyLength {
return result
Expand Down Expand Up @@ -153,7 +153,7 @@ extension JSONWebKeyCurve {
]

/// Key size in bytes.
public var keySize: Int? {
public var coordinateSize: Int? {
Self.keySizes[self]
}

Expand Down
72 changes: 72 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/CryptoKitAbstract.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,42 @@ extension CryptoECPublicKey {
}
}

protocol CryptoECPublicKeyPortable: JSONWebKeyImportable, JSONWebKeyExportable {
var x963Representation: Data { get }
var derRepresentation: Data { get }

init(x963Representation: Data) throws
init(derRepresentation: Data) throws
}

extension CryptoECPublicKeyPortable {
public init(importing key: Data, format: JSONWebKeyFormat) throws {
switch format {
case .raw:
try self.init(x963Representation: key)
case .spki:
try self.init(derRepresentation: key)
case .jwk:
self = try JSONDecoder().decode(Self.self, from: key)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}

public func exportKey(format: JSONWebKeyFormat) throws -> Data {
switch format {
case .raw:
return x963Representation
case .spki:
return derRepresentation
case .jwk:
return try JSONEncoder().encode(self)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}
}

protocol CryptoECPrivateKey: JSONWebKey {
associatedtype PublicKey: CryptoECPublicKey

Expand Down Expand Up @@ -80,3 +116,39 @@ extension CryptoECPrivateKey {
lhs.publicKey == rhs.publicKey
}
}

protocol CryptoECPrivateKeyPortable: JSONWebKeyImportable, JSONWebKeyExportable {
var x963Representation: Data { get }
var derRepresentation: Data { get }

init(x963Representation: Data) throws
init(derRepresentation: Data) throws
}

extension CryptoECPrivateKeyPortable {
public init(importing key: Data, format: JSONWebKeyFormat) throws {
switch format {
case .raw:
try self.init(x963Representation: key)
case .pkcs8:
try self.init(derRepresentation: key)
case .jwk:
self = try JSONDecoder().decode(Self.self, from: key)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}

public func exportKey(format: JSONWebKeyFormat) throws -> Data {
switch format {
case .raw:
return x963Representation
case .pkcs8:
return derRepresentation
case .jwk:
return try JSONEncoder().encode(self)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}
}
38 changes: 38 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/Ed25519.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ extension Curve25519.Signing.PublicKey: JSONWebValidatingKey {
}
}

extension Curve25519.Signing.PublicKey: CryptoEdKeyPortable {}

extension Curve25519.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
public init(algorithm _: any JSONWebAlgorithm) throws {
self.init()
Expand All @@ -42,6 +44,8 @@ extension Curve25519.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
}
}

extension Curve25519.Signing.PrivateKey: CryptoEdKeyPortable {}

extension Curve25519.KeyAgreement.PublicKey: CryptoECPublicKey {
static var curve: JSONWebKeyCurve { .x25519 }

Expand All @@ -54,8 +58,42 @@ extension Curve25519.KeyAgreement.PublicKey: CryptoECPublicKey {
}
}

extension Curve25519.KeyAgreement.PublicKey: CryptoEdKeyPortable {}

extension Curve25519.KeyAgreement.PrivateKey: CryptoECPrivateKey {
public init(algorithm _: any JSONWebAlgorithm) throws {
self.init()
}
}

extension Curve25519.KeyAgreement.PrivateKey: CryptoEdKeyPortable {}

protocol CryptoEdKeyPortable: JSONWebKeyImportable, JSONWebKeyExportable {
var rawRepresentation: Data { get }

init(rawRepresentation: Data) throws
}

extension CryptoEdKeyPortable {
public init(importing key: Data, format: JSONWebKeyFormat) throws {
switch format {
case .raw:
try self.init(rawRepresentation: key)
case .jwk:
self = try JSONDecoder().decode(Self.self, from: key)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}

public func exportKey(format: JSONWebKeyFormat) throws -> Data {
switch format {
case .raw:
return rawRepresentation
case .jwk:
return try JSONEncoder().encode(self)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}
}
78 changes: 78 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/JWK-EC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,44 @@ public struct JSONWebECPublicKey: MutableJSONWebKey, JSONWebValidatingKey, Senda
}
}

extension JSONWebKeyImportable {
fileprivate init(
key: Data, format: JSONWebKeyFormat,
keyLengthTable: [Int: JSONWebKeyCurve],
keyFinder: (_ keyType: JSONWebKeyType, _ curve: JSONWebKeyCurve) throws -> any JSONWebValidatingKey.Type
) throws {
guard let curve = keyLengthTable[key.count] else {
throw JSONWebKeyError.unknownAlgorithm
}
guard let type = try keyFinder(.ellipticCurve, curve) as? any JSONWebKeyImportable.Type else {
throw JSONWebKeyError.unknownAlgorithm
}
try self = Self.create(storage: type.init(importing: key, format: format).storage)
}
}

extension JSONWebECPublicKey: JSONWebKeyImportable, JSONWebKeyExportable {
public init(importing key: Data, format: JSONWebKeyFormat) throws {
switch format {
case .raw:
try self.init(key: key, format: format, keyLengthTable: JSONWebKeyCurve.publicRawCurve, keyFinder: Self.singingType(for:_:))
case .spki:
try self.init(key: key, format: format, keyLengthTable: JSONWebKeyCurve.spkiCurve, keyFinder: Self.singingType(for:_:))
case .jwk:
self = try JSONDecoder().decode(Self.self, from: key)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}

public func exportKey(format: JSONWebKeyFormat) throws -> Data {
guard let underlyingKey = (try? underlyingKey) as? (any JSONWebKeyExportable) else {
throw JSONWebKeyError.unknownKeyType
}
return try underlyingKey.exportKey(format: format)
}
}

/// JWK container for different types of Elliptic-Curve private keys consists of P-256, P-384, P-521, Ed25519.
public struct JSONWebECPrivateKey: MutableJSONWebKey, JSONWebSigningKey, Sendable {
public var storage: JSONWebValueStorage
Expand Down Expand Up @@ -108,6 +146,28 @@ public struct JSONWebECPrivateKey: MutableJSONWebKey, JSONWebSigningKey, Sendabl
}
}

extension JSONWebECPrivateKey: JSONWebKeyImportable, JSONWebKeyExportable {
public init(importing key: Data, format: JSONWebKeyFormat) throws {
switch format {
case .raw:
try self.init(key: key, format: format, keyLengthTable: JSONWebKeyCurve.privateRawCurve, keyFinder: Self.singingType(for:_:))
case .pkcs8:
try self.init(key: key, format: format, keyLengthTable: JSONWebKeyCurve.pkc8Curve, keyFinder: Self.singingType(for:_:))
case .jwk:
self = try JSONDecoder().decode(Self.self, from: key)
default:
throw JSONWebKeyError.invalidKeyFormat
}
}

public func exportKey(format: JSONWebKeyFormat) throws -> Data {
guard let underlyingKey = (try? underlyingKey) as? (any JSONWebKeyExportable) else {
throw JSONWebKeyError.unknownKeyType
}
return try underlyingKey.exportKey(format: format)
}
}

enum ECHelper {
static func ecComponents(_ data: Data, keyLength: Int) throws -> [Data] {
var data = data
Expand Down Expand Up @@ -151,3 +211,21 @@ enum ECHelper {
}
}
}

extension JSONWebKeyCurve {
fileprivate static let publicRawCurve: [Int: Self] = [
65: .p256, 32: .ed25519, 97: .p384, 133: .p521,
]

fileprivate static let privateRawCurve: [Int: Self] = [
97: .p256, 32: .ed25519, 145: .p384, 199: .p521,
]

fileprivate static let spkiCurve: [Int: Self] = [
91: .p256, 120: .p384, 158: .p521,
]

fileprivate static let pkc8Curve: [Int: Self] = [
138: .p256, 185: .p384, 241: .p521,
]
}
4 changes: 4 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/P256.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ extension P256.Signing.PublicKey: JSONWebValidatingKey {
}
}

extension P256.Signing.PublicKey: CryptoECPublicKeyPortable {}

extension P256.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
public init(algorithm _: any JSONWebAlgorithm) throws {
self.init(compactRepresentable: true)
Expand All @@ -35,6 +37,8 @@ extension P256.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
}
}

extension P256.Signing.PrivateKey: CryptoECPrivateKeyPortable {}

#if canImport(Darwin)
extension SecureEnclave.P256.Signing.PrivateKey: CryptoECPrivateKey {
public var storage: JSONWebValueStorage {
Expand Down
4 changes: 4 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/P384.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ extension P384.Signing.PublicKey: JSONWebValidatingKey {
}
}

extension P384.Signing.PublicKey: CryptoECPublicKeyPortable {}

extension P384.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
public init(algorithm _: any JSONWebAlgorithm) throws {
self.init(compactRepresentable: true)
Expand All @@ -34,3 +36,5 @@ extension P384.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
try signature(for: SHA384.hash(data: data)).rawRepresentation
}
}

extension P384.Signing.PrivateKey: CryptoECPrivateKeyPortable {}
4 changes: 4 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/P521.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ extension P521.Signing.PublicKey: JSONWebValidatingKey {
}
}

extension P521.Signing.PublicKey: CryptoECPublicKeyPortable {}

extension P521.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
public init(algorithm _: any JSONWebAlgorithm) throws {
self.init(compactRepresentable: true)
Expand All @@ -34,3 +36,5 @@ extension P521.Signing.PrivateKey: JSONWebSigningKey, CryptoECPrivateKey {
try signature(for: SHA512.hash(data: data)).rawRepresentation
}
}

extension P521.Signing.PrivateKey: CryptoECPrivateKeyPortable {}
Loading

0 comments on commit e510573

Please sign in to comment.