refactor(ssh): share key workflows and wire encoding
This commit is contained in:
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.
|
||||
|
||||
Reference in new issue
Block a user