Add the ability to read extra attributes during commissioning to Matter.framework. (#40280)

* Add the ability to read extra attributes during commissioning to Matter.framework.

* Address review comments.

* Address review comments.
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
index 0f6e939..37634ca 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
@@ -2762,7 +2762,7 @@
 @end
 
 @implementation MTRAttributePath
-- (instancetype)initWithPath:(const ConcreteDataAttributePath &)path
+- (instancetype)initWithPath:(const ConcreteAttributePath &)path
 {
     if (self = [super initWithPath:path]) {
         _attribute = @(path.mAttributeId);
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
index 4cf02eb..cc9dd9a 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
@@ -223,7 +223,7 @@
 @end
 
 @interface MTRAttributePath ()
-- (instancetype)initWithPath:(const chip::app::ConcreteDataAttributePath &)path;
+- (instancetype)initWithPath:(const chip::app::ConcreteAttributePath &)path;
 @end
 
 @interface MTREventPath ()
diff --git a/src/darwin/Framework/CHIP/MTRCommissioneeInfo.h b/src/darwin/Framework/CHIP/MTRCommissioneeInfo.h
index d2e6bd6..701027a 100644
--- a/src/darwin/Framework/CHIP/MTRCommissioneeInfo.h
+++ b/src/darwin/Framework/CHIP/MTRCommissioneeInfo.h
@@ -14,6 +14,7 @@
  *    limitations under the License.
  */
 
+#import <Matter/MTRBaseDevice.h> // For MTRAttributePath
 #import <Matter/MTRDefines.h>
 
 @class MTRProductIdentity;
@@ -47,6 +48,12 @@
  */
 @property (nonatomic, copy, readonly, nullable) MTREndpointInfo * rootEndpoint;
 
+/**
+ * Attributes that were read from the commissionee.  Will be present only if
+ * extraAttributesToRead is set on MTRCommissioningParameters.
+ */
+@property (nonatomic, copy, readonly, nullable) NSDictionary<MTRAttributePath *, NSDictionary<NSString *, id> *> * attributes MTR_PROVISIONALLY_AVAILABLE;
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/MTRCommissioneeInfo.mm b/src/darwin/Framework/CHIP/MTRCommissioneeInfo.mm
index 0456976..027bbb8 100644
--- a/src/darwin/Framework/CHIP/MTRCommissioneeInfo.mm
+++ b/src/darwin/Framework/CHIP/MTRCommissioneeInfo.mm
@@ -16,26 +16,90 @@
 
 #import "MTRCommissioneeInfo_Internal.h"
 
+#import "MTRBaseDevice.h"
+#import "MTRBaseDevice_Internal.h"
 #import "MTRDefines_Internal.h"
+#import "MTRDeviceDataValidation.h"
 #import "MTREndpointInfo_Internal.h"
+#import "MTRLogging_Internal.h"
 #import "MTRProductIdentity.h"
 #import "MTRUtilities.h"
 
+#include <app/AttributePathParams.h>
+#include <lib/core/TLVReader.h>
+
 NS_ASSUME_NONNULL_BEGIN
 
 MTR_DIRECT_MEMBERS
 @implementation MTRCommissioneeInfo
 
-- (instancetype)initWithCommissioningInfo:(const chip::Controller::ReadCommissioningInfo &)info
+- (instancetype)initWithCommissioningInfo:(const chip::Controller::ReadCommissioningInfo &)info commissioningParameters:(MTRCommissioningParameters *)commissioningParameters
 {
     self = [super init];
     _productIdentity = [[MTRProductIdentity alloc] initWithVendorID:@(info.basic.vendorId) productID:@(info.basic.productId)];
 
-    // TODO: We should probably hold onto our MTRCommissioningParameters so we can look at `readEndpointInformation`
-    // instead of just reading whatever Descriptor cluster information happens to be in the cache.
-    auto * endpoints = [MTREndpointInfo endpointsFromAttributeCache:info.attributes];
-    if (endpoints.count > 0) {
-        _endpointsById = endpoints;
+    if (commissioningParameters.readEndpointInformation) {
+        auto * endpoints = [MTREndpointInfo endpointsFromAttributeCache:info.attributes];
+        if (endpoints.count > 0) {
+            _endpointsById = endpoints;
+        }
+    }
+
+    if (commissioningParameters.extraAttributesToRead != nil && info.attributes != nullptr) {
+        NSMutableDictionary<MTRAttributePath *, NSDictionary<NSString *, id> *> * attributes = [[NSMutableDictionary alloc] init];
+
+        std::vector<chip::app::AttributePathParams> requestPaths;
+        for (MTRAttributeRequestPath * requestPath in commissioningParameters.extraAttributesToRead) {
+            [requestPath convertToAttributePathParams:requestPaths.emplace_back()];
+        }
+
+        info.attributes->ForEachAttribute([&](const chip::app::ConcreteAttributePath & path) -> CHIP_ERROR {
+            // Only grab paths that are included in extraAttributesToRead so that
+            // API consumers don't develop dependencies on implementation details
+            // (like which other attributes we happen to read).
+
+            // TODO: This means API consumers might duplicate attribute reads we
+            // already do.  We should either offer guarantees about things like
+            // "network commissioning feature maps" that will always be present, or
+            // perhaps dedup in some way under the hood when issuing the
+            // reads.
+
+            // This is unfortunately not very efficient; if we have a lot of
+            // paths we may need a better way to do this.
+            bool isRequestedPath = false;
+            for (auto & requestPath : requestPaths) {
+                if (!requestPath.IsAttributePathSupersetOf(path)) {
+                    continue;
+                }
+
+                isRequestedPath = true;
+                break;
+            }
+
+            if (!isRequestedPath) {
+                // Skip it.
+                return CHIP_NO_ERROR;
+            }
+
+            chip::TLV::TLVReader reader;
+            CHIP_ERROR err = info.attributes->Get(path, reader);
+            if (err != CHIP_NO_ERROR) {
+                // We actually got an error, not data.  Just skip this path.
+                return CHIP_NO_ERROR;
+            }
+
+            auto value = MTRDecodeDataValueDictionaryFromCHIPTLV(&reader);
+            if (value == nil) {
+                // Decode errors can happen (e.g. invalid TLV); just skip this path.
+                return CHIP_NO_ERROR;
+            }
+
+            auto * mtrPath = [[MTRAttributePath alloc] initWithPath:path];
+            attributes[mtrPath] = value;
+            return CHIP_NO_ERROR;
+        });
+
+        _attributes = attributes;
     }
 
     return self;
@@ -43,6 +107,7 @@
 
 static NSString * const sProductIdentityCodingKey = @"pi";
 static NSString * const sEndpointsCodingKey = @"ep";
+static NSString * const sAttributesCodingKey = @"at";
 
 - (nullable instancetype)initWithCoder:(NSCoder *)coder
 {
@@ -52,6 +117,34 @@
     _endpointsById = [coder decodeDictionaryWithKeysOfClass:NSNumber.class
                                              objectsOfClass:MTREndpointInfo.class
                                                      forKey:sEndpointsCodingKey];
+
+    // TODO: Can we do better about duplicating the set of classes that appear
+    // in data-values?  We have this set in a bunch of places....  But here we need
+    // not just those, but also MTRAttributePath.
+    static NSSet * const sAttributeClasses = [NSSet setWithObjects:NSDictionary.class, NSArray.class, NSData.class, NSString.class, NSNumber.class, MTRAttributePath.class, nil];
+    _attributes = [coder decodeObjectOfClasses:sAttributeClasses forKey:sAttributesCodingKey];
+
+    if (_attributes != nil) {
+        // Check that the right types are in the right places.
+        if (![_attributes isKindOfClass:NSDictionary.class]) {
+            MTR_LOG_ERROR("MTRCommissioneeInfo decoding: attributes are not a dictionary: %@", _attributes);
+            return nil;
+        }
+
+        for (id key in _attributes) {
+            if (![key isKindOfClass:MTRAttributePath.class]) {
+                MTR_LOG_ERROR("MTRCommissioneeInfo decoding: expected MTRAttributePath but found %@", key);
+                return nil;
+            }
+
+            id value = _attributes[key];
+            if (![value isKindOfClass:NSDictionary.class] || !MTRDataValueDictionaryIsWellFormed(value)) {
+                MTR_LOG_ERROR("MTRCommissioneeInfo decoding: expected data-value dictionary but found %@", value);
+                return nil;
+            }
+        }
+    }
+
     return self;
 }
 
@@ -59,6 +152,7 @@
 {
     [coder encodeObject:_productIdentity forKey:sProductIdentityCodingKey];
     [coder encodeObject:_endpointsById forKey:sEndpointsCodingKey];
+    [coder encodeObject:_attributes forKey:sAttributesCodingKey];
 }
 
 + (BOOL)supportsSecureCoding
@@ -77,6 +171,8 @@
     MTRCommissioneeInfo * other = object;
     VerifyOrReturnValue(MTREqualObjects(_productIdentity, other->_productIdentity), NO);
     VerifyOrReturnValue(MTREqualObjects(_endpointsById, other->_endpointsById), NO);
+    VerifyOrReturnValue(MTREqualObjects(_attributes, other->_attributes), NO);
+
     return YES;
 }
 
diff --git a/src/darwin/Framework/CHIP/MTRCommissioneeInfo_Internal.h b/src/darwin/Framework/CHIP/MTRCommissioneeInfo_Internal.h
index f892c5f..cbb92ca 100644
--- a/src/darwin/Framework/CHIP/MTRCommissioneeInfo_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRCommissioneeInfo_Internal.h
@@ -15,6 +15,7 @@
  */
 
 #import <Matter/MTRCommissioneeInfo.h>
+#import <Matter/MTRCommissioningParameters.h>
 
 #import "MTRDefines_Internal.h"
 
@@ -25,7 +26,7 @@
 MTR_DIRECT_MEMBERS
 @interface MTRCommissioneeInfo ()
 
-- (instancetype)initWithCommissioningInfo:(const chip::Controller::ReadCommissioningInfo &)info;
+- (instancetype)initWithCommissioningInfo:(const chip::Controller::ReadCommissioningInfo &)info commissioningParameters:(MTRCommissioningParameters *)commissioningParameters;
 
 @end
 
diff --git a/src/darwin/Framework/CHIP/MTRCommissioningParameters.h b/src/darwin/Framework/CHIP/MTRCommissioningParameters.h
index d61867a..94822da 100644
--- a/src/darwin/Framework/CHIP/MTRCommissioningParameters.h
+++ b/src/darwin/Framework/CHIP/MTRCommissioningParameters.h
@@ -1,5 +1,5 @@
 /**
- *    Copyright (c) 2022-2024 Project CHIP Authors
+ *    Copyright (c) 2022-2025 Project CHIP Authors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
  */
 
 #import <Foundation/Foundation.h>
+#import <Matter/MTRBaseDevice.h>
 #import <Matter/MTRDefines.h>
 
 NS_ASSUME_NONNULL_BEGIN
@@ -113,6 +114,13 @@
  */
 @property (nonatomic, copy, nullable) NSNumber * acceptedTermsAndConditionsVersion MTR_PROVISIONALLY_AVAILABLE;
 
+/**
+ * List of attribute paths to read from the commissionee (in addition to
+ * whatever attributes are already read to handle readEndpointInformation being
+ * YES, or to handle other commissioning tasks).
+ */
+@property (nonatomic, copy, nullable) NSArray<MTRAttributeRequestPath *> * extraAttributesToRead MTR_UNSTABLE_API;
+
 @end
 
 @interface MTRCommissioningParameters (Deprecated)
diff --git a/src/darwin/Framework/CHIP/MTRCommissioningParameters.mm b/src/darwin/Framework/CHIP/MTRCommissioningParameters.mm
index 895c6fb..e76cb39 100644
--- a/src/darwin/Framework/CHIP/MTRCommissioningParameters.mm
+++ b/src/darwin/Framework/CHIP/MTRCommissioningParameters.mm
@@ -21,6 +21,25 @@
 
 @implementation MTRCommissioningParameters : NSObject
 
+- (id)copyWithZone:(NSZone * _Nullable)zone
+{
+    auto other = [[MTRCommissioningParameters alloc] init];
+    other.csrNonce = self.csrNonce;
+    other.attestationNonce = self.attestationNonce;
+    other.wifiSSID = self.wifiSSID;
+    other.wifiCredentials = self.wifiCredentials;
+    other.threadOperationalDataset = self.threadOperationalDataset;
+    other.deviceAttestationDelegate = self.deviceAttestationDelegate;
+    other.failSafeTimeout = self.failSafeTimeout;
+    other.skipCommissioningComplete = self.skipCommissioningComplete;
+    other.countryCode = self.countryCode;
+    other.readEndpointInformation = self.readEndpointInformation;
+    other.acceptedTermsAndConditions = self.acceptedTermsAndConditions;
+    other.acceptedTermsAndConditionsVersion = self.acceptedTermsAndConditionsVersion;
+    other.extraAttributesToRead = self.extraAttributesToRead;
+    return other;
+}
+
 @end
 
 @implementation MTRCommissioningParameters (Deprecated)
diff --git a/src/darwin/Framework/CHIP/MTRCommissioningParameters_Internal.h b/src/darwin/Framework/CHIP/MTRCommissioningParameters_Internal.h
new file mode 100644
index 0000000..bf1d6d3
--- /dev/null
+++ b/src/darwin/Framework/CHIP/MTRCommissioningParameters_Internal.h
@@ -0,0 +1,22 @@
+/**
+ *    Copyright (c) 2022-2024 Project CHIP Authors
+ *
+ *    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.
+ */
+
+/**
+ * We want to be able to copy MTRCommissioningParameters, but not commit to that
+ * as public API yet.
+ */
+@interface MTRCommissioningParameters () <NSCopying>
+@end
diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.h b/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.h
index 425506f..9b1c22e 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.h
@@ -15,6 +15,7 @@
  *    limitations under the License.
  */
 
+#import "MTRCommissioningParameters.h"
 #import "MTRDeviceControllerDelegate.h"
 
 #include <controller/CHIPDeviceController.h>
@@ -45,12 +46,16 @@
 
     void SetDeviceNodeID(chip::NodeId deviceNodeId);
 
+    void SetCommissioningParameters(MTRCommissioningParameters * commissioningParameters);
+
 private:
     MTRDeviceController * __weak mController;
     _Nullable id<MTRDeviceControllerDelegate> mDelegate;
     _Nullable dispatch_queue_t mQueue;
     chip::NodeId mDeviceNodeId;
 
+    MTRCommissioningParameters * mCommissioningParameters;
+
     MTRCommissioningStatus MapStatus(chip::Controller::DevicePairingDelegate::Status status);
 };
 
diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.mm
index ff9d06d..b463973 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDelegateBridge.mm
@@ -135,7 +135,7 @@
     BOOL wantCommissioneeInfo = [strongDelegate respondsToSelector:@selector(controller:readCommissioneeInfo:)];
     BOOL wantProductIdentity = [strongDelegate respondsToSelector:@selector(controller:readCommissioningInfo:)];
     if (wantCommissioneeInfo || wantProductIdentity) {
-        auto * commissioneeInfo = [[MTRCommissioneeInfo alloc] initWithCommissioningInfo:info];
+        auto * commissioneeInfo = [[MTRCommissioneeInfo alloc] initWithCommissioningInfo:info commissioningParameters:mCommissioningParameters];
         dispatch_async(mQueue, ^{
             if (wantCommissioneeInfo) { // prefer the newer delegate method over the deprecated one
                 [strongDelegate controller:strongController readCommissioneeInfo:commissioneeInfo];
@@ -144,6 +144,9 @@
             }
         });
     }
+
+    // Don't hold on to the commissioning parameters now that we don't need them anymore.
+    mCommissioningParameters = nil;
 }
 
 void MTRDeviceControllerDelegateBridge::OnCommissioningComplete(chip::NodeId nodeId, CHIP_ERROR error)
@@ -214,3 +217,8 @@
 {
     mDeviceNodeId = deviceNodeId;
 }
+
+void MTRDeviceControllerDelegateBridge::SetCommissioningParameters(MTRCommissioningParameters * commissioningParameters)
+{
+    mCommissioningParameters = commissioningParameters;
+}
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController_Concrete.mm b/src/darwin/Framework/CHIP/MTRDeviceController_Concrete.mm
index 56f1ec2..9472ce8 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController_Concrete.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceController_Concrete.mm
@@ -24,6 +24,7 @@
 #import "MTRCommissionableBrowser.h"
 #import "MTRCommissionableBrowserResult_Internal.h"
 #import "MTRCommissioningParameters.h"
+#import "MTRCommissioningParameters_Internal.h"
 #import "MTRConversion.h"
 #import "MTRDeviceControllerDelegateBridge.h"
 #import "MTRDeviceControllerFactory_Internal.h"
@@ -965,9 +966,22 @@
 
     auto block = ^BOOL {
         chip::Controller::CommissioningParameters params;
+
+        std::vector<chip::app::AttributePathParams> extraReadPaths;
         if (commissioningParams.readEndpointInformation) {
-            params.SetExtraReadPaths(MTREndpointInfo.requiredAttributePaths);
+            for (auto & path : MTREndpointInfo.requiredAttributePaths) {
+                extraReadPaths.emplace_back(path);
+            }
         }
+        if (commissioningParams.extraAttributesToRead != nil) {
+            for (MTRAttributeRequestPath * path in commissioningParams.extraAttributesToRead) {
+                [path convertToAttributePathParams:extraReadPaths.emplace_back()];
+            }
+        }
+        if (!extraReadPaths.empty()) {
+            params.SetExtraReadPaths(chip::Span(extraReadPaths.data(), extraReadPaths.size()));
+        }
+
         if (commissioningParams.csrNonce) {
             params.SetCSRNonce(AsByteSpan(commissioningParams.csrNonce));
         }
@@ -1076,6 +1090,8 @@
 
         chip::NodeId deviceId = [nodeID unsignedLongLongValue];
         self->_operationalCredentialsDelegate->SetDeviceID(deviceId);
+        self->_deviceControllerDelegateBridge->SetCommissioningParameters([commissioningParams copy]);
+
         auto errorCode = self->_cppCommissioner->Commission(deviceId, params);
         MATTER_LOG_METRIC(kMetricCommissionNode, errorCode);
         return ![MTRDeviceController_Concrete checkForError:errorCode logMsg:kDeviceControllerErrorPairDevice error:error];
diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
index 62d4bd9..2e3e8e4 100644
--- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
@@ -32,6 +32,7 @@
 #import "MTRDeviceTestDelegate.h"
 #import "MTRDevice_Internal.h"
 #import "MTRErrorTestUtils.h"
+#import "MTRSecureCodingTestHelpers.h"
 #import "MTRTestCase+ServerAppRunner.h"
 #import "MTRTestCase.h"
 #import "MTRTestControllerDelegate.h"
@@ -3329,41 +3330,21 @@
     XCTAssertTrue(storedAttributeCountDifferenceFromMTRDeviceReport > 300);
 }
 
-- (NSData *)_encodeEncodable:(id<NSSecureCoding>)encodable
-{
-    // We know all our encodables are in fact NSObject.
-    NSObject * obj = (NSObject *) encodable;
-
-    NSError * encodeError;
-    NSData * encodedData = [NSKeyedArchiver archivedDataWithRootObject:encodable requiringSecureCoding:YES error:&encodeError];
-    XCTAssertNil(encodeError, @"Failed to encode %@", NSStringFromClass(obj.class));
-    return encodedData;
-}
-
 - (void)doEncodeDecodeRoundTrip:(id<NSSecureCoding>)encodable
 {
-    NSData * encodedData = [self _encodeEncodable:encodable];
-
     // We know all our encodables are in fact NSObject.
     NSObject * obj = (NSObject *) encodable;
 
     NSError * decodeError;
-    id decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:obj.class] fromData:encodedData error:&decodeError];
+    id decodedValue = RoundTripEncodable(encodable, &decodeError);
     XCTAssertNil(decodeError, @"Failed to decode %@", NSStringFromClass([obj class]));
-    XCTAssertTrue([decodedValue isKindOfClass:obj.class], @"Expected %@ but got %@", NSStringFromClass(obj.class), NSStringFromClass([decodedValue class]));
-
     XCTAssertEqualObjects(obj, decodedValue, @"Decoding for %@ did not round-trip correctly", NSStringFromClass([obj class]));
 }
 
 - (void)_ensureDecodeFails:(id<NSSecureCoding>)encodable
 {
-    NSData * encodedData = [self _encodeEncodable:encodable];
-
-    // We know all our encodables are in fact NSObject.
-    NSObject * obj = (NSObject *) encodable;
-
     NSError * decodeError;
-    id decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:obj.class] fromData:encodedData error:&decodeError];
+    id decodedValue = RoundTripEncodable(encodable, &decodeError);
     XCTAssertNil(decodedValue);
     XCTAssertNotNil(decodeError);
 }
