mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-08-26 23:20:57 +00:00
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:
parent
e8c5336888
commit
828c61cb2f
@ -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",
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
"""
|
"""
|
||||||
|
@ -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),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
|
Loading…
Reference in New Issue
Block a user