Skip to content

Commit 46d536a

Browse files
bytedreamerclaude
andcommitted
Add OSDP 2.2.2 secure channel compliance tests (Phase 5)
12 integration tests validating PD secure channel behavior: - 4-step handshake with SCBK and SCBK-D (install mode) - Key update via osdp_KEYSET and reconnection with new key - Old key rejection after update - Install mode to full security transition - Secure channel re-establishment after disconnect - Repeated handshake sequences and mode flexibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3528d0e commit 46d536a

1 file changed

Lines changed: 349 additions & 0 deletions

File tree

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using NUnit.Framework;
4+
using OSDP.Net.Model.CommandData;
5+
6+
namespace OSDP.Net.Tests.IntegrationTests;
7+
8+
/// <summary>
9+
/// OSDP 2.2.2 Compliance Tests - Secure Channel Validation
10+
///
11+
/// Validates PD secure channel behavior per OSDP 2.2.2 Appendix D:
12+
/// - 4-step handshake: CHLNG → CCRYPT → SCRYPT → RMAC_I (Section D.1)
13+
/// - Key type handling: SCBK vs SCBK-D (Section D.1.3)
14+
/// - Security mode transitions and enforcement
15+
/// - Key update via osdp_KEYSET (Section 7.15)
16+
/// - Secure channel re-establishment after disconnect
17+
///
18+
/// NOTE: Lower-level handshake details (cryptogram generation, MAC validation)
19+
/// are tested implicitly through successful secure channel establishment.
20+
/// Packet-level interception of CHLNG/CCRYPT/SCRYPT/RMAC_I would require
21+
/// a message interceptor which is outside the scope of this test suite.
22+
/// </summary>
23+
[TestFixture]
24+
[Category("Integration")]
25+
[Category("Compliance.Security")]
26+
public class SecureChannelComplianceTests : IntegrationTestFixtureBase
27+
{
28+
// OSDP 2.2.2 Appendix D.1 - 4-Step Handshake
29+
// The secure channel must be established through the full handshake sequence.
30+
// Successful establishment is verified by the IsSecureChannelEstablished status
31+
// and the ability to exchange encrypted commands.
32+
33+
[Test]
34+
public async Task SecureChannel_EstablishedWithNonDefaultKey()
35+
{
36+
// OSDP 2.2.2 Appendix D.1 - Full security with SCBK
37+
var secureChannelEstablished = new TaskCompletionSource<bool>();
38+
39+
await InitTestTargets(cfg =>
40+
{
41+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
42+
cfg.RequireSecurity = true;
43+
});
44+
45+
TargetPanel.ConnectionStatusChanged += (_, e) =>
46+
{
47+
if (e.IsConnected && e.IsSecureChannelEstablished)
48+
{
49+
secureChannelEstablished.TrySetResult(true);
50+
}
51+
};
52+
53+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
54+
55+
var result = await Task.WhenAny(secureChannelEstablished.Task, Task.Delay(5000));
56+
Assert.That(result, Is.EqualTo(secureChannelEstablished.Task),
57+
"Secure channel must be established after handshake with SCBK");
58+
}
59+
60+
[Test]
61+
public async Task SecureChannel_EstablishedWithDefaultKey_InstallMode()
62+
{
63+
// OSDP 2.2.2 Appendix D.1.3 - Installation mode with SCBK-D
64+
var secureChannelEstablished = new TaskCompletionSource<bool>();
65+
66+
await InitTestTargets(cfg =>
67+
{
68+
cfg.SecurityKey = IntegrationConsts.DefaultSCBK;
69+
cfg.RequireSecurity = true;
70+
});
71+
72+
TargetPanel.ConnectionStatusChanged += (_, e) =>
73+
{
74+
if (e.IsConnected && e.IsSecureChannelEstablished)
75+
{
76+
secureChannelEstablished.TrySetResult(true);
77+
}
78+
};
79+
80+
AddDeviceToPanel(IntegrationConsts.DefaultSCBK);
81+
82+
var result = await Task.WhenAny(secureChannelEstablished.Task, Task.Delay(5000));
83+
Assert.That(result, Is.EqualTo(secureChannelEstablished.Task),
84+
"Secure channel must be established in installation mode with SCBK-D");
85+
}
86+
87+
[Test]
88+
public async Task SecureChannel_CommandsWorkAfterEstablishment()
89+
{
90+
// OSDP 2.2.2 - Verify encrypted commands work after handshake
91+
await InitTestTargets(cfg =>
92+
{
93+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
94+
cfg.RequireSecurity = true;
95+
});
96+
97+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
98+
await WaitForDeviceOnlineStatus();
99+
100+
// All mandatory commands should work over the established secure channel
101+
var id = await TargetPanel.IdReport(ConnectionId, DeviceAddress);
102+
Assert.That(id, Is.Not.Null);
103+
104+
var caps = await TargetPanel.DeviceCapabilities(ConnectionId, DeviceAddress);
105+
Assert.That(caps, Is.Not.Null);
106+
107+
var localStatus = await TargetPanel.LocalStatus(ConnectionId, DeviceAddress);
108+
Assert.That(localStatus, Is.Not.Null);
109+
110+
var inputStatus = await TargetPanel.InputStatus(ConnectionId, DeviceAddress);
111+
Assert.That(inputStatus, Is.Not.Null);
112+
113+
var outputStatus = await TargetPanel.OutputStatus(ConnectionId, DeviceAddress);
114+
Assert.That(outputStatus, Is.Not.Null);
115+
116+
var readerStatus = await TargetPanel.ReaderStatus(ConnectionId, DeviceAddress);
117+
Assert.That(readerStatus, Is.Not.Null);
118+
}
119+
120+
// OSDP 2.2.2 Appendix D.1.3 - Key Type Mismatch
121+
// Handshake must fail when ACU and PD use different key types.
122+
123+
[Test]
124+
public async Task SecureChannel_FailsWhenKeysMismatch()
125+
{
126+
// ACU uses non-default key, PD uses default key → handshake fails
127+
await InitTestTargets(cfg =>
128+
{
129+
cfg.SecurityKey = IntegrationConsts.DefaultSCBK;
130+
cfg.RequireSecurity = true;
131+
});
132+
133+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
134+
await AssertPanelRemainsDisconnected();
135+
}
136+
137+
[Test]
138+
public async Task SecureChannel_FailsWhenKeysMismatchReverse()
139+
{
140+
// ACU uses default key, PD uses non-default key → handshake fails
141+
await InitTestTargets(cfg =>
142+
{
143+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
144+
cfg.RequireSecurity = true;
145+
});
146+
147+
AddDeviceToPanel(IntegrationConsts.DefaultSCBK);
148+
await AssertPanelRemainsDisconnected();
149+
}
150+
151+
// OSDP 2.2.2 Section 7.15 - osdp_KEYSET
152+
// PD must accept key updates and use the new key for subsequent connections.
153+
154+
[Test]
155+
public async Task KeySet_UpdatesKeyAndReconnectsSuccessfully()
156+
{
157+
// OSDP 2.2.2 Section 7.15 - Key update via osdp_KEYSET
158+
await InitTestTargets(cfg =>
159+
{
160+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
161+
cfg.RequireSecurity = true;
162+
});
163+
164+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
165+
await WaitForDeviceOnlineStatus();
166+
167+
// Update the key
168+
var newKey = new byte[] { 0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87, 0x78, 0x69, 0x5A, 0x4B, 0x3C, 0x2D, 0x1E, 0x0F };
169+
var result = await TargetPanel.EncryptionKeySet(ConnectionId, DeviceAddress,
170+
new EncryptionKeyConfiguration(KeyType.SecureChannelBaseKey, newKey));
171+
Assert.That(result, Is.True, "Key update must succeed");
172+
173+
// Verify commands still work on current session
174+
await AssertPanelToDeviceCommsAreHealthy();
175+
176+
// Reconnect with the new key
177+
RemoveDeviceFromPanel();
178+
AddDeviceToPanel(newKey);
179+
await WaitForDeviceOnlineStatus();
180+
181+
// Verify commands work with the new key
182+
await AssertPanelToDeviceCommsAreHealthy();
183+
}
184+
185+
[Test]
186+
public async Task KeySet_OldKeyNoLongerWorksAfterUpdate()
187+
{
188+
// OSDP 2.2.2 - After key update, old key must not work
189+
await InitTestTargets(cfg =>
190+
{
191+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
192+
cfg.RequireSecurity = true;
193+
});
194+
195+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
196+
await WaitForDeviceOnlineStatus();
197+
198+
// Update the key
199+
var newKey = new byte[] { 0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87, 0x78, 0x69, 0x5A, 0x4B, 0x3C, 0x2D, 0x1E, 0x0F };
200+
var result = await TargetPanel.EncryptionKeySet(ConnectionId, DeviceAddress,
201+
new EncryptionKeyConfiguration(KeyType.SecureChannelBaseKey, newKey));
202+
Assert.That(result, Is.True);
203+
204+
// Try to reconnect with the old key - should fail
205+
RemoveDeviceFromPanel();
206+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
207+
await AssertPanelRemainsDisconnected();
208+
}
209+
210+
// OSDP 2.2.2 - Security Mode Transitions
211+
// PD can transition from installation mode to full security via key update.
212+
213+
[Test]
214+
public async Task SecurityModeTransition_InstallModeToFullSecurity()
215+
{
216+
// Start in installation mode (default key)
217+
await InitTestTargets(cfg =>
218+
{
219+
cfg.SecurityKey = IntegrationConsts.DefaultSCBK;
220+
cfg.RequireSecurity = true;
221+
});
222+
223+
AddDeviceToPanel(IntegrationConsts.DefaultSCBK);
224+
await WaitForDeviceOnlineStatus();
225+
226+
// Update key from default (SCBK-D) to non-default (SCBK)
227+
var result = await TargetPanel.EncryptionKeySet(ConnectionId, DeviceAddress,
228+
new EncryptionKeyConfiguration(KeyType.SecureChannelBaseKey, IntegrationConsts.NonDefaultSCBK));
229+
Assert.That(result, Is.True, "Key update from SCBK-D to SCBK must succeed");
230+
231+
// Reconnect with the new non-default key
232+
RemoveDeviceFromPanel();
233+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
234+
await WaitForDeviceOnlineStatus();
235+
236+
await AssertPanelToDeviceCommsAreHealthy();
237+
}
238+
239+
// OSDP 2.2.2 - Secure Channel Re-establishment
240+
// PD must be able to re-establish secure channel after a disconnection.
241+
242+
[Test]
243+
public async Task SecureChannel_ReEstablishedAfterDisconnect()
244+
{
245+
await InitTestTargets(cfg =>
246+
{
247+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
248+
cfg.RequireSecurity = true;
249+
});
250+
251+
// First connection
252+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
253+
await WaitForDeviceOnlineStatus();
254+
await AssertPanelToDeviceCommsAreHealthy();
255+
256+
// Disconnect
257+
RemoveDeviceFromPanel();
258+
await Task.Delay(TimeSpan.FromSeconds(9));
259+
Assert.That(TargetDevice.IsConnected, Is.False);
260+
261+
// Re-establish secure channel
262+
var secureChannelEstablished = new TaskCompletionSource<bool>();
263+
TargetPanel.ConnectionStatusChanged += (_, e) =>
264+
{
265+
if (e.IsConnected && e.IsSecureChannelEstablished)
266+
{
267+
secureChannelEstablished.TrySetResult(true);
268+
}
269+
};
270+
271+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
272+
273+
var waitResult = await Task.WhenAny(secureChannelEstablished.Task, Task.Delay(5000));
274+
Assert.That(waitResult, Is.EqualTo(secureChannelEstablished.Task),
275+
"Secure channel must be re-established after disconnect");
276+
277+
await AssertPanelToDeviceCommsAreHealthy();
278+
}
279+
280+
[Test]
281+
public async Task SecureChannel_MultipleHandshakesSucceed()
282+
{
283+
// OSDP 2.2.2 - PD must support repeated handshake sequences
284+
await InitTestTargets(cfg =>
285+
{
286+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
287+
cfg.RequireSecurity = true;
288+
});
289+
290+
for (int i = 0; i < 3; i++)
291+
{
292+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
293+
await WaitForDeviceOnlineStatus();
294+
await AssertPanelToDeviceCommsAreHealthy();
295+
RemoveDeviceFromPanel();
296+
297+
// Wait for PD to go offline before next iteration
298+
await Task.Delay(TimeSpan.FromSeconds(9));
299+
Assert.That(TargetDevice.IsConnected, Is.False, $"Iteration {i}: PD should be offline");
300+
}
301+
}
302+
303+
// OSDP 2.2.2 - Unsecured Connection with Security Key Configured
304+
// When PD does not require security, it can still accept unsecured connections
305+
// even if a security key is configured.
306+
307+
[Test]
308+
public async Task UnsecuredConnection_WorksWhenSecurityNotRequired()
309+
{
310+
await InitTestTargets(cfg =>
311+
{
312+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
313+
cfg.RequireSecurity = false;
314+
});
315+
316+
AddDeviceToPanel(useSecureChannel: false);
317+
await WaitForDeviceOnlineStatus();
318+
319+
await AssertPanelToDeviceCommsAreHealthy();
320+
}
321+
322+
[Test]
323+
public async Task SecuredConnection_WorksWhenSecurityNotRequired()
324+
{
325+
// OSDP 2.2.2 - PD can accept secure channel even when not required
326+
await InitTestTargets(cfg =>
327+
{
328+
cfg.SecurityKey = IntegrationConsts.NonDefaultSCBK;
329+
cfg.RequireSecurity = false;
330+
});
331+
332+
var secureChannelEstablished = new TaskCompletionSource<bool>();
333+
TargetPanel.ConnectionStatusChanged += (_, e) =>
334+
{
335+
if (e.IsConnected && e.IsSecureChannelEstablished)
336+
{
337+
secureChannelEstablished.TrySetResult(true);
338+
}
339+
};
340+
341+
AddDeviceToPanel(IntegrationConsts.NonDefaultSCBK);
342+
343+
var result = await Task.WhenAny(secureChannelEstablished.Task, Task.Delay(5000));
344+
Assert.That(result, Is.EqualTo(secureChannelEstablished.Task),
345+
"Secure channel should be established even when security is not required");
346+
347+
await AssertPanelToDeviceCommsAreHealthy();
348+
}
349+
}

0 commit comments

Comments
 (0)