This commit is contained in:
Max Goedjen 2025-08-24 14:42:43 -07:00
parent 7d6223a327
commit 698b7571c8
No known key found for this signature in database
13 changed files with 170 additions and 85 deletions

View File

@ -9,7 +9,8 @@ public final class Agent: Sendable {
private let storeList: SecretStoreList private let storeList: SecretStoreList
private let witness: SigningWitness? private let witness: SigningWitness?
private let writer = OpenSSHKeyWriter() private let publicKeyWriter = OpenSSHPublicKeyWriter()
private let signatureWriter = OpenSSHSignatureWriter()
private let requestTracer = SigningRequestTracer() private let requestTracer = SigningRequestTracer()
private let certificateHandler = OpenSSHCertificateHandler() private let certificateHandler = OpenSSHCertificateHandler()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
@ -43,7 +44,7 @@ extension Agent {
guard data.count > 4 else { return false} guard data.count > 4 else { return false}
let requestTypeInt = data[4] let requestTypeInt = data[4]
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else { guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data)) writer.write(SSHAgent.ResponseType.agentFailure.data.lengthAndData)
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
return true return true
} }
@ -75,8 +76,7 @@ extension Agent {
response.append(SSHAgent.ResponseType.agentFailure.data) response.append(SSHAgent.ResponseType.agentFailure.data)
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
} }
let full = OpenSSHKeyWriter().lengthAndData(of: response) return response.lengthAndData
return full
} }
} }
@ -92,14 +92,14 @@ extension Agent {
var keyData = Data() var keyData = Data()
for secret in secrets { for secret in secrets {
let keyBlob = writer.data(secret: secret) let keyBlob = publicKeyWriter.data(secret: secret)
let curveData = Data(writer.curveType(for: secret.keyType).utf8) let curveData = publicKeyWriter.openSSHIdentifier(for: secret.keyType)
keyData.append(writer.lengthAndData(of: keyBlob)) keyData.append(keyBlob.lengthAndData)
keyData.append(writer.lengthAndData(of: curveData)) keyData.append(curveData.lengthAndData)
if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) { if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {
keyData.append(writer.lengthAndData(of: certificateData)) keyData.append(certificateData.lengthAndData)
keyData.append(writer.lengthAndData(of: name)) keyData.append(name.lengthAndData)
count += 1 count += 1
} }
} }
@ -137,7 +137,7 @@ extension Agent {
let dataToSign = reader.readNextChunk() let dataToSign = reader.readNextChunk()
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance) let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
let curveData = Data(writer.curveType(for: secret.keyType).utf8) let curveData = publicKeyWriter.openSSHIdentifier(for: secret.keyType)
let signedData: Data let signedData: Data
if secret.keyType.algorithm == .ecdsa { if secret.keyType.algorithm == .ecdsa {
@ -155,20 +155,20 @@ extension Agent {
} }
var signatureChunk = Data() var signatureChunk = Data()
signatureChunk.append(writer.lengthAndData(of: r)) signatureChunk.append(r.lengthAndData)
signatureChunk.append(writer.lengthAndData(of: s)) signatureChunk.append(s.lengthAndData)
var mutSignedData = Data() var mutSignedData = Data()
var sub = Data() var sub = Data()
sub.append(writer.lengthAndData(of: curveData)) sub.append(curveData.lengthAndData)
sub.append(writer.lengthAndData(of: signatureChunk)) sub.append(signatureChunk.lengthAndData)
mutSignedData.append(writer.lengthAndData(of: sub)) mutSignedData.append(sub.lengthAndData)
signedData = mutSignedData signedData = mutSignedData
} else { } else {
var mutSignedData = Data() var mutSignedData = Data()
var sub = Data() var sub = Data()
sub.append(writer.lengthAndData(of: Data("rsa-sha2-512".utf8))) sub.append("rsa-sha2-512".lengthAndData)
sub.append(writer.lengthAndData(of: rawRepresentation)) sub.append(rawRepresentation.lengthAndData)
mutSignedData.append(writer.lengthAndData(of: sub)) mutSignedData.append(sub.lengthAndData)
signedData = mutSignedData signedData = mutSignedData
} }
@ -203,7 +203,7 @@ extension Agent {
func secret(matching hash: Data) async -> (AnySecretStore, AnySecret)? { func secret(matching hash: Data) async -> (AnySecretStore, AnySecret)? {
for store in await storeList.stores { for store in await storeList.stores {
let allMatching = await store.secrets.filter { secret in let allMatching = await store.secrets.filter { secret in
hash == writer.data(secret: secret) hash == publicKeyWriter.data(secret: secret)
} }
if let matching = allMatching.first { if let matching = allMatching.first {
return (store, matching) return (store, matching)

View File

@ -22,7 +22,7 @@ SecretKit is a collection of protocols describing secrets and stores.
### OpenSSH ### OpenSSH
- ``OpenSSHKeyWriter`` - ``OpenSSHPublicKeyWriter``
- ``OpenSSHReader`` - ``OpenSSHReader``
### Signing Process ### Signing Process

View File

@ -52,16 +52,16 @@ public extension SecretStore {
/// - Parameters: /// - Parameters:
/// - secret: The secret which will be used for signing. /// - secret: The secret which will be used for signing.
/// - Returns: The appropriate algorithm. /// - Returns: The appropriate algorithm.
func signatureAlgorithm(for secret: SecretType) -> SecKeyAlgorithm { func signatureAlgorithm(for secret: SecretType) -> SecKeyAlgorithm? {
switch (secret.keyType.algorithm, secret.keyType.size) { switch (secret.keyType.algorithm, secret.keyType.size) {
case (.ecdsa, 256): case (.ecdsa, 256):
return .ecdsaSignatureMessageX962SHA256 .ecdsaSignatureMessageX962SHA256
case (.ecdsa, 384): case (.ecdsa, 384):
return .ecdsaSignatureMessageX962SHA384 .ecdsaSignatureMessageX962SHA384
case (.rsa, 2048): case (.rsa, 2048):
return .rsaSignatureMessagePKCS1v15SHA512 .rsaSignatureMessagePKCS1v15SHA512
default: default:
fatalError() nil
} }
} }

View File

@ -0,0 +1,23 @@
import Foundation
extension Data {
/// Creates an OpenSSH protocol style data object, which has a length header, followed by the data payload.
/// - Returns: OpenSSH data.
package var lengthAndData: Data {
let rawLength = UInt32(count)
var endian = rawLength.bigEndian
return Data(bytes: &endian, count: UInt32.bitWidth/8) + self
}
}
extension String {
/// Creates an OpenSSH protocol style data object, which has a length header, followed by the data payload.
/// - Returns: OpenSSH data.
package var lengthAndData: Data {
Data(utf8).lengthAndData
}
}

View File

@ -6,7 +6,7 @@ public actor OpenSSHCertificateHandler: Sendable {
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
private let writer = OpenSSHKeyWriter() private let writer = OpenSSHPublicKeyWriter()
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
/// Initializes an OpenSSHCertificateHandler. /// Initializes an OpenSSHCertificateHandler.
@ -40,10 +40,10 @@ public actor OpenSSHCertificateHandler: Sendable {
let curveIdentifier = reader.readNextChunk() let curveIdentifier = reader.readNextChunk()
let publicKey = reader.readNextChunk() let publicKey = reader.readNextChunk()
let curveType = Data(certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").utf8) let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
return writer.lengthAndData(of: curveType) + return openSSHIdentifier.lengthAndData +
writer.lengthAndData(of: curveIdentifier) + curveIdentifier.lengthAndData +
writer.lengthAndData(of: publicKey) publicKey.lengthAndData
default: default:
return nil return nil
} }

View File

@ -1,8 +1,8 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
/// Generates OpenSSH representations of Secrets. /// Generates OpenSSH representations of the public key sof secrets.
public struct OpenSSHKeyWriter: Sendable { public struct OpenSSHPublicKeyWriter: Sendable {
/// Initializes the writer. /// Initializes the writer.
public init() { public init() {
@ -13,15 +13,18 @@ public struct OpenSSHKeyWriter: Sendable {
public func data<SecretType: Secret>(secret: SecretType) -> Data { public func data<SecretType: Secret>(secret: SecretType) -> Data {
switch secret.keyType.algorithm { switch secret.keyType.algorithm {
case .ecdsa: case .ecdsa:
lengthAndData(of: Data(curveType(for: secret.keyType).utf8)) + // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
lengthAndData(of: Data(curveIdentifier(for: secret.keyType).utf8)) + openSSHIdentifier(for: secret.keyType).lengthAndData +
lengthAndData(of: secret.publicKey) ("nistp" + String(describing: secret.keyType.size)).lengthAndData +
secret.publicKey.lengthAndData
case .mldsa: case .mldsa:
lengthAndData(of: Data(curveType(for: secret.keyType).utf8)) + // https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt
lengthAndData(of: secret.publicKey) openSSHIdentifier(for: secret.keyType).lengthAndData +
secret.publicKey.lengthAndData
case .rsa: case .rsa:
lengthAndData(of: Data(curveType(for: secret.keyType).utf8)) + // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
rsa(secret: secret) openSSHIdentifier(for: secret.keyType).lengthAndData +
rsaPublicKeyBlob(secret: secret)
} }
} }
@ -39,7 +42,7 @@ public struct OpenSSHKeyWriter: Sendable {
.replacingOccurrences(of: " ", with: "-") .replacingOccurrences(of: " ", with: "-")
resolvedComment = "\(dashedKeyName)@\(dashedHostName)" resolvedComment = "\(dashedKeyName)@\(dashedHostName)"
} }
return [curveType(for: secret.keyType), data(secret: secret).base64EncodedString(), resolvedComment] return [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), resolvedComment]
.compactMap { $0 } .compactMap { $0 }
.joined(separator: " ") .joined(separator: " ")
} }
@ -64,63 +67,43 @@ public struct OpenSSHKeyWriter: Sendable {
} }
extension OpenSSHKeyWriter { extension OpenSSHPublicKeyWriter {
/// Creates an OpenSSH protocol style data object, which has a length header, followed by the data payload.
/// - Parameter data: The data payload.
/// - Returns: OpenSSH data.
public func lengthAndData(of data: Data) -> Data {
let rawLength = UInt32(data.count)
var endian = rawLength.bigEndian
return Data(bytes: &endian, count: UInt32.bitWidth/8) + data
}
/// The fully qualified OpenSSH identifier for the algorithm. /// The fully qualified OpenSSH identifier for the algorithm.
/// - Parameters: /// - Parameters:
/// - algorithm: The algorithm to identify. /// - algorithm: The algorithm to identify.
/// - length: The key length of the algorithm. /// - length: The key length of the algorithm.
/// - Returns: The OpenSSH identifier for the algorithm. /// - Returns: The OpenSSH identifier for the algorithm.
public func curveType(for keyType: KeyType) -> String { public func openSSHIdentifier(for keyType: KeyType) -> String {
switch (keyType.algorithm, keyType.size) { switch (keyType.algorithm, keyType.size) {
case (.ecdsa, 256), (.ecdsa, 384): case (.ecdsa, 256), (.ecdsa, 384):
"ecdsa-sha2-nistp" + String(describing: keyType.size) "ecdsa-sha2-nistp" + String(describing: keyType.size)
case (.mldsa, 65), (.mldsa, 87): case (.mldsa, 65), (.mldsa, 87):
"ssh-mldsa" + String(describing: keyType.size) "ssh-mldsa-" + String(describing: keyType.size)
case (.rsa, _): case (.rsa, _):
"ssh-rsa" "ssh-rsa"
default: default:
"unknown" "unknown"
} }
} }
/// The OpenSSH identifier for an algorithm. }
/// - Parameters:
/// - algorithm: The algorithm to identify.
/// - length: The key length of the algorithm.
/// - Returns: The OpenSSH identifier for the algorithm.
private func curveIdentifier(for keyType: KeyType) -> String {
switch keyType.algorithm {
case .ecdsa:
"nistp" + String(describing: keyType.size)
case .mldsa:
"mldsa" + String(describing: keyType.size)
default:
fatalError()
}
}
public func rsa<SecretType: Secret>(secret: SecretType) -> Data { extension OpenSSHPublicKeyWriter {
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253 // Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
// Keychain stores it as a thin ASN.1 wrapper with this format: // Keychain stores it as a thin ASN.1 wrapper with this format:
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e] // [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
// Rather than parse out the whole ASN.1 blob, we know how this should be formatted, so pull values directly. // Rather than parse out the whole ASN.1 blob, we'll cheat and pull values directly since
// we only support one key type, and the keychain always gives it in a specific format.
let keySize = secret.keyType.size let keySize = secret.keyType.size
guard secret.keyType.algorithm == .rsa && keySize == 2048 else { fatalError() } guard secret.keyType.algorithm == .rsa && keySize == 2048 else { fatalError() }
let length = secret.keyType.size/8 let length = secret.keyType.size/8
let data = secret.publicKey let data = secret.publicKey
let n = Data(data[8..<(9+length)]) let n = Data(data[8..<(9+length)])
let e = Data(data[(2+9+length)...]) let e = Data(data[(2+9+length)...])
return lengthAndData(of: e) + lengthAndData(of: n) return e.lengthAndData + n.lengthAndData
} }
} }

View File

@ -0,0 +1,69 @@
import Foundation
import CryptoKit
/// Generates OpenSSH representations of Secrets.
public struct OpenSSHSignatureWriter: Sendable {
/// Initializes the writer.
public init() {
}
/// Generates an OpenSSH data payload identifying the secret.
/// - Returns: OpenSSH data payload identifying the secret.
public func data<SecretType: Secret>(secret: SecretType, signature: Data) -> Data {
switch secret.keyType.algorithm {
case .ecdsa:
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
fatalError()
case .mldsa:
// https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt
fatalError()
case .rsa:
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
fatalError()
}
}
}
extension OpenSSHSignatureWriter {
/// The fully qualified OpenSSH identifier for the algorithm.
/// - Parameters:
/// - algorithm: The algorithm to identify.
/// - length: The key length of the algorithm.
/// - Returns: The OpenSSH identifier for the algorithm.
public func openSSHIdentifier(for keyType: KeyType) -> String {
switch (keyType.algorithm, keyType.size) {
case (.ecdsa, 256), (.ecdsa, 384):
"ecdsa-sha2-nistp" + String(describing: keyType.size)
case (.mldsa, 65), (.mldsa, 87):
"ssh-mldsa-" + String(describing: keyType.size)
case (.rsa, _):
"ssh-rsa"
default:
"unknown"
}
}
}
extension OpenSSHSignatureWriter {
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
// Keychain stores it as a thin ASN.1 wrapper with this format:
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
// Rather than parse out the whole ASN.1 blob, we'll cheat and pull values directly since
// we only support one key type, and the keychain always gives it in a specific format.
let keySize = secret.keyType.size
guard secret.keyType.algorithm == .rsa && keySize == 2048 else { fatalError() }
let length = secret.keyType.size/8
let data = secret.publicKey
let n = Data(data[8..<(9+length)])
let e = Data(data[(2+9+length)...])
return e.lengthAndData + n.lengthAndData
}
}

View File

@ -6,7 +6,7 @@ public final class PublicKeyFileStoreController: Sendable {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
private let directory: String private let directory: String
private let keyWriter = OpenSSHKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
/// Initializes a PublicKeyFileStoreController. /// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: String) { public init(homeDirectory: String) {

View File

@ -79,7 +79,8 @@ extension SmartCard {
} }
let key = untypedSafe as! SecKey let key = untypedSafe as! SecKey
var signError: SecurityError? var signError: SecurityError?
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm(for: secret), data as CFData, &signError) else { guard let algorithm = signatureAlgorithm(for: secret) else { throw UnsupportKeyType() }
guard let signature = SecKeyCreateSignature(key, algorithm, data as CFData, &signError) else {
throw SigningError(error: signError) throw SigningError(error: signError)
} }
return signature as Data return signature as Data
@ -153,7 +154,7 @@ extension SmartCard.Store {
var untyped: CFTypeRef? var untyped: CFTypeRef?
SecItemCopyMatching(attributes, &untyped) SecItemCopyMatching(attributes, &untyped)
guard let typed = untyped as? [[CFString: Any]] else { return } guard let typed = untyped as? [[CFString: Any]] else { return }
let wrapped = typed.map { let wrapped: [SecretType] = typed.compactMap {
let name = $0[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret) let name = $0[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
let tokenID = $0[kSecAttrApplicationLabel] as! Data let tokenID = $0[kSecAttrApplicationLabel] as! Data
let algorithmSecAttr = $0[kSecAttrKeyType] as! NSNumber let algorithmSecAttr = $0[kSecAttrKeyType] as! NSNumber
@ -163,7 +164,9 @@ extension SmartCard.Store {
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any] let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
let publicKey = publicKeyAttributes[kSecValueData] as! Data let publicKey = publicKeyAttributes[kSecValueData] as! Data
let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .unknown) let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .unknown)
return SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes) let secret = SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes)
guard signatureAlgorithm(for: secret) != nil else { return nil }
return secret
} }
state.secrets.append(contentsOf: wrapped) state.secrets.append(contentsOf: wrapped)
} }
@ -178,3 +181,9 @@ extension TKTokenWatcher {
} }
} }
extension SmartCard {
public struct UnsupportKeyType: Error {}
}

