]> git.saurik.com Git - apple/security.git/blobdiff - keychain/TrustedPeersHelper/Container.swift
Security-59306.11.20.tar.gz
[apple/security.git] / keychain / TrustedPeersHelper / Container.swift
diff --git a/keychain/TrustedPeersHelper/Container.swift b/keychain/TrustedPeersHelper/Container.swift
new file mode 100644 (file)
index 0000000..271e6c1
--- /dev/null
@@ -0,0 +1,3926 @@
+/*
+ * Copyright (c) 2018 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+import CloudKitCode
+import CoreData
+import Foundation
+import os
+import Security
+import SecurityFoundation
+
+let tplogDebug = OSLog(subsystem: "com.apple.security.trustedpeers", category: "debug")
+let tplogTrace = OSLog(subsystem: "com.apple.security.trustedpeers", category: "trace")
+
+let egoIdentitiesAccessGroup = "com.apple.security.egoIdentities"
+
+public enum ContainerError: Error {
+    case unableToCreateKeyPair
+    case noPreparedIdentity
+    case failedToStoreIdentity
+    case needsAuthentication
+    case missingStableInfo
+    case missingDynamicInfo
+    case nonMember
+    case invalidPermanentInfoOrSig
+    case invalidStableInfoOrSig
+    case invalidVoucherOrSig
+    case sponsorNotRegistered(String)
+    case unknownPolicyVersion(UInt64)
+    case preparedIdentityNotOnAllowedList(String)
+    case couldNotLoadAllowedList
+    case noPeersPreapprovePreparedIdentity
+    case policyDocumentDoesNotValidate
+    case tooManyBottlesForPeer
+    case noBottleForPeer
+    case restoreBottleFailed
+    case noBottlesForEscrowRecordID
+    case bottleDoesNotContainContents
+    case bottleDoesNotContainEscrowKeySignature
+    case bottleDoesNotContainerPeerKeySignature
+    case bottleDoesNotContainPeerID
+    case failedToCreateBottledPeer
+    case signatureVerificationFailed
+    case bottleDoesNotContainerEscrowKeySPKI
+    case failedToFetchEscrowContents
+    case failedToCreateRecoveryKey
+    case untrustedRecoveryKeys
+    case noBottlesPresent
+    case recoveryKeysNotEnrolled
+    case bottleCreatingPeerNotFound
+    case unknownCloudKitError
+    case cloudkitResponseMissing
+    case failedToLoadSecret(errorCode: Int)
+    case failedToLoadSecretDueToType
+    case failedToAssembleBottle
+    case invalidPeerID
+    case failedToStoreSecret(errorCode: Int)
+}
+
+extension ContainerError: LocalizedError {
+    public var errorDescription: String? {
+        switch self {
+        case .unableToCreateKeyPair:
+            return "unable to create key pair"
+        case .noPreparedIdentity:
+            return "no prepared identity"
+        case .failedToStoreIdentity:
+            return "failed to stored identity"
+        case .needsAuthentication:
+            return "needs authentication"
+        case .missingStableInfo:
+            return "missing stable info"
+        case .missingDynamicInfo:
+            return "missing dynamic info"
+        case .nonMember:
+            return "non member"
+        case .invalidPermanentInfoOrSig:
+            return "invalid permanent info or signature"
+        case .invalidStableInfoOrSig:
+            return "invalid stable info or signature"
+        case .invalidVoucherOrSig:
+            return "invalid voucher or signature"
+        case .sponsorNotRegistered(let s):
+            return "sponsor not registered: \(s)"
+        case .unknownPolicyVersion(let v):
+            return "unknown policy version: \(v)"
+        case .preparedIdentityNotOnAllowedList(let id):
+            return "prepared identity (\(id)) not on allowed machineID list"
+        case .couldNotLoadAllowedList:
+            return "could not load allowed machineID list"
+        case .noPeersPreapprovePreparedIdentity:
+            return "no peers preapprove prepared identity"
+        case .policyDocumentDoesNotValidate:
+            return "policy document from server doesn't validate"
+        case .tooManyBottlesForPeer:
+            return "too many bottles exist for peer"
+        case .noBottleForPeer:
+            return "no bottle exists for peer"
+        case .restoreBottleFailed:
+            return "failed to restore bottle"
+        case .noBottlesForEscrowRecordID:
+            return "0 bottles exist for escrow record id"
+        case .bottleDoesNotContainContents:
+            return "bottle does not contain encrypted contents"
+        case .bottleDoesNotContainEscrowKeySignature:
+            return "bottle does not contain escrow signature"
+        case .bottleDoesNotContainerPeerKeySignature:
+            return "bottle does not contain peer signature"
+        case .bottleDoesNotContainPeerID:
+            return "bottle does not contain peer id"
+        case .failedToCreateBottledPeer:
+            return "failed to create a bottled peer"
+        case .signatureVerificationFailed:
+            return "failed to verify signature"
+        case .bottleDoesNotContainerEscrowKeySPKI:
+            return "bottle does not contain escrowed key spki"
+        case .failedToFetchEscrowContents:
+            return "failed to fetch escrow contents"
+        case .failedToCreateRecoveryKey:
+            return "failed to create recovery keys"
+        case .untrustedRecoveryKeys:
+            return "untrusted recovery keys"
+        case .noBottlesPresent:
+            return "no bottle present"
+        case .recoveryKeysNotEnrolled:
+            return "recovery key is not enrolled with octagon"
+        case .bottleCreatingPeerNotFound:
+            return "The peer that created the bottle was not found"
+        case .unknownCloudKitError:
+            return "unknown error from cloudkit"
+        case .cloudkitResponseMissing:
+            return "Response missing from CloudKit"
+        case .failedToLoadSecret(errorCode: let errorCode):
+            return "failed to load secret: \(errorCode)"
+        case .failedToLoadSecretDueToType:
+            return "Failed to load secret due to type mismatch (value was not dictionary)"
+        case .failedToAssembleBottle:
+            return "failed to assemble bottle for peer"
+        case .invalidPeerID:
+            return "peerID is invalid"
+        case .failedToStoreSecret(errorCode: let errorCode):
+            return "failed to store the secret in the keychain \(errorCode)"
+        }
+    }
+}
+
+extension ContainerError: CustomNSError {
+
+    public static var errorDomain: String {
+        return "com.apple.security.trustedpeers.container"
+    }
+
+    public var errorCode: Int {
+        switch self {
+        case .unableToCreateKeyPair:
+            return 0
+        case .noPreparedIdentity:
+            return 1
+        case .failedToStoreIdentity:
+            return 2
+        case .needsAuthentication:
+            return 3
+        case .missingStableInfo:
+            return 4
+        case .missingDynamicInfo:
+            return 5
+        case .nonMember:
+            return 6
+        case .invalidPermanentInfoOrSig:
+            return 7
+        case .invalidVoucherOrSig:
+            return 8
+        case .invalidStableInfoOrSig:
+            return 9
+        case .sponsorNotRegistered:
+            return 11
+        case .unknownPolicyVersion:
+            return 12
+        case .preparedIdentityNotOnAllowedList:
+            return 13
+        case .noPeersPreapprovePreparedIdentity:
+            return 14
+        case .policyDocumentDoesNotValidate:
+            return 15
+        // 16 was invalidPeerID and failedToAssembleBottle
+        case .tooManyBottlesForPeer:
+            return 17
+        case .noBottleForPeer:
+            return 18
+        case .restoreBottleFailed:
+            return 19
+        case .noBottlesForEscrowRecordID:
+            return 20
+        case .bottleDoesNotContainContents:
+            return 21
+        case .bottleDoesNotContainEscrowKeySignature:
+            return 22
+        case .bottleDoesNotContainerPeerKeySignature:
+            return 23
+        case .bottleDoesNotContainPeerID:
+            return 24
+        case .failedToCreateBottledPeer:
+            return 25
+        case .signatureVerificationFailed:
+            return 26
+        // 27 was failedToLoadSecret*
+        case .bottleDoesNotContainerEscrowKeySPKI:
+            return 28
+        case .failedToFetchEscrowContents:
+            return 29
+        case .couldNotLoadAllowedList:
+            return 30
+        case .failedToCreateRecoveryKey:
+            return 31
+        case .untrustedRecoveryKeys:
+            return 32
+        case .noBottlesPresent:
+            return 33
+        case .recoveryKeysNotEnrolled:
+            return 34
+        case .bottleCreatingPeerNotFound:
+            return 35
+        case .unknownCloudKitError:
+            return 36
+        case .cloudkitResponseMissing:
+            return 37
+        case .failedToLoadSecret:
+            return 38
+        case .failedToLoadSecretDueToType:
+            return 39
+        case .failedToStoreSecret:
+            return 40
+        case .failedToAssembleBottle:
+            return 41
+        case .invalidPeerID:
+            return 42
+        }
+    }
+
+    public var underlyingError: NSError? {
+        switch self {
+        case .failedToLoadSecret(errorCode: let errorCode):
+            return NSError(domain: "securityd", code: errorCode)
+        case .failedToStoreSecret(errorCode: let errorCode):
+            return NSError(domain: "securityd", code: errorCode)
+        default:
+            return nil
+        }
+    }
+
+    public var errorUserInfo: [String: Any] {
+        var ret = [String: Any]()
+        if let desc = self.errorDescription {
+            ret[NSLocalizedDescriptionKey] = desc
+        }
+        if let underlyingError = self.underlyingError {
+            ret[NSUnderlyingErrorKey] = underlyingError
+        }
+        return ret
+    }
+}
+
+internal func traceError(_ error: Error?) -> String {
+    if let error = error {
+        return "error: \(String(describing: error))"
+    } else {
+        return "success"
+    }
+}
+
+func saveSecret(_ secret: Data, label: String) throws {
+
+    let query: [CFString: Any] = [
+        kSecClass: kSecClassInternetPassword,
+        kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
+        kSecUseDataProtectionKeychain: true,
+        kSecAttrAccessGroup: "com.apple.security.octagon",
+        kSecAttrSynchronizable: kCFBooleanFalse,
+        kSecAttrDescription: label,
+        kSecAttrPath: label,
+        kSecValueData: secret,
+        ]
+
+    var results: CFTypeRef?
+    var status = SecItemAdd(query as CFDictionary, &results)
+
+    if status == errSecSuccess {
+        return
+    }
+
+    if status == errSecDuplicateItem {
+        // Add every primary key attribute to this find dictionary
+        var findQuery: [CFString: Any] = [:]
+        findQuery[kSecClass]              = query[kSecClass]
+        findQuery[kSecAttrSynchronizable] = query[kSecAttrSynchronizable]
+        findQuery[kSecAttrAccessGroup]    = query[kSecAttrAccessGroup]
+        findQuery[kSecAttrServer]         = query[kSecAttrDescription]
+        findQuery[kSecAttrPath]           = query[kSecAttrPath]
+        findQuery[kSecUseDataProtectionKeychain] = query[kSecUseDataProtectionKeychain];
+
+        var updateQuery: [CFString: Any] = query
+        updateQuery[kSecClass] = nil
+
+        status = SecItemUpdate(findQuery as CFDictionary, updateQuery as CFDictionary)
+
+        if status != errSecSuccess {
+            throw ContainerError.failedToStoreSecret(errorCode: Int(status))
+        }
+    } else {
+        throw ContainerError.failedToStoreSecret(errorCode: Int(status))
+    }
+}
+
+func loadSecret(label: String) throws -> (Data?) {
+    var secret: Data?
+
+    let query: [CFString: Any] = [
+        kSecClass: kSecClassInternetPassword,
+        kSecAttrAccessGroup: "com.apple.security.octagon",
+        kSecAttrDescription: label,
+        kSecReturnAttributes: true,
+        kSecReturnData: true,
+        kSecAttrSynchronizable: kCFBooleanFalse,
+        kSecMatchLimit: kSecMatchLimitOne,
+        ]
+
+    var result: CFTypeRef?
+    let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+    if status != errSecSuccess || result == nil {
+        throw ContainerError.failedToLoadSecret(errorCode: Int(status))
+    }
+
+    if result != nil {
+        if let dictionary = result as? [CFString: Any] {
+            secret = dictionary[kSecValueData] as? Data
+        } else {
+            throw ContainerError.failedToLoadSecretDueToType
+        }
+    }
+    return secret
+}
+
+func saveEgoKeyPair(_ keyPair: _SFECKeyPair, identifier: String, resultHandler: @escaping (Bool, Error?) -> Void) {
+    let keychainManager = _SFKeychainManager.default()
+    let signingIdentity = _SFIdentity(keyPair: keyPair)
+    let accessibility = SFAccessibilityMakeWithMode(SFAccessibilityMode.accessibleWhenUnlocked)  // class A
+    let accessPolicy = _SFAccessPolicy(accessibility: accessibility,
+                                       sharingPolicy: SFSharingPolicy.thisDeviceOnly)
+    accessPolicy.accessGroup = egoIdentitiesAccessGroup
+    keychainManager.setIdentity(signingIdentity,
+                                forIdentifier: identifier,
+                                accessPolicy: accessPolicy,
+                                resultHandler: resultHandler)
+}
+
+func loadEgoKeyPair(identifier: String, resultHandler: @escaping (_SFECKeyPair?, Error?) -> Void) {
+    let keychainManager = _SFKeychainManager.default()
+
+    // FIXME constrain to egoIdentitiesAccessGroup, <rdar://problem/39597940>
+    keychainManager.identity(forIdentifier: identifier) { result in
+        switch result.resultType {
+        case .valueAvailable:
+            resultHandler(result.value?.keyPair as? _SFECKeyPair, nil)
+        case .needsAuthentication:
+            resultHandler(nil, ContainerError.needsAuthentication)
+        case .error:
+            resultHandler(nil, result.error)
+        }
+    }
+}
+
+func loadEgoKeys(peerID: String, resultHandler: @escaping (OctagonSelfPeerKeys?, Error?) -> Void) {
+    loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: peerID)) { signingKey, error in
+        guard let signingKey = signingKey else {
+            os_log("Unable to load signing key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing")
+            resultHandler(nil, error)
+            return
+        }
+
+        loadEgoKeyPair(identifier: encryptionKeyIdentifier(peerID: peerID)) { encryptionKey, error in
+            guard let encryptionKey = encryptionKey else {
+                os_log("Unable to load encryption key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing")
+                resultHandler(nil, error)
+                return
+            }
+
+            do {
+                resultHandler(try OctagonSelfPeerKeys(peerID: peerID, signingKey: signingKey, encryptionKey: encryptionKey), nil)
+            } catch {
+                resultHandler(nil, error)
+            }
+        }
+    }
+}
+
+func removeEgoKeysSync(peerID: String) throws -> Bool {
+    var resultSema = DispatchSemaphore(value: 0)
+
+    let keychainManager = _SFKeychainManager.default()
+
+    var retresultForSigningDeletion: Bool = false
+    var reterrorForSigningDeletion: Error? = nil
+
+    //remove signing keys
+    keychainManager.removeItem(withIdentifier: signingKeyIdentifier(peerID: peerID)) { result, error in
+        retresultForSigningDeletion = result
+        reterrorForSigningDeletion = error
+        resultSema.signal()
+    }
+
+    resultSema.wait()
+
+    if let error = reterrorForSigningDeletion {
+        throw error
+    }
+
+    if retresultForSigningDeletion == false {
+        return retresultForSigningDeletion
+    }
+
+    // now let's do the same thing with the encryption keys
+    resultSema = DispatchSemaphore(value: 0)
+    var retresultForEncryptionDeletion: Bool = false
+    var reterrorForEncryptionDeletion: Error? = nil
+
+    keychainManager.removeItem(withIdentifier: encryptionKeyIdentifier(peerID: peerID)) { result, error in
+        retresultForEncryptionDeletion = result
+        reterrorForEncryptionDeletion = error
+        resultSema.signal()
+    }
+    resultSema.wait()
+
+    if let error = reterrorForEncryptionDeletion {
+        throw error
+    }
+
+    return retresultForEncryptionDeletion && retresultForSigningDeletion
+}
+
+func loadEgoKeysSync(peerID: String) throws -> OctagonSelfPeerKeys {
+    // Gotta promote to synchronous; 'antipattern' ahoy
+    let resultSema = DispatchSemaphore(value: 0)
+
+    var result: OctagonSelfPeerKeys?
+    var reserror: Error?
+
+    loadEgoKeys(peerID: peerID) { keys, error in
+        result = keys
+        reserror = error
+        resultSema.signal()
+    }
+    resultSema.wait()
+
+    if let error = reserror {
+        throw error
+    }
+
+    if let result = result {
+        return result
+    }
+
+    abort()
+}
+
+func signingKeyIdentifier(peerID: String) -> String {
+    return "signing-key " + peerID
+}
+
+func encryptionKeyIdentifier(peerID: String) -> String {
+    return "encryption-key " + peerID
+}
+
+func makeTLKShares(ckksTLKs: [CKKSKeychainBackedKey]?, asPeer: CKKSSelfPeer, toPeer: CKKSPeer, epoch: Int) throws -> [TLKShare] {
+    return try (ckksTLKs ?? []).map { tlk in
+        os_log("Making TLKShare for %@ for key %@", log: tplogDebug, type: .default, toPeer.description, tlk)
+        // Not being able to convert a TLK to a TLKShare is a failure, but not having a TLK is only half bad
+        do {
+            return TLKShare.convert(ckksTLKShare: try CKKSTLKShare(tlk, as: asPeer, to: toPeer, epoch: epoch, poisoned: 0))
+        } catch {
+            let nserror = error as NSError
+            if nserror.domain == "securityd" && nserror.code == errSecItemNotFound {
+                os_log("No TLK contents for %@, no TLK share possible", log: tplogDebug, type: .default, tlk)
+                return nil
+            } else {
+                throw error
+            }
+        }
+    }.compactMap { $0 }
+}
+
+func extract(tlkShares: [CKKSTLKShare], peer: CKKSSelfPeer) {
+    os_log("Attempting to recover %d TLK shares for peer %@", log: tplogDebug, type: .default, tlkShares.count, peer.peerID)
+    for share in tlkShares {
+        guard share.receiverPeerID == peer.peerID else {
+            os_log("Skipping %@ (wrong peerID)", log: tplogDebug, type: .default, share)
+            continue
+        }
+
+        do {
+            // TODO: how should we handle peer sets here?
+            let key = try share.recoverTLK(peer,
+                                           trustedPeers: [peer as! AnyHashable],
+                                           ckrecord: nil)
+
+            try key.saveMaterialToKeychain()
+            os_log("Recovered %@ (from %@)", log: tplogDebug, type: .default, key, share)
+        } catch {
+            os_log("Failed to recover share %@: %@", log: tplogDebug, type: .default, share, error as CVarArg)
+        }
+    }
+
+}
+
+struct ContainerState {
+    var egoPeerID: String?
+    var peers: [String: TPPeer] = [:]
+    var vouchers: [TPVoucher] = []
+    var bottles = Set<BottleMO>()
+    var recoverySigningKey: Data?
+    var recoveryEncryptionKey: Data?
+}
+
+internal struct StableChanges {
+    let deviceName: String?
+    let serialNumber: String?
+    let osVersion: String?
+    let policyVersion: UInt64?
+    let policySecrets: [String: Data]?
+    let recoverySigningPubKey: Data?
+    var recoveryEncryptionPubKey: Data?
+}
+
+// CoreData doesn't handle creating an identical model from an identical URL. Help it out.
+private var nsObjectModels: [URL: NSManagedObjectModel] = [:]
+private let nsObjectModelsQueue = DispatchQueue(label: "com.apple.security.TrustedPeersHelper.nsObjectModels")
+func getOrMakeModel(url: URL) -> NSManagedObjectModel {
+    return nsObjectModelsQueue.sync {
+        if let model = nsObjectModels[url] {
+            return model
+        }
+        let newModel = NSManagedObjectModel(contentsOf: url)!
+        nsObjectModels[url] = newModel
+        return newModel
+    }
+}
+
+struct ContainerName: Hashable, CustomStringConvertible {
+    let container: String
+    let context: String
+
+    func asSingleString() -> String {
+        // Just to be nice, hide the fact that multiple contexts exist on most machines
+        if self.context == OTDefaultContext {
+            return self.container
+        } else {
+            return self.container + "-" + self.context
+        }
+    }
+
+    var description: String {
+        return "Container(\(self.container),\(self.context))"
+    }
+}
+
+/// This maps to a Cuttlefish service backed by a CloudKit container,
+/// and a corresponding local Core Data persistent container.
+///
+/// Methods may be invoked concurrently.
+class Container: NSObject {
+    let name: ContainerName
+
+    private let cuttlefish: CuttlefishAPIAsync
+
+    // Only one request (from Client) is permitted to be in progress at a time.
+    // That includes while waiting for network, i.e. one request must complete
+    // before the next can begin. Otherwise two requests could in parallel be
+    // fetching updates from Cuttlefish, with ensuing changeToken overwrites etc.
+    // This applies for mutating requests -- requests that can only read the current
+    // state (on the moc queue) do not need to.
+    internal let semaphore = DispatchSemaphore(value: 1)
+
+    // All Core Data access happens through moc: NSManagedObjectContext. The
+    // moc insists on having its own queue, and all operations must happen on
+    // that queue.
+    internal let moc: NSManagedObjectContext
+
+    // Rather than Container having its own dispatch queue, we use moc's queue
+    // to synchronise access to our own state as well. So the following instance
+    // variables must only be accessed within blocks executed by calling
+    // moc.perform() or moc.performAndWait().
+    internal var containerMO: ContainerMO
+    internal var model: TPModel
+
+    /**
+     Construct a Container.
+
+     - Parameter name: The name the CloudKit container to which requests will be routed.
+
+     The "real" container that drives CKKS etc should be named `"com.apple.security.keychain"`.
+     Use other names for test containers such as for rawfish (rawhide-style testing for cuttlefish)
+
+     - Parameter persistentStoreURL: The location the local Core Data database for this container will be stored.
+
+     - Parameter cuttlefish: Interface to cuttlefish.
+     */
+    init(name: ContainerName, persistentStoreDescription: NSPersistentStoreDescription, cuttlefish: CuttlefishAPIAsync) throws {
+        var initError: Error?
+        var containerMO: ContainerMO?
+        var model: TPModel?
+
+        // Set up Core Data stack
+        let url = Bundle(for: type(of: self)).url(forResource: "TrustedPeersHelper", withExtension: "momd")!
+        let mom = getOrMakeModel(url: url)
+        let persistentContainer = NSPersistentContainer(name: "TrustedPeersHelper", managedObjectModel: mom)
+        persistentContainer.persistentStoreDescriptions = [persistentStoreDescription]
+
+        persistentContainer.loadPersistentStores { _, error in
+            initError = error
+        }
+        if let initError = initError {
+            throw initError
+        }
+
+        let moc = persistentContainer.newBackgroundContext()
+        moc.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
+
+        moc.performAndWait {
+            // Fetch an existing ContainerMO record if it exists, or create and save one
+            do {
+                let containerFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Container")
+                containerFetch.predicate = NSPredicate(format: "name == %@", name.asSingleString())
+                let fetchedContainers = try moc.fetch(containerFetch)
+                if let container = fetchedContainers.first as? ContainerMO {
+                    containerMO = container
+                } else {
+                    containerMO = ContainerMO(context: moc)
+                    containerMO!.name = name.asSingleString()
+                }
+
+                // Perform upgrades as needed
+                Container.onqueueUpgradeMachineIDSetToModel(container: containerMO!, moc: moc)
+                Container.onqueueUpgradeMachineIDSetToUseStatus(container: containerMO!, moc: moc)
+
+                model = Container.loadModel(from: containerMO!)
+                Container.ensureEgoConsistency(from: containerMO!, model: model!)
+                try moc.save()
+            } catch {
+                initError = error
+                return
+            }
+        }
+        if let initError = initError {
+            throw initError
+        }
+
+        self.name = name
+        self.moc = moc
+        self.containerMO = containerMO!
+        self.cuttlefish = cuttlefish
+        self.model = model!
+
+        super.init()
+    }
+
+    // Must be on containerMO's moc queue to call this
+    internal static func loadModel(from containerMO: ContainerMO) -> TPModel {
+        // Populate model from persistent store
+        let model = TPModel(decrypter: Decrypter())
+        let keyFactory = TPECPublicKeyFactory()
+        let peers = containerMO.peers as? Set<PeerMO>
+        peers?.forEach { peer in
+            guard let permanentInfo = TPPeerPermanentInfo(peerID: peer.peerID!,
+                                                          data: peer.permanentInfo! as Data,
+                                                          sig: peer.permanentInfoSig! as Data,
+                                                          keyFactory: keyFactory) else {
+                                                            return
+            }
+            model.registerPeer(with: permanentInfo)
+            if let data = peer.stableInfo, let sig = peer.stableInfoSig {
+                if let stableInfo = TPPeerStableInfo(data: data as Data, sig: sig as Data) {
+                    do {
+                        try model.update(stableInfo, forPeerWithID: permanentInfo.peerID)
+                    } catch {
+                        os_log("loadModel unable to update stable info for peer(%@): %@", log: tplogDebug, type: .default, peer, error as CVarArg)
+                    }
+                } else {
+                    os_log("loadModel: peer %@ has unparseable stable info", log: tplogDebug, type: .default, permanentInfo.peerID)
+                }
+            } else {
+                os_log("loadModel: peer %@ has no stable info", log: tplogDebug, type: .default, permanentInfo.peerID)
+            }
+            if let data = peer.dynamicInfo, let sig = peer.dynamicInfoSig {
+                if let dynamicInfo = TPPeerDynamicInfo(data: data as Data, sig: sig as Data) {
+                    do {
+                        try model.update(dynamicInfo, forPeerWithID: permanentInfo.peerID)
+                    } catch {
+                        os_log("loadModel unable to update dynamic info for peer(%@): %@", log: tplogDebug, type: .default, peer, error as CVarArg)
+                    }
+                } else {
+                    os_log("loadModel: peer %@ has unparseable dynamic info", log: tplogDebug, type: .default, permanentInfo.peerID)
+                }
+            } else {
+                os_log("loadModel: peer %@ has no dynamic info", log: tplogDebug, type: .default, permanentInfo.peerID)
+            }
+            peer.vouchers?.forEach {
+                let v = $0 as! VoucherMO
+                if let data = v.voucherInfo, let sig = v.voucherInfoSig {
+                    if let voucher = TPVoucher(infoWith: data, sig: sig) {
+                        model.register(voucher)
+                    }
+                }
+            }
+        }
+
+        // Register persisted policies (cached from cuttlefish)
+        let policies = containerMO.policies as? Set<PolicyMO>
+        policies?.forEach { policyMO in
+            if let policyHash = policyMO.policyHash,
+                let policyData = policyMO.policyData {
+                if let policyDoc = TPPolicyDocument.policyDoc(withHash: policyHash, data: policyData) {
+                    model.register(policyDoc)
+                }
+            }
+        }
+
+        // Register built-in policies
+        builtInPolicyDocuments().forEach { policyDoc in
+            model.register(policyDoc)
+        }
+
+        let knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
+        let allowedMachineIDs = Set(knownMachines.filter { $0.status == TPMachineIDStatus.allowed.rawValue }.compactMap { $0.machineID })
+        let disallowedMachineIDs = Set(knownMachines.filter { $0.status == TPMachineIDStatus.disallowed.rawValue }.compactMap { $0.machineID })
+
+        os_log("loadModel: allowedMachineIDs: %@", log: tplogDebug, type: .default, allowedMachineIDs)
+        os_log("loadModel: disallowedMachineIDs: %@", log: tplogDebug, type: .default, disallowedMachineIDs)
+
+        if allowedMachineIDs.count == 0 {
+            os_log("loadModel: no allowedMachineIDs?", log: tplogDebug, type: .default)
+        }
+
+        return model
+    }
+
+    // Must be on containerMO's moc queue to call this
+    internal static func ensureEgoConsistency(from containerMO: ContainerMO, model: TPModel) {
+        guard let egoPeerID = containerMO.egoPeerID,
+            let egoStableData = containerMO.egoPeerStableInfo,
+            let egoStableSig = containerMO.egoPeerStableInfoSig
+            else {
+                os_log("ensureEgoConsistency failed to find ego peer information", log: tplogDebug, type: .error)
+                return
+        }
+
+        guard let containerEgoStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else {
+            os_log("ensureEgoConsistency failed to create TPPeerStableInfo from container", log: tplogDebug, type: .error)
+            return
+        }
+
+        guard let modelStableInfo = model.getStableInfoForPeer(withID: egoPeerID) else {
+            os_log("ensureEgoConsistency failed to create TPPeerStableInfo from model", log: tplogDebug, type: .error)
+            return
+        }
+
+        if modelStableInfo.clock > containerEgoStableInfo.clock {
+            containerMO.egoPeerStableInfo = modelStableInfo.data
+            containerMO.egoPeerStableInfoSig = modelStableInfo.sig
+        }
+
+    }
+
+    static func dictionaryRepresentation(bottle: BottleMO) -> [String: Any] {
+        var dict: [String: String] = [:]
+
+        dict["bottleID"] = bottle.bottleID
+        dict["peerID"] = bottle.peerID
+        dict["signingSPKI"] = bottle.escrowedSigningSPKI?.base64EncodedString()
+        dict["signatureUsingPeerKey"] = bottle.signatureUsingPeerKey?.base64EncodedString()
+        dict["signatureUsingSPKI"] = bottle.signatureUsingEscrowKey?.base64EncodedString()
+        // ignore the bottle contents; they're mostly unreadable
+
+        return dict
+    }
+
+    static func peerdictionaryRepresentation(peer: TPPeer) -> [String: Any] {
+        var peerDict: [String: Any] = [
+            "permanentInfo": peer.permanentInfo.dictionaryRepresentation(),
+            "peerID": peer.peerID,
+            ]
+        if let stableInfo = peer.stableInfo {
+            peerDict["stableInfo"] = stableInfo.dictionaryRepresentation()
+        }
+        if let dynamicInfo = peer.dynamicInfo {
+            peerDict["dynamicInfo"] = dynamicInfo.dictionaryRepresentation()
+        }
+
+        return peerDict
+    }
+
+    func onQueueDetermineLocalTrustStatus(reply: @escaping (TrustedPeersHelperEgoPeerStatus, Error?) -> Void) {
+        let peerCountsByModelID = self.model.peerCountsByModelID()
+
+        if let egoPeerID = self.containerMO.egoPeerID {
+            var status = self.model.statusOfPeer(withID: egoPeerID)
+            var isExcluded: Bool = (status == .excluded)
+
+            loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, loadError in
+                var returnError = loadError
+
+                guard returnError == nil else {
+                    var isLocked = false
+                    if let error = (loadError as NSError?) {
+                        os_log("trust status: Unable to load ego keys: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        if error.code == errSecItemNotFound && error.domain == NSOSStatusErrorDomain {
+                            os_log("trust status: Lost the ego key pair, returning 'excluded' in hopes of fixing up the identity", log: tplogDebug, type: .debug)
+                            isExcluded = true
+                            status = .excluded
+                        } else if error.code == errSecInteractionNotAllowed && error.domain == NSOSStatusErrorDomain {
+                            // we have an item, but can't read it, suppressing error
+                            isLocked = true
+                            returnError = nil
+                        }
+                    }
+
+                    let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: egoPeerID,
+                                                                    status: status,
+                                                                    peerCountsByModelID: peerCountsByModelID,
+                                                                    isExcluded: isExcluded,
+                                                                    isLocked: isLocked)
+                    reply(egoStatus, returnError)
+                    return
+                }
+
+                //ensure egoPeerKeys are populated
+                guard egoPeerKeys != nil else {
+                    os_log("trust status: No error but Ego Peer Keys are nil", log: tplogDebug, type: .default)
+                    let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: egoPeerID,
+                                                                    status: .excluded,
+                                                                    peerCountsByModelID: peerCountsByModelID,
+                                                                    isExcluded: true,
+                                                                    isLocked: false)
+
+                    reply(egoStatus, loadError)
+                    return
+                }
+
+                let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: egoPeerID,
+                                                                status: status,
+                                                                peerCountsByModelID: peerCountsByModelID,
+                                                                isExcluded: isExcluded,
+                                                                isLocked: false)
+                reply(egoStatus, nil)
+                return
+            }
+
+        } else {
+            // With no ego peer ID, either return 'excluded' if there are extant peers, or 'unknown' to signal no peers at all
+            if self.model.allPeerIDs().isEmpty {
+                os_log("No existing peers in account", log: tplogDebug, type: .debug)
+                let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: nil,
+                                                                status: .unknown,
+                                                                peerCountsByModelID: peerCountsByModelID,
+                                                                isExcluded: false,
+                                                                isLocked: false)
+                reply(egoStatus, nil)
+                return
+            } else {
+                os_log("Existing peers in account, but we don't have a peer ID. We are excluded.", log: tplogDebug, type: .debug)
+                let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: nil,
+                                                                status: .excluded,
+                                                                peerCountsByModelID: peerCountsByModelID,
+                                                                isExcluded: true,
+                                                                isLocked: false)
+                reply(egoStatus, nil)
+                return
+            }
+        }
+    }
+
+    func trustStatus(reply: @escaping (TrustedPeersHelperEgoPeerStatus, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (TrustedPeersHelperEgoPeerStatus, Error?) -> Void = {
+            // Suppress logging of successful replies here; it's not that useful
+            let logType: OSLogType = $1 == nil ? .debug : .info
+            os_log("trustStatus complete: %@ %@",
+                   log: tplogTrace, type: logType, TPPeerStatusToString($0.egoStatus), traceError($1))
+
+            self.semaphore.signal()
+            reply($0, $1)
+        }
+        self.moc.performAndWait {
+            // Knowledge of your peer status only exists if you know about other peers. If you haven't fetched, fetch.
+            if self.containerMO.changeToken == nil {
+                self.fetchAndPersistChanges { fetchError in
+                    guard fetchError == nil else {
+                        if let error = fetchError {
+                            os_log("Unable to fetch changes, trust status is unknown: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        }
+
+                        let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: nil,
+                                                                        status: .unknown,
+                                                                        peerCountsByModelID: [:],
+                                                                        isExcluded: false,
+                                                                        isLocked: false)
+                        reply(egoStatus, fetchError)
+                        return
+                    }
+
+                    self.moc.performAndWait {
+                        self.onQueueDetermineLocalTrustStatus(reply: reply)
+                    }
+                }
+            } else {
+                self.onQueueDetermineLocalTrustStatus(reply: reply)
+            }
+        }
+    }
+
+    func fetchTrustState(reply: @escaping (TrustedPeersHelperPeerState?, [TrustedPeersHelperPeer]?, Error?) -> Void) {
+        let reply: (TrustedPeersHelperPeerState?, [TrustedPeersHelperPeer]?, Error?) -> Void = {
+            os_log("fetch trust state complete: %@ %@",
+                   log: tplogTrace, type: .info, String(reflecting: $0), traceError($2))
+            reply($0, $1, $2)
+        }
+
+        self.moc.performAndWait {
+            if let egoPeerID = self.containerMO.egoPeerID,
+               let egoPermData = self.containerMO.egoPeerPermanentInfo,
+               let egoPermSig = self.containerMO.egoPeerPermanentInfoSig {
+
+                let keyFactory = TPECPublicKeyFactory()
+                guard let permanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else {
+                    os_log("fetchTrustState failed to create TPPeerPermanentInfo", log: tplogDebug, type: .error)
+                    reply(nil, nil, ContainerError.invalidPermanentInfoOrSig)
+                    return
+                }
+
+                let isPreapproved = self.model.hasPeerPreapprovingKey(permanentInfo.signingPubKey.spki())
+                os_log("fetchTrustState: ego peer is %@", log: tplogDebug, type: .default, isPreapproved ? "preapproved" : "not yet preapproved")
+
+                let egoPeerStatus = TrustedPeersHelperPeerState(peerID: egoPeerID,
+                                                                isPreapproved: isPreapproved,
+                                                                status: self.model.statusOfPeer(withID: egoPeerID),
+                                                                memberChanges: false,
+                                                                unknownMachineIDs: self.onqueueFullIDMSListWouldBeHelpful())
+
+                var tphPeers: [TrustedPeersHelperPeer] = []
+
+                if let egoPeer = self.model.peer(withID: egoPeerID) {
+                    egoPeer.trustedPeerIDs.forEach { trustedPeerID in
+                        if let peer = self.model.peer(withID: trustedPeerID) {
+                            let peerViews = try? self.model.getViewsForPeer(peer.permanentInfo,
+                                                                            stableInfo: peer.stableInfo,
+                                                                            inViews: Set())
+
+                            tphPeers.append(TrustedPeersHelperPeer(peerID: trustedPeerID,
+                                                                   signingSPKI: peer.permanentInfo.signingPubKey.spki(),
+                                                                   encryptionSPKI: peer.permanentInfo.encryptionPubKey.spki(),
+                                                                   viewList: peerViews ?? Set()))
+                        } else {
+                            os_log("No peer for trusted ID %@", log: tplogDebug, type: .default, trustedPeerID)
+                        }
+                    }
+                } else {
+                    os_log("No ego peer in model; no trusted peers", log: tplogDebug, type: .default)
+                }
+
+                os_log("Returning trust state: %@ %@", log: tplogDebug, type: .default, egoPeerStatus, tphPeers)
+                reply(egoPeerStatus, tphPeers, nil)
+            } else {
+                // With no ego peer ID, there are no trusted peers
+                os_log("No peer ID => no trusted peers", log: tplogDebug, type: .debug)
+                reply(TrustedPeersHelperPeerState(peerID: nil, isPreapproved: false, status: .unknown, memberChanges: false, unknownMachineIDs: false), [], nil)
+            }
+        }
+    }
+
+    func dump(reply: @escaping ([AnyHashable: Any]?, Error?) -> Void) {
+        let reply: ([AnyHashable: Any]?, Error?) -> Void = {
+            os_log("dump complete: %@",
+                   log: tplogTrace, type: .info, traceError($1))
+            reply($0, $1)
+       }
+       self.moc.performAndWait {
+            var d: [AnyHashable: Any] = [:]
+
+            if let egoPeerID = self.containerMO.egoPeerID {
+                if let peer = self.model.peer(withID: egoPeerID) {
+                    d["self"] = Container.peerdictionaryRepresentation(peer: peer)
+                } else {
+                    d["self"] = ["peerID": egoPeerID]
+                }
+            } else {
+                d["self"] = [:]
+            }
+
+            d["peers"] = self.model.allPeers().filter { $0.peerID != self.containerMO.egoPeerID }.map { peer in
+                Container.peerdictionaryRepresentation(peer: peer)
+            }
+
+            d["vouchers"] = self.model.allVouchers().map { $0.dictionaryRepresentation() }
+
+            if let bottles = self.containerMO.bottles as? Set<BottleMO> {
+                d["bottles"] = bottles.map { Container.dictionaryRepresentation(bottle: $0) }
+            } else {
+                d["bottles"] = []
+            }
+
+            let midList = self.onqueueCurrentMIDList()
+            d["machineIDsAllowed"] = midList.machineIDs(in: .allowed).sorted()
+            d["machineIDsDisallowed"] = midList.machineIDs(in: .disallowed).sorted()
+
+            reply(d, nil)
+        }
+    }
+
+    func dumpEgoPeer(reply: @escaping (String?, TPPeerPermanentInfo?, TPPeerStableInfo?, TPPeerDynamicInfo?, Error?) -> Void) {
+       let reply: (String?, TPPeerPermanentInfo?, TPPeerStableInfo?, TPPeerDynamicInfo?, Error?) -> Void = {
+           os_log("dumpEgoPeer complete: %@", log: tplogTrace, type: .info, traceError($4))
+           reply($0, $1, $2, $3, $4)
+       }
+       self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                reply(nil, nil, nil, nil, ContainerError.noPreparedIdentity)
+                return
+            }
+
+            guard let peer = self.model.peer(withID: egoPeerID) else {
+                reply(egoPeerID, nil, nil, nil, nil)
+                return
+            }
+
+            reply(egoPeerID, peer.permanentInfo, peer.stableInfo, peer.dynamicInfo, nil)
+        }
+    }
+
+    func validatePeers(request: ValidatePeersRequest, reply: @escaping ([AnyHashable: Any]?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: ([AnyHashable: Any]?, Error?) -> Void = {
+            os_log("validatePeers complete %@", log: tplogTrace, type: .info, traceError($1))
+            self.semaphore.signal()
+            reply($0, $1)
+        }
+
+        self.cuttlefish.validatePeers(request) { response, error in
+            os_log("ValidatePeers(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+            guard let response = response, error == nil else {
+                os_log("validatePeers failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                reply(nil, error ?? ContainerError.cloudkitResponseMissing)
+                return
+            }
+
+            var info: [AnyHashable: Any]  = [:]
+            info["health"] = response.validatorsHealth as AnyObject
+            info["results"] = try? JSONSerialization.jsonObject(with: response.jsonUTF8Data())
+
+            reply(info, nil)
+        }
+    }
+
+    func getViews(inViews: [String], reply: @escaping ([String]?, Error?) -> Void) {
+        let reply: ([String]?, Error?) -> Void = {
+            os_log("getViews complete %@", log: tplogTrace, type: .info, traceError($1))
+            reply($0, $1)
+        }
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID,
+                let egoPermData = self.containerMO.egoPeerPermanentInfo,
+                let egoPermSig = self.containerMO.egoPeerPermanentInfoSig,
+                let egoStableData = self.containerMO.egoPeerStableInfo,
+                let egoStableSig = self.containerMO.egoPeerStableInfoSig
+            else {
+                 os_log("getViews failed to find ego peer information", log: tplogDebug, type: .error)
+                 reply(nil, ContainerError.noPreparedIdentity)
+                 return
+            }
+            guard let stableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else {
+                 os_log("getViews failed to create TPPeerStableInfo", log: tplogDebug, type: .error)
+                 reply(nil, ContainerError.invalidStableInfoOrSig)
+                 return
+            }
+
+            let keyFactory = TPECPublicKeyFactory()
+            guard let permanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else {
+                 os_log("getViews failed to create TPPeerPermanentInfo", log: tplogDebug, type: .error)
+                 reply(nil, ContainerError.invalidPermanentInfoOrSig)
+                 return
+            }
+
+            do {
+                let views = try self.model.getViewsForPeer(permanentInfo, stableInfo: stableInfo, inViews: Set(inViews))
+                reply(Array(views), nil)
+            } catch {
+                reply(nil, error)
+                return
+            }
+        }
+    }
+
+    func reset(reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("reset complete %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        self.moc.performAndWait {
+            let request = ResetRequest()
+            self.cuttlefish.reset(request) { response, error in
+                os_log("Reset(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                guard let response = response, error == nil else {
+                    os_log("reset failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                    reply(error ?? ContainerError.cloudkitResponseMissing)
+                    return
+                }
+
+                // Erase container's persisted state
+                self.moc.performAndWait {
+                    self.moc.delete(self.containerMO)
+                    self.containerMO = ContainerMO(context: self.moc)
+                    self.containerMO.name = self.name.asSingleString()
+                    self.model = Container.loadModel(from: self.containerMO)
+                    do {
+                        try self.onQueuePersist(changes: response.changes)
+                        os_log("reset succeded", log: tplogDebug, type: .default)
+                        reply(nil)
+                    } catch {
+                        os_log("reset persist failed: %@", log: tplogDebug, type: .default, (error as CVarArg))
+                        reply(error)
+                    }
+                }
+            }
+        }
+    }
+
+    func localReset(reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("localReset complete %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        self.moc.performAndWait {
+            do {
+                // Erase container's persisted state
+                self.moc.delete(self.containerMO)
+                self.containerMO = ContainerMO(context: self.moc)
+                self.containerMO.name = self.name.asSingleString()
+                self.model = Container.loadModel(from: self.containerMO)
+                try self.moc.save()
+            } catch {
+                reply(error)
+                return
+            }
+            reply(nil)
+        }
+    }
+
+    func loadOrCreateKeyPair(privateKeyPersistentRef: Data?) throws -> _SFECKeyPair {
+        if let privateKeyPersistentRef = privateKeyPersistentRef {
+            return try TPHObjectiveC.fetchKeyPair(withPrivateKeyPersistentRef: privateKeyPersistentRef)
+        } else {
+            let keySpecifier = _SFECKeySpecifier(curve: SFEllipticCurve.nistp384)
+            guard let keyPair = _SFECKeyPair(randomKeyPairWith: keySpecifier) else {
+                throw ContainerError.unableToCreateKeyPair
+            }
+            return keyPair
+        }
+    }
+
+    // policyVersion should only be non-nil for testing, to override prevailingPolicyVersion
+    func prepare(epoch: UInt64,
+                 machineID: String,
+                 bottleSalt: String,
+                 bottleID: String,
+                 modelID: String,
+                 deviceName: String?,
+                 serialNumber: String,
+                 osVersion: String,
+                 policyVersion: UInt64?,
+                 policySecrets: [String: Data]?,
+                 signingPrivateKeyPersistentRef: Data?,
+                 encryptionPrivateKeyPersistentRef: Data?,
+                 reply: @escaping (String?, Data?, Data?, Data?, Data?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (String?, Data?, Data?, Data?, Data?, Error?) -> Void = {
+            os_log("prepare complete peerID: %@ %@",
+                   log: tplogTrace, type: .info, ($0 ?? "NULL") as CVarArg, traceError($5))
+            self.semaphore.signal()
+            reply($0, $1, $2, $3, $4, $5)
+        }
+
+        // Create a new peer identity with random keys, and store the keys in keychain
+        let permanentInfo: TPPeerPermanentInfo
+        let signingKeyPair: _SFECKeyPair
+        let encryptionKeyPair: _SFECKeyPair
+        do {
+            signingKeyPair = try self.loadOrCreateKeyPair(privateKeyPersistentRef: signingPrivateKeyPersistentRef)
+            encryptionKeyPair = try self.loadOrCreateKeyPair(privateKeyPersistentRef: encryptionPrivateKeyPersistentRef)
+
+            permanentInfo = try TPPeerPermanentInfo(machineID: machineID,
+                                                    modelID: modelID,
+                                                    epoch: 1,
+                                                    signing: signingKeyPair,
+                                                    encryptionKeyPair: encryptionKeyPair,
+                                                    peerIDHashAlgo: TPHashAlgo.SHA256)
+
+        } catch {
+            reply(nil, nil, nil, nil, nil, error)
+            return
+        }
+
+        let peerID = permanentInfo.peerID
+
+        let bottle: BottledPeer
+        do {
+            bottle = try BottledPeer(peerID: peerID,
+                                     bottleID: bottleID,
+                                     peerSigningKey: signingKeyPair,
+                                     peerEncryptionKey: encryptionKeyPair,
+                                     bottleSalt: bottleSalt)
+
+            _ = try saveSecret(bottle.secret, label: peerID)
+        } catch {
+            os_log("bottle creation failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+            reply(nil, nil, nil, nil, nil, error)
+            return
+        }
+
+        saveEgoKeyPair(signingKeyPair, identifier: signingKeyIdentifier(peerID: peerID)) { success, error in
+            guard success else {
+                os_log("Unable to save signing key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing")
+                reply(nil, nil, nil, nil, nil, error ?? ContainerError.failedToStoreIdentity)
+                return
+            }
+            saveEgoKeyPair(encryptionKeyPair, identifier: encryptionKeyIdentifier(peerID: peerID)) { success, error in
+                guard success else {
+                    os_log("Unable to save encryption key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing")
+                    reply(nil, nil, nil, nil, nil, error ?? ContainerError.failedToStoreIdentity)
+                    return
+                }
+
+                // Save the prepared identity as containerMO.egoPeer* and its bottle
+                self.moc.performAndWait {
+                    do {
+                        let policyVersion = policyVersion ?? prevailingPolicyVersion
+                        let policyDoc = try self.getPolicyDoc(policyVersion)
+
+                        let stableInfo = TPPeerStableInfo(clock: 1,
+                                                          policyVersion: policyDoc.policyVersion,
+                                                          policyHash: policyDoc.policyHash,
+                                                          policySecrets: policySecrets,
+                                                          deviceName: deviceName,
+                                                          serialNumber: serialNumber,
+                                                          osVersion: osVersion,
+                                                          signing: signingKeyPair,
+                                                          recoverySigningPubKey: nil,
+                                                          recoveryEncryptionPubKey: nil,
+                                                          error: nil)
+
+                        self.containerMO.egoPeerID = permanentInfo.peerID
+                        self.containerMO.egoPeerPermanentInfo = permanentInfo.data
+                        self.containerMO.egoPeerPermanentInfoSig = permanentInfo.sig
+                        self.containerMO.egoPeerStableInfo = stableInfo.data
+                        self.containerMO.egoPeerStableInfoSig = stableInfo.sig
+
+                        let bottleMO = BottleMO(context: self.moc)
+                        bottleMO.peerID = bottle.peerID
+                        bottleMO.bottleID = bottle.bottleID
+                        bottleMO.escrowedSigningSPKI = bottle.escrowSigningSPKI
+                        bottleMO.signatureUsingEscrowKey = bottle.signatureUsingEscrowKey
+                        bottleMO.signatureUsingPeerKey = bottle.signatureUsingPeerKey
+                        bottleMO.contents = bottle.contents
+
+                        self.containerMO.addToBottles(bottleMO)
+
+                        try self.moc.save()
+
+                        reply(permanentInfo.peerID, permanentInfo.data, permanentInfo.sig, stableInfo.data, stableInfo.sig, nil)
+                    } catch {
+                        reply(nil, nil, nil, nil, nil, error)
+                    }
+                }
+            }
+        }
+    }
+    func getEgoEpoch(reply: @escaping (UInt64, Error?) -> Void) {
+        let reply: (UInt64, Error?) -> Void = {
+            os_log("getEgoEpoch complete: %d %@", log: tplogTrace, type: .info, $0, traceError($1))
+            reply($0, $1)
+        }
+
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                reply(0, ContainerError.noPreparedIdentity)
+                return
+            }
+            guard let egoPeer = self.model.peer(withID: egoPeerID) else {
+                reply(0, ContainerError.noPreparedIdentity)
+                return
+            }
+
+            reply(egoPeer.permanentInfo.epoch, nil)
+        }
+    }
+    func establish(ckksKeys: [CKKSKeychainBackedKeySet],
+                   tlkShares: [CKKSTLKShare],
+                   preapprovedKeys: [Data]?,
+                   reply: @escaping (String?, [CKRecord], Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (String?, [CKRecord], Error?) -> Void = {
+            os_log("establish complete peer: %@ %@",
+                   log: tplogTrace, type: .default, ($0 ?? "NULL") as CVarArg, traceError($2))
+            self.semaphore.signal()
+            reply($0, $1, $2)
+        }
+
+        self.moc.performAndWait {
+            self.onqueueEstablish(ckksKeys: ckksKeys,
+                                  tlkShares: tlkShares,
+                                  preapprovedKeys: preapprovedKeys,
+                                  reply: reply)
+        }
+    }
+
+    func onqueueTTRUntrusted() -> Void {
+        let ttr = SecTapToRadar(tapToRadar: "Device not IDMS trusted",
+                                description: "Device not IDMS trusted",
+                                radar: "52874119")
+        ttr.trigger()
+    }
+
+    func onqueueEstablish(ckksKeys: [CKKSKeychainBackedKeySet],
+                          tlkShares: [CKKSTLKShare],
+                          preapprovedKeys: [Data]?,
+                          reply: @escaping (String?, [CKRecord], Error?) -> Void) {
+        // Fetch ego peer identity from local storage.
+        guard let egoPeerID = self.containerMO.egoPeerID,
+            let egoPermData = self.containerMO.egoPeerPermanentInfo,
+            let egoPermSig = self.containerMO.egoPeerPermanentInfoSig,
+            let egoStableData = self.containerMO.egoPeerStableInfo,
+            let egoStableSig = self.containerMO.egoPeerStableInfoSig
+            else {
+                reply(nil, [], ContainerError.noPreparedIdentity)
+                return
+        }
+
+        let keyFactory = TPECPublicKeyFactory()
+        guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else {
+            reply(nil, [], ContainerError.invalidPermanentInfoOrSig)
+            return
+        }
+        guard let selfStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else {
+            os_log("cannot create TPPeerStableInfo", log: tplogDebug, type: .default)
+            reply(nil, [], ContainerError.invalidStableInfoOrSig)
+            return
+        }
+        guard self.onqueueMachineIDAllowedByIDMS(machineID: selfPermanentInfo.machineID) else {
+            os_log("establish: self machineID %@ not on list", log: tplogDebug, type: .debug, selfPermanentInfo.machineID)
+            self.onqueueTTRUntrusted()
+            reply(nil, [], ContainerError.preparedIdentityNotOnAllowedList(selfPermanentInfo.machineID))
+            return
+        }
+
+        loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in
+             guard let egoPeerKeys = egoPeerKeys else {
+                os_log("Don't have my own peer keys; can't establish: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing")
+                reply(nil, [], error)
+                return
+            }
+            self.moc.performAndWait {
+                let viewKeys: [ViewKeys] = ckksKeys.map(ViewKeys.convert)
+                let allTLKShares: [TLKShare]
+                do {
+                    let octagonShares = try makeTLKShares(ckksTLKs: ckksKeys.map { $0.tlk }, asPeer: egoPeerKeys, toPeer: egoPeerKeys, epoch: Int(selfPermanentInfo.epoch))
+                    let sosShares = tlkShares.map { TLKShare.convert(ckksTLKShare: $0) }
+
+                    allTLKShares = octagonShares + sosShares
+                } catch {
+                    os_log("Unable to make TLKShares for self: %@", log: tplogDebug, type: .default, error as CVarArg)
+                    reply(nil, [], error)
+                    return
+                }
+
+                let dynamicInfo: TPPeerDynamicInfo
+                do {
+                    dynamicInfo = try self.model.dynamicInfo(forJoiningPeerID: egoPeerID,
+                                                             peerPermanentInfo: selfPermanentInfo,
+                                                             peerStableInfo: selfStableInfo,
+                                                             sponsorID: nil,
+                                                             preapprovedKeys: preapprovedKeys,
+                                                             signing: egoPeerKeys.signingKey,
+                                                             currentMachineIDs: self.onqueueCurrentMIDList())
+
+                    os_log("dynamic info: %@", log: tplogDebug, type: .default, dynamicInfo)
+                } catch {
+                    reply(nil, [], error)
+                    return
+                }
+
+                let peer = Peer.with {
+                    $0.peerID = egoPeerID
+                    $0.permanentInfoAndSig.peerPermanentInfo = egoPermData
+                    $0.permanentInfoAndSig.sig = egoPermSig
+                    $0.stableInfoAndSig.peerStableInfo = egoStableData
+                    $0.stableInfoAndSig.sig = egoStableSig
+                    $0.dynamicInfoAndSig = SignedPeerDynamicInfo(dynamicInfo)
+                }
+
+                let bottle: Bottle
+                do {
+                    bottle = try self.assembleBottle(egoPeerID: egoPeerID)
+                } catch {
+                    reply(nil, [], error)
+                    return
+                }
+                os_log("Beginning establish for peer %@", log: tplogDebug, type: .default, egoPeerID)
+                os_log("Establish permanentInfo: %@", log: tplogDebug, type: .debug, egoPermData.base64EncodedString())
+                os_log("Establish permanentInfoSig: %@", log: tplogDebug, type: .debug, egoPermSig.base64EncodedString())
+                os_log("Establish stableInfo: %@", log: tplogDebug, type: .debug, egoStableData.base64EncodedString())
+                os_log("Establish stableInfoSig: %@", log: tplogDebug, type: .debug, egoStableSig.base64EncodedString())
+                os_log("Establish dynamicInfo: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.peerDynamicInfo.base64EncodedString())
+                os_log("Establish dynamicInfoSig: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.sig.base64EncodedString())
+
+                os_log("Establish introducing %d key sets, %d tlk shares", log: tplogDebug, type: .default, viewKeys.count, allTLKShares.count)
+
+                do {
+                    os_log("Establish bottle: %@", log: tplogDebug, type: .debug, try bottle.serializedData().base64EncodedString())
+                    os_log("Establish peer: %@", log: tplogDebug, type: .debug, try peer.serializedData().base64EncodedString())
+                } catch {
+                    os_log("Establish unable to encode bottle/peer: %@", log: tplogDebug, type: .debug, error as CVarArg)
+                }
+
+                let request = EstablishRequest.with {
+                    $0.peer = peer
+                    $0.bottle = bottle
+                    $0.viewKeys = viewKeys
+                    $0.tlkShares = allTLKShares
+                }
+                self.cuttlefish.establish(request) { response, error in
+                    os_log("Establish(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                    os_log("Establish: viewKeys: %@", String(describing: viewKeys))
+                    guard let response = response, error == nil else {
+                        os_log("establish failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                        reply(nil, [], error ?? ContainerError.cloudkitResponseMissing)
+                        return
+                    }
+
+                    do {
+                        os_log("Establish returned changes: %@", log: tplogDebug, type: .default, try response.changes.jsonString())
+                    } catch {
+                        os_log("Establish returned changes, but they can't be serialized", log: tplogDebug, type: .default)
+                    }
+
+                    let keyHierarchyRecords = response.zoneKeyHierarchyRecords.compactMap { CKRecord($0) }
+
+                    do {
+                        try self.persist(changes: response.changes)
+
+                        guard response.changes.more == false else {
+                            os_log("establish succeeded, but more changes need fetching...", log: tplogDebug, type: .default)
+
+                            self.fetchAndPersistChanges { fetchError in
+                                guard fetchError == nil else {
+                                    // This is an odd error condition: we might be able to fetch again and be in a good state...
+                                    os_log("fetch-after-establish failed: %@", log: tplogDebug, type: .default, (fetchError as CVarArg?) ?? "no error")
+                                    reply(nil, keyHierarchyRecords, fetchError)
+                                    return;
+                                }
+
+                                os_log("fetch-after-establish succeeded", log: tplogDebug, type: .default)
+                                reply(egoPeerID, keyHierarchyRecords, nil)
+                            }
+                            return
+                        }
+
+                        os_log("establish succeeded", log: tplogDebug, type: .default)
+                        reply(egoPeerID, keyHierarchyRecords, nil)
+                    } catch {
+                        os_log("establish handling failed: %@", log: tplogDebug, type: .default, (error as CVarArg))
+                        reply(nil, keyHierarchyRecords, error)
+                    }
+                }
+            }
+        }
+    }
+
+    func setRecoveryKey(recoveryKey: String, salt: String, ckksKeys: [CKKSKeychainBackedKeySet], reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("setRecoveryKey complete: %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        os_log("beginning a setRecoveryKey", log: tplogDebug, type: .default)
+
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                os_log("no prepared identity, cannot set recovery key", log: tplogDebug, type: .default)
+                reply(ContainerError.noPreparedIdentity)
+                return
+            }
+
+            var recoveryKeys: RecoveryKey
+            do {
+                recoveryKeys = try RecoveryKey(recoveryKeyString: recoveryKey, recoverySalt: salt)
+            } catch {
+                os_log("failed to create recovery keys: %@", log: tplogDebug, type: .default, error as CVarArg)
+                reply(ContainerError.failedToCreateRecoveryKey)
+                return
+            }
+
+            let signingPublicKey: Data = recoveryKeys.peerKeys.signingVerificationKey.keyData
+            let encryptionPublicKey: Data = recoveryKeys.peerKeys.encryptionVerificationKey.keyData
+
+            os_log("setRecoveryKey signingPubKey: %@", log: tplogDebug, type: .debug, signingPublicKey.base64EncodedString())
+            os_log("setRecoveryKey encryptionPubKey: %@", log: tplogDebug, type: .debug, encryptionPublicKey.base64EncodedString())
+
+            guard let stableInfoData = self.containerMO.egoPeerStableInfo else {
+                os_log("stableInfo does not exist", log: tplogDebug, type: .default)
+                reply(ContainerError.nonMember)
+                return
+            }
+            guard let stableInfoSig = self.containerMO.egoPeerStableInfoSig else {
+                os_log("stableInfoSig does not exist", log: tplogDebug, type: .default)
+                reply(ContainerError.nonMember)
+                return
+            }
+            guard let permInfoData = self.containerMO.egoPeerPermanentInfo else {
+                os_log("permanentInfo does not exist", log: tplogDebug, type: .default)
+                reply(ContainerError.nonMember)
+                return
+            }
+            guard let permInfoSig = self.containerMO.egoPeerPermanentInfoSig else {
+                os_log("permInfoSig does not exist", log: tplogDebug, type: .default)
+                reply(ContainerError.nonMember)
+                return
+            }
+            guard let stableInfo = TPPeerStableInfo(data: stableInfoData, sig: stableInfoSig) else {
+                os_log("cannot create TPPeerStableInfo", log: tplogDebug, type: .default)
+                reply(ContainerError.invalidStableInfoOrSig)
+                return
+            }
+            let keyFactory = TPECPublicKeyFactory()
+            guard let permanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: permInfoData, sig: permInfoSig, keyFactory: keyFactory) else {
+                os_log("cannot create TPPeerPermanentInfo", log: tplogDebug, type: .default)
+                reply(ContainerError.invalidStableInfoOrSig)
+                return
+            }
+
+            loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in
+                guard let signingKeyPair = signingKeyPair else {
+                    os_log("handle: no signing key pair: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                    reply(error)
+                    return
+                }
+                self.moc.performAndWait {
+                    do {
+                        let tlkShares = try makeTLKShares(ckksTLKs: ckksKeys.map { $0.tlk },
+                                                          asPeer: recoveryKeys.peerKeys,
+                                                          toPeer: recoveryKeys.peerKeys,
+                                                          epoch: Int(permanentInfo.epoch))
+
+                        let policyVersion = stableInfo.policyVersion
+                        let policyDoc = try self.getPolicyDoc(policyVersion)
+
+                        let updatedStableInfo = TPPeerStableInfo(clock: stableInfo.clock + 1,
+                                                                 policyVersion: policyDoc.policyVersion,
+                                                                 policyHash: policyDoc.policyHash,
+                                                                 policySecrets: stableInfo.policySecrets,
+                                                                 deviceName: stableInfo.deviceName,
+                                                                 serialNumber: stableInfo.serialNumber,
+                                                                 osVersion: stableInfo.osVersion,
+                                                                 signing: signingKeyPair,
+                                                                 recoverySigningPubKey: signingPublicKey,
+                                                                 recoveryEncryptionPubKey: encryptionPublicKey,
+                                                                 error: nil)
+                        let signedStableInfo = SignedPeerStableInfo(updatedStableInfo)
+
+                        let request = SetRecoveryKeyRequest.with {
+                            $0.peerID = egoPeerID
+                            $0.recoverySigningPubKey = signingPublicKey
+                            $0.recoveryEncryptionPubKey = encryptionPublicKey
+                            $0.stableInfoAndSig = signedStableInfo
+                            $0.tlkShares = tlkShares
+                            $0.changeToken = self.containerMO.changeToken ?? ""
+                        }
+
+                        self.cuttlefish.setRecoveryKey(request) { response, error in
+                            os_log("SetRecoveryKey(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                            guard let response = response, error == nil else {
+                                os_log("setRecoveryKey failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                                reply(error ?? ContainerError.cloudkitResponseMissing)
+                                return
+                            }
+
+                            self.moc.performAndWait {
+                                do {
+                                    self.containerMO.egoPeerStableInfo = updatedStableInfo.data
+                                    self.containerMO.egoPeerStableInfoSig = updatedStableInfo.sig
+                                    try self.onQueuePersist(changes: response.changes)
+
+                                    os_log("setRecoveryKey succeeded", log: tplogDebug, type: .default)
+                                    reply(nil)
+                                } catch {
+                                    os_log("setRecoveryKey handling failed: %@", log: tplogDebug, type: .default, (error as CVarArg))
+                                    reply(error)
+                                }
+                            }
+                        }
+                    } catch {
+                        reply(error)
+                    }
+                }
+            }
+        }
+    }
+
+    func currentSetContainerBottleID(bottleMOs: Set<BottleMO>, bottleID: String) -> (Bool) {
+        let bmos = bottleMOs.filter {
+            $0.bottleID == bottleID
+        }
+        return !bmos.isEmpty
+    }
+
+    func findBottleForEscrowRecordID(bottleID: String, reply: @escaping (BottleMO?, Error?) -> Void) {
+
+        var bmo: BottleMO?
+        var bottles: Set<BottleMO> = []
+        var shouldPerformFetch = false
+
+        if let containerBottles = self.containerMO.bottles as? Set<BottleMO> {
+            if self.currentSetContainerBottleID(bottleMOs: containerBottles, bottleID: bottleID) == false {
+                shouldPerformFetch = true
+            } else {
+                bottles = containerBottles
+            }
+        } else {
+            shouldPerformFetch = true
+        }
+
+        if shouldPerformFetch == true {
+            self.fetchViableBottlesWithSemaphore { _, _, error in
+                guard error == nil else {
+                    os_log("fetchViableBottles failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                    reply(nil, error)
+                    return
+                }
+
+                guard let newBottles = self.containerMO.bottles as? Set<BottleMO> else {
+                    os_log("no bottles on container: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                    reply(nil, ContainerError.noBottlesPresent)
+                    return
+                }
+
+                guard self.currentSetContainerBottleID(bottleMOs: newBottles, bottleID: bottleID) == true else {
+                    reply(nil, ContainerError.noBottlesForEscrowRecordID)
+                    return
+                }
+
+                os_log("findBottleForEscrowRecordID found bottle: %@", log: tplogDebug, type: .default, newBottles)
+
+                bottles = newBottles.filter {
+                    $0.bottleID == bottleID
+                }
+                if bottles.count > 1 {
+                    reply(nil, ContainerError.tooManyBottlesForPeer)
+                    return
+                }
+                bmo = bottles.removeFirst()
+                reply(bmo, nil)
+            }
+        } else {
+            var filteredBottles = bottles.filter {
+                $0.bottleID == bottleID
+            }
+            if filteredBottles.count > 1 {
+                reply(nil, ContainerError.tooManyBottlesForPeer)
+                return
+            }
+            bmo = filteredBottles.removeFirst()
+            reply(bmo, nil)
+        }
+    }
+
+    func onqueueRecoverBottle(managedBottle: BottleMO, entropy: Data, bottleSalt: String) throws -> BottledPeer {
+        guard let bottledContents = managedBottle.contents else {
+            throw ContainerError.bottleDoesNotContainContents
+        }
+        guard let signatureUsingEscrowKey = managedBottle.signatureUsingEscrowKey else {
+            throw ContainerError.bottleDoesNotContainEscrowKeySignature
+        }
+
+        guard let signatureUsingPeerKey = managedBottle.signatureUsingPeerKey else {
+            throw ContainerError.bottleDoesNotContainerPeerKeySignature
+        }
+        guard let sponsorPeerID = managedBottle.peerID else {
+            throw ContainerError.bottleDoesNotContainPeerID
+        }
+
+        //verify bottle signature using peer
+        do {
+            guard let sponsorPeer = self.model.peer(withID: sponsorPeerID) else {
+                os_log("recover bottle: Unable to find peer that created the bottle", log: tplogDebug, type: .default)
+                throw ContainerError.bottleCreatingPeerNotFound
+            }
+            guard let signingKey: _SFECPublicKey = sponsorPeer.permanentInfo.signingPubKey as? _SFECPublicKey else {
+                os_log("recover bottle: Unable to create a sponsor public key", log: tplogDebug, type: .default)
+                throw ContainerError.signatureVerificationFailed
+            }
+
+            _ = try BottledPeer.verifyBottleSignature(data: bottledContents, signature: signatureUsingPeerKey, pubKey: signingKey)
+        } catch {
+            os_log("Verification of bottled signature failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+            throw ContainerError.failedToCreateBottledPeer
+        }
+
+        do {
+            return try BottledPeer(contents: bottledContents,
+                                   secret: entropy,
+                                   bottleSalt: bottleSalt,
+                                   signatureUsingEscrow: signatureUsingEscrowKey,
+                                   signatureUsingPeerKey: signatureUsingPeerKey)
+        } catch {
+            os_log("Creation of Bottled Peer failed with bottle salt: %@,\nAttempting with empty bottle salt", bottleSalt)
+
+            do {
+                return try BottledPeer(contents: bottledContents,
+                                       secret: entropy,
+                                       bottleSalt: "",
+                                       signatureUsingEscrow: signatureUsingEscrowKey,
+                                       signatureUsingPeerKey: signatureUsingPeerKey)
+            } catch {
+                os_log("Creation of Bottled Peer failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+                throw ContainerError.failedToCreateBottledPeer
+            }
+        }
+    }
+
+    func vouchWithBottle(bottleID: String,
+                         entropy: Data,
+                         bottleSalt: String,
+                         tlkShares: [CKKSTLKShare],
+                         reply: @escaping (Data?, Data?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Data?, Data?, Error?) -> Void = {
+            os_log("vouchWithBottle complete: %@",
+                   log: tplogTrace, type: .info, traceError($2))
+            self.semaphore.signal()
+            reply($0, $1, $2)
+        }
+
+        self.fetchAndPersistChanges { error in
+            guard error == nil else {
+                os_log("vouchWithBottle unable to fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "")
+                reply(nil, nil, error)
+                return
+            }
+
+            self.findBottleForEscrowRecordID(bottleID: bottleID) { returnedBMO, error in
+                self.moc.performAndWait {
+                    guard error == nil else {
+                        os_log("vouchWithBottle unable to find bottle for escrow record id: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "")
+                        reply(nil, nil, error)
+                        return
+                    }
+
+                    guard let bmo: BottleMO = returnedBMO else {
+                        os_log("vouchWithBottle bottle is nil: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "")
+                        reply(nil, nil, error)
+                        return
+                    }
+
+                    guard let bottledContents = bmo.contents else {
+                        reply(nil, nil, ContainerError.bottleDoesNotContainContents)
+                        return
+                    }
+                    guard let signatureUsingEscrowKey = bmo.signatureUsingEscrowKey else {
+                        reply(nil, nil, ContainerError.bottleDoesNotContainEscrowKeySignature)
+                        return
+                    }
+
+                    guard let signatureUsingPeerKey = bmo.signatureUsingPeerKey else {
+                        reply(nil, nil, ContainerError.bottleDoesNotContainerPeerKeySignature)
+                        return
+                    }
+                    guard let sponsorPeerID = bmo.peerID else {
+                        reply(nil, nil, ContainerError.bottleDoesNotContainPeerID)
+                        return
+                    }
+
+                    //verify bottle signature using peer
+                    do {
+                        guard let sponsorPeer = self.model.peer(withID: sponsorPeerID) else {
+                            os_log("vouchWithBottle: Unable to find peer that created the bottle", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.bottleCreatingPeerNotFound)
+                            return
+                        }
+                        guard let signingKey: _SFECPublicKey = sponsorPeer.permanentInfo.signingPubKey as? _SFECPublicKey else {
+                            os_log("vouchWithBottle: Unable to create a sponsor public key", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.signatureVerificationFailed)
+                            return
+                        }
+
+                        _ = try BottledPeer.verifyBottleSignature(data: bottledContents, signature: signatureUsingPeerKey, pubKey: signingKey)
+                    } catch {
+                        os_log("vouchWithBottle: Verification of bottled signature failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        reply(nil, nil, ContainerError.failedToCreateBottledPeer)
+                        return
+                    }
+
+                    //create bottled peer
+                    let bottledPeer: BottledPeer
+                    do {
+                        bottledPeer = try BottledPeer(contents: bottledContents,
+                                                      secret: entropy,
+                                                      bottleSalt: bottleSalt,
+                                                      signatureUsingEscrow: signatureUsingEscrowKey,
+                                                      signatureUsingPeerKey: signatureUsingPeerKey)
+                    } catch {
+                        os_log("Creation of Bottled Peer failed with bottle salt: %@,\nAttempting with empty bottle salt", bottleSalt)
+
+                        do {
+                            bottledPeer = try BottledPeer(contents: bottledContents,
+                                                          secret: entropy,
+                                                          bottleSalt: "",
+                                                          signatureUsingEscrow: signatureUsingEscrowKey,
+                                                          signatureUsingPeerKey: signatureUsingPeerKey)
+                        } catch {
+
+                            os_log("Creation of Bottled Peer failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(nil, nil, ContainerError.failedToCreateBottledPeer)
+                            return
+                        }
+                    }
+
+                    os_log("Have a bottle for peer %@", log: tplogDebug, type: .default, bottledPeer.peerID)
+
+                    // Extract any TLKs we have been given
+                    extract(tlkShares: tlkShares, peer: bottledPeer.peerKeys)
+
+                    self.moc.performAndWait {
+                        // I must have an ego identity in order to vouch using bottle
+                        guard let egoPeerID = self.containerMO.egoPeerID else {
+                            os_log("As a nonmember, can't vouch for someone else", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.nonMember)
+                            return
+                        }
+                        guard let permanentInfo = self.containerMO.egoPeerPermanentInfo else {
+                            os_log("permanentInfo does not exist", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.nonMember)
+                            return
+                        }
+                        guard let permanentInfoSig = self.containerMO.egoPeerPermanentInfoSig else {
+                            os_log("permanentInfoSig does not exist", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.nonMember)
+                            return
+                        }
+                        guard let stableInfo = self.containerMO.egoPeerStableInfo else {
+                            os_log("stableInfo does not exist", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.nonMember)
+                            return
+                        }
+                        guard let stableInfoSig = self.containerMO.egoPeerStableInfoSig else {
+                            os_log("stableInfoSig does not exist", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.nonMember)
+                            return
+                        }
+                        let keyFactory = TPECPublicKeyFactory()
+                        guard let beneficiaryPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: permanentInfo, sig: permanentInfoSig, keyFactory: keyFactory) else {
+                            os_log("Invalid permenent info or signature; can't vouch for them", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.invalidPermanentInfoOrSig)
+                            return
+                        }
+                        guard let beneficiaryStableInfo = TPPeerStableInfo(data: stableInfo, sig: stableInfoSig) else {
+                            os_log("Invalid stableinfo or signature; van't vouch for them", log: tplogDebug, type: .default)
+                            reply(nil, nil, ContainerError.invalidStableInfoOrSig)
+                            return
+                        }
+
+                        do {
+                            let voucher = try self.model.createVoucher(forCandidate: beneficiaryPermanentInfo,
+                                                                       stableInfo: beneficiaryStableInfo,
+                                                                       withSponsorID: sponsorPeerID,
+                                                                       reason: TPVoucherReason.restore,
+                                                                       signing: bottledPeer.peerKeys.signingKey)
+                            reply(voucher.data, voucher.sig, nil)
+                            return
+                        } catch {
+                            os_log("Error creating voucher: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(nil, nil, error)
+                            return
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    func vouchWithRecoveryKey(recoveryKey: String,
+                              salt: String,
+                              tlkShares: [CKKSTLKShare],
+                              reply: @escaping (Data?, Data?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Data?, Data?, Error?) -> Void = {
+            os_log("vouchWithRecoveryKey complete: %@",
+                   log: tplogTrace, type: .info, traceError($2))
+            self.semaphore.signal()
+            reply($0, $1, $2)
+        }
+
+        self.moc.performAndWait {
+            os_log("beginning a vouchWithRecoveryKey", log: tplogDebug, type: .default)
+
+            // I must have an ego identity in order to vouch using bottle
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                os_log("As a nonmember, can't vouch for someone else", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.nonMember)
+                return
+            }
+            guard let permanentInfo = self.containerMO.egoPeerPermanentInfo else {
+                os_log("permanentInfo does not exist", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.nonMember)
+                return
+            }
+            guard let permanentInfoSig = self.containerMO.egoPeerPermanentInfoSig else {
+                os_log("permanentInfoSig does not exist", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.nonMember)
+                return
+            }
+            guard let stableInfo = self.containerMO.egoPeerStableInfo else {
+                os_log("stableInfo does not exist", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.nonMember)
+                return
+            }
+            guard let stableInfoSig = self.containerMO.egoPeerStableInfoSig else {
+                os_log("stableInfoSig does not exist", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.nonMember)
+                return
+            }
+            let keyFactory = TPECPublicKeyFactory()
+            guard let beneficiaryPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: permanentInfo, sig: permanentInfoSig, keyFactory: keyFactory) else {
+                os_log("Invalid permenent info or signature; can't vouch for them", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.invalidPermanentInfoOrSig)
+                return
+            }
+            guard let beneficiaryStableInfo = TPPeerStableInfo(data: stableInfo, sig: stableInfoSig) else {
+                os_log("Invalid stableinfo or signature; van't vouch for them", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.invalidStableInfoOrSig)
+                return
+            }
+
+            //create recovery key set
+            var recoveryKeys: RecoveryKey
+            do {
+                recoveryKeys = try RecoveryKey(recoveryKeyString: recoveryKey, recoverySalt: salt)
+            } catch {
+                os_log("failed to create recovery keys: %@", log: tplogDebug, type: .default, error as CVarArg)
+                reply(nil, nil, ContainerError.failedToCreateRecoveryKey)
+                return
+            }
+
+            extract(tlkShares: tlkShares, peer: recoveryKeys.peerKeys)
+
+            let signingPublicKey: Data = recoveryKeys.peerKeys.signingKey.publicKey.keyData
+            let encryptionPublicKey: Data = recoveryKeys.peerKeys.encryptionKey.publicKey.keyData
+
+            os_log("vouchWithRecoveryKey signingPubKey: %@", log: tplogDebug, type: .debug, signingPublicKey.base64EncodedString())
+            os_log("vouchWithRecoveryKey encryptionPubKey: %@", log: tplogDebug, type: .debug, encryptionPublicKey.base64EncodedString())
+
+            guard self.model.isRecoveryKeyEnrolled() else {
+                os_log("Recovery Key is not enrolled", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.recoveryKeysNotEnrolled)
+                return
+            }
+
+            //find matching peer containing recovery keys
+            guard let sponsorPeerID = self.model.peerIDThatTrustsRecoveryKeys(TPRecoveryKeyPair(signingSPKI: signingPublicKey, encryptionSPKI: encryptionPublicKey)) else {
+                os_log("Untrusted recovery key set", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.untrustedRecoveryKeys)
+                return
+            }
+
+            do {
+                let voucher = try self.model.createVoucher(forCandidate: beneficiaryPermanentInfo,
+                                                           stableInfo: beneficiaryStableInfo,
+                                                           withSponsorID: sponsorPeerID,
+                                                           reason: TPVoucherReason.recoveryKey,
+                                                           signing: recoveryKeys.peerKeys.signingKey)
+                reply(voucher.data, voucher.sig, nil)
+                return
+            } catch {
+                os_log("Error creating voucher using recovery key set: %@", log: tplogDebug, type: .default, error as CVarArg)
+                reply(nil, nil, error)
+                return
+            }
+        }
+    }
+
+    func vouch(peerID: String,
+               permanentInfo: Data,
+               permanentInfoSig: Data,
+               stableInfo: Data,
+               stableInfoSig: Data,
+               ckksKeys: [CKKSKeychainBackedKeySet],
+               reply: @escaping (Data?, Data?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Data?, Data?, Error?) -> Void = {
+            os_log("vouch complete: %@", log: tplogTrace, type: .info, traceError($2))
+            self.semaphore.signal()
+            reply($0, $1, $2)
+        }
+
+        self.moc.performAndWait {
+            // I must have an ego identity in order to vouch for someone else.
+            guard let egoPeerID = self.containerMO.egoPeerID,
+                let egoPermData = self.containerMO.egoPeerPermanentInfo,
+                let egoPermSig = self.containerMO.egoPeerPermanentInfoSig else {
+                    os_log("As a nonmember, can't vouch for someone else", log: tplogDebug, type: .default)
+                    reply(nil, nil, ContainerError.nonMember)
+                    return
+            }
+
+            let keyFactory = TPECPublicKeyFactory()
+
+            guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else {
+                reply(nil, nil, ContainerError.invalidPermanentInfoOrSig)
+                return
+            }
+
+            guard let beneficiaryPermanentInfo = TPPeerPermanentInfo(peerID: peerID, data: permanentInfo, sig: permanentInfoSig, keyFactory: keyFactory) else {
+                os_log("Invalid permenent info or signature; can't vouch for them", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.invalidPermanentInfoOrSig)
+                return
+            }
+
+            guard let beneficiaryStableInfo = TPPeerStableInfo(data: stableInfo, sig: stableInfoSig) else {
+                os_log("Invalid stableinfo or signature; van't vouch for them", log: tplogDebug, type: .default)
+                reply(nil, nil, ContainerError.invalidStableInfoOrSig)
+                return
+            }
+
+            loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in
+                guard let egoPeerKeys = egoPeerKeys else {
+                    os_log("Don't have my own keys: can't vouch for %@: %@", log: tplogDebug, type: .default, beneficiaryPermanentInfo, (error as CVarArg?) ?? "no error")
+                    reply(nil, nil, error)
+                    return
+                }
+                self.moc.performAndWait {
+                    let voucher: TPVoucher
+                    do {
+                        voucher = try self.model.createVoucher(forCandidate: beneficiaryPermanentInfo,
+                                                               stableInfo: beneficiaryStableInfo,
+                                                               withSponsorID: egoPeerID,
+                                                               reason: TPVoucherReason.secureChannel,
+                                                               signing: egoPeerKeys.signingKey)
+
+                    } catch {
+                        os_log("Error creating voucher: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        reply(nil, nil, error)
+                        return
+                    }
+
+                    // And generate and upload any tlkShares
+                    // Note that this might not be the whole list: <rdar://problem/47899980> Octagon: Limited Peers
+                    let tlkShares: [TLKShare]
+                    do {
+                        // Note: we only want to send up TLKs for uploaded ckks zones
+                        let ckksTLKs = ckksKeys.filter { !$0.newUpload }.map { $0.tlk }
+
+                        tlkShares = try makeTLKShares(ckksTLKs: ckksTLKs,
+                                                      asPeer: egoPeerKeys,
+                                                      toPeer: beneficiaryPermanentInfo,
+                                                      epoch: Int(selfPermanentInfo.epoch))
+                    } catch {
+                        os_log("Unable to make TLKShares for beneficiary %@: %@", log: tplogDebug, type: .default, beneficiaryPermanentInfo, error as CVarArg)
+                        reply(nil, nil, error)
+                        return
+                    }
+
+                    guard !tlkShares.isEmpty else {
+                        os_log("No TLKShares to upload for new peer, returning voucher", log: tplogDebug, type: .default)
+                        reply(voucher.data, voucher.sig, nil)
+                        return
+                    }
+
+                    self.cuttlefish.updateTrust(changeToken: self.containerMO.changeToken ?? "",
+                                                peerID: egoPeerID,
+                                                stableInfoAndSig: nil,
+                                                dynamicInfoAndSig: nil,
+                                                tlkShares: tlkShares,
+                                                viewKeys: []) { response, error in
+                                                    guard let response = response, error == nil else {
+                                                        os_log("Unable to upload new tlkshares: %@", log: tplogDebug, type: .default, error as CVarArg? ?? "no error")
+                                                        reply(voucher.data, voucher.sig, error ?? ContainerError.cloudkitResponseMissing)
+                                                        return
+                                                    }
+
+                                                    let newKeyRecords = response.zoneKeyHierarchyRecords.map(CKRecord.init)
+                                                    os_log("Uploaded new tlkshares: %@", log: tplogDebug, type: .default, newKeyRecords)
+                                                    // We don't need to save these; CKKS will refetch them as needed
+
+                                                    reply(voucher.data, voucher.sig, nil)
+                    }
+                }
+            }
+        }
+    }
+
+    func departByDistrustingSelf(reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("departByDistrustingSelf complete: %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                os_log("No dynamic info for self?", log: tplogDebug, type: .default)
+                reply(ContainerError.noPreparedIdentity)
+                return
+            }
+
+            self.onqueueDistrust(peerIDs: [egoPeerID], reply: reply)
+        }
+    }
+
+    func distrust(peerIDs: Set<String>,
+                  reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("distrust complete: %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                os_log("No dynamic info for self?", log: tplogDebug, type: .default)
+                reply(ContainerError.noPreparedIdentity)
+                return
+            }
+
+            guard !peerIDs.contains(egoPeerID) else {
+                os_log("Self-distrust via peerID not allowed", log: tplogDebug, type: .default)
+                reply(ContainerError.invalidPeerID)
+                return
+            }
+
+            self.onqueueDistrust(peerIDs: peerIDs, reply: reply)
+        }
+    }
+
+    func onqueueDistrust(peerIDs: Set<String>,
+                         reply: @escaping (Error?) -> Void) {
+
+        guard let egoPeerID = self.containerMO.egoPeerID else {
+            os_log("No dynamic info for self?", log: tplogDebug, type: .default)
+            reply(ContainerError.noPreparedIdentity)
+            return
+        }
+
+        loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in
+            guard let signingKeyPair = signingKeyPair else {
+                os_log("No longer have signing key pair; can't sign distrust: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "nil")
+                reply(error)
+                return
+            }
+
+            self.moc.performAndWait {
+                let dynamicInfo: TPPeerDynamicInfo
+                do {
+                    dynamicInfo = try self.model.calculateDynamicInfoForPeer(withID: egoPeerID,
+                                                                             addingPeerIDs: nil,
+                                                                             removingPeerIDs: Array(peerIDs),
+                                                                             preapprovedKeys: nil,
+                                                                             signing: signingKeyPair,
+                                                                             currentMachineIDs: self.onqueueCurrentMIDList())
+
+                } catch {
+                    os_log("Error preparing dynamic info: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "nil")
+                    reply(error)
+                    return
+                }
+
+                let signedDynamicInfo = SignedPeerDynamicInfo(dynamicInfo)
+                os_log("attempting distrust for %@ with: %@", log: tplogDebug, type: .default, peerIDs, dynamicInfo)
+
+                let request = UpdateTrustRequest.with {
+                    $0.changeToken = self.containerMO.changeToken ?? ""
+                    $0.peerID = egoPeerID
+                    $0.dynamicInfoAndSig = signedDynamicInfo
+                }
+                self.cuttlefish.updateTrust(request) { response, error in
+                    os_log("UpdateTrust(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                    guard let response = response, error == nil else {
+                        os_log("updateTrust failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                        reply(error ?? ContainerError.cloudkitResponseMissing)
+                        return
+                    }
+
+                    do {
+                        try self.persist(changes: response.changes)
+                        os_log("distrust succeeded", log: tplogDebug, type: .default)
+                        reply(nil)
+                    } catch {
+                        os_log("distrust handling failed: %@", log: tplogDebug, type: .default, (error as CVarArg))
+                        reply(error)
+                    }
+                }
+            }
+        }
+    }
+
+    func fetchEscrowContents(reply: @escaping (Data?, String?, Data?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Data?, String?, Data?, Error?) -> Void = {
+            os_log("fetchEscrowContents complete: %@", log: tplogTrace, type: .info, traceError($3))
+            self.semaphore.signal()
+            reply($0, $1, $2, $3)
+        }
+        os_log("beginning a fetchEscrowContents", log: tplogDebug, type: .default)
+
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                os_log("fetchEscrowContents failed", log: tplogDebug, type: .default)
+                reply (nil, nil, nil, ContainerError.noPreparedIdentity)
+                return
+            }
+
+            guard let bottles = self.containerMO.bottles as? Set<BottleMO> else {
+                os_log("fetchEscrowContents failed", log: tplogDebug, type: .default)
+                reply (nil, nil, nil, ContainerError.noBottleForPeer)
+                return
+            }
+
+            var bmoSet = bottles.filter { $0.peerID == egoPeerID }
+            let bmo = bmoSet.removeFirst()
+            let bottleID = bmo.bottleID
+            var entropy: Data
+
+            do {
+                guard let loaded = try loadSecret(label: egoPeerID) else {
+                    os_log("fetchEscrowContents failed to load entropy", log: tplogDebug, type: .default)
+                    reply (nil, nil, nil, ContainerError.failedToFetchEscrowContents)
+                    return
+                }
+                entropy = loaded
+            } catch {
+                os_log("fetchEscrowContents failed to load entropy: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                reply (nil, nil, nil, error)
+                return
+            }
+
+            guard let signingPublicKey = bmo.escrowedSigningSPKI else {
+                os_log("fetchEscrowContents no escrow signing spki", log: tplogDebug, type: .default)
+                reply (nil, nil, nil, ContainerError.bottleDoesNotContainerEscrowKeySPKI)
+                return
+            }
+            reply(entropy, bottleID, signingPublicKey, nil)
+        }
+    }
+
+    func fetchViableBottles(reply: @escaping ([String]?, [String]?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: ([String]?, [String]?, Error?) -> Void = {
+            os_log("fetchViableBottles complete: %@", log: tplogTrace, type: .info, traceError($2))
+            self.semaphore.signal()
+            reply($0, $1, $2)
+        }
+
+        self.fetchViableBottlesWithSemaphore(reply: reply)
+    }
+
+    func onqueueCachedBottlesContainEgoPeerBottle(cachedBottles: TPCachedViableBottles) -> Bool {
+        guard let egoPeerID = self.containerMO.egoPeerID else {
+            os_log("bottleForEgoPeer: No identity.", log: tplogDebug, type: .default)
+            return false
+        }
+        guard let bottles: Set<BottleMO> = self.containerMO.bottles as? Set<BottleMO>  else {
+            os_log("bottleForEgoPeer: No Bottles.", log: tplogDebug, type: .default)
+            return false
+        }
+        var matchesCached: Bool = false
+        for bottle in bottles {
+            guard let bottleID: String = bottle.bottleID else {
+                continue
+            }
+            if bottle.peerID == egoPeerID && (cachedBottles.viableBottles.contains(bottleID) || cachedBottles.partialBottles.contains(bottleID)) {
+                matchesCached = true
+                break
+            }
+        }
+        return matchesCached
+    }
+
+    func fetchViableBottlesWithSemaphore(reply: @escaping ([String]?, [String]?, Error?) -> Void) {
+        os_log("beginning a fetchViableBottles", log: tplogDebug, type: .default)
+
+        let cachedBottles:TPCachedViableBottles = self.model.currentCachedViableBottlesSet()
+        self.moc.performAndWait {
+            if self.onqueueCachedBottlesContainEgoPeerBottle(cachedBottles: cachedBottles)
+                && (cachedBottles.viableBottles.count > 0 || cachedBottles.partialBottles.count > 0) {
+                os_log("returning from fetchViableBottles, using cached bottles", log: tplogDebug, type: .default)
+                reply (cachedBottles.viableBottles, cachedBottles.partialBottles, nil)
+                return
+            }
+
+            self.cuttlefish.fetchViableBottles { response, error in
+                guard error == nil else {
+                    os_log("fetchViableBottles failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                    reply(nil, nil, error)
+                    return
+                }
+
+                self.moc.performAndWait {
+
+                    guard let escrowPairs = response?.viableBottles else {
+                        os_log("fetchViableBottles returned no viable bottles: %@", log: tplogDebug, type: .default)
+                        reply([], [], nil)
+                        return
+                    }
+
+                    var partialPairs: [EscrowPair] = []
+                    if let partial = response?.partialBottles {
+                        partialPairs = partial
+                    } else {
+                        os_log("fetchViableBottles returned no partially viable bottles, but that's ok: %@", log: tplogDebug, type: .default)
+                    }
+
+                    let viableBottleIDs = escrowPairs.compactMap { $0.bottle.bottleID }
+                    os_log("fetchViableBottles returned viable bottles: %@", log: tplogDebug, type: .default, viableBottleIDs)
+
+                    let partialBottleIDs = partialPairs.compactMap { $0.bottle.bottleID }
+                    os_log("fetchViableBottles returned partial bottles: %@", log: tplogDebug, type: .default, partialBottleIDs)
+
+                    escrowPairs.forEach { pair in
+                        let bottle = pair.bottle
+
+                        // Save this bottle only if we don't already have it
+                        if let existingBottles = self.containerMO.bottles as? Set<BottleMO> {
+                            let matchingBottles: Set<BottleMO> = existingBottles.filter { existing in
+                                existing.peerID == bottle.peerID &&
+                                    existing.bottleID == bottle.bottleID &&
+                                    existing.escrowedSigningSPKI == bottle.escrowedSigningSpki &&
+                                    existing.signatureUsingEscrowKey == bottle.signatureUsingEscrowKey &&
+                                    existing.signatureUsingPeerKey == bottle.signatureUsingPeerKey &&
+                                    existing.contents == bottle.contents
+                            }
+                            if !matchingBottles.isEmpty {
+                                os_log("fetchViableBottles already knows about bottle", log: tplogDebug, type: .default, bottle.bottleID)
+                                return
+                            }
+                        }
+
+                        let bmo = BottleMO(context: self.moc)
+                        bmo.peerID = bottle.peerID
+                        bmo.bottleID = bottle.bottleID
+                        bmo.escrowedSigningSPKI = bottle.escrowedSigningSpki
+                        bmo.signatureUsingEscrowKey = bottle.signatureUsingEscrowKey
+                        bmo.signatureUsingPeerKey = bottle.signatureUsingPeerKey
+                        bmo.contents = bottle.contents
+
+                        os_log("fetchViableBottles saving new bottle: %@", log: tplogDebug, type: .default, bmo)
+                        self.containerMO.addToBottles(bmo)
+                    }
+
+                    partialPairs.forEach { pair in
+                        let bottle = pair.bottle
+
+                        // Save this bottle only if we don't already have it
+                        if let existingBottles = self.containerMO.bottles as? Set<BottleMO> {
+                            let matchingBottles: Set<BottleMO> = existingBottles.filter { existing in
+                                existing.peerID == bottle.peerID &&
+                                    existing.bottleID == bottle.bottleID &&
+                                    existing.escrowedSigningSPKI == bottle.escrowedSigningSpki &&
+                                    existing.signatureUsingEscrowKey == bottle.signatureUsingEscrowKey &&
+                                    existing.signatureUsingPeerKey == bottle.signatureUsingPeerKey &&
+                                    existing.contents == bottle.contents
+                            }
+                            if !matchingBottles.isEmpty {
+                                os_log("fetchViableBottles already knows about bottle", log: tplogDebug, type: .default, bottle.bottleID)
+                                return
+                            }
+                        }
+
+                        let bmo = BottleMO(context: self.moc)
+                        bmo.peerID = bottle.peerID
+                        bmo.bottleID = bottle.bottleID
+                        bmo.escrowedSigningSPKI = bottle.escrowedSigningSpki
+                        bmo.signatureUsingEscrowKey = bottle.signatureUsingEscrowKey
+                        bmo.signatureUsingPeerKey = bottle.signatureUsingPeerKey
+                        bmo.contents = bottle.contents
+
+                        os_log("fetchViableBottles saving new bottle: %@", log: tplogDebug, type: .default, bmo)
+                        self.containerMO.addToBottles(bmo)
+                    }
+
+                    do {
+                        try self.moc.save()
+                        os_log("fetchViableBottles saved bottles", log: tplogDebug, type: .default)
+                        let cached = TPCachedViableBottles.init(viableBottles: viableBottleIDs, partialBottles: partialBottleIDs)
+                        self.model.setViableBottles(cached)
+                        reply(viableBottleIDs, partialBottleIDs, nil)
+                    } catch {
+                        os_log("fetchViableBottles unable to save bottles: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                        reply(nil, nil, error)
+                    }
+
+                }
+            }
+        }
+    }
+
+    func fetchPolicy(reply: @escaping (TPPolicy?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (TPPolicy?, Error?) -> Void = {
+            os_log("fetchPolicy complete: %@", log: tplogTrace, type: .info, traceError($1))
+            self.semaphore.signal()
+            reply($0, $1)
+        }
+
+        self.moc.performAndWait {
+            var keys: [NSNumber: String] = [:]
+
+             guard let stableInfoData = self.containerMO.egoPeerStableInfo,
+                 let stableInfoSig = self.containerMO.egoPeerStableInfoSig else {
+                 os_log("fetchPolicy failed to find ego peer stableinfodata/sig", log: tplogDebug, type: .error)
+                 reply(nil, ContainerError.noPreparedIdentity)
+                 return
+             }
+             guard let stableInfo = TPPeerStableInfo(data: stableInfoData, sig: stableInfoSig) else {
+                 os_log("fetchPolicy failed to create TPPeerStableInfo", log: tplogDebug, type: .error)
+                 reply(nil, ContainerError.invalidStableInfoOrSig)
+                 return
+             }
+
+             let policyVersionCounter = stableInfo.policyVersion
+             let policyVersion = NSNumber(value: policyVersionCounter)
+             keys[policyVersion] = stableInfo.policyHash
+
+            if let policyDocument = self.model.policy(withVersion: policyVersionCounter) {
+                os_log("fetchPolicy: have a local version of policy %@: %@", log: tplogDebug, type: .default, policyVersion, policyDocument)
+                do {
+                    let policy = try policyDocument.policy(withSecrets: stableInfo.policySecrets, decrypter: Decrypter())
+                    reply(policy, nil)
+                    return
+                } catch {
+                    os_log("TPPolicyDocument failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+                    reply(nil, error)
+                    return
+                }
+            }
+
+            self.fetchPolicyDocuments(keys: keys) { result, error in
+                guard error == nil else {
+                    reply(nil, error)
+                    return
+                }
+                guard let result = result else {
+                    os_log("fetchPolicy: nil policies returned")
+                    reply(nil, ContainerError.policyDocumentDoesNotValidate)
+                    return
+                }
+                guard result.count == 1 else {
+                    os_log("fetchPolicy: wrong length returned")
+                    reply(nil, ContainerError.policyDocumentDoesNotValidate)
+                    return
+                }
+                guard let r = result[policyVersion] else {
+                    os_log("fetchPolicy: version not found")
+                    reply(nil, ContainerError.unknownPolicyVersion(policyVersion.uint64Value))
+                    return
+                }
+                guard let data = r[1].data(using: .utf8) else {
+                    os_log("fetchPolicy: failed to convert data")
+                    reply(nil, ContainerError.unknownPolicyVersion(policyVersion.uint64Value))
+                    return
+                }
+                guard let pd = TPPolicyDocument.policyDoc(withHash: r[0], data: data) else {
+                    os_log("fetchPolicy: pd is nil")
+                    reply(nil, ContainerError.policyDocumentDoesNotValidate)
+                    return
+                }
+                do {
+                    let policy = try pd.policy(withSecrets: stableInfo.policySecrets, decrypter: Decrypter())
+                    reply(policy, nil)
+                } catch {
+                    os_log("TPPolicyDocument: %@", log: tplogDebug, type: .default, error as CVarArg)
+                    reply(nil, error)
+                }
+            }
+        }
+    }
+
+    // All-or-nothing: return an error in case full list cannot be returned.
+    // Completion handler data format: [version : [hash, data]]
+    func fetchPolicyDocuments(keys: [NSNumber: String],
+                              reply: @escaping ([NSNumber: [String]]?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: ([NSNumber: [String]]?, Error?) -> Void = {
+            os_log("fetchPolicyDocuments complete: %@", log: tplogTrace, type: .info, traceError($1))
+            self.semaphore.signal()
+            reply($0, $1)
+        }
+
+        var keys = keys
+        var docs: [NSNumber: [String]] = [:]
+
+        self.moc.performAndWait {
+            for (version, hash) in keys {
+                if let policydoc = try? self.getPolicyDoc(version.uint64Value), policydoc.policyHash == hash {
+                    docs[version] = [policydoc.policyHash, policydoc.protobuf.base64EncodedString()]
+                    keys[version] = nil
+                }
+            }
+        }
+
+        if keys.isEmpty {
+            reply(docs, nil)
+            return
+        }
+
+        let request = FetchPolicyDocumentsRequest.with {
+            $0.keys = keys.map { key, value in
+                PolicyDocumentKey.with { $0.version = key.uint64Value; $0.hash = value }}
+        }
+
+        self.cuttlefish.fetchPolicyDocuments(request) { response, error in
+            os_log("FetchPolicyDocuments(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+            guard let response = response, error == nil else {
+                os_log("FetchPolicyDocuments failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                reply(nil, error ?? ContainerError.cloudkitResponseMissing)
+                return
+            }
+
+            self.moc.performAndWait {
+                for mapEntry in response.entries {
+                    // TODO: validate the policy's signature
+
+                    guard let doc = TPPolicyDocument.policyDoc(withHash: mapEntry.key.hash, data: mapEntry.value) else {
+                        os_log("Can't make policy document with hash %@ and data %@",
+                               log: tplogDebug, type: .default, mapEntry.key.hash, mapEntry.value.base64EncodedString())
+                        reply(nil, ContainerError.policyDocumentDoesNotValidate)
+                        return
+                    }
+
+                    guard let hash = keys[NSNumber(value: doc.policyVersion)], hash == doc.policyHash else {
+                        os_log("Requested hash %@ does not match fetched hash %@", log: tplogDebug, type: .default,
+                               keys[NSNumber(value: doc.policyVersion)] ?? "<nil>", doc.policyHash)
+                        reply(nil, ContainerError.policyDocumentDoesNotValidate)
+                        return
+                    }
+                    keys[NSNumber(value: doc.policyVersion)] = nil  // Server responses should be unique, let's enforce
+                    docs[NSNumber(value: doc.policyVersion)] = [doc.policyHash, doc.protobuf.base64EncodedString()]
+                    self.model.register(doc)
+                }
+
+                do {
+                    try self.moc.save()     // if this fails callers might make bad data assumptions
+                } catch {
+                    reply(nil, error)
+                    return
+                }
+
+                if !keys.isEmpty {
+                    let (unknownVersion, _) = keys.first!
+                    reply(nil, ContainerError.unknownPolicyVersion(unknownVersion.uint64Value))
+                    return
+                }
+
+                reply(docs, nil)
+            }
+        }
+    }
+
+    // Must be on moc queue to call this.
+    // Caller is responsible for saving the moc afterwards.
+    @discardableResult
+    private func registerPeerMO(permanentInfo: TPPeerPermanentInfo,
+                                stableInfo: TPPeerStableInfo? = nil,
+                                dynamicInfo: TPPeerDynamicInfo? = nil,
+                                vouchers: [TPVoucher]? = nil,
+                                isEgoPeer: Bool = false) throws -> PeerMO {
+        let peerID = permanentInfo.peerID
+
+        let peer = PeerMO(context: self.moc)
+        peer.peerID = peerID
+        peer.permanentInfo = permanentInfo.data
+        peer.permanentInfoSig = permanentInfo.sig
+        peer.stableInfo = stableInfo?.data
+        peer.stableInfoSig = stableInfo?.sig
+        peer.dynamicInfo = dynamicInfo?.data
+        peer.dynamicInfoSig = dynamicInfo?.sig
+        peer.isEgoPeer = isEgoPeer
+        self.containerMO.addToPeers(peer)
+
+        self.model.registerPeer(with: permanentInfo)
+        if let stableInfo = stableInfo {
+            try self.model.update(stableInfo, forPeerWithID: peerID)
+        }
+        if let dynamicInfo = dynamicInfo {
+            try self.model.update(dynamicInfo, forPeerWithID: peerID)
+        }
+        vouchers?.forEach { voucher in
+            self.model.register(voucher)
+            let voucherMO = VoucherMO(context: self.moc)
+            voucherMO.voucherInfo = voucher.data
+            voucherMO.voucherInfoSig = voucher.sig
+            peer.addToVouchers(voucherMO)
+        }
+        return peer
+    }
+
+    /* Returns any new CKKS keys that need uploading, as well as any TLKShares necessary for those keys */
+    func makeSharesForNewKeySets(ckksKeys: [CKKSKeychainBackedKeySet],
+                                tlkShares: [CKKSTLKShare],
+                                egoPeerKeys: OctagonSelfPeerKeys,
+                                egoPeerDynamicInfo: TPPeerDynamicInfo,
+                                epoch: Int) throws -> ([ViewKeys], [TLKShare]) {
+        let newCKKSKeys = ckksKeys.filter { $0.newUpload }
+        let newViewKeys: [ViewKeys] = newCKKSKeys.map(ViewKeys.convert)
+
+        let octagonSelfShares = try makeTLKShares(ckksTLKs: ckksKeys.map { $0.tlk },
+                                                  asPeer: egoPeerKeys,
+                                                  toPeer: egoPeerKeys,
+                                                  epoch: epoch)
+        let extraShares = tlkShares.map { TLKShare.convert(ckksTLKShare: $0) }
+
+        var peerShares: [TLKShare] = []
+
+        for keyset in newCKKSKeys {
+            do {
+                let peerIDsWithAccess = try self.model.getPeerIDsTrustedByPeer(with: egoPeerDynamicInfo,
+                                                                               toAccessView: keyset.tlk.zoneID.zoneName)
+                os_log("Planning to share %@ with peers %@", log: tplogDebug, type: .default, String(describing: keyset.tlk), peerIDsWithAccess)
+
+                let peers = peerIDsWithAccess.compactMap { self.model.peer(withID: $0) }
+                let viewPeerShares = try peers.map { receivingPeer in
+                    TLKShare.convert(ckksTLKShare: try CKKSTLKShare(keyset.tlk,
+                                                                    as: egoPeerKeys,
+                                                                    to: receivingPeer.permanentInfo,
+                                                                    epoch: epoch,
+                                                                    poisoned: 0))
+                }
+
+                peerShares = peerShares + viewPeerShares
+
+            } catch {
+                os_log("Unable to create TLKShares for keyset %@: %@", log: tplogDebug, type: .default, String(describing: keyset), error as CVarArg)
+            }
+        }
+
+        return (newViewKeys, octagonSelfShares + peerShares + extraShares)
+    }
+
+    func onqueuePreparePeerForJoining(egoPeerID: String,
+                                      peerPermanentInfo: TPPeerPermanentInfo,
+                                      stableInfo: TPPeerStableInfo,
+                                      sponsorID: String?,
+                                      preapprovedKeys: [Data]?,
+                                      vouchers: [SignedVoucher],
+                                      egoPeerKeys: OctagonSelfPeerKeys) throws -> (Peer, TPPeerDynamicInfo) {
+        let dynamicInfo = try self.model.dynamicInfo(forJoiningPeerID: egoPeerID,
+                                                     peerPermanentInfo: peerPermanentInfo,
+                                                     peerStableInfo: stableInfo,
+                                                     sponsorID: sponsorID,
+                                                     preapprovedKeys: preapprovedKeys,
+                                                     signing: egoPeerKeys.signingKey,
+                                                     currentMachineIDs: self.onqueueCurrentMIDList())
+
+        let newStableInfo = try self.createNewStableInfoIfNeeded(stableChanges: nil,
+                                                                 egoPeerID: egoPeerID,
+                                                                 dynamicInfo: dynamicInfo,
+                                                                 signingKeyPair: egoPeerKeys.signingKey)
+
+        let peer = Peer.with {
+            $0.peerID = egoPeerID
+            $0.permanentInfoAndSig = SignedPeerPermanentInfo(peerPermanentInfo)
+            $0.stableInfoAndSig = SignedPeerStableInfo(newStableInfo ?? stableInfo)
+            $0.dynamicInfoAndSig = SignedPeerDynamicInfo(dynamicInfo)
+            $0.vouchers = vouchers
+        }
+
+        return (peer, dynamicInfo)
+    }
+
+    func join(voucherData: Data,
+              voucherSig: Data,
+              ckksKeys: [CKKSKeychainBackedKeySet],
+              tlkShares: [CKKSTLKShare],
+              preapprovedKeys: [Data]?,
+              reply: @escaping (String?, [CKRecord], Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (String?, [CKRecord], Error?) -> Void = {
+            os_log("join complete: %@", log: tplogTrace, type: .info, traceError($2))
+            self.semaphore.signal()
+            reply($0, $1, $2)
+        }
+
+        self.fetchAndPersistChanges { error in
+            guard error == nil else {
+                reply(nil, [], error)
+                return
+            }
+            self.moc.performAndWait {
+                guard let voucher = TPVoucher(infoWith: voucherData, sig: voucherSig) else {
+                    reply(nil, [], ContainerError.invalidVoucherOrSig)
+                    return
+                }
+                guard let sponsor = self.model.peer(withID: voucher.sponsorID) else {
+                    reply(nil, [], ContainerError.sponsorNotRegistered(voucher.sponsorID))
+                    return
+                }
+
+                // Fetch ego peer identity from local storage.
+                guard let egoPeerID = self.containerMO.egoPeerID,
+                    let egoPermData = self.containerMO.egoPeerPermanentInfo,
+                    let egoPermSig = self.containerMO.egoPeerPermanentInfoSig,
+                    let egoStableData = self.containerMO.egoPeerStableInfo,
+                    let egoStableSig = self.containerMO.egoPeerStableInfoSig
+                    else {
+                        reply(nil, [], ContainerError.noPreparedIdentity)
+                        return
+                }
+
+                let keyFactory = TPECPublicKeyFactory()
+                guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else {
+                    reply(nil, [], ContainerError.invalidPermanentInfoOrSig)
+                    return
+                }
+                guard let selfStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else {
+                    reply(nil, [], ContainerError.invalidStableInfoOrSig)
+                    return
+                }
+                guard self.onqueueMachineIDAllowedByIDMS(machineID: selfPermanentInfo.machineID) else {
+                    os_log("join: self machineID %@ not on list", log: tplogDebug, type: .debug, selfPermanentInfo.machineID)
+                    self.onqueueTTRUntrusted()
+                    reply(nil, [], ContainerError.preparedIdentityNotOnAllowedList(selfPermanentInfo.machineID))
+                    return
+                }
+
+                loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in
+                    guard let egoPeerKeys = egoPeerKeys else {
+                        os_log("Don't have my own peer keys; can't join: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing")
+                        reply(nil, [], error)
+                        return
+                    }
+                    self.moc.performAndWait {
+                        let peer: Peer
+                        let newDynamicInfo: TPPeerDynamicInfo
+                        do {
+                            (peer, newDynamicInfo) = try self.onqueuePreparePeerForJoining(egoPeerID: egoPeerID,
+                                                                                           peerPermanentInfo: selfPermanentInfo,
+                                                                                           stableInfo: selfStableInfo,
+                                                                                           sponsorID: sponsor.peerID,
+                                                                                           preapprovedKeys: preapprovedKeys,
+                                                                                           vouchers: [SignedVoucher.with {
+                                                                                               $0.voucher = voucher.data
+                                                                                               $0.sig = voucher.sig
+                                                                                               }, ],
+                                                                                           egoPeerKeys: egoPeerKeys)
+                        } catch {
+                            os_log("Unable to create peer for joining: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(nil, [], error)
+                            return
+                        }
+
+                        let allTLKShares: [TLKShare]
+                        let viewKeys: [ViewKeys]
+                        do {
+                            (viewKeys, allTLKShares) = try self.makeSharesForNewKeySets(ckksKeys: ckksKeys,
+                                                                                        tlkShares: tlkShares,
+                                                                                        egoPeerKeys: egoPeerKeys,
+                                                                                        egoPeerDynamicInfo: newDynamicInfo,
+                                                                                        epoch: Int(selfPermanentInfo.epoch))
+                        } catch {
+                            os_log("Unable to process keys before joining: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(nil, [], error)
+                            return
+                        }
+
+                        do {
+                            try self.model.checkIntroduction(forCandidate: selfPermanentInfo,
+                                                             stableInfo: peer.stableInfoAndSig.toStableInfo(),
+                                                             withSponsorID: sponsor.peerID)
+                        } catch {
+                            os_log("Error checking introduction: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(nil, [], error)
+                            return
+                        }
+
+                        var bottle: Bottle
+                        do {
+                            bottle = try self.assembleBottle(egoPeerID: egoPeerID)
+                        } catch {
+                            reply(nil, [], error)
+                            return
+                        }
+
+                        os_log("Beginning join for peer %@", log: tplogDebug, type: .default, egoPeerID)
+                        os_log("Join permanentInfo: %@", log: tplogDebug, type: .debug, egoPermData.base64EncodedString())
+                        os_log("Join permanentInfoSig: %@", log: tplogDebug, type: .debug, egoPermSig.base64EncodedString())
+                        os_log("Join stableInfo: %@", log: tplogDebug, type: .debug, peer.stableInfoAndSig.peerStableInfo.base64EncodedString())
+                        os_log("Join stableInfoSig: %@", log: tplogDebug, type: .debug, peer.stableInfoAndSig.sig.base64EncodedString())
+                        os_log("Join dynamicInfo: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.peerDynamicInfo.base64EncodedString())
+                        os_log("Join dynamicInfoSig: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.sig.base64EncodedString())
+
+                        os_log("Join vouchers: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.voucher.base64EncodedString() })
+                        os_log("Join voucher signatures: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.sig.base64EncodedString() })
+
+                        os_log("Uploading %d tlk shares", log: tplogDebug, type: .default, allTLKShares.count)
+
+                        do {
+                            os_log("Join peer: %@", log: tplogDebug, type: .debug, try peer.serializedData().base64EncodedString())
+                        } catch {
+                            os_log("Join unable to encode peer: %@", log: tplogDebug, type: .debug, error as CVarArg)
+                        }
+
+                        let changeToken = self.containerMO.changeToken ?? ""
+                        let request = JoinWithVoucherRequest.with {
+                            $0.changeToken = changeToken
+                            $0.peer = peer
+                            $0.bottle = bottle
+                            $0.tlkShares = allTLKShares
+                            $0.viewKeys = viewKeys
+                        }
+                        self.cuttlefish.joinWithVoucher(request) { response, error in
+                            os_log("JoinWithVoucher(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                            guard let response = response, error == nil else {
+                                os_log("joinWithVoucher failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                                reply(nil, [], error ?? ContainerError.cloudkitResponseMissing)
+                                return
+                            }
+
+                            self.moc.performAndWait {
+                                do {
+                                    self.containerMO.egoPeerStableInfo = peer.stableInfoAndSig.peerStableInfo
+                                    self.containerMO.egoPeerStableInfoSig = peer.stableInfoAndSig.sig
+                                    try self.onQueuePersist(changes: response.changes)
+                                    os_log("JoinWithVoucher succeeded", log: tplogDebug)
+
+                                    let keyHierarchyRecords = response.zoneKeyHierarchyRecords.compactMap { CKRecord($0) }
+                                    reply(egoPeerID, keyHierarchyRecords, nil)
+                                } catch {
+                                    os_log("JoinWithVoucher failed: %@", log: tplogDebug, String(describing: error))
+                                    reply(nil, [], error)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    func requestHealthCheck(requiresEscrowCheck: Bool, reply: @escaping (Bool, Bool, Bool, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Bool, Bool, Bool, Error?) -> Void = {
+            os_log("health check complete: %@", log: tplogTrace, type: .info, traceError($3))
+            self.semaphore.signal()
+            reply($0, $1, $2, $3)
+        }
+
+        os_log("requestHealthCheck requiring escrow check: %d", log: tplogDebug, type: .default, requiresEscrowCheck)
+
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                // No identity, nothing to do
+                os_log("requestHealthCheck: No identity.", log: tplogDebug, type: .default)
+                reply(false, false, false, ContainerError.noPreparedIdentity)
+                return
+            }
+            let request = GetRepairActionRequest.with {
+                $0.peerID = egoPeerID
+                $0.requiresEscrowCheck = requiresEscrowCheck
+            }
+
+            self.cuttlefish.getRepairAction(request) { response, error in
+                guard error == nil else {
+                    reply(false, false, false, error)
+                    return
+                }
+                guard let action = response?.repairAction else {
+                    os_log("repair response is empty, returning false: %@", log: tplogDebug, type: .default)
+                    reply(false, false, false, nil)
+                    return
+                }
+                var postRepairAccount: Bool = false
+                var postRepairEscrow: Bool = false
+                var resetOctagon: Bool = false
+
+                switch action {
+                case .noAction:
+                    break
+                case .postRepairAccount:
+                    postRepairAccount = true
+                    break
+                case .postRepairEscrow:
+                    postRepairEscrow = true
+                    break
+                case .resetOctagon:
+                    resetOctagon = true
+                    break
+                case .UNRECOGNIZED:
+                    break
+                }
+                reply(postRepairAccount, postRepairEscrow, resetOctagon, nil)
+            }
+        }
+    }
+
+    func preflightPreapprovedJoin(reply: @escaping (Bool, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Bool, Error?) -> Void = {
+            os_log("preflightPreapprovedJoin complete: %@", log: tplogTrace, type: .info, traceError($1))
+            self.semaphore.signal()
+            reply($0, $1)
+        }
+
+        self.fetchAndPersistChanges { error in
+            guard error == nil else {
+                os_log("preflightPreapprovedJoin unable to fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "")
+                reply(false, error)
+                return
+            }
+
+            // We explicitly ignore the machine ID list here; we're only interested in the peer states: do they preapprove us?
+
+            guard !self.model.allPeerIDs().isEmpty else {
+                // If, after fetch and handle changes, there's no peers, then we can likely establish.
+                reply(true, nil)
+                return
+            }
+
+            guard let egoPeerID = self.containerMO.egoPeerID,
+                let egoPermData = self.containerMO.egoPeerPermanentInfo,
+                let egoPermSig = self.containerMO.egoPeerPermanentInfoSig
+                else {
+                    os_log("preflightPreapprovedJoin: no prepared identity", log: tplogDebug, type: .debug)
+                    reply(false, ContainerError.noPreparedIdentity)
+                    return
+            }
+
+            let keyFactory = TPECPublicKeyFactory()
+            guard let egoPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else {
+                os_log("preflightPreapprovedJoin: invalid permanent info", log: tplogDebug, type: .debug)
+                reply(false, ContainerError.invalidPermanentInfoOrSig)
+                return
+            }
+
+            guard self.model.hasPeerPreapprovingKey(egoPermanentInfo.signingPubKey.spki()) else {
+                os_log("preflightPreapprovedJoin: no peers preapprove our key", log: tplogDebug, type: .debug)
+                reply(false, ContainerError.noPeersPreapprovePreparedIdentity)
+                return
+            }
+
+            reply(true, nil)
+        }
+    }
+
+    func preapprovedJoin(ckksKeys: [CKKSKeychainBackedKeySet],
+                         tlkShares: [CKKSTLKShare],
+                         preapprovedKeys: [Data]?,
+                         reply: @escaping (String?, [CKRecord], Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (String?, [CKRecord], Error?) -> Void = {
+            os_log("preapprovedJoin complete: %@", log: tplogTrace, type: .info, traceError($2))
+            self.semaphore.signal()
+            reply($0, $1, $2)
+        }
+
+        self.fetchAndPersistChangesIfNeeded { error in
+            guard error == nil else {
+                os_log("preapprovedJoin unable to fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "")
+                reply(nil, [], error)
+                return
+            }
+            self.moc.performAndWait {
+                // If, after fetch and handle changes, there's no peers, then fire off an establish
+                // Note that if the establish fails, retrying this call might work.
+                // That's up to the caller.
+                if self.model.allPeerIDs().isEmpty {
+                    os_log("preapprovedJoin but no existing peers, attempting establish", log: tplogDebug, type: .debug)
+                    self.onqueueEstablish(ckksKeys: ckksKeys,
+                        tlkShares: tlkShares,
+                        preapprovedKeys: preapprovedKeys,
+                        reply: reply)
+                    return
+                }
+
+                // Fetch ego peer identity from local storage.
+                guard let egoPeerID = self.containerMO.egoPeerID,
+                    let egoPermData = self.containerMO.egoPeerPermanentInfo,
+                    let egoPermSig = self.containerMO.egoPeerPermanentInfoSig,
+                    let egoStableData = self.containerMO.egoPeerStableInfo,
+                    let egoStableSig = self.containerMO.egoPeerStableInfoSig
+                    else {
+                        os_log("preapprovedJoin: no prepared identity", log: tplogDebug, type: .debug)
+                        reply(nil, [], ContainerError.noPreparedIdentity)
+                        return
+                }
+
+                let keyFactory = TPECPublicKeyFactory()
+                guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else {
+                    reply(nil, [], ContainerError.invalidPermanentInfoOrSig)
+                    return
+                }
+                guard let selfStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else {
+                    reply(nil, [], ContainerError.invalidStableInfoOrSig)
+                    return
+                }
+
+                guard self.onqueueMachineIDAllowedByIDMS(machineID: selfPermanentInfo.machineID) else {
+                    os_log("preapprovedJoin: self machineID %@ (me) not on list", log: tplogDebug, type: .debug, selfPermanentInfo.machineID)
+                    self.onqueueTTRUntrusted()
+                    reply(nil, [], ContainerError.preparedIdentityNotOnAllowedList(selfPermanentInfo.machineID))
+                    return
+                }
+                loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in
+                    guard let egoPeerKeys = egoPeerKeys else {
+                        os_log("preapprovedJoin: Don't have my own keys: can't join", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                        reply(nil, [], error)
+                        return
+                    }
+
+                    guard self.model.hasPeerPreapprovingKey(egoPeerKeys.signingKey.publicKey().spki()) else {
+                        os_log("preapprovedJoin: no peers preapprove our key", log: tplogDebug, type: .debug)
+                        reply(nil, [], ContainerError.noPeersPreapprovePreparedIdentity)
+                        return
+                    }
+
+                    self.moc.performAndWait {
+
+                        let peer: Peer
+                        let newDynamicInfo: TPPeerDynamicInfo
+                        do {
+                            (peer, newDynamicInfo) = try self.onqueuePreparePeerForJoining(egoPeerID: egoPeerID,
+                                                                                           peerPermanentInfo: selfPermanentInfo,
+                                                                                           stableInfo: selfStableInfo,
+                                                                                           sponsorID: nil,
+                                                                                           preapprovedKeys: preapprovedKeys,
+                                                                                           vouchers: [],
+                                                                                           egoPeerKeys: egoPeerKeys)
+                        } catch {
+                            os_log("Unable to create peer for joining: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(nil, [], error)
+                            return
+                        }
+
+                        let allTLKShares: [TLKShare]
+                        let viewKeys: [ViewKeys]
+                        do {
+                            (viewKeys, allTLKShares) = try self.makeSharesForNewKeySets(ckksKeys: ckksKeys,
+                                                                                   tlkShares: tlkShares,
+                                                                                   egoPeerKeys: egoPeerKeys,
+                                                                                   egoPeerDynamicInfo: newDynamicInfo,
+                                                                                   epoch: Int(selfPermanentInfo.epoch))
+                        } catch {
+                            os_log("Unable to process keys before joining: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(nil, [], error)
+                            return
+                        }
+
+                        var bottle: Bottle
+                        do {
+                            bottle = try self.assembleBottle(egoPeerID: egoPeerID)
+                        } catch {
+                            reply(nil, [], error)
+                            return
+                        }
+
+                        os_log("Beginning preapprovedJoin for peer %@", log: tplogDebug, type: .default, egoPeerID)
+                        os_log("preapprovedJoin permanentInfo: %@", log: tplogDebug, type: .debug, egoPermData.base64EncodedString())
+                        os_log("preapprovedJoin permanentInfoSig: %@", log: tplogDebug, type: .debug, egoPermSig.base64EncodedString())
+                        os_log("preapprovedJoin stableInfo: %@", log: tplogDebug, type: .debug, egoStableData.base64EncodedString())
+                        os_log("preapprovedJoin stableInfoSig: %@", log: tplogDebug, type: .debug, egoStableSig.base64EncodedString())
+                        os_log("preapprovedJoin dynamicInfo: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.peerDynamicInfo.base64EncodedString())
+                        os_log("preapprovedJoin dynamicInfoSig: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.sig.base64EncodedString())
+
+                        os_log("preapprovedJoin vouchers: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.voucher.base64EncodedString() })
+                        os_log("preapprovedJoin voucher signatures: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.sig.base64EncodedString() })
+
+                        os_log("preapprovedJoin: uploading %d tlk shares", log: tplogDebug, type: .default, allTLKShares.count)
+
+                        do {
+                            os_log("preapprovedJoin peer: %@", log: tplogDebug, type: .debug, try peer.serializedData().base64EncodedString())
+                        } catch {
+                            os_log("preapprovedJoin unable to encode peer: %@", log: tplogDebug, type: .debug, error as CVarArg)
+                        }
+
+                        let changeToken = self.containerMO.changeToken ?? ""
+                        let request = JoinWithVoucherRequest.with {
+                            $0.changeToken = changeToken
+                            $0.peer = peer
+                            $0.bottle = bottle
+                            $0.tlkShares = allTLKShares
+                            $0.viewKeys = viewKeys
+                        }
+                        self.cuttlefish.joinWithVoucher(request) { response, error in
+                            os_log("preapprovedJoin(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                            guard let response = response, error == nil else {
+                                os_log("preapprovedJoin failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                                reply(nil, [], error ?? ContainerError.cloudkitResponseMissing)
+                                return
+                            }
+
+                            self.moc.performAndWait {
+                                do {
+                                    self.containerMO.egoPeerStableInfo = peer.stableInfoAndSig.peerStableInfo
+                                    self.containerMO.egoPeerStableInfoSig = peer.stableInfoAndSig.sig
+                                    try self.onQueuePersist(changes: response.changes)
+                                    os_log("preapprovedJoin succeeded", log: tplogDebug)
+
+                                    let keyHierarchyRecords = response.zoneKeyHierarchyRecords.compactMap { CKRecord($0) }
+                                    reply(egoPeerID, keyHierarchyRecords, nil)
+                                } catch {
+                                    os_log("preapprovedJoin failed: %@", log: tplogDebug, String(describing: error))
+                                    reply(nil, [], error)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    func update(deviceName: String?,
+                serialNumber: String?,
+                osVersion: String?,
+                policyVersion: UInt64?,
+                policySecrets: [String: Data]?,
+                reply: @escaping (TrustedPeersHelperPeerState?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (TrustedPeersHelperPeerState?, Error?) -> Void = {
+            os_log("update complete: %@", log: tplogTrace, type: .info, traceError($1))
+            self.semaphore.signal()
+            reply($0, $1)
+        }
+
+        // Get (and save) the latest from cuttlefish
+        let stableChanges = StableChanges(deviceName: deviceName,
+                                          serialNumber: serialNumber,
+                                          osVersion: osVersion,
+                                          policyVersion: policyVersion,
+                                          policySecrets: policySecrets,
+                                          recoverySigningPubKey: nil,
+                                          recoveryEncryptionPubKey: nil)
+        self.fetchChangesAndUpdateTrustIfNeeded(stableChanges: stableChanges, reply: reply)
+    }
+
+    func set(preapprovedKeys: [Data],
+             reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("setPreapprovedKeys complete: %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        self.moc.performAndWait {
+            os_log("setPreapprovedKeys: %@", log: tplogDebug, type: .default, preapprovedKeys)
+
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                // No identity, nothing to do
+                os_log("setPreapprovedKeys: No identity.", log: tplogDebug, type: .default)
+                reply(ContainerError.noPreparedIdentity)
+                return
+            }
+            loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in
+                guard let signingKeyPair = signingKeyPair else {
+                    os_log("setPreapprovedKeys: no signing key pair: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                    reply(error ?? ContainerError.unableToCreateKeyPair)
+                    return
+                }
+
+                self.moc.performAndWait {
+                    let dynamicInfo: TPPeerDynamicInfo
+                    do {
+                        dynamicInfo = try self.model.calculateDynamicInfoForPeer(withID: egoPeerID,
+                                                                                 addingPeerIDs: nil,
+                                                                                 removingPeerIDs: nil,
+                                                                                 preapprovedKeys: preapprovedKeys,
+                                                                                 signing: signingKeyPair,
+                                                                                 currentMachineIDs: self.onqueueCurrentMIDList())
+                    } catch {
+                        os_log("setPreapprovedKeys: couldn't calculate dynamic info: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        reply(error)
+                        return
+                    }
+
+                    os_log("setPreapprovedKeys: produced a dynamicInfo: %@", log: tplogDebug, type: .default, dynamicInfo)
+
+                    if dynamicInfo == self.model.peer(withID: egoPeerID)?.dynamicInfo {
+                        os_log("setPreapprovedKeys: no change; nothing to do.", log: tplogDebug, type: .default)
+                        reply(nil)
+                        return
+                    }
+
+                    os_log("setPreapprovedKeys: attempting updateTrust for %@ with: %@", log: tplogDebug, type: .default, egoPeerID, dynamicInfo)
+                    let request = UpdateTrustRequest.with {
+                        $0.changeToken = self.containerMO.changeToken ?? ""
+                        $0.peerID = egoPeerID
+                        $0.dynamicInfoAndSig = SignedPeerDynamicInfo(dynamicInfo)
+                    }
+
+                    self.cuttlefish.updateTrust(request) { response, error in
+                        os_log("setPreapprovedKeys(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                        guard let response = response, error == nil else {
+                            os_log("setPreapprovedKeys failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                            reply(error ?? ContainerError.cloudkitResponseMissing)
+                            return
+                        }
+                        os_log("setPreapprovedKeys: updateTrust suceeded", log: tplogDebug, type: .default)
+
+                        do {
+                            try self.persist(changes: response.changes)
+                        } catch {
+                            os_log("setPreapprovedKeys: could not persist changes: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(error)
+                            return
+                        }
+
+                        reply(nil)
+                    }
+                }
+            }
+        }
+    }
+
+    func updateTLKs(ckksKeys: [CKKSKeychainBackedKeySet],
+                    tlkShares: [CKKSTLKShare],
+                    reply: @escaping ([CKRecord]?, Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: ([CKRecord]?, Error?) -> Void = {
+            os_log("updateTLKs complete: %@", log: tplogTrace, type: .info, traceError($1))
+            self.semaphore.signal()
+            reply($0, $1)
+        }
+
+        os_log("Uploading some new TLKs: %@", log: tplogDebug, type: .default, ckksKeys)
+
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID,
+                let egoPermData = self.containerMO.egoPeerPermanentInfo,
+                let egoPermSig = self.containerMO.egoPeerPermanentInfoSig
+                else {
+                    os_log("Have no self identity, can't make tlk shares", log: tplogDebug, type: .default)
+                    reply(nil, ContainerError.noPreparedIdentity)
+                    return
+            }
+
+            guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: TPECPublicKeyFactory()) else {
+                os_log("Couldn't parse self identity", log: tplogDebug, type: .default, ckksKeys)
+                reply(nil, ContainerError.invalidPermanentInfoOrSig)
+                return
+            }
+
+            loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in
+                guard let egoPeerKeys = egoPeerKeys else {
+                    os_log("Don't have my own peer keys; can't upload new TLKs: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing")
+                    reply(nil, error)
+                    return
+                }
+                self.moc.performAndWait {
+                    guard let egoPeerDynamicInfo = self.model.getDynamicInfoForPeer(withID: egoPeerID) else {
+                        os_log("Unable to fetch dynamic info for self", log: tplogDebug, type: .default)
+                        reply(nil, ContainerError.missingDynamicInfo)
+                        return
+                    }
+
+                    let allTLKShares: [TLKShare]
+                    let viewKeys: [ViewKeys]
+                    do {
+                        (viewKeys, allTLKShares) = try self.makeSharesForNewKeySets(ckksKeys: ckksKeys,
+                                                                                    tlkShares: tlkShares,
+                                                                                    egoPeerKeys: egoPeerKeys,
+                                                                                    egoPeerDynamicInfo: egoPeerDynamicInfo,
+                                                                                    epoch: Int(selfPermanentInfo.epoch))
+                    } catch {
+                        os_log("Unable to process keys before uploading: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        reply(nil, error)
+                        return
+                    }
+
+                    let request = UpdateTrustRequest.with {
+                        $0.changeToken = self.containerMO.changeToken ?? ""
+                        $0.peerID = egoPeerID
+                        $0.tlkShares = allTLKShares
+                        $0.viewKeys = viewKeys
+                    }
+
+                    self.cuttlefish.updateTrust(request) { response, error in
+                        os_log("UpdateTrust(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+
+                        guard error == nil else {
+                            reply(nil, error)
+                            return
+                        }
+
+                        let keyHierarchyRecords = response?.zoneKeyHierarchyRecords.compactMap { CKRecord($0) } ?? []
+                        os_log("Recevied %d CKRecords back", log: tplogDebug, type: .default, keyHierarchyRecords.count)
+                        reply(keyHierarchyRecords, nil)
+                    }
+                }
+            }
+        }
+    }
+
+    func getState(reply: @escaping (ContainerState) -> Void) {
+        self.semaphore.wait()
+        let reply: (ContainerState) -> Void = {
+            os_log("getState complete: %@", log: tplogTrace, type: .info, $0.egoPeerID ?? "<NULL>")
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        self.moc.performAndWait {
+            var state = ContainerState()
+            state.egoPeerID = self.containerMO.egoPeerID
+
+            if self.containerMO.bottles != nil {
+                self.containerMO.bottles!.forEach { bottle in
+                    state.bottles.insert(bottle as! BottleMO)
+                }
+            }
+
+            self.model.allPeers().forEach { peer in
+                state.peers[peer.peerID] = peer
+            }
+            state.vouchers = Array(self.model.allVouchers())
+            reply(state)
+        }
+    }
+
+    // This will only fetch changes if no changes have ever been fetched before
+    private func fetchAndPersistChangesIfNeeded(reply: @escaping (Error?) -> Void) {
+        self.moc.performAndWait {
+            if self.containerMO.changeToken == nil {
+                self.onqueueFetchAndPersistChanges(reply: reply)
+            } else {
+                reply(nil)
+            }
+        }
+    }
+
+    private func fetchAndPersistChanges(reply: @escaping (Error?) -> Void) {
+        self.moc.performAndWait {
+            self.onqueueFetchAndPersistChanges(reply: reply)
+        }
+    }
+
+    private func onqueueFetchAndPersistChanges(reply: @escaping (Error?) -> Void) {
+        let request = FetchChangesRequest.with {
+            $0.changeToken = self.containerMO.changeToken ?? ""
+        }
+        os_log("Fetching with change token: %@", log: tplogDebug, type: .default, request.changeToken.count > 0 ? request.changeToken : "empty")
+
+        self.cuttlefish.fetchChanges(request) { response, error in
+            os_log("FetchChanges(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+            guard let response = response, error == nil else {
+                switch error {
+                case CuttlefishErrorMatcher(code: CuttlefishErrorCode.changeTokenExpired):
+                    os_log("change token is expired; resetting local CK storage", log: tplogDebug, type: .default)
+
+                    self.moc.performAndWait {
+                        do {
+                            try self.deleteLocalCloudKitData()
+                        } catch {
+                            os_log("Failed to reset local data: %@", log: tplogDebug, type: .default, error as CVarArg)
+                            reply(error)
+                            return
+                        }
+
+                        self.fetchAndPersistChanges(reply: reply)
+                    }
+
+                    return
+                default:
+                    os_log("Fetch error is an unknown error: %@", log: tplogDebug, type: .default, String(describing: error))
+                }
+
+                os_log("Could not fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                reply(error)
+                return
+            }
+
+            do {
+                try self.persist(changes: response.changes)
+            } catch {
+                os_log("Could not persist changes: %@", log: tplogDebug, type: .default, error as CVarArg)
+                reply(error)
+                return
+            }
+
+            if response.changes.more {
+                os_log("persist: More changes indicated. Fetching...", log: tplogDebug, type: .default)
+                self.fetchAndPersistChanges(reply: reply)
+                return
+            } else {
+                os_log("persist: no more changes!", log: tplogDebug, type: .default)
+                reply(nil)
+            }
+        }
+    }
+
+    //
+    // Check for delta update in trust lists, that should lead to update of TLKShares
+    //
+    private func haveTrustMemberChanges(newDynamicInfo: TPPeerDynamicInfo, oldDynamicInfo: TPPeerDynamicInfo?) -> Bool {
+        guard let oldDynamicInfo = oldDynamicInfo else {
+            return true
+        }
+        if (newDynamicInfo.includedPeerIDs != oldDynamicInfo.includedPeerIDs) {
+            return true
+        }
+        if (newDynamicInfo.excludedPeerIDs != oldDynamicInfo.excludedPeerIDs) {
+            return true
+        }
+        if (newDynamicInfo.preapprovals != oldDynamicInfo.preapprovals) {
+            return true
+        }
+        return false
+    }
+
+    // Fetch and persist changes from Cuttlefish. If this results
+    // in us calculating a new dynamicInfo then give the new dynamicInfo
+    // to Cuttlefish. If Cuttlefish returns more changes then persist
+    // them locally, update dynamicInfo if needed, and keep doing that
+    // until dynamicInfo is unchanged and there are no more changes to fetch.
+    //
+    // Must be holding the semaphore to call this, and it remains
+    // the caller's responsibility to release it after it completes
+    // (i.e. after reply is invoked).
+    internal func fetchChangesAndUpdateTrustIfNeeded(stableChanges: StableChanges? = nil,
+                                                    changesPending: Bool = false,
+                                                    reply: @escaping (TrustedPeersHelperPeerState?, Error?) -> Void) {
+        self.fetchAndPersistChanges { error in
+            if let error = error {
+                os_log("fetchChangesAndUpdateTrustIfNeeded: fetching failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+                reply(nil, error)
+            }
+
+            self.updateTrustIfNeeded(stableChanges: stableChanges, changesPending: changesPending, reply: reply)
+        }
+    }
+
+    // If this results in us calculating a new dynamicInfo then,
+    // upload the new dynamicInfo
+    // to Cuttlefish. If Cuttlefish returns more changes then persist
+    // them locally, update dynamicInfo if needed, and keep doing that
+    // until dynamicInfo is unchanged and there are no more changes to fetch.
+    //
+    // Must be holding the semaphore to call this, and it remains
+    // the caller's responsibility to release it after it completes
+    // (i.e. after reply is invoked).
+    private func updateTrustIfNeeded(stableChanges: StableChanges? = nil,
+                                     changesPending: Bool = false,
+                                     reply: @escaping (TrustedPeersHelperPeerState?, Error?) -> Void) {
+        self.moc.performAndWait {
+            guard let egoPeerID = self.containerMO.egoPeerID else {
+                // No identity, nothing to do
+                os_log("updateTrustIfNeeded: No identity.", log: tplogDebug, type: .default)
+                reply(TrustedPeersHelperPeerState(peerID: nil, isPreapproved: false, status: .unknown, memberChanges: changesPending, unknownMachineIDs: false), nil)
+                return
+            }
+            loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in
+                guard let signingKeyPair = signingKeyPair else {
+                    os_log("updateTrustIfNeeded: no signing key pair: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                    reply(TrustedPeersHelperPeerState(peerID: nil, isPreapproved: false, status: .unknown, memberChanges: changesPending, unknownMachineIDs: false), error)
+                    return
+                }
+                guard self.model.hasPeer(withID: egoPeerID) else {
+                    // Not in circle, nothing to do
+                    let isPreapproved = self.model.hasPeerPreapprovingKey(signingKeyPair.publicKey().spki())
+                    os_log("updateTrustIfNeeded: ego peer is not in model, is %@", log: tplogDebug, type: .default, isPreapproved ? "preapproved" : "not yet preapproved")
+                    reply(TrustedPeersHelperPeerState(peerID: egoPeerID,
+                                                      isPreapproved: isPreapproved,
+                                                      status: .unknown,
+                                                      memberChanges: changesPending,
+                                                      unknownMachineIDs: false),
+                          nil)
+                    return
+                }
+                self.moc.performAndWait {
+                    let dynamicInfo: TPPeerDynamicInfo
+                    var stableInfo: TPPeerStableInfo?
+                    do {
+                        // FIXME We should be able to calculate the contents of dynamicInfo without the signingKeyPair,
+                        // and then only load the key if it has changed and we need to sign a new one. This would also
+                        // help make our detection of change immune from non-canonical serialization of dynamicInfo.
+                        dynamicInfo = try self.model.calculateDynamicInfoForPeer(withID: egoPeerID,
+                                                                                 addingPeerIDs: nil,
+                                                                                 removingPeerIDs: nil,
+                                                                                 preapprovedKeys: nil,
+                                                                                 signing: signingKeyPair,
+                                                                                 currentMachineIDs: self.onqueueCurrentMIDList())
+
+                        stableInfo = try self.createNewStableInfoIfNeeded(stableChanges: stableChanges,
+                                                                          egoPeerID: egoPeerID,
+                                                                          dynamicInfo: dynamicInfo,
+                                                                          signingKeyPair: signingKeyPair)
+                    } catch {
+                        os_log("updateTrustIfNeeded: couldn't calculate dynamic info: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        reply(TrustedPeersHelperPeerState(peerID: egoPeerID,
+                                                          isPreapproved: false,
+                                                          status: self.model.statusOfPeer(withID: egoPeerID),
+                                                          memberChanges: changesPending,
+                                                          unknownMachineIDs: false),
+                              error)
+                        return
+                    }
+
+                    os_log("updateTrustIfNeeded: produced a stableInfo: %@", log: tplogDebug, type: .default, String(describing: stableInfo))
+                    os_log("updateTrustIfNeeded: produced a dynamicInfo: %@", log: tplogDebug, type: .default, dynamicInfo)
+
+                    let peer = self.model.peer(withID: egoPeerID)
+                    if (stableInfo == nil || stableInfo == peer?.stableInfo) &&
+                        dynamicInfo == peer?.dynamicInfo {
+                        os_log("updateTrustIfNeeded: complete.", log: tplogDebug, type: .default)
+                        // No change to the dynamicInfo: update the MID list now that we've reached a steady state
+                        do {
+                            self.onqueueUpdateMachineIDListFromModel(dynamicInfo: dynamicInfo)
+                            try self.moc.save()
+                        } catch {
+                            os_log("updateTrustIfNeeded: unable to remove untrusted MachineIDs: %@", log: tplogDebug, type: .default, error as CVarArg)
+                        }
+
+                        reply(TrustedPeersHelperPeerState(peerID: egoPeerID,
+                                                          isPreapproved: false,
+                                                          status: self.model.statusOfPeer(withID: egoPeerID),
+                                                          memberChanges: changesPending,
+                                                          unknownMachineIDs: self.onqueueFullIDMSListWouldBeHelpful()),
+                              nil)
+                        return
+                    }
+                    // Check if we change that should trigger a notification that should trigger TLKShare updates
+                    let haveChanges = changesPending || self.haveTrustMemberChanges(newDynamicInfo: dynamicInfo, oldDynamicInfo: peer?.dynamicInfo)
+
+                    let signedDynamicInfo = SignedPeerDynamicInfo(dynamicInfo)
+                    os_log("updateTrustIfNeeded: attempting updateTrust for %@ with: %@", log: tplogDebug, type: .default, egoPeerID, dynamicInfo)
+                    var request = UpdateTrustRequest.with {
+                        $0.changeToken = self.containerMO.changeToken ?? ""
+                        $0.peerID = egoPeerID
+                        $0.dynamicInfoAndSig = signedDynamicInfo
+                    }
+                    if let stableInfo = stableInfo {
+                        request.stableInfoAndSig = SignedPeerStableInfo(stableInfo)
+                    }
+                    self.cuttlefish.updateTrust(request) { response, error in
+                        os_log("UpdateTrust(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                        guard let response = response, error == nil else {
+                            os_log("UpdateTrust failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
+                            reply(nil, error ?? ContainerError.cloudkitResponseMissing)
+                            return
+                        }
+
+                        do {
+                            try self.persist(changes: response.changes)
+                        } catch {
+                            os_log("updateTrust failed: %@", log: tplogDebug, String(describing: error))
+                            reply(nil, error)
+                            return
+                        }
+
+                        if response.changes.more {
+                            self.fetchChangesAndUpdateTrustIfNeeded(stableChanges: stableChanges,
+                                                                    changesPending: haveChanges,
+                                                                    reply: reply)
+                        } else {
+                            self.updateTrustIfNeeded(stableChanges: stableChanges,
+                                                     changesPending: haveChanges,
+                                                     reply: reply)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+
+    private func persist(changes: Changes) throws {
+        // This is some nonsense: I can't figure out how to tell swift to throw an exception across performAndWait.
+        // So, do it ourself
+        var outsideBlockError: Error?
+
+        self.moc.performAndWait {
+            os_log("persist: Received %d peer differences, more: %d", log: tplogDebug, type: .default,
+                   changes.differences.count,
+                   changes.more)
+            os_log("persist: New change token: %@", log: tplogDebug, type: .default, changes.changeToken)
+
+            do {
+                try self.onQueuePersist(changes: changes)
+            } catch {
+                outsideBlockError = error
+            }
+        }
+
+        if let outsideBlockError = outsideBlockError {
+            throw outsideBlockError
+        }
+    }
+
+    // Must be on moc queue to call this.
+    // Changes are registered in the model and stored in moc.
+    private func onQueuePersist(changes: Changes) throws {
+        self.containerMO.changeToken = changes.changeToken
+        self.containerMO.moreChanges = changes.more
+
+        if changes.differences.count > 0 {
+            self.model.clearViableBottles()
+        }
+        
+        try changes.differences.forEach { peerDifference in
+            if let operation = peerDifference.operation {
+                switch operation {
+                case .add(let peer):
+                    try self.addOrUpdate(peer: peer)
+
+                case .update(let peer):
+                    try self.addOrUpdate(peer: peer)
+                    // Update containerMO ego data if it has changed.
+                    if peer.peerID == self.containerMO.egoPeerID {
+                        guard let stableInfoAndSig:TPPeerStableInfo = peer.stableInfoAndSig.toStableInfo() else {
+                            break
+                        }
+                        self.containerMO.egoPeerStableInfo = stableInfoAndSig.data
+                        self.containerMO.egoPeerStableInfoSig = stableInfoAndSig.sig
+                    }
+
+                case .remove(let peer):
+                    self.model.deletePeer(withID: peer.peerID)
+                    if let peerMO = try self.fetchPeerMO(peerID: peer.peerID) {
+                        self.moc.delete(peerMO)
+                    }
+                }
+            }
+        }
+
+        let signingKey = changes.recoverySigningPubKey
+        let encryptionKey = changes.recoveryEncryptionPubKey
+
+        if signingKey.count > 0 && encryptionKey.count > 0 {
+            self.addOrUpdate(signingKey: signingKey, encryptionKey: encryptionKey)
+        }
+        try self.moc.save()
+    }
+
+    // Must be on moc queue to call this
+    // Deletes all things that should come back from CloudKit. Keeps the egoPeer data, though.
+    private func deleteLocalCloudKitData() throws {
+        os_log("Deleting all CloudKit data", log: tplogDebug, type: .default)
+
+        do {
+            let peerRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Peer")
+            peerRequest.predicate = NSPredicate(format: "container == %@", self.containerMO)
+            try self.moc.execute(NSBatchDeleteRequest(fetchRequest: peerRequest))
+
+            let bottleRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Bottle")
+            bottleRequest.predicate = NSPredicate(format: "container == %@", self.containerMO)
+            try self.moc.execute(NSBatchDeleteRequest(fetchRequest: bottleRequest))
+
+            self.containerMO.peers = nil
+            self.containerMO.bottles = nil
+            self.containerMO.changeToken = nil
+            self.containerMO.moreChanges = false
+
+            self.model = Container.loadModel(from: self.containerMO)
+            try self.moc.save()
+        } catch {
+            os_log("Local delete failed: %@", log: tplogDebug, type: .default, error as CVarArg)
+            throw error
+        }
+
+        os_log("Saved model with %d peers", log: tplogDebug, type: .default, self.model.allPeerIDs().count)
+    }
+
+    // Must be on moc queue to call this.
+    private func addOrUpdate(signingKey: Data, encryptionKey: Data) {
+        self.model.setRecoveryKeys(
+            TPRecoveryKeyPair(signingSPKI: signingKey, encryptionSPKI: encryptionKey))
+    }
+
+    // Must be on moc queue to call this.
+    private func addOrUpdate(peer: Peer) throws {
+        if !self.model.hasPeer(withID: peer.peerID) {
+            // Add:
+            guard let permanentInfo = peer.permanentInfoAndSig.toPermanentInfo(peerID: peer.peerID) else {
+                // Ignoring bad peer
+                return
+            }
+            let stableInfo = peer.stableInfoAndSig.toStableInfo()
+            let dynamicInfo = peer.dynamicInfoAndSig.toDynamicInfo()
+            let vouchers = peer.vouchers.compactMap {
+                TPVoucher(infoWith: $0.voucher, sig: $0.sig)
+            }
+            let isEgoPeer = peer.peerID == self.containerMO.egoPeerID
+            try self.registerPeerMO(permanentInfo: permanentInfo,
+                                    stableInfo: stableInfo,
+                                    dynamicInfo: dynamicInfo,
+                                    vouchers: vouchers,
+                                    isEgoPeer: isEgoPeer)
+        } else {
+            // Update:
+            // The assertion here is that every peer registered in model is also present in containerMO
+            let peerMO = try self.fetchPeerMO(peerID: peer.peerID)!
+
+            if let stableInfo = peer.stableInfoAndSig.toStableInfo() {
+                try self.model.update(stableInfo, forPeerWithID: peer.peerID)
+                // Pull the stableInfo back out of the model, and persist that.
+                // The model checks signatures and clocks to prevent replay attacks.
+                let modelPeer = self.model.peer(withID: peer.peerID)
+                peerMO.stableInfo = modelPeer?.stableInfo?.data
+                peerMO.stableInfoSig = modelPeer?.stableInfo?.sig
+            }
+            if let dynamicInfo = peer.dynamicInfoAndSig.toDynamicInfo() {
+                try self.model.update(dynamicInfo, forPeerWithID: peer.peerID)
+                // Pull the dynamicInfo back out of the model, and persist that.
+                // The model checks signatures and clocks to prevent replay attacks.
+                let modelPeer = self.model.peer(withID: peer.peerID)
+                peerMO.dynamicInfo = modelPeer?.dynamicInfo?.data
+                peerMO.dynamicInfoSig = modelPeer?.dynamicInfo?.sig
+            }
+            peer.vouchers.forEach {
+                if let voucher = TPVoucher(infoWith: $0.voucher, sig: $0.sig) {
+                    self.model.register(voucher)
+                    let voucherMO = VoucherMO(context: self.moc)
+                    voucherMO.voucherInfo = voucher.data
+                    voucherMO.voucherInfoSig = voucher.sig
+                    peerMO.addToVouchers(voucherMO)
+                }
+            }
+        }
+    }
+
+    // Must be on moc queue to call this.
+    private func fetchPeerMO(peerID: String) throws -> PeerMO? {
+        let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Peer")
+        fetch.predicate = NSPredicate(format: "peerID == %@ && container == %@", peerID, self.containerMO)
+        let peers = try self.moc.fetch(fetch)
+        return peers.first as? PeerMO
+    }
+
+    // Must be on moc queue to call this.
+    private func getPolicyDoc(_ policyVersion: UInt64) throws -> TPPolicyDocument {
+        guard let policyDoc = self.model.policy(withVersion: policyVersion) else {
+            throw ContainerError.unknownPolicyVersion(policyVersion)
+        }
+        assert(policyVersion == policyDoc.policyVersion)
+        if policyVersion == prevailingPolicyVersion {
+            assert(policyDoc.policyHash == prevailingPolicyHash)
+        }
+        return policyDoc
+    }
+
+    // Must be on moc queue to call this.
+    private func createNewStableInfoIfNeeded(stableChanges: StableChanges?,
+                                             egoPeerID: String,
+                                             dynamicInfo: TPPeerDynamicInfo,
+                                             signingKeyPair: _SFECKeyPair) throws -> TPPeerStableInfo? {
+        func noChange<T: Equatable>(_ change: T?, _ existing: T?) -> Bool {
+            return (nil == change) || change == existing
+        }
+        let existingStableInfo = self.model.peer(withID: egoPeerID)?.stableInfo
+        if noChange(stableChanges?.deviceName, existingStableInfo?.deviceName) &&
+            noChange(stableChanges?.serialNumber, existingStableInfo?.serialNumber) &&
+            noChange(stableChanges?.osVersion, existingStableInfo?.osVersion) &&
+            noChange(stableChanges?.policyVersion, existingStableInfo?.policyVersion) &&
+            noChange(stableChanges?.policySecrets, existingStableInfo?.policySecrets) &&
+            noChange(stableChanges?.recoverySigningPubKey, existingStableInfo?.recoverySigningPublicKey) &&
+            noChange(stableChanges?.recoveryEncryptionPubKey, existingStableInfo?.recoveryEncryptionPublicKey) &&
+            self.model.doesPeerRecoveryKeyMatchPeers(egoPeerID) {
+            return nil
+        }
+        let policyHash: String?
+        if let policyVersion = stableChanges?.policyVersion {
+            let policyDoc = try self.getPolicyDoc(policyVersion)
+            policyHash = policyDoc.policyHash
+        } else {
+            policyHash = nil
+        }
+
+        // Determine which recovery key we'd like to be using, given our current idea of who to trust
+        let newRecoveryKeys = self.model.bestRecoveryKey(with: dynamicInfo)
+
+        return try self.model.createStableInfo(withPolicyVersion: stableChanges?.policyVersion ?? existingStableInfo?.policyVersion ?? prevailingPolicyVersion,
+                                               policyHash: policyHash ?? existingStableInfo?.policyHash ?? prevailingPolicyHash,
+                                               policySecrets: stableChanges?.policySecrets ?? existingStableInfo?.policySecrets,
+                                               deviceName: stableChanges?.deviceName ?? existingStableInfo?.deviceName ?? "",
+                                               serialNumber: stableChanges?.serialNumber ?? existingStableInfo?.serialNumber ?? "",
+                                               osVersion: stableChanges?.osVersion ?? existingStableInfo?.osVersion ?? "",
+                                               signing: signingKeyPair,
+                                               recoverySigningPubKey: newRecoveryKeys?.signingSPKI ?? existingStableInfo?.recoverySigningPublicKey,
+                                               recoveryEncryptionPubKey: newRecoveryKeys?.encryptionSPKI ?? existingStableInfo?.recoveryEncryptionPublicKey)
+    }
+
+    private func assembleBottle(egoPeerID: String) throws -> Bottle {
+        let bottleMoSet = containerMO.bottles as? Set<BottleMO>
+
+        var bottleMOs = bottleMoSet?.filter {
+            $0.peerID == egoPeerID
+        }
+
+        if let count = bottleMOs?.count {
+            if count > 1 {
+                throw ContainerError.tooManyBottlesForPeer
+            } else if count == 0 {
+                throw ContainerError.noBottleForPeer
+            }
+        } else {
+            throw ContainerError.failedToAssembleBottle
+        }
+
+        let BM: BottleMO? = (bottleMOs?.removeFirst())
+
+        let bottle = try Bottle.with {
+            $0.peerID = egoPeerID
+
+            if let bottledPeerData = BM?.contents {
+                $0.contents = bottledPeerData
+            } else {
+                throw ContainerError.failedToAssembleBottle
+            }
+
+            if let bottledPeerEscrowSigningSPKI = BM?.escrowedSigningSPKI {
+                $0.escrowedSigningSpki = bottledPeerEscrowSigningSPKI
+            } else {
+                throw ContainerError.failedToAssembleBottle
+            }
+
+            if let bottledPeerSignatureUsingEscrowKey = BM?.signatureUsingEscrowKey {
+                $0.signatureUsingEscrowKey = bottledPeerSignatureUsingEscrowKey
+            } else {
+                throw ContainerError.failedToAssembleBottle
+            }
+
+            if let bottledPeerSignatureUsingPeerKey = BM?.signatureUsingPeerKey {
+                $0.signatureUsingPeerKey = bottledPeerSignatureUsingPeerKey
+            } else {
+                throw ContainerError.failedToAssembleBottle
+            }
+
+            if let bID = BM?.bottleID {
+                $0.bottleID = bID
+            } else {
+                throw ContainerError.failedToAssembleBottle
+            }
+        }
+
+        return bottle
+    }
+
+    func reportHealth(request: ReportHealthRequest, reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("reportHealth complete %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        var updatedRequest = request
+
+        if let egoPeerID = self.containerMO.egoPeerID {
+            loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, _ in
+                updatedRequest.octagonEgoIdentity = (egoPeerKeys != nil)
+            }
+        }
+
+        self.moc.performAndWait {
+            self.cuttlefish.reportHealth(updatedRequest) { response, error in
+                os_log("reportHealth(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))")
+                guard error == nil else {
+                    reply(error)
+                    return
+                }
+                reply(nil)
+            }
+        }
+    }
+
+    func pushHealthInquiry(reply: @escaping (Error?) -> Void) {
+        self.semaphore.wait()
+        let reply: (Error?) -> Void = {
+            os_log("reportHealth complete %@", log: tplogTrace, type: .info, traceError($0))
+            self.semaphore.signal()
+            reply($0)
+        }
+
+        self.moc.performAndWait {
+            self.cuttlefish.pushHealthInquiry(HealthInquiryRequest()) { response, error in
+                os_log("pushHealthInquiry(): %@, error: %@", log: tplogDebug, "\(String(describing: response))", "\(String(describing: error))")
+                guard error == nil else {
+                    reply(error)
+                    return
+                }
+                reply(nil)
+            }
+        }
+    }
+}