From da56dd6434d7d27318d13c2d2545ad27319ed071 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 6 Sep 2026 18:08:16 -0700 Subject: [PATCH] WIP --- .../Packages/Resources/Localizable.xcstrings | 27 +++++++++ .../Sources/SecretAgentKit/Agent.swift | 19 ++++++ .../SecretKit/Types/CreationOptions.swift | 58 +++++++++++++++++-- .../CryptoKitMigrator.swift | 2 +- .../SmartCardSecretKit/SmartCardSecret.swift | 1 + .../SmartCardSecretKit/SmartCardStore.swift | 2 +- .../Preview Content/PreviewStore.swift | 1 + .../Views/Secrets/CreateSecretView.swift | 58 +++++++++++++++++-- 8 files changed, 157 insertions(+), 11 deletions(-) diff --git a/Sources/Packages/Resources/Localizable.xcstrings b/Sources/Packages/Resources/Localizable.xcstrings index 0a2de48..9207220 100644 --- a/Sources/Packages/Resources/Localizable.xcstrings +++ b/Sources/Packages/Resources/Localizable.xcstrings @@ -3879,6 +3879,21 @@ } } } + }, + "All" : { + + }, + "Allow Connection Operations" : { + + }, + "Allow Forwarding" : { + + }, + "Allow Signing Operations" : { + + }, + "Allowed Domains" : { + }, "app_menu_help_button" : { "extractionState" : "manual", @@ -12333,6 +12348,9 @@ } } } + }, + "example.com" : { + }, "export SSH_AUTH_SOCK=%@" : { "localizations" : { @@ -19205,6 +19223,9 @@ } } } + }, + "Key Properties" : { + }, "no_secure_storage_description" : { "extractionState" : "manual", @@ -20154,6 +20175,9 @@ } } } + }, + "Restrictions" : { + }, "reveal_in_finder_button" : { "extractionState" : "manual", @@ -24973,6 +24997,9 @@ } } } + }, + "Specific" : { + }, "unnamed_secret" : { "extractionState" : "manual", diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index bffd050..cc67812 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -86,6 +86,7 @@ extension Agent { response = try await MainActor.run { guard sessionID == nil else { logger.error("Agent received bind request, but already bound.") + // FIXME: This will break forwarding for now. throw BindingFailure() } logger.debug("Agent bound") @@ -147,6 +148,8 @@ extension Agent { throw NoMatchingKeyError() } + try evaluateRestrictions(secret.attributes.restrictions, from: provenance, for: target) + try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance, target: target) let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance, target: target) @@ -161,6 +164,22 @@ extension Agent { } +extension Agent { + + func evaluateRestrictions(_ restrictions: Restrictions?, from provenance: SigningRequestProvenance, for target: SigningRequestTarget?) throws(RestrictionError) { + guard let restrictions else { return } + print(restrictions) + throw .connectionsNotPermitted + } + + enum RestrictionError: Error { + case signingNotPermitted + case connectionsNotPermitted + case hostNotAllowed(String) + } + +} + extension Agent { /// Gives any store with no loaded secrets a chance to reload. diff --git a/Sources/Packages/Sources/SecretKit/Types/CreationOptions.swift b/Sources/Packages/Sources/SecretKit/Types/CreationOptions.swift index 99ab8f3..8152062 100644 --- a/Sources/Packages/Sources/SecretKit/Types/CreationOptions.swift +++ b/Sources/Packages/Sources/SecretKit/Types/CreationOptions.swift @@ -7,7 +7,10 @@ public struct Attributes: Sendable, Codable, Hashable { /// The authentication requirements for the key. This is simply a description of the option recorded at creation – modifying it doers not modify the key's authentication requirements. public let authentication: AuthenticationRequirement - + + /// The authentication restrictions for the key. + public var restrictions: Restrictions? + /// The string appended to the end of the SSH Public Key. /// If nil, a default value will be used. public var publicKeyAttribution: String? @@ -15,10 +18,12 @@ public struct Attributes: Sendable, Codable, Hashable { public init( keyType: KeyType, authentication: AuthenticationRequirement, + restrictions: Restrictions?, publicKeyAttribution: String? = nil ) { self.keyType = keyType self.authentication = authentication + self.restrictions = restrictions self.publicKeyAttribution = publicKeyAttribution } @@ -33,17 +38,17 @@ public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Iden /// Authentication is not required for usage. case notRequired - + /// The user needs to authenticate, using either a biometric option, a connected authorized watch, or password entry.. case presenceRequired - + /// ONLY the current set of biometric data, as matching at time of creation, is accepted. /// - Warning: This is a dangerous option prone to data loss. The user should be warned before configuring this key that if they modify their enrolled biometry INCLUDING by simply adding a new entry (ie, adding another fingeprting), the key will no longer be able to be accessed. This cannot be overridden with a password. case biometryCurrent - + /// The authentication requirement was not recorded at creation, and is unknown. case unknown - + /// Whether or not the key is known to require authentication. public var required: Bool { self == .presenceRequired || self == .biometryCurrent @@ -53,3 +58,46 @@ public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Iden self } } + +/// The restrictions for the key. +public struct Restrictions: Hashable, Sendable, Codable, Identifiable { + + public enum AllowedDomains: Hashable, Sendable, Codable { + case all + case specific([String]) + + public func hash(into hasher: inout Hasher) { + switch self { + case .all: + break + case .specific(let values): + values.hash(into: &hasher) + } + } + + } + + public enum AllowedProvenancePaths: Hashable, Sendable, Codable, Identifiable { + case all + case specific([String]) + + public var id: Int { + hashValue + } + + } + + public var allowForwarding: Bool + public var allowSigning: Bool + public var allowConnections: Bool + public var allowedDomains: AllowedDomains + public var allowedProvenancePaths: AllowedProvenancePaths + + public var id: Restrictions { + self + } + +// public static let `default` = Restrictions(allowForwarding: true, allowSigning: true, allowConnections: true, allowedDomains: .all, allowedProvenancePaths: .all) + public static let `default` = Restrictions(allowForwarding: true, allowSigning: true, allowConnections: true, allowedDomains: .specific(["example.com"]), allowedProvenancePaths: .all) + +} diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift index 68c73b2..3452d9e 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift @@ -47,7 +47,7 @@ extension SecureEnclave { .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown do { let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) - let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) + let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth, restrictions: .default)) guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { logger.log("Skipping \(name), public key already present. Marking as migrated.") markMigrated(secret: secret, oldID: id) diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardSecret.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardSecret.swift index 977355e..57e2c0f 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardSecret.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardSecret.swift @@ -10,6 +10,7 @@ extension SmartCard { public let name: String public let publicKey: Data public var attributes: Attributes + public var restrictions: Restrictions? } diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index 1efbb06..a519a6d 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -171,7 +171,7 @@ extension SmartCard.Store { let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)! let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any] let publicKey = publicKeyAttributes[kSecValueData] as! Data - let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .presenceRequired) + let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .presenceRequired, restrictions: .default) let secret = SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes) guard signatureAlgorithm(for: secret) != nil else { return nil } return secret diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift index f8406bf..9884073 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -14,6 +14,7 @@ extension Preview { Attributes( keyType: .init(algorithm: .ecdsa, size: 256), authentication: .presenceRequired, + restrictions: .default ) } } diff --git a/Sources/Secretive/Views/Secrets/CreateSecretView.swift b/Sources/Secretive/Views/Secrets/CreateSecretView.swift index ca77124..a4d98e7 100644 --- a/Sources/Secretive/Views/Secrets/CreateSecretView.swift +++ b/Sources/Secretive/Views/Secrets/CreateSecretView.swift @@ -10,8 +10,9 @@ struct CreateSecretView: View { @State private var name = "" @State private var keyAttribution = "" @State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired + @State private var restrictions: Restrictions = .default @State private var keyType: KeyType? - @State var advanced = false + @State var advanced = true // FIXME: Set back @State var errorText: String? private var authenticationOptions: [AuthenticationRequirement] { @@ -72,6 +73,7 @@ struct CreateSecretView: View { } } if advanced { + SecretRestrictionsView(restrictions: $restrictions) Section { VStack { Picker(.createSecretKeyTypeLabel, selection: $keyType) { @@ -107,6 +109,8 @@ struct CreateSecretView: View { .font(.subheadline) .foregroundStyle(.secondary) } + } header: { + Text("Key Properties") } } if let errorText { @@ -146,6 +150,7 @@ struct CreateSecretView: View { attributes: .init( keyType: keyType!, authentication: authenticationRequirement, + restrictions: restrictions, publicKeyAttribution: attribution ) ) @@ -158,7 +163,52 @@ struct CreateSecretView: View { } } +struct SecretRestrictionsView: View { -//#Preview { -// CreateSecretView(store: Preview.StoreModifiable()) { _ in } -//} + @Binding var restrictions: Restrictions + + struct IdentifiedString: Identifiable { + let value: String + var id: String { value } + } + + var body: some View { + Section { + Toggle("Allow Forwarding", isOn: $restrictions.allowForwarding) + Toggle("Allow Signing Operations", isOn: $restrictions.allowSigning) + Toggle("Allow Connection Operations", isOn: $restrictions.allowConnections) + Picker(selection: $restrictions.allowedDomains) { + Text("All") + .tag(Restrictions.AllowedDomains.all) + Text("Specific") + .tag(Restrictions.AllowedDomains.specific([])) + } label: { + Text("Allowed Domains") + } + } header: { + Text("Restrictions") + } + if restrictions.allowedDomains != .all { + Section { + switch restrictions.allowedDomains { + case .all: + EmptyView() + case .specific(let array): + if restrictions.allowedDomains != .all { + ForEach((array + [""]).map({IdentifiedString(value: $0)})) { + TextField("", text: .constant($0.value), prompt: Text("example.com")) + .labelsHidden() + } + } + } + } header: { + Text("Allowed Domains") + } + } + } +} + +#Preview { + CreateSecretView(store: Preview.StoreModifiable()) { _ in } + .frame(height: 1000) +}