forked from orlandos-nl/Citadel
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: ECDSA key handling and ByteBuffer conversions for OpenSSH format #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a6261ea
feat: implement ECDSA key handling and ByteBuffer conversions for Ope…
nedithgar f10a3fa
feat: add writeSSHString methods to ByteBuffer for improved SSH data …
nedithgar b6c6dd1
feat: enhance ECDSA public key writing with optional curve name and i…
nedithgar a67b386
feat: improve ECDSA private key processing and unify point format han…
nedithgar 5c58576
fix: clarify documentation for readSSHBignum and writeSSHBignum methods
nedithgar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,272 @@ | ||
| import Foundation | ||
| import Crypto | ||
| import _CryptoExtras | ||
| import NIOCore | ||
| import BigInt | ||
|
|
||
| // MARK: - Constants | ||
|
|
||
| /// ECDSA point format identifier for uncompressed points | ||
| /// In the x963 representation, uncompressed points start with 0x04 | ||
| private let uncompressedPointPrefix: UInt8 = 0x04 | ||
|
|
||
| // MARK: - Helper Functions | ||
|
|
||
| /// Writes ECDSA public key data to a buffer in SSH format | ||
| /// - Parameters: | ||
| /// - buffer: The buffer to write to | ||
| /// - curveName: The curve name (e.g., "nistp256", "nistp384", "nistp521"), if provided | ||
| /// - publicKeyData: The public key data in x963 representation | ||
| /// - Returns: The number of bytes written | ||
| @discardableResult | ||
| private func writeECDSAPublicKey(to buffer: inout ByteBuffer, curveName: String? = nil, publicKeyData: Data) -> Int { | ||
| let start = buffer.writerIndex | ||
| if let curveName = curveName { | ||
| buffer.writeSSHString(curveName) | ||
| } | ||
| buffer.writeSSHString(publicKeyData) | ||
| return buffer.writerIndex - start | ||
| } | ||
|
|
||
| /// Processes ECDSA private key data by validating its size and removing the leading zero byte if present. | ||
| /// | ||
| /// SSH bignum format may include a leading zero byte to ensure the number is interpreted as unsigned. | ||
| /// This function removes that zero byte if present and validates that the resulting data matches | ||
| /// the expected key size for the curve. | ||
| /// | ||
| /// - Parameters: | ||
| /// - privateKeyData: The raw private key data from SSH format | ||
| /// - expectedKeySize: The expected size in bytes for the specific curve (32 for P-256, 48 for P-384, 66 for P-521) | ||
| /// - Returns: The processed private key data with the correct size | ||
| /// - Throws: `InvalidOpenSSHKey.invalidLayout` if the data size is invalid | ||
| private func processECDSAPrivateKeyData(_ privateKeyData: Data, expectedKeySize: Int) throws -> Data { | ||
| // Check if we have the expected size with a leading zero byte | ||
| if privateKeyData.count == expectedKeySize + 1 && privateKeyData[0] == 0 { | ||
| // Remove the leading zero byte | ||
| return privateKeyData.dropFirst() | ||
| } else if privateKeyData.count == expectedKeySize { | ||
| // Already the correct size | ||
| return privateKeyData | ||
| } else { | ||
| // Invalid size | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
| } | ||
|
|
||
| extension P256.Signing.PrivateKey: ByteBufferConvertible { | ||
| public static func read(consuming buffer: inout ByteBuffer) throws -> Self { | ||
| guard | ||
| let curveName = buffer.readSSHString(), | ||
| let _ = buffer.readSSHBuffer(), // public key - we don't need it for reconstruction | ||
| let privateKeyData = buffer.readSSHBignum() | ||
| else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| guard curveName == "nistp256" else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| // Process the private key data to validate size and remove leading zero if present | ||
| let keyData = try processECDSAPrivateKeyData(privateKeyData, expectedKeySize: 32) | ||
|
|
||
| return try P256.Signing.PrivateKey(rawRepresentation: keyData) | ||
| } | ||
|
|
||
| public func write(to buffer: inout ByteBuffer) -> Int { | ||
| let start = buffer.writerIndex | ||
|
|
||
| // Write curve name and public key | ||
| writeECDSAPublicKey(to: &buffer, curveName: "nistp256", publicKeyData: publicKey.x963Representation) | ||
|
|
||
| // Write private key as bignum (matching OpenSSH format) | ||
| let privateKeyData = self.rawRepresentation | ||
| let bignum = BigInt(privateKeyData) | ||
| buffer.writeSSHBignum(bignum) | ||
|
|
||
| return buffer.writerIndex - start | ||
| } | ||
| } | ||
|
|
||
| extension P384.Signing.PrivateKey: ByteBufferConvertible { | ||
| public static func read(consuming buffer: inout ByteBuffer) throws -> Self { | ||
| guard | ||
| let curveName = buffer.readSSHString(), | ||
| let _ = buffer.readSSHBuffer(), // public key - we don't need it for reconstruction | ||
| let privateKeyData = buffer.readSSHBignum() | ||
| else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| guard curveName == "nistp384" else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| // Process the private key data to validate size and remove leading zero if present | ||
| let keyData = try processECDSAPrivateKeyData(privateKeyData, expectedKeySize: 48) | ||
|
|
||
| return try P384.Signing.PrivateKey(rawRepresentation: keyData) | ||
| } | ||
|
|
||
| public func write(to buffer: inout ByteBuffer) -> Int { | ||
| let start = buffer.writerIndex | ||
|
|
||
| // Write curve name and public key | ||
| writeECDSAPublicKey(to: &buffer, curveName: "nistp384", publicKeyData: publicKey.x963Representation) | ||
|
|
||
| // Write private key as bignum (matching OpenSSH format) | ||
| let privateKeyData = self.rawRepresentation | ||
| let bignum = BigInt(privateKeyData) | ||
| buffer.writeSSHBignum(bignum) | ||
|
|
||
| return buffer.writerIndex - start | ||
| } | ||
| } | ||
|
|
||
| extension P521.Signing.PrivateKey: ByteBufferConvertible { | ||
| public static func read(consuming buffer: inout ByteBuffer) throws -> Self { | ||
| guard | ||
| let curveName = buffer.readSSHString(), | ||
| let _ = buffer.readSSHBuffer(), // public key - we don't need it for reconstruction | ||
| let privateKeyData = buffer.readSSHBignum() | ||
| else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| guard curveName == "nistp521" else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| // Process the private key data to validate size and remove leading zero if present | ||
| let keyData = try processECDSAPrivateKeyData(privateKeyData, expectedKeySize: 66) | ||
|
|
||
| return try P521.Signing.PrivateKey(rawRepresentation: keyData) | ||
| } | ||
|
|
||
| public func write(to buffer: inout ByteBuffer) -> Int { | ||
| let start = buffer.writerIndex | ||
|
|
||
| // Write curve name and public key | ||
| writeECDSAPublicKey(to: &buffer, curveName: "nistp521", publicKeyData: publicKey.x963Representation) | ||
|
|
||
| // Write private key as bignum (matching OpenSSH format) | ||
| let privateKeyData = self.rawRepresentation | ||
| let bignum = BigInt(privateKeyData) | ||
| buffer.writeSSHBignum(bignum) | ||
|
|
||
| return buffer.writerIndex - start | ||
| } | ||
| } | ||
|
|
||
| // Public key types for ECDSA | ||
| extension P256.Signing.PublicKey: ByteBufferConvertible { | ||
| public static func read(consuming buffer: inout ByteBuffer) throws -> Self { | ||
| // First read the curve name | ||
| guard let curveName = buffer.readSSHString() else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| guard curveName == "nistp256" else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| // Then read the EC point data | ||
| guard let pointData = buffer.readSSHBuffer() else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| let pointBytes = pointData.getBytes(at: 0, length: pointData.readableBytes) ?? [] | ||
| guard pointBytes.first == uncompressedPointPrefix else { // Uncompressed point | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| return try P256.Signing.PublicKey(x963Representation: pointBytes) | ||
| } | ||
|
|
||
| public func write(to buffer: inout ByteBuffer) -> Int { | ||
| return writeECDSAPublicKey(to: &buffer, publicKeyData: self.x963Representation) | ||
nedithgar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| extension P384.Signing.PublicKey: ByteBufferConvertible { | ||
| public static func read(consuming buffer: inout ByteBuffer) throws -> Self { | ||
| // First read the curve name | ||
| guard let curveName = buffer.readSSHString() else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| guard curveName == "nistp384" else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| // Then read the EC point data | ||
| guard let pointData = buffer.readSSHBuffer() else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| let pointBytes = pointData.getBytes(at: 0, length: pointData.readableBytes) ?? [] | ||
| guard pointBytes.first == uncompressedPointPrefix else { // Uncompressed point | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| return try P384.Signing.PublicKey(x963Representation: pointBytes) | ||
| } | ||
|
|
||
| public func write(to buffer: inout ByteBuffer) -> Int { | ||
| return writeECDSAPublicKey(to: &buffer, publicKeyData: self.x963Representation) | ||
| } | ||
| } | ||
|
|
||
| extension P521.Signing.PublicKey: ByteBufferConvertible { | ||
| public static func read(consuming buffer: inout ByteBuffer) throws -> Self { | ||
| // First read the curve name | ||
| guard let curveName = buffer.readSSHString() else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| guard curveName == "nistp521" else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| // Then read the EC point data | ||
| guard let pointData = buffer.readSSHBuffer() else { | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| let pointBytes = pointData.getBytes(at: 0, length: pointData.readableBytes) ?? [] | ||
| guard pointBytes.first == uncompressedPointPrefix else { // Uncompressed point | ||
| throw InvalidOpenSSHKey.invalidLayout | ||
| } | ||
|
|
||
| return try P521.Signing.PublicKey(x963Representation: pointBytes) | ||
| } | ||
|
|
||
| public func write(to buffer: inout ByteBuffer) -> Int { | ||
| return writeECDSAPublicKey(to: &buffer, publicKeyData: self.x963Representation) | ||
| } | ||
| } | ||
|
|
||
| // OpenSSHPrivateKey conformances | ||
| extension P256.Signing.PrivateKey: OpenSSHPrivateKey { | ||
| public typealias PublicKey = P256.Signing.PublicKey | ||
|
|
||
| public static var publicKeyPrefix: String { "ecdsa-sha2-nistp256" } | ||
| public static var privateKeyPrefix: String { "ecdsa-sha2-nistp256" } | ||
| public static var keyType: OpenSSH.KeyType { .ecdsaP256 } | ||
| } | ||
|
|
||
| extension P384.Signing.PrivateKey: OpenSSHPrivateKey { | ||
| public typealias PublicKey = P384.Signing.PublicKey | ||
|
|
||
| public static var publicKeyPrefix: String { "ecdsa-sha2-nistp384" } | ||
| public static var privateKeyPrefix: String { "ecdsa-sha2-nistp384" } | ||
| public static var keyType: OpenSSH.KeyType { .ecdsaP384 } | ||
| } | ||
|
|
||
| extension P521.Signing.PrivateKey: OpenSSHPrivateKey { | ||
| public typealias PublicKey = P521.Signing.PublicKey | ||
|
|
||
| public static var publicKeyPrefix: String { "ecdsa-sha2-nistp521" } | ||
| public static var privateKeyPrefix: String { "ecdsa-sha2-nistp521" } | ||
| public static var keyType: OpenSSH.KeyType { .ecdsaP521 } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.