diff --git a/src/darwin/Framework/CHIPTests/MTRPairingTests.m b/src/darwin/Framework/CHIPTests/MTRPairingTests.m
index 4aade77..ba1356c 100644
--- a/src/darwin/Framework/CHIPTests/MTRPairingTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRPairingTests.m
@@ -19,6 +19,7 @@
 
 #import "MTRDefines_Internal.h"
 #import "MTRErrorTestUtils.h"
+#import "MTRSecureCodingTestHelpers.h"
 #import "MTRTestCase+ServerAppRunner.h"
 #import "MTRTestCase.h"
 #import "MTRTestDeclarations.h"
@@ -107,6 +108,7 @@
 @property (nonatomic, nullable) id<MTRDeviceAttestationDelegate> attestationDelegate;
 @property (nonatomic, nullable) NSNumber * failSafeExtension;
 @property (nonatomic) BOOL shouldReadEndpointInformation;
+@property (nullable) NSArray<MTRAttributeRequestPath *> * extraAttributesToRead;
 @property (nullable) NSError * commissioningCompleteError;
 @end
 
@@ -132,6 +134,7 @@
     params.deviceAttestationDelegate = self.attestationDelegate;
     params.failSafeTimeout = self.failSafeExtension;
     params.readEndpointInformation = self.shouldReadEndpointInformation;
