[darwin-framework-tool] Add an option to use per-controller storage instead of a global shared storage (#36044)

diff --git a/examples/darwin-framework-tool/BUILD.gn b/examples/darwin-framework-tool/BUILD.gn
index 5e7b7d6..0d1e142 100644
--- a/examples/darwin-framework-tool/BUILD.gn
+++ b/examples/darwin-framework-tool/BUILD.gn
@@ -190,10 +190,16 @@
     "commands/common/CHIPCommandBridge.mm",
     "commands/common/CHIPCommandStorageDelegate.mm",
     "commands/common/CHIPToolKeypair.mm",
+    "commands/common/CertificateIssuer.h",
+    "commands/common/CertificateIssuer.mm",
+    "commands/common/ControllerStorage.h",
+    "commands/common/ControllerStorage.mm",
     "commands/common/MTRDevice_Externs.h",
     "commands/common/MTRError.mm",
     "commands/common/MTRError_Utils.h",
     "commands/common/MTRLogging.h",
+    "commands/common/PreferencesStorage.h",
+    "commands/common/PreferencesStorage.mm",
     "commands/common/RemoteDataModelLogger.h",
     "commands/common/RemoteDataModelLogger.mm",
     "commands/configuration/Commands.h",
diff --git a/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h b/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h
index 9458d30..f58251b 100644
--- a/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h
+++ b/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h
@@ -41,6 +41,8 @@
             "Sets the commissioner node ID of the given "
             "commissioner-name. Interactive mode will only set a single commissioner on the inital command. "
             "The commissioner node ID will be persisted until a different one is specified.");
+        AddArgument("commissioner-shared-storage", 0, 1, &mCommissionerSharedStorage,
+            "Use a shared storage instance instead of individual storage for each commissioner. Default is true.");
         AddArgument("paa-trust-store-path", &mPaaTrustStorePath,
             "Path to directory holding PAA certificate information.  Can be absolute or relative to the current working "
             "directory.");
@@ -87,6 +89,7 @@
 
     // This method returns the commissioner instance to be used for running the command.
     MTRDeviceController * CurrentCommissioner();
+    NSNumber * CurrentCommissionerFabricId();
 
     MTRDeviceController * GetCommissioner(const char * identity);
 
@@ -130,6 +133,8 @@
     void StopWaiting();
 
     CHIP_ERROR MaybeSetUpStack();
+    CHIP_ERROR SetUpStackWithSharedStorage(NSArray<NSData *> * productAttestationAuthorityCertificates);
+    CHIP_ERROR SetUpStackWithPerControllerStorage(NSArray<NSData *> * productAttestationAuthorityCertificates);
     void MaybeTearDownStack();
 
     CHIP_ERROR GetPAACertsFromFolder(NSArray<NSData *> * __autoreleasing * paaCertsResult);
@@ -140,6 +145,9 @@
     // The current controller; the one the current command should be using.
     MTRDeviceController * mCurrentController;
 
+    static bool sUseSharedStorage;
+    chip::Optional<bool> mCommissionerSharedStorage;
+
     std::condition_variable cvWaitingForResponse;
     std::mutex cvWaitingForResponseMutex;
     chip::Optional<char *> mCommissionerName;
@@ -148,4 +156,5 @@
     static dispatch_queue_t mOTAProviderCallbackQueue;
     chip::Optional<char *> mPaaTrustStorePath;
     chip::Optional<chip::VendorId> mCommissionerVendorId;
+    std::string mCurrentIdentity;
 };
diff --git a/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.mm b/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.mm
index d52c29a..724a9a2 100644
--- a/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.mm
+++ b/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.mm
@@ -23,7 +23,11 @@
 
 #include <lib/core/CHIPConfig.h>
 #include <lib/core/CHIPVendorIdentifiers.hpp>
+#include <protocols/secure_channel/PASESession.h> // for chip::kTestControllerNodeId
 
+#import "CHIPCommandStorageDelegate.h"
+#import "CertificateIssuer.h"
+#import "ControllerStorage.h"
 #include "MTRError_Utils.h"
 
 #include <map>
@@ -34,10 +38,9 @@
 std::map<std::string, MTRDeviceController *> CHIPCommandBridge::mControllers;
 dispatch_queue_t CHIPCommandBridge::mOTAProviderCallbackQueue;
 OTAProviderDelegate * CHIPCommandBridge::mOTADelegate;
+bool CHIPCommandBridge::sUseSharedStorage = true;
 constexpr char kTrustStorePathVariable[] = "PAA_TRUST_STORE_PATH";
 
