Allow limited key validity window for authenticated keys (#252)

This commit is contained in:
Max Goedjen
2021-11-07 17:41:59 -08:00
committed by GitHub
parent faed5514e2
commit 7d9fee0546
12 changed files with 205 additions and 26 deletions

View File

@@ -8,7 +8,9 @@ public class AnySecretStore: SecretStore {
private let _id: () -> UUID
private let _name: () -> String
private let _secrets: () -> [AnySecret]
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> SignedData
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
private var sink: AnyCancellable?
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
@@ -18,6 +20,7 @@ public class AnySecretStore: SecretStore {
_id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
sink = secretStore.objectWillChange.sink { _ in
self.objectWillChange.send()
}
@@ -39,10 +42,14 @@ public class AnySecretStore: SecretStore {
return _secrets()
}
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> Data {
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> SignedData {
try _sign(data, secret, provenance)
}
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws {
try _persistAuthentication(secret, duration)
}
}
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
@@ -69,4 +76,5 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
public func update(secret: AnySecret, name: String) throws {
try _update(secret, name)
}
}

View File

@@ -9,7 +9,9 @@ public protocol SecretStore: ObservableObject, Identifiable {
var name: String { get }
var secrets: [SecretType] { get }
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) throws
}

View File

@@ -0,0 +1,13 @@
import Foundation
public struct SignedData {
public let data: Data
public let requiredAuthentication: Bool
public init(data: Data, requiredAuthentication: Bool) {
self.data = data
self.requiredAuthentication = requiredAuthentication
}
}

View File

@@ -17,6 +17,8 @@ extension SecureEnclave {
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
@Published public private(set) var secrets: [Secret] = []
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
self.reloadSecrets(notify: false)
@@ -93,10 +95,16 @@ extension SecureEnclave {
reloadSecrets()
}
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
let context = LAContext()
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData {
let context: LAContext
if let existing = persistedAuthenticationContexts[secret], existing.valid {
context = existing.context
} else {
let newContext = LAContext()
newContext.localizedCancelTitle = "Deny"
context = newContext
}
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
context.localizedCancelTitle = "Deny"
let attributes = [
kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
@@ -117,10 +125,37 @@ extension SecureEnclave {
}
let key = untypedSafe as! SecKey
var signError: SecurityError?
let signingStartTime = Date()
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
throw SigningError(error: signError)
}
return signature as Data
let signatureDuration = Date().timeIntervalSince(signingStartTime)
// Hack to determine if the user had to authenticate to sign.
// Since there's now way to inspect SecAccessControl to determine (afaict).
let requiredAuthentication = signatureDuration > Constants.unauthenticatedThreshold
return SignedData(data: signature as Data, requiredAuthentication: requiredAuthentication)
}
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws {
let newContext = LAContext()
newContext.localizedCancelTitle = "Deny"
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
if let durationString = formatter.string(from: duration) {
newContext.localizedReason = "unlock secret \"\(secret.name)\" for \(durationString)"
} else {
newContext.localizedReason = "unlock secret \"\(secret.name)\""
}
newContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) { [weak self] success, _ in
guard success else { return }
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
self?.persistedAuthenticationContexts[secret] = context
}
}
}
@@ -177,6 +212,7 @@ extension SecureEnclave.Store {
throw SecureEnclave.KeychainError(statusCode: status)
}
}
}
extension SecureEnclave {
@@ -202,6 +238,30 @@ extension SecureEnclave {
enum Constants {
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
static let keyType = kSecAttrKeyTypeECSECPrimeRandom
static let unauthenticatedThreshold: TimeInterval = 0.05
}
}
extension SecureEnclave {
private struct PersistentAuthenticationContext {
let secret: Secret
let context: LAContext
// Monotonic time instead of Date() to prevent people setting the clock back.
let expiration: UInt64
init(secret: Secret, context: LAContext, duration: TimeInterval) {
self.secret = secret
self.context = context
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
self.expiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds)
}
var valid: Bool {
clock_gettime_nsec_np(CLOCK_MONOTONIC) < expiration
}
}
}

View File

@@ -43,7 +43,7 @@ extension SmartCard {
fatalError("Keys must be deleted on the smart card.")
}
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData {
guard let tokenID = tokenID else { fatalError() }
let context = LAContext()
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
@@ -78,7 +78,10 @@ extension SmartCard {
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
throw SigningError(error: signError)
}
return signature as Data
return SignedData(data: signature as Data, requiredAuthentication: false)
}
public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {
}
}