+    params.extraAttributesToRead = self.extraAttributesToRead;
 
     NSError * commissionError = nil;
     XCTAssertTrue([controller commissionNodeWithID:@(sDeviceId) commissioningParams:params error:&commissionError],
@@ -145,6 +148,11 @@
     XCTAssertNotNil(info.productIdentity);
     XCTAssertEqualObjects(info.productIdentity.vendorID, /* Test Vendor 1 */ @0xFFF1);
 
+    NSError * decodeError;
+    id roundTrippedInfo = RoundTripEncodable(info, &decodeError);
+    XCTAssertNil(decodeError);
+    XCTAssertEqualObjects(info, roundTrippedInfo);
+
     if (self.shouldReadEndpointInformation) {
         XCTAssertNotNil(info.endpointsById);
         XCTAssertNotNil(info.rootEndpoint);
@@ -161,8 +169,11 @@
 
         // There is currently no convenient way to initialize an MTRCommissioneeInfo
         // object from basic ObjC data types, so we do some unit testing here.
-        NSData * data = [NSKeyedArchiver archivedDataWithRootObject:info requiringSecureCoding:YES error:NULL];
-        MTRCommissioneeInfo * decoded = [NSKeyedUnarchiver unarchivedObjectOfClass:MTRCommissioneeInfo.class fromData:data error:NULL];
+        NSError * err;
+        NSData * data = [NSKeyedArchiver archivedDataWithRootObject:info requiringSecureCoding:YES error:&err];
+        XCTAssertNil(err);
+        MTRCommissioneeInfo * decoded = [NSKeyedUnarchiver unarchivedObjectOfClass:MTRCommissioneeInfo.class fromData:data error:&err];
+        XCTAssertNil(err);
         XCTAssertNotNil(decoded);
         XCTAssertTrue([decoded isEqual:info]);
         XCTAssertEqualObjects(decoded.productIdentity, info.productIdentity);
@@ -172,6 +183,22 @@
         XCTAssertNil(info.endpointsById);
         XCTAssertNil(info.rootEndpoint);
     }
+
+    if (self.extraAttributesToRead) {
+        // The attributes we tried to read should really have worked.
+        XCTAssertNotNil(info.attributes);
+        XCTAssertEqual(info.attributes.count, 2);
+        for (MTRAttributePath * path in info.attributes) {
+            XCTAssertEqualObjects(path.endpoint, @(0));
+            if ([path.cluster isEqual:@(MTRClusterIDTypeDescriptorID)]) {
+                XCTAssertEqualObjects(path.attribute, @(MTRAttributeIDTypeGlobalAttributeAttributeListID));
+            } else if ([path.cluster isEqual:@(MTRClusterIDTypeBasicInformationID)]) {
+                XCTAssertEqualObjects(path.attribute, @(MTRAttributeIDTypeClusterBasicInformationAttributeVendorNameID));
+            } else {
+                XCTFail("Unexpected cluster id %@", path.cluster);
+            }
+        }
+    }
 }
 
 - (void)controller:(MTRDeviceController *)controller commissioningComplete:(NSError * _Nullable)error