View File

@ -45,7 +45,7 @@ extension Stub {
let privateData = (privateAttributes[kSecValueData] as! Data) let privateData = (privateAttributes[kSecValueData] as! Data)
let secret = Secret(keySize: size, publicKey: publicData, privateKey: privateData) let secret = Secret(keySize: size, publicKey: publicData, privateKey: privateData)
print(secret) print(secret)
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))") print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))")
} }
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {

View File

@ -4,11 +4,12 @@ import Testing
@testable import SecureEnclaveSecretKit @testable import SecureEnclaveSecretKit
@testable import SmartCardSecretKit @testable import SmartCardSecretKit
@Suite struct AnySecretTests { @Suite struct AnySecretTests {
@Test func eraser() { @Test func eraser() {
let data = Data(UUID().uuidString.utf8) let data = Data(UUID().uuidString.utf8)
let secret = SmartCard.Secret(id: data, name: "Name", publicKey: data, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256))) let secret = SmartCard.Secret(id: data, name: "Name", publicKey: data, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), authentication: .notRequired))
let erased = AnySecret(secret) let erased = AnySecret(secret)
#expect(erased.id == secret.id as AnyHashable) #expect(erased.id == secret.id as AnyHashable)
#expect(erased.name == secret.name) #expect(erased.name == secret.name)

View File

@ -4,9 +4,9 @@ import Testing
@testable import SecureEnclaveSecretKit @testable import SecureEnclaveSecretKit
@testable import SmartCardSecretKit @testable import SmartCardSecretKit
@Suite struct OpenSSHWriterTests { @Suite struct OpenSSHPublicKeyWriterTests {
let writer = OpenSSHKeyWriter() let writer = OpenSSHPublicKeyWriter()
@Test func ecdsa256MD5Fingerprint() { @Test func ecdsa256MD5Fingerprint() {
#expect(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa256Secret) == "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5") #expect(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa256Secret) == "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5")
@ -44,11 +44,11 @@ import Testing
} }
extension OpenSSHWriterTests { extension OpenSSHPublicKeyWriterTests {
enum Constants { enum Constants {
static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), publicKeyAttribution: "test@example.com")) static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), authentication: .notRequired, publicKeyAttribution: "test@example.com"))
static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 384), publicKeyAttribution: "test@example.com")) static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 384), authentication: .notRequired, publicKeyAttribution: "test@example.com"))
} }

View File

@ -5,7 +5,7 @@ struct SecretDetailView<SecretType: Secret>: View {
let secret: SecretType let secret: SecretType
private let keyWriter = OpenSSHKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID)) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID))
var body: some View { var body: some View {