refactor(ssh): share key workflows and wire encoding

This commit is contained in:
Kaede Akino committed 2026-09-29 00:05:32 +08:00
1 parent 816a6ff121
commit 565ef17d87
27 files changed
+575 -413

No files matched your search

@@ -59,11 +59,8 @@ nonisolated struct SSHAgentProtocol {
}
enum Failure: Error { case invalid, unavailable }
static func uint32(_ value: UInt32) -> Data {
var value = value.bigEndian
return withUnsafeBytes(of: &value) { Data($0) }
}
static func field(_ data: Data) -> Data { uint32(UInt32(data.count)) + data }
static func uint32(_ value: UInt32) -> Data { SSHWireEncoding.uint32(value) }
static func field(_ data: Data) -> Data { SSHWireEncoding.field(data) }
static func frame(_ data: Data) -> Data { field(data) }
struct Reader {
@@ -1,5 +1,4 @@
import CSSH2
import CryptoKit
import Foundation
import OpenSSL
import TermindDomain
@@ -115,7 +114,7 @@ public nonisolated struct SSHKeyMaterial: Sendable {
+ (trimmedComment.isEmpty ? "" : " \(trimmedComment)")
return SSHKeyMaterial(
privateKey: privateKey, publicKey: publicKey, algorithm: algorithm,
fingerprint: fingerprint(of: publicBlob),
fingerprint: SSHWireEncoding.fingerprint(of: publicBlob),
keySize: keySize ?? inferredKeySize(for: algorithm, publicBlob: publicBlob)
)
}
@@ -269,12 +268,6 @@ public nonisolated struct SSHKeyMaterial: Sendable {
value?.isEmpty == false ? value : nil
}
private static func fingerprint(of blob: Data) -> String {
let digest = Data(SHA256.hash(data: blob)).base64EncodedString()
.replacingOccurrences(of: "=", with: "")
return "SHA256:\(digest)"
}
private static func connectionError(_ session: OpaquePointer, code: Int32) -> String {
var message: UnsafeMutablePointer<CChar>?
var length = Int32(0)
@@ -0,0 +1,25 @@
import CryptoKit
import Foundation
/// RFC 4251 fields shared by public keys, signatures, and agent replies.
nonisolated enum SSHWireEncoding {
static func uint32(_ value: UInt32) -> Data {
var value = value.bigEndian
return withUnsafeBytes(of: &value) { Data($0) }
}
static func field(_ data: Data) -> Data { uint32(UInt32(data.count)) + data }
/// Encodes a nonnegative integer, including its SSH field length.
static func mpint<S: DataProtocol>(_ bytes: S) -> Data {
var value = Data(Data(bytes).drop(while: { $0 == 0 }))
if value.first.map({ $0 & 0x80 != 0 }) == true { value.insert(0, at: value.startIndex) }
return field(value)
}
static func fingerprint(of blob: Data) -> String {
let digest = Data(SHA256.hash(data: blob)).base64EncodedString()
.replacingOccurrences(of: "=", with: "")
return "SHA256:\(digest)"
}
}
@@ -57,7 +57,7 @@ public nonisolated enum SecureEnclaveKey {
dataRepresentation: privateKey.dataRepresentation
),
publicKey: publicKey,
fingerprint: fingerprint(of: publicBlob)
fingerprint: SSHWireEncoding.fingerprint(of: publicBlob)
)
}
@@ -80,8 +80,8 @@ public nonisolated enum SecureEnclaveKey {
let rawSignature = try privateKey.signature(for: data).rawRepresentation
try Task.checkCancellation()
guard rawSignature.count == 64 else { throw SecureEnclaveKeyError.invalidSignature }
return sshField(mpint(rawSignature.prefix(32)))
+ sshField(mpint(rawSignature.suffix(32)))
return SSHWireEncoding.mpint(rawSignature.prefix(32))
+ SSHWireEncoding.mpint(rawSignature.suffix(32))
} catch {
throw signingError(error)
}
@@ -113,30 +113,9 @@ public nonisolated enum SecureEnclaveKey {
}
private static func sshPublicBlob(for x963Representation: Data) -> Data {
sshField(Data("ecdsa-sha2-nistp256".utf8))
+ sshField(Data("nistp256".utf8))
+ sshField(x963Representation)
SSHWireEncoding.field(Data("ecdsa-sha2-nistp256".utf8))
+ SSHWireEncoding.field(Data("nistp256".utf8))
+ SSHWireEncoding.field(x963Representation)
}
private static func fingerprint(of blob: Data) -> String {
let digest = Data(SHA256.hash(data: blob)).base64EncodedString()
.replacingOccurrences(of: "=", with: "")
return "SHA256:\(digest)"
}
private static func mpint<S: DataProtocol>(_ bytes: S) -> Data {
var value = Data(bytes).drop(while: { $0 == 0 })
if value.isEmpty { return Data() }
if value.first.map({ $0 & 0x80 != 0 }) == true {
value.insert(0, at: value.startIndex)
}
return Data(value)
}
private static func sshField(_ value: Data) -> Data {
var length = UInt32(value.count).bigEndian
var result = withUnsafeBytes(of: &length) { Data($0) }
result.append(value)
return result
}
}
@@ -62,18 +62,18 @@ public nonisolated enum WebAuthnSSH {
public static func publicBlob(_ credential: SecurityKeyCredential) throws -> Data {
try validate(credential)
return field(Data(algorithm.utf8)) + field(Data("nistp256".utf8))
+ field(credential.publicKeyX963) + field(Data(SecurityKeyCredential.relyingParty.utf8))
return SSHWireEncoding.field(Data(algorithm.utf8)) + SSHWireEncoding.field(Data("nistp256".utf8))
+ SSHWireEncoding.field(credential.publicKeyX963)
+ SSHWireEncoding.field(Data(SecurityKeyCredential.relyingParty.utf8))
}
public static func keyReference(
_ credential: SecurityKeyCredential, label: String, colorTag: TagColor = .none
) throws -> SSHKeyRef {
let blob = try publicBlob(credential)
let fingerprint = Data(SHA256.hash(data: blob)).base64EncodedString().replacingOccurrences(of: "=", with: "")
return SSHKeyRef(
label: label, algorithm: .securityKey,
publicKey: "\(algorithm) \(blob.base64EncodedString())", fingerprint: "SHA256:\(fingerprint)",
publicKey: "\(algorithm) \(blob.base64EncodedString())", fingerprint: SSHWireEncoding.fingerprint(of: blob),
storage: .thisDevice, securityKey: credential, keySize: 256,
requiresUserPresence: credential.requiresUserPresence, createdAt: .now, colorTag: colorTag)
}
@@ -137,9 +137,10 @@ public nonisolated enum WebAuthnSSH {
throw Failure.invalidResponse
}
let raw = signature.rawRepresentation
let pair = mpint(Data(raw.prefix(32))) + mpint(Data(raw.suffix(32)))
return field(Data(signatureAlgorithm.utf8)) + field(pair) + Data(auth[32..<37])
+ field(Data(origin.utf8)) + field(assertion.clientDataJSON) + field(extensions)
let pair = SSHWireEncoding.mpint(Data(raw.prefix(32))) + SSHWireEncoding.mpint(Data(raw.suffix(32)))
return SSHWireEncoding.field(Data(signatureAlgorithm.utf8)) + SSHWireEncoding.field(pair) + Data(auth[32..<37])
+ SSHWireEncoding.field(Data(origin.utf8)) + SSHWireEncoding.field(assertion.clientDataJSON)
+ SSHWireEncoding.field(extensions)
}
private static func validateAuthenticator(_ data: Data, requiresVerification: Bool, requiresPresence: Bool = true)
@@ -169,18 +170,6 @@ public nonisolated enum WebAuthnSSH {
.replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
}
private static func field(_ data: Data) -> Data {
let count = UInt32(data.count)
return Data([
UInt8(count >> 24), UInt8(truncatingIfNeeded: count >> 16),
UInt8(truncatingIfNeeded: count >> 8), UInt8(truncatingIfNeeded: count),
]) + data
}
private static func mpint(_ data: Data) -> Data {
let value = Data(data.drop(while: { $0 == 0 }))
return field((value.first.map { $0 & 0x80 != 0 } == true ? Data([0]) : Data()) + value)
}
}
/// Bounded definite-length CBOR parser for authenticator data, with duplicate-map-key rejection.