@@ -516,4 +543,35 @@
     XCTAssertNil(controllerDelegate.commissioningCompleteError);
 }
 
+- (void)test010_PairWithReadingExtraAttributes
+{
+    [self startServerApp];
+
+    XCTestExpectation * expectation = [self expectationWithDescription:@"Commissioning Complete"];
+    __auto_type * controllerDelegate = [[MTRPairingTestControllerDelegate alloc] initWithExpectation:expectation
+                                                                                 attestationDelegate:nil
+                                                                                   failSafeExtension:nil];
+
+    controllerDelegate.extraAttributesToRead = @[
+        [MTRAttributeRequestPath requestPathWithEndpointID:@(0)
+                                                 clusterID:@(MTRClusterIDTypeDescriptorID)
+                                               attributeID:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)],
+        [MTRAttributeRequestPath requestPathWithEndpointID:@(0)
+                                                 clusterID:@(MTRClusterIDTypeBasicInformationID)
+                                               attributeID:@(MTRAttributeIDTypeClusterBasicInformationAttributeVendorNameID)],
+    ];
+
+    dispatch_queue_t callbackQueue = dispatch_queue_create("com.chip.pairing", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
+    [sController setDeviceControllerDelegate:controllerDelegate queue:callbackQueue];
+    self.controllerDelegate = controllerDelegate;
+
+    NSError * error;
+    __auto_type * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:kOnboardingPayload error:&error];
+    XCTAssertTrue([sController setupCommissioningSessionWithPayload:payload newNodeID:@(++sDeviceId) error:&error]);
+    XCTAssertNil(error);
+
+    [self waitForExpectations:@[ expectation ] timeout:kPairingTimeoutInSeconds];
+    XCTAssertNil(controllerDelegate.commissioningCompleteError);
+}
+
 @end
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRSecureCodingTestHelpers.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRSecureCodingTestHelpers.h
new file mode 100644
index 0000000..bc7a6d1
--- /dev/null
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRSecureCodingTestHelpers.h
@@ -0,0 +1,27 @@
+/**
+ *    Copyright (c) 2025 Project CHIP Authors
+ *
+ *    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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Helper method to round-trip an NSSecureCoding instance and return the
+ * (possibly nil) decoding result.
+ */
+id _Nullable RoundTripEncodable(id<NSSecureCoding> encodable, NSError * __autoreleasing * _Nullable decodeError);
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRSecureCodingTestHelpers.m b/src/darwin/Framework/CHIPTests/TestHelpers/MTRSecureCodingTestHelpers.m
new file mode 100644
index 0000000..31aea46
--- /dev/null
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRSecureCodingTestHelpers.m
@@ -0,0 +1,36 @@
+/**
+ *    Copyright (c) 2025 Project CHIP Authors
+ *
+ *    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 "MTRSecureCodingTestHelpers.h"
+
+#import <XCTest/XCTest.h>
+
+id _Nullable RoundTripEncodable(id<NSSecureCoding> encodable, NSError * __autoreleasing * decodeError)
+{
+    // We know all our encodables are in fact NSObject.
+    NSObject * obj = (NSObject *) encodable;
+
+    NSError * encodeError;
+    NSData * encodedData = [NSKeyedArchiver archivedDataWithRootObject:encodable requiringSecureCoding:YES error:&encodeError];
+    XCTAssertNil(encodeError, @"Failed to encode %@", NSStringFromClass(obj.class));
+    XCTAssertNotNil(encodedData);
+
+    id decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClass:obj.class fromData:encodedData error:decodeError];
+    if (decodedValue != nil) {
+        XCTAssertTrue([decodedValue isKindOfClass:obj.class], @"Expected %@ but got %@", NSStringFromClass(obj.class), NSStringFromClass([decodedValue class]));
+    }
+    return decodedValue;
+}
diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
index 8a4d44d..a13f20b 100644
--- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
+++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
@@ -149,6 +149,8 @@
 		5109E9C02CCAD64F0006884B /* MTRDeviceDataValidation.h in Headers */ = {isa = PBXBuildFile; fileRef = 5109E9BE2CCAD64F0006884B /* MTRDeviceDataValidation.h */; };
 		5109E9C12CCAD64F0006884B /* MTRDeviceDataValidation.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5109E9BF2CCAD64F0006884B /* MTRDeviceDataValidation.mm */; };
 		510A07492A685D3900A9241C /* Matter.apinotes in Headers */ = {isa = PBXBuildFile; fileRef = 510A07482A685D3900A9241C /* Matter.apinotes */; settings = {ATTRIBUTES = (Public, ); }; };
