Compare commits

..

2 Commits

Author SHA1 Message Date
Max Goedjen
11f1f83113 Cleanup 2025-08-31 17:04:56 -07:00
Max Goedjen
3e128d2a81 WIP 2025-08-31 16:47:19 -07:00
10 changed files with 168 additions and 42 deletions

View File

@@ -18,7 +18,7 @@ Open [Sources/Secretive.xcodeproj](Sources/Secretive.xcodeproj) in Xcode.
### Translate
Navigate to [Sources/Packages/Localizable.xcstrings](Sources/Packages/Localizable.xcstrings).
Navigate to [Secretive/Localizable](Sources/Secretive/Localizable.xcstrings).
<img src="/.github/readme/localize_sidebar.png" alt="Screenshot of Xcode navigating to the Localizable file" width="300">
@@ -32,12 +32,6 @@ Start translating! You'll see a list of english phrases, and a space to add a tr
Push your changes and open a pull request.
### Handling Updates
When your translation is merged, I'll invite you to the [secretive-localizers](https://github.com/secretive-localizers) group. I'll tag this group anytime there's a new set of strings, in the hopes that you'll update the translation. If you don't want to be notified, feel free to decline the invitation or leave the organization at any time.
### Questions
Please open an issue if you have a question about translating the app. I'm more than happy to clarify any terms that are ambiguous or confusing. Thanks for contributing!

View File

