From 7239ed189a5e129c2f7b6e6b642107a9a7bc8dd8 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Fri, 1 Sep 2023 14:58:30 -0400 Subject: [PATCH] Add per-controller control over operational advertising to Matter.framework. (#29018) --- .../CHIP/MTRDeviceControllerFactory.mm | 3 +- .../MTRDeviceControllerStartupParameters.h | 6 + .../CHIP/MTRDeviceControllerStartupParams.mm | 1 + .../CHIPTests/MTRCertificateValidityTests.m | 1 - .../CHIPTests/MTRControllerAdvertisingTests.m | 297 ++++++++++++++++++ .../Matter.xcodeproj/project.pbxproj | 4 + 6 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 src/darwin/Framework/CHIPTests/MTRControllerAdvertisingTests.m diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm index 97785a45a9db99..a11a7d30135e46 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm @@ -851,11 +851,12 @@ - (MTRDeviceController * _Nullable)createController:(MTRDeviceControllerStartupP return [self _startDeviceController:startupParameters fabricChecker:^MTRDeviceControllerStartupParamsInternal *( FabricTable * fabricTable, MTRDeviceController * controller, CHIP_ERROR & fabricError) { + auto advertiseOperational = self.advertiseOperational && startupParameters.shouldAdvertiseOperational; auto * params = [[MTRDeviceControllerStartupParamsInternal alloc] initForNewController:controller fabricTable:fabricTable keystore:self->_keystore - advertiseOperational:self.advertiseOperational + advertiseOperational:advertiseOperational params:startupParameters error:fabricError]; if (params != nil) { diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParameters.h b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParameters.h index b592849f066974..7b365948b61080 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParameters.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParameters.h @@ -46,6 +46,12 @@ MTR_NEWLY_AVAILABLE */ @property (nonatomic, copy, nullable) NSArray * certificationDeclarationCertificates; +/** + * Whether the controller should advertise its operational identity. Defaults + * to NO. + */ +@property (nonatomic, assign) BOOL shouldAdvertiseOperational; + /** * Set an MTROperationalCertificateIssuer to call (on the provided queue) when * operational certificates need to be provided during commissioning. diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm index c4ad49dda014cd..57c9270a682b08 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm @@ -264,6 +264,7 @@ - (instancetype)initWithStorageDelegate:(id) _productAttestationAuthorityCertificates = nil; _certificationDeclarationCertificates = nil; + _shouldAdvertiseOperational = NO; _ipk = ipk; _vendorID = vendorID; diff --git a/src/darwin/Framework/CHIPTests/MTRCertificateValidityTests.m b/src/darwin/Framework/CHIPTests/MTRCertificateValidityTests.m index 34fd5d5814d254..7a66d299e19e71 100644 --- a/src/darwin/Framework/CHIPTests/MTRCertificateValidityTests.m +++ b/src/darwin/Framework/CHIPTests/MTRCertificateValidityTests.m @@ -251,7 +251,6 @@ - (void)initStack:(MTRTestCertificateIssuer *)certificateIssuer __auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage]; factoryParams.port = @(kLocalPort); - factoryParams.shouldStartServer = YES; BOOL ok = [factory startControllerFactory:factoryParams error:nil]; XCTAssertTrue(ok); diff --git a/src/darwin/Framework/CHIPTests/MTRControllerAdvertisingTests.m b/src/darwin/Framework/CHIPTests/MTRControllerAdvertisingTests.m new file mode 100644 index 00000000000000..1276e0ce845bca --- /dev/null +++ b/src/darwin/Framework/CHIPTests/MTRControllerAdvertisingTests.m @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2023 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 + +// system dependencies +#import +#import + +#import "MTRFabricInfoChecker.h" +#import "MTRTestKeys.h" +#import "MTRTestPerControllerStorage.h" + +static const uint16_t kTestVendorId = 0xFFF1u; +static const uint16_t kTimeoutInSeconds = 3; + +static NSString * NodeIDAsString(NSNumber * nodeID) { return [NSString stringWithFormat:@"%016llX", nodeID.unsignedLongLongValue]; } + +@interface MTRControllerAdvertisingTestsOperationalBrowser : NSObject +@property NSSet * discoveredNodes; + +- (instancetype)initWithExpectation:(XCTestExpectation *)expectation nodeIDToExpect:(NSNumber *)nodeIDToExpect; +- (void)discoveredNodeID:(NSString *)nodeID onCompressedFabricID:(NSString *)compressedFabricID; +@end + +static void OnBrowse(DNSServiceRef aServiceRef, DNSServiceFlags aFlags, uint32_t aInterfaceId, DNSServiceErrorType aError, + const char * aName, const char * aType, const char * aDomain, void * aContext) +{ + XCTAssertTrue(aError == kDNSServiceErr_NoError); + + if (!(aFlags & kDNSServiceFlagsAdd)) { + return; + } + + // 16 chars for compressed fabric id, 16 chars for node id, and the dash. + XCTAssertTrue(strlen(aName) == 33); + + NSString * compressedFabricID = [[NSString alloc] initWithBytes:aName length:16 encoding:NSUTF8StringEncoding]; + NSString * nodeID = [[NSString alloc] initWithBytes:aName + 17 length:16 encoding:NSUTF8StringEncoding]; + + __auto_type * self = (__bridge MTRControllerAdvertisingTestsOperationalBrowser *) aContext; + [self discoveredNodeID:nodeID onCompressedFabricID:compressedFabricID]; +} + +static const char kLocalDot[] = "local."; +static const char kOperationalType[] = "_matter._tcp"; +static const DNSServiceFlags kBrowseFlags = 0; + +@implementation MTRControllerAdvertisingTestsOperationalBrowser { + DNSServiceRef _browseRef; + // Key is compressed fabric id, value is the set of discovered node IDs. + NSMutableDictionary *> * _allDiscoveredNodes; + + XCTestExpectation * _expectation; + NSString * _nodeIDToExpect; +} + +- (instancetype)initWithExpectation:(XCTestExpectation *)expectation nodeIDToExpect:(NSNumber *)nodeIDToExpect +{ + XCTAssertNotNil([super init]); + + _allDiscoveredNodes = [[NSMutableDictionary alloc] init]; + _expectation = expectation; + _nodeIDToExpect = NodeIDAsString(nodeIDToExpect); + + __auto_type err = DNSServiceBrowse( + &_browseRef, kBrowseFlags, kDNSServiceInterfaceIndexAny, kOperationalType, kLocalDot, OnBrowse, (__bridge void *) self); + XCTAssertTrue(err == kDNSServiceErr_NoError); + + err = DNSServiceSetDispatchQueue(_browseRef, dispatch_get_main_queue()); + XCTAssertTrue(err == kDNSServiceErr_NoError); + + return self; +} + +- (void)discoveredNodeID:(NSString *)nodeID onCompressedFabricID:(NSString *)compressedFabricID +{ + if (_allDiscoveredNodes[compressedFabricID] == nil) { + _allDiscoveredNodes[compressedFabricID] = [[NSMutableSet alloc] init]; + } + [_allDiscoveredNodes[compressedFabricID] addObject:nodeID]; + + // It would be nice to check the compressedFabricID, but computing the right + // expected value for it is a pain. + if ([nodeID isEqualToString:_nodeIDToExpect]) { + _discoveredNodes = [NSSet setWithSet:_allDiscoveredNodes[compressedFabricID]]; + // Stop our browse so we get no more notifications. + DNSServiceRefDeallocate(_browseRef); + _browseRef = NULL; + [_expectation fulfill]; + } +} + +- (void)dealloc +{ + if (_browseRef) { + DNSServiceRefDeallocate(_browseRef); + } +} + +@end + +@interface MTRControllerAdvertisingTests : XCTestCase +@end + +@implementation MTRControllerAdvertisingTests { + dispatch_queue_t _storageQueue; +} + ++ (void)tearDown +{ +} + +- (void)setUp +{ + // Per-test setup, runs before each test. + [super setUp]; + [self setContinueAfterFailure:NO]; + + _storageQueue = dispatch_queue_create("test.storage.queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); + + [self startFactory]; +} + +- (void)tearDown +{ + // Per-test teardown, runs after each test. + [self stopFactory]; + _storageQueue = nil; + [super tearDown]; +} + +- (void)startFactory +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] init]; + factoryParams.shouldStartServer = YES; + + NSError * error; + BOOL ok = [factory startControllerFactory:factoryParams error:&error]; + XCTAssertNil(error); + XCTAssertTrue(ok); + + XCTAssertTrue(factory.isRunning); +} + +- (void)stopFactory +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + [factory stopControllerFactory]; + XCTAssertFalse(factory.isRunning); +} + +// Test helpers + +- (nullable MTRDeviceController *)startControllerWithRootKeys:(MTRTestKeys *)rootKeys + operationalKeys:(MTRTestKeys *)operationalKeys + fabricID:(NSNumber *)fabricID + nodeID:(NSNumber *)nodeID + storage:(MTRTestPerControllerStorage *)storage + advertiseOperational:(BOOL)advertiseOperational + error:(NSError * __autoreleasing *)error +{ + XCTAssertTrue(error != NULL); + + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + // Specify a fixed issuerID, so we get the same cert if we use the same keys. + __auto_type * root = [MTRCertificates createRootCertificate:rootKeys issuerID:@(1) fabricID:nil error:error]; + XCTAssertNil(*error); + XCTAssertNotNil(root); + + __auto_type * operational = [MTRCertificates createOperationalCertificate:rootKeys + signingCertificate:root + operationalPublicKey:operationalKeys.publicKey + fabricID:fabricID + nodeID:nodeID + caseAuthenticatedTags:nil + error:error]; + XCTAssertNil(*error); + XCTAssertNotNil(operational); + + __auto_type * params = + [[MTRDeviceControllerExternalCertificateStartupParameters alloc] initWithStorageDelegate:storage + storageDelegateQueue:_storageQueue + uniqueIdentifier:storage.controllerID + ipk:rootKeys.ipk + vendorID:@(kTestVendorId) + operationalKeypair:operationalKeys + operationalCertificate:operational + intermediateCertificate:nil + rootCertificate:root]; + XCTAssertNotNil(params); + + params.shouldAdvertiseOperational = advertiseOperational; + + return [factory createController:params error:error]; +} + +- (void)test001_CheckAdvertisingAsExpected +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + // Pick some ids that no other test will be using. + NSNumber * nodeID1 = @(0x1827364554637281); + NSNumber * nodeID2 = @(0x8172635445362718); + NSNumber * nodeID3 = @(0x8811772266335544); + NSNumber * fabricID = @(0x1122334455667788); + + __auto_type * browseExpectation = [self expectationWithDescription:@"Discovered our last controller"]; + // Assume that since we start the controller with nodeID3 last, by the + // time we see its advertisements we will have seen the ones for the one + // with nodeID1 too, if it had any. + __auto_type * operationalBrowser = + [[MTRControllerAdvertisingTestsOperationalBrowser alloc] initWithExpectation:browseExpectation nodeIDToExpect:nodeID3]; + XCTAssertNotNil(operationalBrowser); + + __auto_type * storageDelegate1 = [[MTRTestPerControllerStorage alloc] initWithControllerID:[NSUUID UUID]]; + + NSError * error; + MTRDeviceController * controller1 = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID1 + storage:storageDelegate1 + advertiseOperational:NO + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller1); + XCTAssertTrue([controller1 isRunning]); + XCTAssertEqualObjects(controller1.controllerNodeID, nodeID1); + + __auto_type * storageDelegate2 = [[MTRTestPerControllerStorage alloc] initWithControllerID:[NSUUID UUID]]; + + MTRDeviceController * controller2 = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID2 + storage:storageDelegate2 + advertiseOperational:YES + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller2); + XCTAssertTrue([controller2 isRunning]); + XCTAssertEqualObjects(controller2.controllerNodeID, nodeID2); + + __auto_type * storageDelegate3 = [[MTRTestPerControllerStorage alloc] initWithControllerID:[NSUUID UUID]]; + + MTRDeviceController * controller3 = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID3 + storage:storageDelegate3 + advertiseOperational:YES + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller3); + XCTAssertTrue([controller3 isRunning]); + XCTAssertEqualObjects(controller3.controllerNodeID, nodeID3); + + [self waitForExpectations:@[ browseExpectation ] timeout:kTimeoutInSeconds]; + + __auto_type * expectedDiscoveredNodes = [NSSet setWithArray:@[ NodeIDAsString(nodeID2), NodeIDAsString(nodeID3) ]]; + XCTAssertEqualObjects(operationalBrowser.discoveredNodes, expectedDiscoveredNodes); + + [controller1 shutdown]; + XCTAssertFalse([controller1 isRunning]); + [controller2 shutdown]; + XCTAssertFalse([controller2 isRunning]); + [controller3 shutdown]; + XCTAssertFalse([controller3 isRunning]); +} + +@end diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index edc68cd47eb1c4..a95c790993dfd5 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ 517BF3F1282B62B800A8B7DB /* MTRCertificates.mm in Sources */ = {isa = PBXBuildFile; fileRef = 517BF3EF282B62B800A8B7DB /* MTRCertificates.mm */; }; 517BF3F3282B62CB00A8B7DB /* MTRCertificateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 517BF3F2282B62CB00A8B7DB /* MTRCertificateTests.m */; }; 518D3F832AA132DC008E0007 /* MTRTestPerControllerStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 518D3F812AA132DC008E0007 /* MTRTestPerControllerStorage.m */; }; + 518D3F852AA14006008E0007 /* MTRControllerAdvertisingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 518D3F842AA14006008E0007 /* MTRControllerAdvertisingTests.m */; }; 519498322A25581C00B3BABE /* MTRSetupPayloadSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 519498312A25581C00B3BABE /* MTRSetupPayloadSerializerTests.m */; }; 51A2F1322A00402A00F03298 /* MTRDataValueParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51A2F1312A00402A00F03298 /* MTRDataValueParserTests.m */; }; 51B22C1E2740CB0A008D5055 /* MTRStructsObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 51B22C1D2740CB0A008D5055 /* MTRStructsObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -489,6 +490,7 @@ 517BF3F2282B62CB00A8B7DB /* MTRCertificateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRCertificateTests.m; sourceTree = ""; }; 518D3F812AA132DC008E0007 /* MTRTestPerControllerStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRTestPerControllerStorage.m; sourceTree = ""; }; 518D3F822AA132DC008E0007 /* MTRTestPerControllerStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRTestPerControllerStorage.h; sourceTree = ""; }; + 518D3F842AA14006008E0007 /* MTRControllerAdvertisingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRControllerAdvertisingTests.m; sourceTree = ""; }; 519498312A25581C00B3BABE /* MTRSetupPayloadSerializerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRSetupPayloadSerializerTests.m; sourceTree = ""; }; 51A2F1312A00402A00F03298 /* MTRDataValueParserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRDataValueParserTests.m; sourceTree = ""; }; 51B22C1D2740CB0A008D5055 /* MTRStructsObjc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRStructsObjc.h; sourceTree = ""; }; @@ -1164,6 +1166,7 @@ 519498312A25581C00B3BABE /* MTRSetupPayloadSerializerTests.m */, 5143851D2A65885500EDC8E6 /* MTRSwiftPairingTests.swift */, 51E95DF72A78110900A434F0 /* MTRPerControllerStorageTests.m */, + 518D3F842AA14006008E0007 /* MTRControllerAdvertisingTests.m */, B202529D2459E34F00F97062 /* Info.plist */, 5143851C2A65885400EDC8E6 /* MatterTests-Bridging-Header.h */, ); @@ -1580,6 +1583,7 @@ 51D10D2E2808E2CA00E8CA3D /* MTRTestStorage.m in Sources */, 7596A8512878709F004DAE0E /* MTRAsyncCallbackQueueTests.m in Sources */, 997DED1A26955D0200975E97 /* MTRThreadOperationalDatasetTests.mm in Sources */, + 518D3F852AA14006008E0007 /* MTRControllerAdvertisingTests.m in Sources */, 51C8E3F82825CDB600D47D00 /* MTRTestKeys.m in Sources */, 51C984622A61CE2A00B0AD9A /* MTRFabricInfoChecker.m in Sources */, 99C65E10267282F1003402F6 /* MTRControllerTests.m in Sources */,