+		510B18F22E315731000F9181 /* MTRSecureCodingTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 510B18F12E315731000F9181 /* MTRSecureCodingTestHelpers.m */; };
+		510B18F42E3275D6000F9181 /* MTRCommissioningParameters_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 510B18F32E3275D6000F9181 /* MTRCommissioningParameters_Internal.h */; };
 		510CECA8297F72970064E0B3 /* MTROperationalCertificateIssuerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 510CECA6297F72470064E0B3 /* MTROperationalCertificateIssuerTests.m */; };
 		5117DD3829A931AE00FFA1AA /* MTROperationalBrowser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5117DD3629A931AD00FFA1AA /* MTROperationalBrowser.mm */; };
 		5117DD3929A931AE00FFA1AA /* MTROperationalBrowser.h in Headers */ = {isa = PBXBuildFile; fileRef = 5117DD3729A931AE00FFA1AA /* MTROperationalBrowser.h */; };
@@ -755,6 +757,9 @@
 		5109E9BE2CCAD64F0006884B /* MTRDeviceDataValidation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDeviceDataValidation.h; sourceTree = "<group>"; };
 		5109E9BF2CCAD64F0006884B /* MTRDeviceDataValidation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDeviceDataValidation.mm; sourceTree = "<group>"; };
 		510A07482A685D3900A9241C /* Matter.apinotes */ = {isa = PBXFileReference; lastKnownFileType = text.apinotes; name = Matter.apinotes; path = CHIP/Matter.apinotes; sourceTree = "<group>"; };
