From 828c61cb2f8e0878fc5d95cef416a8b631fc644d Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 24 Aug 2025 20:02:51 -0700 Subject: [PATCH] Add support for MLDSA keys (#631) * WIP. * WIP * WIP Edit * Key selection. * WIP * WIP * Proxy through * WIP * Remove verify. * Migration. * Comment * Add param * Semi-offering key * Ignore updates if test build. * Fix rsa public key gen * Messily fix RSA * Remove 1024 bit rsa * Cleanup * Cleanup * MLDSA warning. * MLDSA working. * Strings. * Put back UI changes --- Sources/Packages/Localizable.xcstrings | 24 +++++++------ .../OpenSSH/OpenSSHPublicKeyWriter.swift | 14 ++++++-- .../OpenSSH/OpenSSHSignatureWriter.swift | 12 +++++++ .../Sources/SecretKit/Types/Secret.swift | 3 ++ .../SecureEnclaveStore.swift | 35 ++++++++++++++++--- .../Tests/SecretAgentKitTests/StubStore.swift | 2 +- .../Preview Content/PreviewStore.swift | 2 ++ .../Secretive/Views/CreateSecretView.swift | 6 ++++ 8 files changed, 81 insertions(+), 17 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 07e3026..2e29ce5 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1083,16 +1083,6 @@ } } }, - "auth_context_request_signature_description_%@_%@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "auth_context_request_signature_description_%1$@_%2$@" - } - } - } - }, "auth_context_request_verify_description" : { "comment" : "When the user performs a signature verification action using a secret, they are shown a prompt to approve the action. This is the description, showing which secret will be used. The placeholder is the name of the secret. NOTE: This is currently not exposed in UI.", "extractionState" : "manual", @@ -1534,6 +1524,17 @@ } } }, + "create_secret_mldsa_warning" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warning: ML-DSA keys are very new, and not supported by many servers yet. Please verify the server you'll be using this key for accepts ML-DSA keys." + } + } + } + }, "create_secret_name_label" : { "extractionState" : "manual", "localizations" : { @@ -5188,6 +5189,9 @@ } } } + }, + "Test" : { + }, "unnamed_secret" : { "extractionState" : "manual", diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift index 25016a8..d809755 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift @@ -17,6 +17,10 @@ public struct OpenSSHPublicKeyWriter: Sendable { openSSHIdentifier(for: secret.keyType).lengthAndData + ("nistp" + String(describing: secret.keyType.size)).lengthAndData + secret.publicKey.lengthAndData + case .mldsa: + // https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt + openSSHIdentifier(for: secret.keyType).lengthAndData + + secret.publicKey.lengthAndData case .rsa: // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 openSSHIdentifier(for: secret.keyType).lengthAndData + @@ -72,8 +76,14 @@ extension OpenSSHPublicKeyWriter { /// - 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 (.ecdsa, 256): + "ecdsa-sha2-nistp256" + case (.ecdsa, 384): + "ecdsa-sha2-nistp384" + case (.mldsa, 65): + "ssh-mldsa-65" + case (.mldsa, 87): + "ssh-mldsa-87" case (.rsa, _): "ssh-rsa" default: diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift index 217e3bc..b713d53 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift @@ -15,6 +15,9 @@ public struct OpenSSHSignatureWriter: Sendable { case .ecdsa: // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1 ecdsaSignature(signature, keyType: secret.keyType) + case .mldsa: + // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-00#name-public-key-algorithms + mldsaSignature(signature, keyType: secret.keyType) case .rsa: // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 rsaSignature(signature) @@ -51,6 +54,15 @@ extension OpenSSHSignatureWriter { return mutSignedData } + func mldsaSignature(_ rawRepresentation: Data, keyType: KeyType) -> Data { + var mutSignedData = Data() + var sub = Data() + sub.append(OpenSSHPublicKeyWriter().openSSHIdentifier(for: keyType).lengthAndData) + sub.append(rawRepresentation.lengthAndData) + mutSignedData.append(sub.lengthAndData) + return mutSignedData + } + func rsaSignature(_ rawRepresentation: Data) -> Data { var mutSignedData = Data() var sub = Data() diff --git a/Sources/Packages/Sources/SecretKit/Types/Secret.swift b/Sources/Packages/Sources/SecretKit/Types/Secret.swift index bb70fc8..0f74b48 100644 --- a/Sources/Packages/Sources/SecretKit/Types/Secret.swift +++ b/Sources/Packages/Sources/SecretKit/Types/Secret.swift @@ -35,6 +35,7 @@ public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible { public enum Algorithm: Hashable, Sendable, Codable { case ecdsa + case mldsa case rsa } @@ -67,6 +68,8 @@ public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible { kSecAttrKeyTypeEC case .rsa: kSecAttrKeyTypeRSA + case .mldsa: + nil } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 0f467b5..a001c4f 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -39,10 +39,10 @@ extension SecureEnclave { context = existing.context } else { let newContext = LAContext() - newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button") + newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name)) + newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton) context = newContext } - context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") let queryAttributes = KeychainDictionary([ kSecClass: Constants.keyClass, @@ -68,8 +68,16 @@ extension SecureEnclave { switch (attributes.keyType.algorithm, attributes.keyType.size) { case (.ecdsa, 256): - let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData) + let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData, authenticationContext: context) return try key.signature(for: data).rawRepresentation + case (.mldsa, 65): + guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } + let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData) + return try key.signature(for: data) + case (.mldsa, 87): + guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } + let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData) + return try key.signature(for: data) default: throw UnsupportedAlgorithmError() } @@ -115,6 +123,14 @@ extension SecureEnclave { case (.ecdsa, 256): let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!) dataRep = created.dataRepresentation + case (.mldsa, 65): + guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() } + let created = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(accessControl: access!) + dataRep = created.dataRepresentation + case (.mldsa, 87): + guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() } + let created = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(accessControl: access!) + dataRep = created.dataRepresentation default: throw Attributes.UnsupportedOptionError() } @@ -127,7 +143,8 @@ extension SecureEnclave { kSecClass: Constants.keyClass, kSecAttrService: Constants.keyTag, kSecUseDataProtectionKeychain: true, - kSecAttrAccount: String(decoding: secret.id, as: UTF8.self) + kSecAttrAccount: String(decoding: secret.id, as: UTF8.self), + kSecAttrCanSign: true, ]) let status = SecItemDelete(deleteAttributes) if status != errSecSuccess { @@ -156,6 +173,8 @@ extension SecureEnclave { public var supportedKeyTypes: [KeyType] { [ .init(algorithm: .ecdsa, size: 256), + .init(algorithm: .mldsa, size: 65), + .init(algorithm: .mldsa, size: 87), ] } @@ -205,6 +224,14 @@ extension SecureEnclave.Store { case (.ecdsa, 256): let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData) publicKey = key.publicKey.x963Representation + case (.mldsa, 65): + guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } + let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData) + publicKey = key.publicKey.rawRepresentation + case (.mldsa, 87): + guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } + let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData) + publicKey = key.publicKey.rawRepresentation default: throw UnsupportedAlgorithmError() } diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift index 6f37469..222588a 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift @@ -89,7 +89,7 @@ extension Stub { var debugDescription: String { """ - Key Size \(keyType.size) + Key Size \(attributes.keyType.size) Private: \(privateKey.base64EncodedString()) Public: \(publicKey.base64EncodedString()) """ diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift index e17c232..ff8f8da 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -63,6 +63,8 @@ extension Preview { var supportedKeyTypes: [KeyType] { [ .init(algorithm: .ecdsa, size: 256), + .init(algorithm: .mldsa, size: 65), + .init(algorithm: .mldsa, size: 87), ] } diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index 573e44d..7185bf2 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/CreateSecretView.swift @@ -79,6 +79,12 @@ struct CreateSecretView: View { .font(.caption) } } + if keyType?.algorithm == .mldsa { + Text(.createSecretMldsaWarning) + .padding(.horizontal, 10) + .padding(.vertical, 3) + .background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5)) + } } VStack(alignment: .leading) { TextField(.createSecretKeyAttributionLabel, text: $keyAttribution, prompt: Text(verbatim: "test@example.com"))