-CHIPToolKeypair * gNocSigner = [[CHIPToolKeypair alloc] init];
-
 CHIP_ERROR CHIPCommandBridge::Run()
 {
     // In interactive mode, we want to avoid memory accumulating in the main autorelease pool,
@@ -120,61 +123,113 @@
 
 CHIP_ERROR CHIPCommandBridge::MaybeSetUpStack()
 {
-    if (IsInteractive()) {
-        return CHIP_NO_ERROR;
-    }
-    NSData * ipk;
-    gNocSigner = [[CHIPToolKeypair alloc] init];
-    storage = [[CHIPToolPersistentStorageDelegate alloc] init];
+    VerifyOrReturnError(!IsInteractive(), CHIP_NO_ERROR);
 
     mOTADelegate = [[OTAProviderDelegate alloc] init];
-
-    auto factory = [MTRDeviceControllerFactory sharedInstance];
-    if (factory == nil) {
-        ChipLogError(chipTool, "Controller factory is nil");
-        return CHIP_ERROR_INTERNAL;
-    }
-
-    auto params = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage];
-    params.shouldStartServer = YES;
-    params.otaProviderDelegate = mOTADelegate;
-    NSArray<NSData *> * paaCertResults;
-    ReturnLogErrorOnFailure(GetPAACertsFromFolder(&paaCertResults));
-    if ([paaCertResults count] > 0) {
-        params.productAttestationAuthorityCertificates = paaCertResults;
-    }
+    storage = [[CHIPToolPersistentStorageDelegate alloc] init];
 
     NSError * error;
-    if ([factory startControllerFactory:params error:&error] == NO) {
-        ChipLogError(chipTool, "Controller factory startup failed");
-        return MTRErrorToCHIPErrorCode(error);
+    __auto_type * certificateIssuer = [CertificateIssuer sharedInstance];
+    [certificateIssuer startWithStorage:storage error:&error];
+    VerifyOrReturnError(nil == error, MTRErrorToCHIPErrorCode(error), ChipLogError(chipTool, "Can not start the certificate issuer: %@", error));
+
+    NSArray<NSData *> * productAttestationAuthorityCertificates = nil;
+    ReturnLogErrorOnFailure(GetPAACertsFromFolder(&productAttestationAuthorityCertificates));
+    if ([productAttestationAuthorityCertificates count] == 0) {
+        productAttestationAuthorityCertificates = nil;
     }
 
-    ReturnLogErrorOnFailure([gNocSigner createOrLoadKeys:storage]);
+    sUseSharedStorage = mCommissionerSharedStorage.ValueOr(true);
+    if (sUseSharedStorage) {
+        return SetUpStackWithSharedStorage(productAttestationAuthorityCertificates);
+    }
 
-    ipk = [gNocSigner getIPK];
+    return SetUpStackWithPerControllerStorage(productAttestationAuthorityCertificates);
+}
+
+CHIP_ERROR CHIPCommandBridge::SetUpStackWithPerControllerStorage(NSArray<NSData *> * productAttestationAuthorityCertificates)
+{
+    __auto_type * certificateIssuer = [CertificateIssuer sharedInstance];
 
     constexpr const char * identities[] = { kIdentityAlpha, kIdentityBeta, kIdentityGamma };
     std::string commissionerName = mCommissionerName.HasValue() ? mCommissionerName.Value() : kIdentityAlpha;
     for (size_t i = 0; i < ArraySize(identities); ++i) {
-        auto controllerParams = [[MTRDeviceControllerStartupParams alloc] initWithIPK:ipk fabricID:@(i + 1) nocSigner:gNocSigner];
+        __auto_type * uuidString = [NSString stringWithFormat:@"%@%@", @"8DCADB14-AF1F-45D0-B084-00000000000", @(i)];
+        __auto_type * controllerId = [[NSUUID alloc] initWithUUIDString:uuidString];
+        __auto_type * vendorId = @(mCommissionerVendorId.ValueOr(chip::VendorId::TestVendor1));
+        __auto_type * fabricId = @(i + 1);
+        __auto_type * nodeId = @(chip::kTestControllerNodeId);
 
         if (commissionerName.compare(identities[i]) == 0 && mCommissionerNodeId.HasValue()) {
-            controllerParams.nodeId = @(mCommissionerNodeId.Value());
-        }
-        // We're not sure whether we're creating a new fabric or using an
-        // existing one, so just try both.
-        auto controller = [factory createControllerOnExistingFabric:controllerParams error:&error];
-        if (controller == nil) {
-            // Maybe we didn't have this fabric yet.
-            controllerParams.vendorID = @(mCommissionerVendorId.ValueOr(chip::VendorId::TestVendor1));
-            controller = [factory createControllerOnNewFabric:controllerParams error:&error];
-        }
-        if (controller == nil) {
-            ChipLogError(chipTool, "Controller startup failure.");
-            return MTRErrorToCHIPErrorCode(error);
+            nodeId = @(mCommissionerNodeId.Value());
         }
 
+        __auto_type * controllerStorage = [[ControllerStorage alloc] initWithControllerID:controllerId];
+
+        NSError * error;
+        __auto_type * operationalKeypair = [certificateIssuer issueOperationalKeypairWithControllerStorage:controllerStorage error:&error];
+        __auto_type * operational = [certificateIssuer issueOperationalCertificateForNodeID:nodeId
+                                                                                   fabricID:fabricId
+                                                                                  publicKey:operationalKeypair.publicKey
+                                                                                      error:&error];
+        VerifyOrReturnError(nil == error, MTRErrorToCHIPErrorCode(error), ChipLogError(chipTool, "Can not issue an operational certificate: %@", error));
+
+        __auto_type * controllerStorageQueue = dispatch_queue_create("com.chip.storage", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
+        __auto_type * params = [[MTRDeviceControllerExternalCertificateParameters alloc] initWithStorageDelegate:controllerStorage
+                                                                                            storageDelegateQueue:controllerStorageQueue
+                                                                                                uniqueIdentifier:controllerId
+                                                                                                             ipk:certificateIssuer.ipk
+                                                                                                        vendorID:vendorId
+                                                                                              operationalKeypair:operationalKeypair
+                                                                                          operationalCertificate:operational
+                                                                                         intermediateCertificate:nil
+                                                                                                 rootCertificate:certificateIssuer.rootCertificate];
+        [params setOperationalCertificateIssuer:certificateIssuer queue:controllerStorageQueue];
+        params.productAttestationAuthorityCertificates = productAttestationAuthorityCertificates;
+
+        __auto_type * controller = [[MTRDeviceController alloc] initWithParameters:params error:&error];
+        VerifyOrReturnError(nil != controller, MTRErrorToCHIPErrorCode(error), ChipLogError(chipTool, "Controller startup failure: %@", error));
+        mControllers[identities[i]] = controller;
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR CHIPCommandBridge::SetUpStackWithSharedStorage(NSArray<NSData *> * productAttestationAuthorityCertificates)
+{
+    __auto_type * factory = [MTRDeviceControllerFactory sharedInstance];
+    VerifyOrReturnError(nil != factory, CHIP_ERROR_INTERNAL, ChipLogError(chipTool, "Controller factory is nil"));
+
+    auto factoryParams = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage];
+    factoryParams.shouldStartServer = YES;
+    factoryParams.otaProviderDelegate = mOTADelegate;
+    factoryParams.productAttestationAuthorityCertificates = productAttestationAuthorityCertificates;
+
+    NSError * error;
+    auto started = [factory startControllerFactory:factoryParams error:&error];
+    VerifyOrReturnError(started, MTRErrorToCHIPErrorCode(error), ChipLogError(chipTool, "Controller factory startup failed"));
+
+    __auto_type * certificateIssuer = [CertificateIssuer sharedInstance];
+
+    constexpr const char * identities[] = { kIdentityAlpha, kIdentityBeta, kIdentityGamma };
+    std::string commissionerName = mCommissionerName.HasValue() ? mCommissionerName.Value() : kIdentityAlpha;
+    for (size_t i = 0; i < ArraySize(identities); ++i) {
+        __auto_type * fabricId = @(i + 1);
+        __auto_type * params = [[MTRDeviceControllerStartupParams alloc] initWithIPK:certificateIssuer.ipk
+                                                                            fabricID:fabricId
+                                                                           nocSigner:certificateIssuer.signingKey];
+        if (commissionerName.compare(identities[i]) == 0 && mCommissionerNodeId.HasValue()) {
+            params.nodeId = @(mCommissionerNodeId.Value());
+        }
+
+        // We're not sure whether we're creating a new fabric or using an existing one, so just try both.
+        auto controller = [factory createControllerOnExistingFabric:params error:&error];
+        if (controller == nil) {
+            // Maybe we didn't have this fabric yet.
+            params.vendorID = @(mCommissionerVendorId.ValueOr(chip::VendorId::TestVendor1));
+            controller = [factory createControllerOnNewFabric:params error:&error];
+        }
+        VerifyOrReturnError(nil != controller, MTRErrorToCHIPErrorCode(error), ChipLogError(chipTool, "Controller startup failure: %@", error));
         mControllers[identities[i]] = controller;
     }
 
@@ -197,11 +252,29 @@
             kIdentityBeta, kIdentityGamma);
         chipDie();
     }
+    mCurrentIdentity = name;
     mCurrentController = mControllers[name];
 }
 
 MTRDeviceController * CHIPCommandBridge::CurrentCommissioner() { return mCurrentController; }
 
+NSNumber * CHIPCommandBridge::CurrentCommissionerFabricId()
+{
+    if (mCurrentIdentity.compare(kIdentityAlpha) == 0) {
+        return @(1);
+    } else if (mCurrentIdentity.compare(kIdentityBeta) == 0) {
+        return @(2);
+    } else if (mCurrentIdentity.compare(kIdentityGamma) == 0) {
+        return @(3);
+    } else {
+        ChipLogError(chipTool, "Unknown commissioner name: %s. Supported names are [%s, %s, %s]", mCurrentIdentity.c_str(), kIdentityAlpha,
+            kIdentityBeta, kIdentityGamma);
+        chipDie();
+    }
+
+    return @(0); // This should never happens.
+}
+
 MTRDeviceController * CHIPCommandBridge::GetCommissioner(const char * identity) { return mControllers[identity]; }
 
 MTRBaseDevice * CHIPCommandBridge::BaseDeviceWithNodeId(chip::NodeId nodeId)
@@ -223,15 +296,25 @@
 {
     StopCommissioners();
 
-    auto factory = [MTRDeviceControllerFactory sharedInstance];
-    NSData * ipk = [gNocSigner getIPK];
+    if (sUseSharedStorage) {
+        auto factory = [MTRDeviceControllerFactory sharedInstance];
 
-    constexpr const char * identities[] = { kIdentityAlpha, kIdentityBeta, kIdentityGamma };
-    for (size_t i = 0; i < ArraySize(identities); ++i) {
-        auto controllerParams = [[MTRDeviceControllerStartupParams alloc] initWithIPK:ipk fabricID:@(i + 1) nocSigner:gNocSigner];
+        constexpr const char * identities[] = { kIdentityAlpha, kIdentityBeta, kIdentityGamma };
+        for (size_t i = 0; i < ArraySize(identities); ++i) {
+            __auto_type * certificateIssuer = [CertificateIssuer sharedInstance];
+            auto controllerParams = [[MTRDeviceControllerStartupParams alloc] initWithIPK:certificateIssuer.ipk fabricID:@(i + 1) nocSigner:certificateIssuer.signingKey];
 
-        auto controller = [factory createControllerOnExistingFabric:controllerParams error:nil];
-        mControllers[identities[i]] = controller;
+            auto controller = [factory createControllerOnExistingFabric:controllerParams error:nil];
+            mControllers[identities[i]] = controller;
+        }
+    } else {
+        NSArray<NSData *> * productAttestationAuthorityCertificates = nil;
+        ReturnOnFailure(GetPAACertsFromFolder(&productAttestationAuthorityCertificates));
+        if ([productAttestationAuthorityCertificates count] == 0) {
+            productAttestationAuthorityCertificates = nil;
+        }
+
+        ReturnOnFailure(SetUpStackWithPerControllerStorage(productAttestationAuthorityCertificates));
     }
 }
 
diff --git a/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.h b/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.h
index d5f743d..1da1b6b 100644
--- a/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.h
+++ b/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.h
@@ -1,8 +1,28 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
 #import <Foundation/Foundation.h>
 #import <Matter/Matter.h>
 
 NS_ASSUME_NONNULL_BEGIN
 
+extern NSString * const kDarwinFrameworkToolCertificatesDomain;
+
 @interface CHIPToolPersistentStorageDelegate : NSObject <MTRStorage>
 - (nullable NSData *)storageDataForKey:(NSString *)key;
 - (BOOL)setStorageData:(NSData *)value forKey:(NSString *)key;
diff --git a/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.mm b/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.mm
index 7cbdce8..3ec3dda 100644
--- a/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.mm
+++ b/examples/darwin-framework-tool/commands/common/CHIPCommandStorageDelegate.mm
@@ -1,89 +1,70 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
 #include "CHIPCommandStorageDelegate.h"
 
 #import <Matter/Matter.h>
 
-#define LOG_DEBUG_PERSISTENT_STORAGE_DELEGATE 0
+#import "PreferencesStorage.h"
 
-NSString * const kCHIPToolDefaultsDomain = @"com.apple.chiptool";
+NSString * const kDarwinFrameworkToolCertificatesDomain = @"com.apple.chiptool";
 
-id MTRGetDomainValueForKey(NSString * domain, NSString * key)
-{
-    id value = (id) CFBridgingRelease(CFPreferencesCopyAppValue((CFStringRef) key, (CFStringRef) domain));
-    if (value) {
-        return value;
-    }
-    return nil;
-}
-
-BOOL MTRSetDomainValueForKey(NSString * domain, NSString * key, id value)
-{
-    CFPreferencesSetAppValue((CFStringRef) key, (__bridge CFPropertyListRef _Nullable)(value), (CFStringRef) domain);
-    return CFPreferencesAppSynchronize((CFStringRef) domain) == true;
-}
-
-BOOL MTRRemoveDomainValueForKey(NSString * domain, NSString * key)
-{
-    CFPreferencesSetAppValue((CFStringRef) key, nullptr, (CFStringRef) domain);
-    return CFPreferencesAppSynchronize((CFStringRef) domain) == true;
-}
-
-id CHIPGetDomainKeyList(NSString * domain)
-{
-    id value
-        = (id) CFBridgingRelease(CFPreferencesCopyKeyList((CFStringRef) domain, kCFPreferencesCurrentUser, kCFPreferencesAnyHost));
-    if (value) {
-        return value;
-    }
-    return nil;
-}
-
-BOOL CHIPClearAllDomain(NSString * domain)
-{
-
-    NSArray * allKeys = CHIPGetDomainKeyList(domain);
-#if LOG_DEBUG_PERSISTENT_STORAGE_DELEGATE
-    NSLog(@"Removing keys: %@ %@", allKeys, domain);
-#endif
-    for (id key in allKeys) {
-#if LOG_DEBUG_PERSISTENT_STORAGE_DELEGATE
-        NSLog(@"Removing key: %@", key);
-#endif
-        if (!MTRRemoveDomainValueForKey(domain, (NSString *) key)) {
-            return NO;
-        }
-    }
-    return YES;
-}
+@interface CHIPToolPersistentStorageDelegate ()
+@property (nonatomic, readonly) PreferencesStorage * storage;
+@end
 
 @implementation CHIPToolPersistentStorageDelegate
 
+- (instancetype)init
+{
+    if (!(self = [super init])) {
+        return nil;
+    }
+
+    _storage = [[PreferencesStorage alloc] initWithDomain:kDarwinFrameworkToolCertificatesDomain];
+    return self;
+}
+
 - (BOOL)deleteAllStorage
 {
-    return CHIPClearAllDomain(kCHIPToolDefaultsDomain);
+    return [_storage reset];
 }
 
 // MARK: CHIPPersistentStorageDelegate
 
 - (nullable NSData *)storageDataForKey:(NSString *)key
 {
-    NSData * value = MTRGetDomainValueForKey(kCHIPToolDefaultsDomain, key);
-#if LOG_DEBUG_PERSISTENT_STORAGE_DELEGATE
-    NSLog(@"CHIPPersistentStorageDelegate Get Value for Key: %@, value %@", key, value);
-#endif
-    return value;
+    return _storage[key];
 }
 
 - (BOOL)setStorageData:(NSData *)value forKey:(NSString *)key
 {
-    return MTRSetDomainValueForKey(kCHIPToolDefaultsDomain, key, value);
+    _storage[key] = value;
+    return YES;
 }
 
 - (BOOL)removeStorageDataForKey:(NSString *)key
 {
-    if (MTRGetDomainValueForKey(kCHIPToolDefaultsDomain, key) == nil) {
+    if (_storage[key] == nil) {
         return NO;
     }
-    return MTRRemoveDomainValueForKey(kCHIPToolDefaultsDomain, key);
+    _storage[key] = nil;
+    return YES;
 }
 
 @end
diff --git a/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.h b/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.h
index a58d2e8..f0ee3f8 100644
--- a/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.h
+++ b/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.h
@@ -1,4 +1,21 @@
-#include "CHIPCommandStorageDelegate.h"
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
 #import <Matter/Matter.h>
 #include <crypto/CHIPCryptoPAL.h>
 
@@ -8,7 +25,7 @@
 - (SecKeyRef)publicKey;
 - (CHIP_ERROR)Serialize:(chip::Crypto::P256SerializedKeypair &)output;
 - (CHIP_ERROR)Deserialize:(chip::Crypto::P256SerializedKeypair &)input;
-- (CHIP_ERROR)createOrLoadKeys:(CHIPToolPersistentStorageDelegate *)storage;
+- (CHIP_ERROR)createOrLoadKeys:(id)storage;
 - (NSData *)getIPK;
 
 @end
diff --git a/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.mm b/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.mm
index 4d6f53d..ce0ef58 100644
--- a/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.mm
+++ b/examples/darwin-framework-tool/commands/common/CHIPToolKeypair.mm
@@ -1,3 +1,21 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
 #import "CHIPToolKeypair.h"
 #import <Matter/Matter.h>
 #include <credentials/CHIPCert.h>
@@ -5,6 +23,9 @@
 #include <lib/asn1/ASN1.h>
 #include <stddef.h>
 
+#import "CHIPCommandStorageDelegate.h"
+#import "ControllerStorage.h"
+
 #define CHIPPlugin_CAKeyTag "com.apple.matter.commissioner.ca.issuer.id"
 #define Public_KeySize "256"
 
@@ -76,12 +97,10 @@
     return _ipk;
 }
 
-- (CHIP_ERROR)createOrLoadKeys:(CHIPToolPersistentStorageDelegate *)storage
+- (CHIP_ERROR)createOrLoadKeys:(id)storage
 {
     chip::ASN1::ASN1UniversalTime effectiveTime;
     chip::Crypto::P256SerializedKeypair serializedKey;
-    NSData * value;
-    CHIP_ERROR err = CHIP_NO_ERROR;
 
     // Initializing the default start validity to start of 2021. The default validity duration is 10 years.
     CHIP_ZERO_AT(effectiveTime);
@@ -90,8 +109,8 @@
     effectiveTime.Day = 1;
     ReturnErrorOnFailure(chip::Credentials::ASN1ToChipEpochTime(effectiveTime, _mNow));
 
-    value = [storage storageDataForKey:kOperationalCredentialsIssuerKeypairStorage];
-    err = [self initSerializedKeyFromValue:value serializedKey:serializedKey];
+    __auto_type * value = [self _getValueForKeyWithStorage:storage key:kOperationalCredentialsIssuerKeypairStorage];
+    __auto_type err = [self initSerializedKeyFromValue:value serializedKey:serializedKey];
 
     if (err != CHIP_NO_ERROR) {
         // Storage doesn't have an existing keypair. Let's create one and add it to the storage.
@@ -101,12 +120,12 @@
         ReturnErrorOnFailure([self Serialize:serializedKey]);
 
         NSData * valueData = [NSData dataWithBytes:serializedKey.Bytes() length:serializedKey.Length()];
-        [storage setStorageData:valueData forKey:kOperationalCredentialsIssuerKeypairStorage];
+        [self _setValueForKeyWithStorage:storage key:kOperationalCredentialsIssuerKeypairStorage value:valueData];
     } else {
         ReturnErrorOnFailure([self Deserialize:serializedKey]);
     }
 
-    NSData * ipk = [storage storageDataForKey:kOperationalCredentialsIPK];
+    NSData * ipk = [self _getValueForKeyWithStorage:storage key:kOperationalCredentialsIPK];
     if (ipk == nil) {
         err = CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND;
     }
@@ -116,7 +135,7 @@
         ReturnLogErrorOnFailure(chip::Crypto::DRBG_get_bytes(tempIPK, sizeof(tempIPK)));
 
         _ipk = [NSData dataWithBytes:tempIPK length:sizeof(tempIPK)];
-        [storage setStorageData:_ipk forKey:kOperationalCredentialsIPK];
+        [self _setValueForKeyWithStorage:storage key:kOperationalCredentialsIPK value:_ipk];
     } else {
         _ipk = ipk;
     }
@@ -124,6 +143,25 @@
     return CHIP_NO_ERROR;
 }
 
+- (NSData *)_getValueForKeyWithStorage:(id)storage key:(NSString *)key
+{
+    if ([storage isKindOfClass:[CHIPToolPersistentStorageDelegate class]]) {
+        return [storage storageDataForKey:key];
+    } else if ([storage isKindOfClass:[ControllerStorage class]]) {
+        return [storage valueForKey:key];
+    }
+    return nil;
+}
+
+- (void)_setValueForKeyWithStorage:(id)storage key:(NSString *)key value:(NSData *)value
+{
+    if ([storage isKindOfClass:[CHIPToolPersistentStorageDelegate class]]) {
+        [storage setStorageData:value forKey:key];
+    } else if ([storage isKindOfClass:[ControllerStorage class]]) {
+        [storage storeValue:value forKey:key];
+    }
+}
+
 - (CHIP_ERROR)initSerializedKeyFromValue:(NSData *)value serializedKey:(chip::Crypto::P256SerializedKeypair &)serializedKey
 {
     if (value == nil) {
diff --git a/examples/darwin-framework-tool/commands/common/CertificateIssuer.h b/examples/darwin-framework-tool/commands/common/CertificateIssuer.h
new file mode 100644
index 0000000..0034f28
--- /dev/null
+++ b/examples/darwin-framework-tool/commands/common/CertificateIssuer.h
@@ -0,0 +1,51 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#import <Matter/Matter.h>
+
+#import "ControllerStorage.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface CertificateIssuer : NSObject <MTROperationalCertificateIssuer>
+- (instancetype)init NS_UNAVAILABLE;
++ (instancetype)new NS_UNAVAILABLE;
+
++ (CertificateIssuer *)sharedInstance;
+
+- (void)startWithStorage:(id<MTRStorage>)storage
+                   error:(NSError * _Nullable __autoreleasing * _Nonnull)error;
+
+- (id<MTRKeypair>)issueOperationalKeypairWithControllerStorage:(ControllerStorage *)storage error:(NSError * _Nullable __autoreleasing * _Nonnull)error;
+
+- (MTRCertificateDERBytes _Nullable)issueOperationalCertificateForNodeID:(NSNumber *)nodeID
+                                                                fabricID:(NSNumber *)fabricID
+                                                               publicKey:(SecKeyRef)publicKey
+                                                                   error:(NSError * _Nullable __autoreleasing * _Nonnull)error;
+
+@property (nonatomic, readonly) MTRCertificateDERBytes rootCertificate;
+@property (nonatomic, readonly) id<MTRKeypair> signingKey;
+@property (nonatomic, readonly) NSData * ipk;
+
+@property (nonatomic, nullable) NSNumber * fabricID;
+@property (nonatomic, nullable) NSNumber * nextNodeID;
+@property (nonatomic, readonly) BOOL shouldSkipAttestationCertificateValidation;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/examples/darwin-framework-tool/commands/common/CertificateIssuer.mm b/examples/darwin-framework-tool/commands/common/CertificateIssuer.mm
new file mode 100644
index 0000000..bd12878
--- /dev/null
+++ b/examples/darwin-framework-tool/commands/common/CertificateIssuer.mm
@@ -0,0 +1,158 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#import "CertificateIssuer.h"
+#import "CHIPToolKeypair.h"
+
+#include <lib/support/logging/CHIPLogging.h>
+
+@interface CertificateIssuer ()
+- (MTRCertificateDERBytes _Nullable)issueOperationalCertificateForNodeID:(NSNumber *)nodeID
+                                                                fabricID:(NSNumber *)fabricID
+                                                               publicKey:(SecKeyRef)publicKey
+                                                                   error:(NSError * _Nullable __autoreleasing * _Nonnull)error;
+@end
+
+@implementation CertificateIssuer
+
++ (CertificateIssuer *)sharedInstance
+{
+    static CertificateIssuer * certificateIssuer = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        certificateIssuer = [[CertificateIssuer alloc] init];
+    });
+    return certificateIssuer;
+}
+
+- (instancetype)init
+{
+    if (!(self = [super init])) {
+        return nil;
+    }
+
+    _rootCertificate = nil;
+    _ipk = nil;
+    _signingKey = nil;
+    _fabricID = nil;
+    _nextNodeID = nil;
+    _shouldSkipAttestationCertificateValidation = NO;
+
+    return self;
+}
+
+- (void)startWithStorage:(id<MTRStorage>)storage
+                   error:(NSError * _Nullable __autoreleasing * _Nonnull)error
+{
+    __auto_type * signingKey = [[CHIPToolKeypair alloc] init];
+
+    __auto_type err = [signingKey createOrLoadKeys:storage];
+    if (CHIP_NO_ERROR != err) {
+        *error = [NSError errorWithDomain:@"Error" code:0 userInfo:@{ @"reason" : @"Error creating or loading keys" }];
+        return;
+    }
+
+    __auto_type * rootCertificate = [MTRCertificates createRootCertificate:signingKey issuerID:nil fabricID:nil error:error];
+    if (nil == rootCertificate) {
+        *error = [NSError errorWithDomain:@"Error" code:0 userInfo:@{ @"reason" : @"Error creating root certificate" }];
+        return;
+    }
+
+    _rootCertificate = rootCertificate;
+    _signingKey = signingKey;
+    _ipk = [signingKey getIPK];
+}
+
+- (id<MTRKeypair>)issueOperationalKeypairWithControllerStorage:(ControllerStorage *)storage error:(NSError * _Nullable __autoreleasing * _Nonnull)error
+{
+    __auto_type * keypair = [[CHIPToolKeypair alloc] init];
+
+    __auto_type err = [keypair createOrLoadKeys:storage];
+    if (CHIP_NO_ERROR != err) {
+        *error = [NSError errorWithDomain:@"Error" code:0 userInfo:@{ @"reason" : @"Error creating or loading keys" }];
+        return nil;
+    }
+
+    return keypair;
+}
+
+- (void)issueOperationalCertificateForRequest:(MTROperationalCSRInfo *)csrInfo
+                              attestationInfo:(MTRDeviceAttestationInfo *)attestationInfo
+                                   controller:(MTRDeviceController *)controller
+                                   completion:(void (^)(MTROperationalCertificateChain * _Nullable info,
+                                                  NSError * _Nullable error))completion
+{
+    NSError * error = nil;
+
+    if (self.nextNodeID == nil) {
+        error = [NSError errorWithDomain:@"Error" code:0 userInfo:@{ @"reason" : @"nextNodeID is nil" }];
+        completion(nil, error);
+        return;
+    }
+
+    __auto_type * csr = csrInfo.csr;
+    __auto_type * rawPublicKey = [MTRCertificates publicKeyFromCSR:csr error:&error];
+    if (error != nil) {
+        completion(nil, error);
+        return;
+    }
+
+    NSDictionary * attributes = @{
+        (__bridge NSString *) kSecAttrKeyType : (__bridge NSString *) kSecAttrKeyTypeECSECPrimeRandom,
+        (__bridge NSString *) kSecAttrKeyClass : (__bridge NSString *) kSecAttrKeyClassPublic
+    };
+    CFErrorRef keyCreationError = NULL;
+    SecKeyRef publicKey
+        = SecKeyCreateWithData((__bridge CFDataRef) rawPublicKey, (__bridge CFDictionaryRef) attributes, &keyCreationError);
+
+    __auto_type * operationalCert = [self issueOperationalCertificateForNodeID:self.nextNodeID
+                                                                      fabricID:self.fabricID
+                                                                     publicKey:publicKey
+                                                                         error:&error];
+
+    // Release no-longer-needed key before we do anything else.
+    CFRelease(publicKey);
+
+    if (error != nil) {
+        completion(nil, error);
+        return;
+    }
+
+    __auto_type * certChain = [[MTROperationalCertificateChain alloc] initWithOperationalCertificate:operationalCert
+                                                                             intermediateCertificate:nil
+                                                                                     rootCertificate:self.rootCertificate
+                                                                                        adminSubject:nil];
+    completion(certChain, nil);
+}
+
+- (MTRCertificateDERBytes _Nullable)issueOperationalCertificateForNodeID:(NSNumber *)nodeID
+                                                                fabricID:(NSNumber *)fabricID
+                                                               publicKey:(SecKeyRef)publicKey
+                                                                   error:(NSError * _Nullable __autoreleasing * _Nonnull)error
+{
+    __auto_type * operationalCert = [MTRCertificates createOperationalCertificate:self.signingKey
+                                                               signingCertificate:self.rootCertificate
+                                                             operationalPublicKey:publicKey
+                                                                         fabricID:fabricID
+                                                                           nodeID:nodeID
+                                                            caseAuthenticatedTags:nil
+                                                                            error:error];
+    return operationalCert;
+}
+
+@end
diff --git a/examples/darwin-framework-tool/commands/common/ControllerStorage.h b/examples/darwin-framework-tool/commands/common/ControllerStorage.h
new file mode 100644
index 0000000..f70081f
--- /dev/null
+++ b/examples/darwin-framework-tool/commands/common/ControllerStorage.h
@@ -0,0 +1,49 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#import <Matter/Matter.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSString * const kDarwinFrameworkToolControllerDomain;
+
+@interface ControllerStorage : NSObject <MTRDeviceControllerStorageDelegate>
+- (instancetype)initWithControllerID:(NSUUID *)controllerID;
+@property (nonatomic, readonly) NSUUID * controllerID;
+
+- (nullable id<NSSecureCoding>)controller:(MTRDeviceController *)controller
+                              valueForKey:(NSString *)key
+                            securityLevel:(MTRStorageSecurityLevel)securityLevel
+                              sharingType:(MTRStorageSharingType)sharingType;
+- (BOOL)controller:(MTRDeviceController *)controller
+        storeValue:(id<NSSecureCoding>)value
+            forKey:(NSString *)key
+     securityLevel:(MTRStorageSecurityLevel)securityLevel
+       sharingType:(MTRStorageSharingType)sharingType;
+- (BOOL)controller:(MTRDeviceController *)controller
+    removeValueForKey:(NSString *)key
+        securityLevel:(MTRStorageSecurityLevel)securityLevel
+          sharingType:(MTRStorageSharingType)sharingType;
+- (NSDictionary<NSString *, id<NSSecureCoding>> *)valuesForController:(MTRDeviceController *)controller securityLevel:(MTRStorageSecurityLevel)securityLevel sharingType:(MTRStorageSharingType)sharingType;
+- (BOOL)controller:(MTRDeviceController *)controller storeValues:(NSDictionary<NSString *, id<NSSecureCoding>> *)values securityLevel:(MTRStorageSecurityLevel)securityLevel sharingType:(MTRStorageSharingType)sharingType;
+
+- (NSData *)valueForKey:(NSString *)key;
+- (void)storeValue:(NSData *)value forKey:key;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/examples/darwin-framework-tool/commands/common/ControllerStorage.mm b/examples/darwin-framework-tool/commands/common/ControllerStorage.mm
new file mode 100644
index 0000000..058f00a
--- /dev/null
+++ b/examples/darwin-framework-tool/commands/common/ControllerStorage.mm
@@ -0,0 +1,160 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#import "ControllerStorage.h"
+#import "PreferencesStorage.h"
+
+#include <lib/support/logging/CHIPLogging.h>
+
+NSString * const kDarwinFrameworkToolControllerDomain = @"com.apple.darwin-framework-tool.controller";
+
+@interface ControllerStorage ()
+@property (nonatomic, readonly) PreferencesStorage * storage;
+@property (nonatomic, readonly) NSString * keyScopingPrefix;
+
+- (NSString *)_keyToControllerScopedKey:(NSString *)key;
+- (NSString *)_controllerScopedKeyToKey:(NSString *)controllerKey;
+- (BOOL)_isControllerScopedKey:(NSString *)controllerKey;
+@end
+
+@implementation ControllerStorage
+- (instancetype)initWithControllerID:(NSUUID *)controllerID
+{
+    if (!(self = [super init])) {
+        return nil;
+    }
+
+    _storage = [[PreferencesStorage alloc] initWithDomain:kDarwinFrameworkToolControllerDomain];
+    _controllerID = controllerID;
+    _keyScopingPrefix = [NSString stringWithFormat:@"%@/", [_controllerID UUIDString]];
+    return self;
+}
+
+- (nullable id<NSSecureCoding>)controller:(MTRDeviceController *)controller
+                              valueForKey:(NSString *)key
+                            securityLevel:(MTRStorageSecurityLevel)securityLevel
+                              sharingType:(MTRStorageSharingType)sharingType
+{
+#ifdef LOG_DEBUG_CONTROLLER_STORAGE
+    ChipLogError(chipTool, "Controller(%@) Storage - Get value for %@", controller, key);
+#endif // LOG_DEBUG_CONTROLLER_STORAGE
+
+    __auto_type * controllerKey = [self _keyToControllerScopedKey:key];
+    __auto_type * data = self.storage[controllerKey];
+    if (data == nil) {
+        return data;
+    }
+
+    NSError * error;
+    id value = [NSKeyedUnarchiver unarchivedObjectOfClasses:MTRDeviceControllerStorageClasses() fromData:data error:&error];
+    return value;
+}
+
+- (BOOL)controller:(MTRDeviceController *)controller
+        storeValue:(id<NSSecureCoding>)value
+            forKey:(NSString *)key
+     securityLevel:(MTRStorageSecurityLevel)securityLevel
+       sharingType:(MTRStorageSharingType)sharingType
+{
+#ifdef LOG_DEBUG_CONTROLLER_STORAGE
+    ChipLogError(chipTool, "Controller(%@) Storage - Set value for %@", controller, key);
+#endif // LOG_DEBUG_CONTROLLER_STORAGE
+    NSError * error;
+    NSData * data = [NSKeyedArchiver archivedDataWithRootObject:value requiringSecureCoding:YES error:&error];
+
+    __auto_type * controllerKey = [self _keyToControllerScopedKey:key];
+    self.storage[controllerKey] = data;
+    return YES;
+}
+
+- (BOOL)controller:(MTRDeviceController *)controller
+    removeValueForKey:(NSString *)key
+        securityLevel:(MTRStorageSecurityLevel)securityLevel
+          sharingType:(MTRStorageSharingType)sharingType
+{
+#ifdef LOG_DEBUG_CONTROLLER_STORAGE
+    ChipLogError(chipTool, "Controller(%@) Storage - Remove value for %@", controller, key);
+#endif // LOG_DEBUG_CONTROLLER_STORAGE
+
+    __auto_type * controllerKey = [self _keyToControllerScopedKey:key];
+    self.storage[controllerKey] = nil;
+    return YES;
+}
+
+- (NSDictionary<NSString *, id<NSSecureCoding>> *)valuesForController:(MTRDeviceController *)controller securityLevel:(MTRStorageSecurityLevel)securityLevel sharingType:(MTRStorageSharingType)sharingType
+{
+#ifdef LOG_DEBUG_CONTROLLER_STORAGE
+    ChipLogError(chipTool, "Controller(%@) Storage - Get all values", controller);
+#endif // LOG_DEBUG_CONTROLLER_STORAGE
+
+    NSMutableDictionary * valuesToReturn = [NSMutableDictionary dictionary];
+    for (NSString * controllerKey in self.storage) {
+        if (![self _isControllerScopedKey:controllerKey]) {
+            continue;
+        }
+        __auto_type * key = [self _controllerScopedKeyToKey:controllerKey];
+        valuesToReturn[key] = [self controller:controller valueForKey:key securityLevel:securityLevel sharingType:sharingType];
+    }
+
+    if (!valuesToReturn.count) {
+        return nil;
+    }
+
+    return valuesToReturn;
+}
+
+- (BOOL)controller:(MTRDeviceController *)controller storeValues:(NSDictionary<NSString *, id<NSSecureCoding>> *)values securityLevel:(MTRStorageSecurityLevel)securityLevel sharingType:(MTRStorageSharingType)sharingType
+{
+#ifdef LOG_DEBUG_CONTROLLER_STORAGE
+    ChipLogError(chipTool, "Controller(%@) Storage - store values", controller);
+#endif // LOG_DEBUG_CONTROLLER_STORAGE
+
+    for (NSString * key in values) {
+        [self controller:controller storeValue:values[key] forKey:key securityLevel:securityLevel sharingType:sharingType];
+    }
+
+    return YES;
+}
+
+- (NSData *)valueForKey:(NSString *)key
+{
+    __auto_type * controllerKey = [self _keyToControllerScopedKey:key];
+    return self.storage[controllerKey];
+}
+
+- (void)storeValue:(NSData *)value forKey:key
+{
+    __auto_type * controllerKey = [self _keyToControllerScopedKey:key];
+    self.storage[controllerKey] = value;
+}
+
+- (NSString *)_keyToControllerScopedKey:(NSString *)key
+{
+    return [NSString stringWithFormat:@"%@%@", _keyScopingPrefix, key];
+}
+
+- (NSString *)_controllerScopedKeyToKey:(NSString *)controllerKey
+{
+    return [controllerKey substringFromIndex:_keyScopingPrefix.length];
+}
+
+- (BOOL)_isControllerScopedKey:(NSString *)controllerKey
+{
+    return [controllerKey hasPrefix:_keyScopingPrefix];
+}
+@end
diff --git a/examples/darwin-framework-tool/commands/common/PreferencesStorage.h b/examples/darwin-framework-tool/commands/common/PreferencesStorage.h
new file mode 100644
index 0000000..85c1203
--- /dev/null
+++ b/examples/darwin-framework-tool/commands/common/PreferencesStorage.h
@@ -0,0 +1,32 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface PreferencesStorage : NSObject <NSFastEnumeration>
+
+@property (nonatomic, strong) NSString * domain;
+
+- (instancetype)initWithDomain:(NSString *)domain;
+- (NSData *)objectForKeyedSubscript:(NSString *)key;
+- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key;
+- (NSArray<NSString *> *)allKeys;
+- (bool)reset;
+- (void)print;
+
+@end
diff --git a/examples/darwin-framework-tool/commands/common/PreferencesStorage.mm b/examples/darwin-framework-tool/commands/common/PreferencesStorage.mm
new file mode 100644
index 0000000..e7ddba1
--- /dev/null
+++ b/examples/darwin-framework-tool/commands/common/PreferencesStorage.mm
@@ -0,0 +1,110 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#import "PreferencesStorage.h"
+
+@implementation PreferencesStorage
+- (instancetype)initWithDomain:(NSString *)domain
+{
+    self = [super init];
+    if (self) {
+        _domain = domain;
+    }
+
+    return self;
+}
+
+- (NSData *)objectForKeyedSubscript:(NSString *)key
+{
+    __auto_type domainRef = (__bridge CFStringRef) self.domain;
+    __auto_type keyRef = (__bridge CFStringRef) key;
+    __auto_type value = CFPreferencesCopyAppValue(keyRef, domainRef);
+    if (value) {
+        id obj = (__bridge_transfer id) value;
+        return obj;
+    }
+    return nil;
+}
+
+- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key
+{
+    __auto_type domainRef = (__bridge CFStringRef) self.domain;
+    __auto_type keyRef = (__bridge CFStringRef) key;
+    __auto_type value = (__bridge CFPropertyListRef) obj;
+
+    CFPreferencesSetAppValue(keyRef, value, domainRef);
+    CFPreferencesAppSynchronize(domainRef);
+}
+
+- (NSArray<NSString *> *)allKeys
+{
+    __auto_type domainRef = (__bridge CFStringRef) self.domain;
+    __auto_type keys = CFPreferencesCopyKeyList(domainRef, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
+
+    if (!keys) {
+        return @[];
+    }
+
+    return (__bridge_transfer NSArray *) keys;
+}
+
+- (bool)reset
+{
+    __auto_type * keys = [self allKeys];
+    __auto_type domainRef = (__bridge CFStringRef) self.domain;
+
+    for (NSString * key in keys) {
+        __auto_type keyRef = (__bridge CFStringRef) key;
+        CFPreferencesSetAppValue(keyRef, NULL, domainRef);
+    }
+
+    return CFPreferencesAppSynchronize(domainRef);
+}
+
+- (void)print
+{
+    NSLog(@"%@:", self.domain);
+    NSArray<NSString *> * keys = [self allKeys];
+    for (NSString * key in keys) {
+        __auto_type * data = [self objectForKeyedSubscript:key];
+        NSLog(@" * %@: %@", key, data);
+    }
+}
+
+#pragma mark - NSFastEnumeration
+
+- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id _Nullable __unsafe_unretained[])buffer count:(NSUInteger)len
+{
+    __auto_type * keys = [self allKeys];
+    if (state->state >= keys.count) {
+        return 0;
+    }
+
+    state->itemsPtr = buffer;
+    state->mutationsPtr = &state->extra[0];
+
+    NSUInteger count = 0;
+    while (state->state < keys.count && count < len) {
+        buffer[count] = keys[state->state];
+        state->state++;
+        count++;
+    }
+    return count;
+}
+
+@end
diff --git a/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm b/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm
index 1a06081..3ec4aee 100644
--- a/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm
+++ b/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm
@@ -19,6 +19,7 @@
 #import <Matter/Matter.h>
 
 #include "../common/CHIPCommandBridge.h"
+#include "../common/CertificateIssuer.h"
 #include "DeviceControllerDelegateBridge.h"
 #include "PairingCommandBridge.h"
 #include <lib/support/logging/CHIPLogging.h>
@@ -52,6 +53,12 @@
     [deviceControllerDelegate setCommandBridge:this];
     [deviceControllerDelegate setDeviceID:mNodeId];
 
+    // With per-controller storage, the certificate issuer creates the operational certificate.
+    // When using shared storage, this step is a no-op.
+    auto * certificateIssuer = [CertificateIssuer sharedInstance];
+    certificateIssuer.nextNodeID = @(mNodeId);
+    certificateIssuer.fabricID = CurrentCommissionerFabricId();
+
     if (mCommissioningType != CommissioningType::None) {
         MTRCommissioningParameters * params = [[MTRCommissioningParameters alloc] init];
         switch (mCommissioningType) {
diff --git a/examples/darwin-framework-tool/commands/storage/Commands.h b/examples/darwin-framework-tool/commands/storage/Commands.h
index 6553427..983fb22 100644
--- a/examples/darwin-framework-tool/commands/storage/Commands.h
+++ b/examples/darwin-framework-tool/commands/storage/Commands.h
@@ -25,7 +25,10 @@
 {
     const char * clusterName = "storage";
 
-    commands_list clusterCommands = { make_unique<StorageClearAll>() };
+    commands_list clusterCommands = {
+        make_unique<StorageViewAll>(),  //
+        make_unique<StorageClearAll>(), //
+    };
 
     commands.RegisterCommandSet(clusterName, clusterCommands,
                                 "Commands for managing persistent data stored by darwin-framework-tool.");
diff --git a/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.h b/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.h
index c2abddf..8c3ed69 100644
--- a/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.h
+++ b/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.h
@@ -29,3 +29,11 @@
 
     CHIP_ERROR Run() override;
 };
+
+class StorageViewAll : public Command
+{
+public:
+    StorageViewAll() : Command("view-all") {}
+
+    CHIP_ERROR Run() override;
+};
diff --git a/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.mm b/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.mm
index e0c1b18..007d282 100644
--- a/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.mm
+++ b/examples/darwin-framework-tool/commands/storage/StorageManagementCommand.mm
@@ -17,18 +17,43 @@
  */
 
 #include "../common/CHIPCommandStorageDelegate.h"
+#include "../common/ControllerStorage.h"
+#include "../common/PreferencesStorage.h"
 
 #include "StorageManagementCommand.h"
 
 #import <Matter/Matter.h>
 
-static CHIPToolPersistentStorageDelegate * storage = nil;
+namespace {
+NSArray<NSString *> * GetDomains()
+{
+    __auto_type * domains = @[
+        kDarwinFrameworkToolCertificatesDomain,
+        kDarwinFrameworkToolControllerDomain
+    ];
+
+    return domains;
+}
+}
 
 CHIP_ERROR StorageClearAll::Run()
 {
-    storage = [[CHIPToolPersistentStorageDelegate alloc] init];
-    if (![storage deleteAllStorage]) {
-        return CHIP_ERROR_INTERNAL;
+    __auto_type * domains = GetDomains();
+    for (NSString * domain in domains) {
+        __auto_type * storage = [[PreferencesStorage alloc] initWithDomain:domain];
+        VerifyOrReturnError([storage reset], CHIP_ERROR_INTERNAL);
     }
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR StorageViewAll::Run()
+{
+    __auto_type * domains = GetDomains();
+    for (NSString * domain in domains) {
+        __auto_type * storage = [[PreferencesStorage alloc] initWithDomain:domain];
+        [storage print];
+    }
+
     return CHIP_NO_ERROR;
 }
diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
index 173fb7b..e191785 100644
--- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
+++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
@@ -334,6 +334,12 @@
 		B43B39EC2CB859A5006AA284 /* DumpMemoryGraphCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = B43B39E52CB859A5006AA284 /* DumpMemoryGraphCommand.h */; };
 		B43B39ED2CB859A5006AA284 /* Commands.h in Headers */ = {isa = PBXBuildFile; fileRef = B43B39E42CB859A5006AA284 /* Commands.h */; };
 		B43B39EE2CB859A5006AA284 /* LeaksTool.h in Headers */ = {isa = PBXBuildFile; fileRef = B43B39E72CB859A5006AA284 /* LeaksTool.h */; };
+		B43B39F52CB99090006AA284 /* ControllerStorage.mm in Sources */ = {isa = PBXBuildFile; fileRef = B43B39F22CB99090006AA284 /* ControllerStorage.mm */; };
+		B43B39F62CB99090006AA284 /* CertificateIssuer.mm in Sources */ = {isa = PBXBuildFile; fileRef = B43B39F02CB99090006AA284 /* CertificateIssuer.mm */; };
+		B43B39F72CB99090006AA284 /* PreferencesStorage.mm in Sources */ = {isa = PBXBuildFile; fileRef = B43B39F42CB99090006AA284 /* PreferencesStorage.mm */; };
+		B43B39F82CB99090006AA284 /* CertificateIssuer.h in Headers */ = {isa = PBXBuildFile; fileRef = B43B39EF2CB99090006AA284 /* CertificateIssuer.h */; };
+		B43B39F92CB99090006AA284 /* PreferencesStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = B43B39F32CB99090006AA284 /* PreferencesStorage.h */; };
+		B43B39FA2CB99090006AA284 /* ControllerStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = B43B39F12CB99090006AA284 /* ControllerStorage.h */; };
 		B45373AA2A9FE73400807602 /* WebSocketServer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B45373A92A9FE73400807602 /* WebSocketServer.cpp */; };
 		B45373BD2A9FEA9100807602 /* service.c in Sources */ = {isa = PBXBuildFile; fileRef = B45373B22A9FEA9000807602 /* service.c */; settings = {COMPILER_FLAGS = "-Wno-error -Wno-unreachable-code -Wno-conversion -Wno-format-nonliteral"; }; };
 		B45373BE2A9FEA9100807602 /* network.c in Sources */ = {isa = PBXBuildFile; fileRef = B45373B32A9FEA9000807602 /* network.c */; settings = {COMPILER_FLAGS = "-Wno-error -Wno-unreachable-code -Wno-conversion -Wno-format-nonliteral"; }; };
@@ -785,6 +791,12 @@
 		B43B39E62CB859A5006AA284 /* DumpMemoryGraphCommand.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = DumpMemoryGraphCommand.mm; sourceTree = "<group>"; };
 		B43B39E72CB859A5006AA284 /* LeaksTool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LeaksTool.h; sourceTree = "<group>"; };
 		B43B39E82CB859A5006AA284 /* LeaksTool.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = LeaksTool.mm; sourceTree = "<group>"; };
+		B43B39EF2CB99090006AA284 /* CertificateIssuer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CertificateIssuer.h; sourceTree = "<group>"; };
+		B43B39F02CB99090006AA284 /* CertificateIssuer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CertificateIssuer.mm; sourceTree = "<group>"; };
+		B43B39F12CB99090006AA284 /* ControllerStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllerStorage.h; sourceTree = "<group>"; };
+		B43B39F22CB99090006AA284 /* ControllerStorage.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ControllerStorage.mm; sourceTree = "<group>"; };
+		B43B39F32CB99090006AA284 /* PreferencesStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PreferencesStorage.h; sourceTree = "<group>"; };
+		B43B39F42CB99090006AA284 /* PreferencesStorage.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PreferencesStorage.mm; sourceTree = "<group>"; };
 		B45373A92A9FE73400807602 /* WebSocketServer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WebSocketServer.cpp; sourceTree = "<group>"; };
 		B45373B22A9FEA9000807602 /* service.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = service.c; path = "repo/lib/core-net/service.c"; sourceTree = "<group>"; };
 		B45373B32A9FEA9000807602 /* network.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = network.c; path = "repo/lib/core-net/network.c"; sourceTree = "<group>"; };
@@ -987,6 +999,12 @@
 		037C3D9B2991BD4F00B7EEE2 /* common */ = {
 			isa = PBXGroup;
 			children = (
+				B43B39EF2CB99090006AA284 /* CertificateIssuer.h */,
+				B43B39F02CB99090006AA284 /* CertificateIssuer.mm */,
+				B43B39F12CB99090006AA284 /* ControllerStorage.h */,
+				B43B39F22CB99090006AA284 /* ControllerStorage.mm */,
+				B43B39F32CB99090006AA284 /* PreferencesStorage.h */,
+				B43B39F42CB99090006AA284 /* PreferencesStorage.mm */,
 				B4E262132AA0C7A300DBA5BC /* RemoteDataModelLogger.h */,
 				B4E262122AA0C7A300DBA5BC /* RemoteDataModelLogger.mm */,
 				037C3D9C2991BD4F00B7EEE2 /* CHIPCommandBridge.mm */,
@@ -1632,6 +1650,9 @@
 				B4FCD5712B603A6300832859 /* DownloadLogCommand.h in Headers */,
 				037C3DC32991BD5100B7EEE2 /* Commands.h in Headers */,
 				B4F773CA2CB54B61008C6B23 /* LeakChecker.h in Headers */,
+				B43B39F82CB99090006AA284 /* CertificateIssuer.h in Headers */,
+				B43B39F92CB99090006AA284 /* PreferencesStorage.h in Headers */,
+				B43B39FA2CB99090006AA284 /* ControllerStorage.h in Headers */,
 				037C3DB82991BD5000B7EEE2 /* ClusterCommandBridge.h in Headers */,
 				037C3DC82991BD5100B7EEE2 /* CHIPToolKeypair.h in Headers */,
 				037C3DB52991BD5000B7EEE2 /* WriteAttributeCommandBridge.h in Headers */,
@@ -1971,6 +1992,9 @@
 				B45373DF2A9FEB6F00807602 /* system.c in Sources */,
 				B45373FC2A9FEC4F00807602 /* unix-caps.c in Sources */,
 				B45373FE2A9FEC4F00807602 /* unix-fds.c in Sources */,
+				B43B39F52CB99090006AA284 /* ControllerStorage.mm in Sources */,
+				B43B39F62CB99090006AA284 /* CertificateIssuer.mm in Sources */,
+				B43B39F72CB99090006AA284 /* PreferencesStorage.mm in Sources */,
 				B43B39EA2CB859A5006AA284 /* DumpMemoryGraphCommand.mm in Sources */,
 				B43B39EB2CB859A5006AA284 /* LeaksTool.mm in Sources */,
 				B45374002A9FEC4F00807602 /* unix-init.c in Sources */,