+		510B18F02E315731000F9181 /* MTRSecureCodingTestHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRSecureCodingTestHelpers.h; sourceTree = "<group>"; };
+		510B18F12E315731000F9181 /* MTRSecureCodingTestHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRSecureCodingTestHelpers.m; sourceTree = "<group>"; };
+		510B18F32E3275D6000F9181 /* MTRCommissioningParameters_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRCommissioningParameters_Internal.h; sourceTree = "<group>"; };
 		510CECA6297F72470064E0B3 /* MTROperationalCertificateIssuerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTROperationalCertificateIssuerTests.m; sourceTree = "<group>"; };
 		5117DD3629A931AD00FFA1AA /* MTROperationalBrowser.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTROperationalBrowser.mm; sourceTree = "<group>"; };
 		5117DD3729A931AE00FFA1AA /* MTROperationalBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTROperationalBrowser.h; sourceTree = "<group>"; };
@@ -1554,6 +1559,8 @@
 				75139A6C2B7FE19100E3A919 /* MTRTestDeclarations.h */,
 				3DF5219C2D62C3E5008F8E52 /* MTRMockCB.h */,
 				3DF5219D2D62C3E5008F8E52 /* MTRMockCB.m */,
+				510B18F02E315731000F9181 /* MTRSecureCodingTestHelpers.h */,
+				510B18F12E315731000F9181 /* MTRSecureCodingTestHelpers.m */,
 			);
 			path = TestHelpers;
 			sourceTree = "<group>";