@@ -61,4 +61,4 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
## Security
Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)
If you discover any vulnerabilities in this project, please notify [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with the subject containing "SECRETIVE SECURITY."

View File

@@ -24,4 +24,4 @@ The latest version on the [Releases page](https://github.com/maxgoedjen/secretiv
## Reporting a Vulnerability
To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY."

View File

@@ -43,7 +43,7 @@ extension Agent {
}
let requestTypeInt = data[4]
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription) for unknown request type \(requestTypeInt)")
return SSHAgent.ResponseType.agentFailure.data.lengthAndData
}
logger.debug("Agent handling request of type \(requestType.debugDescription)")
@@ -66,10 +66,25 @@ extension Agent {
response.append(SSHAgent.ResponseType.agentSignResponse.data)
response.append(try await sign(data: data, provenance: provenance))
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
case .protocolExtension:
response.append(SSHAgent.ResponseType.agentExtensionResponse.data)
try await handleExtension(data)
default:
let reader = OpenSSHReader(data: data)
while true {
do {
let payloadHash = try reader.readNextChunk()
print(String(String(decoding: payloadHash, as: UTF8.self)))
print(payloadHash)
} catch {
break
}
}
logger.debug("Agent received valid request of type \(requestType.debugDescription), but not currently supported.")
response.append(SSHAgent.ResponseType.agentFailure.data)
}
} catch {
response.removeAll()
response.append(SSHAgent.ResponseType.agentFailure.data)
response = SSHAgent.ResponseType.agentFailure.data
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
}
return response.lengthAndData
@@ -77,6 +92,28 @@ extension Agent {
}
// PROTOCOL EXTENSIONS
extension Agent {
func handleExtension(_ data: Data) async throws {
let reader = OpenSSHReader(data: data)
guard try reader.readNextChunkAsString() == "session-bind@openssh.com" else { throw UnsupportedExtensionError() }
let hostKey = try reader.readNextChunk()
let keyReader = OpenSSHReader(data: hostKey)
_ = try keyReader.readNextChunkAsString() // Key Type
let keyData = try keyReader.readNextChunk()
let sessionID = try reader.readNextChunk()
let signatureData = try reader.readNextChunk()
let forwarding = try reader.readNextBytes(as: Bool.self)
let signatureReader = OpenSSHSignatureReader()
guard try signatureReader.verify(signatureData, for: sessionID, with: keyData) else { throw SignatureVerificationFailedError() }
print("Fowarding: \(forwarding)")
}
struct UnsupportedExtensionError: Error {}
struct SignatureVerificationFailedError: Error {}
}
extension Agent {
/// Lists the identities available for signing operations
@@ -112,7 +149,7 @@ extension Agent {
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
let reader = OpenSSHReader(data: data)
let payloadHash = reader.readNextChunk()
let payloadHash = try reader.readNextChunk()
let hash: Data
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
@@ -129,7 +166,7 @@ extension Agent {
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
let dataToSign = reader.readNextChunk()
let dataToSign = try reader.readNextChunk()
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)

View File

@@ -10,13 +10,32 @@ extension SSHAgent {
case requestIdentities = 11
case signRequest = 13
case addIdentity = 17
case removeIdentity = 18
case removeAllIdentities = 19
case addIDConstrained = 25
case addSmartcardKey = 20
case removeSmartcardKey = 21
case lock = 22
case unlock = 23
case addSmartcardKeyConstrained = 26
case protocolExtension = 27
public var debugDescription: String {
switch self {
case .requestIdentities:
return "RequestIdentities"
case .signRequest:
return "SignRequest"
case .requestIdentities: "SSH_AGENTC_REQUEST_IDENTITIES"
case .signRequest: "SSH_AGENTC_SIGN_REQUEST"
case .addIdentity: "SSH_AGENTC_ADD_IDENTITY"
case .removeIdentity: "SSH_AGENTC_REMOVE_IDENTITY"
case .removeAllIdentities: "SSH_AGENTC_REMOVE_ALL_IDENTITIES"
case .addIDConstrained: "SSH_AGENTC_ADD_ID_CONSTRAINED"
case .addSmartcardKey: "SSH_AGENTC_ADD_SMARTCARD_KEY"
case .removeSmartcardKey: "SSH_AGENTC_REMOVE_SMARTCARD_KEY"
case .lock: "SSH_AGENTC_LOCK"
case .unlock: "SSH_AGENTC_UNLOCK"
case .addSmartcardKeyConstrained: "SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED"
case .protocolExtension: "SSH_AGENTC_EXTENSION"
}
}
}
@@ -28,17 +47,17 @@ extension SSHAgent {
case agentSuccess = 6
case agentIdentitiesAnswer = 12
case agentSignResponse = 14
case agentExtensionFailure = 28
case agentExtensionResponse = 29
public var debugDescription: String {
switch self {
case .agentFailure:
return "AgentFailure"
case .agentSuccess:
return "AgentSuccess"
case .agentIdentitiesAnswer:
return "AgentIdentitiesAnswer"
case .agentSignResponse:
return "AgentSignResponse"
case .agentFailure: "SSH_AGENT_FAILURE"
case .agentSuccess: "SSH_AGENT_SUCCESS"
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER"
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE"
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
}
}
}

View File

@@ -30,20 +30,24 @@ public actor OpenSSHCertificateHandler: Sendable {
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
public func publicKeyHash(from hash: Data) -> Data? {
let reader = OpenSSHReader(data: hash)
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
switch certType {
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
_ = reader.readNextChunk() // nonce
let curveIdentifier = reader.readNextChunk()
let publicKey = reader.readNextChunk()
do {
let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
switch certType {
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
_ = try reader.readNextChunk() // nonce
let curveIdentifier = try reader.readNextChunk()
let publicKey = try reader.readNextChunk()
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
return openSSHIdentifier.lengthAndData +
curveIdentifier.lengthAndData +
publicKey.lengthAndData
default:
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
return openSSHIdentifier.lengthAndData +
curveIdentifier.lengthAndData +
publicKey.lengthAndData
default:
return nil
}
} catch {
return nil
}
}

View File

@@ -97,7 +97,7 @@ extension OpenSSHPublicKeyWriter {
extension OpenSSHPublicKeyWriter {
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
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]

View File

@@ -13,7 +13,8 @@ public final class OpenSSHReader {
/// Reads the next chunk of data from the playload.
/// - Returns: The next chunk of data.
public func readNextChunk() -> Data {
public func readNextChunk() throws -> Data {
guard remaining.count > UInt32.bitWidth/8 else { throw EndOfData() }
let lengthRange = 0..<(UInt32.bitWidth/8)
let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange)
@@ -25,4 +26,18 @@ public final class OpenSSHReader {
return ret
}
public func readNextBytes<T>(as: T.Type) throws -> T {
let lengthRange = 0..<MemoryLayout<T>.size
let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange)
return lengthChunk.bytes.unsafeLoad(as: T.self)
}
public func readNextChunkAsString() throws -> String {
try String(decoding: readNextChunk(), as: UTF8.self)
}
public struct EndOfData: Error {}
}

View File

@@ -0,0 +1,57 @@
import Foundation
import CryptoKit
import Security
/// Reads OpenSSH representations of Secrets.
public struct OpenSSHSignatureReader: Sendable {
/// Initializes the reader.
public init() {
}
public func verify(_ signatureData: Data, for signedData: Data, with publicKey: Data) throws -> Bool {
let reader = OpenSSHReader(data: signatureData)
let signatureType = try reader.readNextChunkAsString()
let signatureData = try reader.readNextChunk()
switch signatureType {
case "ssh-rsa":
let attributes = KeychainDictionary([
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: 2048,
kSecAttrKeyClass: kSecAttrKeyClassPublic
])
var verifyError: SecurityError?
let untyped: CFTypeRef? = SecKeyCreateWithData(publicKey as CFData, attributes, &verifyError)
guard let untypedSafe = untyped else {
throw KeychainError(statusCode: errSecSuccess)
}
let key = untypedSafe as! SecKey
return SecKeyVerifySignature(key, .rsaSignatureMessagePKCS1v15SHA512, signedData as CFData, signatureData as CFData, nil)
case "ecdsa-sha2-nistp256":
return try P256.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
case "ecdsa-sha2-nistp384":
return try P384.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
case "ecdsa-sha2-nistp521":
return try P521.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
case "ssh-ed25519":
return try Curve25519.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
case "ssh-mldsa-65":
if #available(macOS 26.0, *) {
return try MLDSA65.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
} else {
throw UnsupportedSignatureType()
}
case "ssh-mldsa-87":
if #available(macOS 26.0, *) {
return try MLDSA87.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
} else {
throw UnsupportedSignatureType()
}
default:
throw UnsupportedSignatureType()
}
}
public struct UnsupportedSignatureType: Error {}
}

View File

@@ -112,7 +112,7 @@ extension SecureEnclave {
var accessError: SecurityError?
let flags: SecAccessControlCreateFlags = switch attributes.authentication {
case .notRequired:
[.privateKeyUsage]
[]
case .presenceRequired:
[.userPresence, .privateKeyUsage]
case .biometryCurrent: