This commit is contained in:
Max Goedjen
2026-09-06 18:08:16 -07:00
parent 00804587de
commit da56dd6434
8 changed files with 157 additions and 11 deletions
@@ -3879,6 +3879,21 @@
} }
} }
} }
},
"All" : {
},
"Allow Connection Operations" : {
},
"Allow Forwarding" : {
},
"Allow Signing Operations" : {
},
"Allowed Domains" : {
}, },
"app_menu_help_button" : { "app_menu_help_button" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -12333,6 +12348,9 @@
} }
} }
} }
},
"example.com" : {
}, },
"export SSH_AUTH_SOCK=%@" : { "export SSH_AUTH_SOCK=%@" : {
"localizations" : { "localizations" : {
@@ -19205,6 +19223,9 @@
} }
} }
} }
},
"Key Properties" : {
}, },
"no_secure_storage_description" : { "no_secure_storage_description" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -20154,6 +20175,9 @@
} }
} }
} }
},
"Restrictions" : {
}, },
"reveal_in_finder_button" : { "reveal_in_finder_button" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -24973,6 +24997,9 @@
} }
} }
} }
},
"Specific" : {
}, },
"unnamed_secret" : { "unnamed_secret" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -86,6 +86,7 @@ extension Agent {
response = try await MainActor.run { response = try await MainActor.run {
guard sessionID == nil else { guard sessionID == nil else {
logger.error("Agent received bind request, but already bound.") logger.error("Agent received bind request, but already bound.")
// FIXME: This will break forwarding for now.
throw BindingFailure() throw BindingFailure()
} }
logger.debug("Agent bound") logger.debug("Agent bound")
@@ -147,6 +148,8 @@ extension Agent {
throw NoMatchingKeyError() throw NoMatchingKeyError()
} }
try evaluateRestrictions(secret.attributes.restrictions, from: provenance, for: target)
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance, target: 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) 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 { extension Agent {
/// Gives any store with no loaded secrets a chance to reload. /// Gives any store with no loaded secrets a chance to reload.
@@ -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. /// 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 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. /// The string appended to the end of the SSH Public Key.
/// If nil, a default value will be used. /// If nil, a default value will be used.
public var publicKeyAttribution: String? public var publicKeyAttribution: String?
@@ -15,10 +18,12 @@ public struct Attributes: Sendable, Codable, Hashable {
public init( public init(
keyType: KeyType, keyType: KeyType,
authentication: AuthenticationRequirement, authentication: AuthenticationRequirement,
restrictions: Restrictions?,
publicKeyAttribution: String? = nil publicKeyAttribution: String? = nil
) { ) {
self.keyType = keyType self.keyType = keyType
self.authentication = authentication self.authentication = authentication
self.restrictions = restrictions
self.publicKeyAttribution = publicKeyAttribution self.publicKeyAttribution = publicKeyAttribution
} }
@@ -33,17 +38,17 @@ public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Iden
/// Authentication is not required for usage. /// Authentication is not required for usage.
case notRequired case notRequired
/// The user needs to authenticate, using either a biometric option, a connected authorized watch, or password entry.. /// The user needs to authenticate, using either a biometric option, a connected authorized watch, or password entry..
case presenceRequired case presenceRequired
/// ONLY the current set of biometric data, as matching at time of creation, is accepted. /// 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. /// - 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 case biometryCurrent
/// The authentication requirement was not recorded at creation, and is unknown. /// The authentication requirement was not recorded at creation, and is unknown.
case unknown case unknown
/// Whether or not the key is known to require authentication. /// Whether or not the key is known to require authentication.
public var required: Bool { public var required: Bool {
self == .presenceRequired || self == .biometryCurrent self == .presenceRequired || self == .biometryCurrent
@@ -53,3 +58,46 @@ public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Iden
self 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)
}
@@ -47,7 +47,7 @@ extension SecureEnclave {
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
do { do {
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) 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 { guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
logger.log("Skipping \(name), public key already present. Marking as migrated.") logger.log("Skipping \(name), public key already present. Marking as migrated.")
markMigrated(secret: secret, oldID: id) markMigrated(secret: secret, oldID: id)
@@ -10,6 +10,7 @@ extension SmartCard {
public let name: String public let name: String
public let publicKey: Data public let publicKey: Data
public var attributes: Attributes public var attributes: Attributes
public var restrictions: Restrictions?
} }
@@ -171,7 +171,7 @@ extension SmartCard.Store {
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)! let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any] let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
let publicKey = publicKeyAttributes[kSecValueData] as! Data 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) let secret = SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes)
guard signatureAlgorithm(for: secret) != nil else { return nil } guard signatureAlgorithm(for: secret) != nil else { return nil }
return secret return secret
@@ -14,6 +14,7 @@ extension Preview {
Attributes( Attributes(
keyType: .init(algorithm: .ecdsa, size: 256), keyType: .init(algorithm: .ecdsa, size: 256),
authentication: .presenceRequired, authentication: .presenceRequired,
restrictions: .default
) )
} }
} }
@@ -10,8 +10,9 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State private var name = "" @State private var name = ""
@State private var keyAttribution = "" @State private var keyAttribution = ""
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired @State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
@State private var restrictions: Restrictions = .default
@State private var keyType: KeyType? @State private var keyType: KeyType?
@State var advanced = false @State var advanced = true // FIXME: Set back
@State var errorText: String? @State var errorText: String?
private var authenticationOptions: [AuthenticationRequirement] { private var authenticationOptions: [AuthenticationRequirement] {
@@ -72,6 +73,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
if advanced { if advanced {
SecretRestrictionsView(restrictions: $restrictions)
Section { Section {
VStack { VStack {
Picker(.createSecretKeyTypeLabel, selection: $keyType) { Picker(.createSecretKeyTypeLabel, selection: $keyType) {
@@ -107,6 +109,8 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} header: {
Text("Key Properties")
} }
} }
if let errorText { if let errorText {
@@ -146,6 +150,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
attributes: .init( attributes: .init(
keyType: keyType!, keyType: keyType!,
authentication: authenticationRequirement, authentication: authenticationRequirement,
restrictions: restrictions,
publicKeyAttribution: attribution publicKeyAttribution: attribution
) )
) )
@@ -158,7 +163,52 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
struct SecretRestrictionsView: View {
//#Preview { @Binding var restrictions: Restrictions
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
//} 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)
}