@@ -1617,7 +1624,6 @@
 				7534D1762CF8CDDF00F64654 /* AttributePersistenceProvider.h */,
 				7534D1772CF8CDDF00F64654 /* AttributePersistenceProviderInstance.cpp */,
 			);
-			name = persistence;
 			path = persistence;
 			sourceTree = "<group>";
 		};
@@ -1747,6 +1753,7 @@
 				3D010DD12D4091C800CFFA02 /* MTRCommissioneeInfo_Internal.h */,
 				3D010DCE2D408FA300CFFA02 /* MTRCommissioneeInfo.mm */,
 				99D466E02798936D0089A18F /* MTRCommissioningParameters.h */,
+				510B18F32E3275D6000F9181 /* MTRCommissioningParameters_Internal.h */,
 				99AECC7F2798A57E00B6355B /* MTRCommissioningParameters.mm */,
 				3DFCB32B29678C9500332B35 /* MTRConversion.h */,
 				51565CAD2A79D42100469F18 /* MTRConversion.mm */,
@@ -2238,6 +2245,7 @@
 				998F286D26D55E10001846C6 /* MTRKeypair.h in Headers */,
 				1ED276E426C5832500547A89 /* MTRCluster.h in Headers */,
 				3D843711294977000070D20A /* NSStringSpanConversion.h in Headers */,
+				510B18F42E3275D6000F9181 /* MTRCommissioningParameters_Internal.h in Headers */,
 				757DA3102DB0369100E4AD75 /* ota-provider-cluster.h in Headers */,
 				757DA3112DB0369100E4AD75 /* ota-provider-delegate.h in Headers */,
 				B4FCD56A2B5EDBD300832859 /* MTRDiagnosticLogsType.h in Headers */,
@@ -2726,6 +2734,7 @@
 				1E5801C328941C050033A199 /* MTRTestOTAProvider.m in Sources */,
 				5A6FEC9D27B5E48900F25F42 /* MTRXPCProtocolTests.m in Sources */,
 				3DB9DAE52D67EE5A00704FAB /* MTRBleTests.m in Sources */,
+				510B18F22E315731000F9181 /* MTRSecureCodingTestHelpers.m in Sources */,
 				1EE0805E2A44875E008A03C2 /* MTRCommissionableBrowserTests.m in Sources */,
 				518D3F832AA132DC008E0007 /* MTRTestPerControllerStorage.m in Sources */,
 				51339B1F2A0DA64D00C798C1 /* MTRCertificateValidityTests.m in Sources */,