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
This commit is contained in:
Max Goedjen 2025-08-24 20:02:51 -07:00 committed by GitHub
parent e8c5336888
commit 828c61cb2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 81 additions and 17 deletions

View File

@ -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" : { "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.", "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", "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" : { "create_secret_name_label" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@ -5188,6 +5189,9 @@
} }
} }
} }
},
"Test" : {
}, },
"unnamed_secret" : { "unnamed_secret" : {
"extractionState" : "manual", "extractionState" : "manual",

View File

@ -17,6 +17,10 @@ public struct OpenSSHPublicKeyWriter: Sendable {
openSSHIdentifier(for: secret.keyType).lengthAndData + openSSHIdentifier(for: secret.keyType).lengthAndData +
("nistp" + String(describing: secret.keyType.size)).lengthAndData + ("nistp" + String(describing: secret.keyType.size)).lengthAndData +
secret.publicKey.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: case .rsa:
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
openSSHIdentifier(for: secret.keyType).lengthAndData + openSSHIdentifier(for: secret.keyType).lengthAndData +
@ -72,8 +76,14 @@ extension OpenSSHPublicKeyWriter {
/// - Returns: The OpenSSH identifier for the algorithm. /// - Returns: The OpenSSH identifier for the algorithm.
public func openSSHIdentifier(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-sha2-nistp" + String(describing: keyType.size) "ecdsa-sha2-nistp256"
case (.ecdsa, 384):
"ecdsa-sha2-nistp384"
case (.mldsa, 65):
"ssh-mldsa-65"
case (.mldsa, 87):
"ssh-mldsa-87"
case (.rsa, _): case (.rsa, _):
"ssh-rsa" "ssh-rsa"
default: default:

View File

@ -15,6 +15,9 @@ public struct OpenSSHSignatureWriter: Sendable {
case .ecdsa: case .ecdsa:
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1 // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
ecdsaSignature(signature, keyType: secret.keyType) 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: case .rsa:
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
rsaSignature(signature) rsaSignature(signature)
@ -51,6 +54,15 @@ extension OpenSSHSignatureWriter {
return mutSignedData 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 { func rsaSignature(_ rawRepresentation: Data) -> Data {
var mutSignedData = Data() var mutSignedData = Data()
var sub = Data() var sub = Data()

View File

@ -35,6 +35,7 @@ public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible {
public enum Algorithm: Hashable, Sendable, Codable { public enum Algorithm: Hashable, Sendable, Codable {
case ecdsa case ecdsa
case mldsa
case rsa case rsa
} }
@ -67,6 +68,8 @@ public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible {
kSecAttrKeyTypeEC kSecAttrKeyTypeEC
case .rsa: case .rsa:
kSecAttrKeyTypeRSA kSecAttrKeyTypeRSA
case .mldsa:
nil
} }
} }

View File

@ -39,10 +39,10 @@ extension SecureEnclave {
context = existing.context context = existing.context
} else { } else {
let newContext = LAContext() 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 = newContext
} }
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
let queryAttributes = KeychainDictionary([ let queryAttributes = KeychainDictionary([
kSecClass: Constants.keyClass, kSecClass: Constants.keyClass,
@ -68,8 +68,16 @@ extension SecureEnclave {
switch (attributes.keyType.algorithm, attributes.keyType.size) { switch (attributes.keyType.algorithm, attributes.keyType.size) {
case (.ecdsa, 256): 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 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: default:
throw UnsupportedAlgorithmError() throw UnsupportedAlgorithmError()
} }
@ -115,6 +123,14 @@ extension SecureEnclave {
case (.ecdsa, 256): case (.ecdsa, 256):
let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!) let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!)
dataRep = created.dataRepresentation 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: default:
throw Attributes.UnsupportedOptionError() throw Attributes.UnsupportedOptionError()
} }
@ -127,7 +143,8 @@ extension SecureEnclave {
kSecClass: Constants.keyClass, kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag, kSecAttrService: Constants.keyTag,
kSecUseDataProtectionKeychain: true, kSecUseDataProtectionKeychain: true,
kSecAttrAccount: String(decoding: secret.id, as: UTF8.self) kSecAttrAccount: String(decoding: secret.id, as: UTF8.self),
kSecAttrCanSign: true,
]) ])
let status = SecItemDelete(deleteAttributes) let status = SecItemDelete(deleteAttributes)
if status != errSecSuccess { if status != errSecSuccess {
@ -156,6 +173,8 @@ extension SecureEnclave {
public var supportedKeyTypes: [KeyType] { public var supportedKeyTypes: [KeyType] {
[ [
.init(algorithm: .ecdsa, size: 256), .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): case (.ecdsa, 256):
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.x963Representation 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: default:
throw UnsupportedAlgorithmError() throw UnsupportedAlgorithmError()
} }

View File

@ -89,7 +89,7 @@ extension Stub {
var debugDescription: String { var debugDescription: String {
""" """
Key Size \(keyType.size) Key Size \(attributes.keyType.size)
Private: \(privateKey.base64EncodedString()) Private: \(privateKey.base64EncodedString())
Public: \(publicKey.base64EncodedString()) Public: \(publicKey.base64EncodedString())
""" """

View File

@ -63,6 +63,8 @@ extension Preview {
var supportedKeyTypes: [KeyType] { var supportedKeyTypes: [KeyType] {
[ [
.init(algorithm: .ecdsa, size: 256), .init(algorithm: .ecdsa, size: 256),
.init(algorithm: .mldsa, size: 65),
.init(algorithm: .mldsa, size: 87),
] ]
} }

View File

@ -79,6 +79,12 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
.font(.caption) .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) { VStack(alignment: .leading) {
TextField(.createSecretKeyAttributionLabel, text: $keyAttribution, prompt: Text(verbatim: "test@example.com")) TextField(.createSecretKeyAttributionLabel, text: $keyAttribution, prompt: Text(verbatim: "test@example.com"))