PHP Classes

File: tests/Unit/Protocol/SessionServiceCoverageTest.php

Recommend this page to a friend!
  Packages of Gianfrancesco Aurecchia   OPC UA Client   tests/Unit/Protocol/SessionServiceCoverageTest.php   Download  
File: tests/Unit/Protocol/SessionServiceCoverageTest.php
Role: Example script
Content type: text/plain
Description: Example script
Class: OPC UA Client
Control devices that support the OPC UA protocol
Author: By
Last change:
Date: 16 days ago
Size: 25,313 bytes
 

Contents

Class file image Download
<?php declare(strict_types=1); use PhpOpcua\Client\Encoding\BinaryDecoder; use PhpOpcua\Client\Encoding\BinaryEncoder; use PhpOpcua\Client\Protocol\MessageHeader; use PhpOpcua\Client\Protocol\SessionService; use PhpOpcua\Client\Security\CertificateManager; use PhpOpcua\Client\Security\MessageSecurity; use PhpOpcua\Client\Security\SecureChannel; use PhpOpcua\Client\Security\SecurityMode; use PhpOpcua\Client\Security\SecurityPolicy; use PhpOpcua\Client\Types\NodeId; function writeSessionResponsePrefix(BinaryEncoder $encoder, int $typeId): void { $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, $typeId)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 0)); $encoder->writeByte(0); } function writeCreateSessionResponseBody(BinaryEncoder $encoder, int $authTokenId = 2, string $cert = "\x30\x03\x01\x01\xFF"): void { $encoder->writeNodeId(NodeId::numeric(0, 1)); $encoder->writeNodeId(NodeId::numeric(0, $authTokenId)); $encoder->writeDouble(120000.0); $encoder->writeByteString('server-nonce'); $encoder->writeByteString($cert); $encoder->writeInt32(0); $encoder->writeInt32(0); $encoder->writeString(null); $encoder->writeByteString(null); $encoder->writeUInt32(0); } describe('SessionService wrapWithSecureChannel (non-secure)', function () { it('wraps inner body without secure channel', function () { $session = new SessionService(1, 1); $inner = new BinaryEncoder(); $inner->writeNodeId(NodeId::numeric(0, 631)); $inner->writeNodeId(NodeId::numeric(0, 0)); $inner->writeInt64(0); $inner->writeUInt32(42); $inner->writeUInt32(0); $inner->writeString(null); $inner->writeUInt32(10000); $inner->writeNodeId(NodeId::numeric(0, 0)); $inner->writeByte(0); $result = $session->wrapWithSecureChannel($inner->getBuffer()); expect(substr($result, 0, 3))->toBe('MSG'); $decoder = new BinaryDecoder($result); $header = MessageHeader::decode($decoder); expect($header->getMessageSize())->toBe(strlen($result)); }); it('wraps with CLO message type', function () { $session = new SessionService(1, 1); $inner = new BinaryEncoder(); $inner->writeNodeId(NodeId::numeric(0, 473)); $inner->writeNodeId(NodeId::numeric(0, 0)); $inner->writeInt64(0); $inner->writeUInt32(1); $inner->writeUInt32(0); $inner->writeString(null); $inner->writeUInt32(10000); $inner->writeNodeId(NodeId::numeric(0, 0)); $inner->writeByte(0); $result = $session->wrapWithSecureChannel($inner->getBuffer(), 'CLO'); expect(substr($result, 0, 3))->toBe('CLO'); }); }); describe('SessionService decodeActivateSessionResponse with non-empty string table', function () { it('decodes response with non-empty string table', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(2); $encoder->writeString('string1'); $encoder->writeString('string2'); $encoder->writeNodeId(NodeId::numeric(0, 0)); $encoder->writeByte(0); $encoder->writeByteString('nonce-data'); $encoder->writeInt32(0); $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); }); describe('SessionService extractLeafCertificate edge cases', function () { it('handles certificate chain with short-form length', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 464); writeCreateSessionResponseBody($encoder, 2, "\x30\x03\x01\x01\xFF"); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result['authenticationToken']->getIdentifier())->toBe(2); }); it('handles certificate shorter than 4 bytes', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 464); writeCreateSessionResponseBody($encoder, 3, "\xAB\xCD"); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result['authenticationToken']->getIdentifier())->toBe(3); }); }); describe('SessionService skipDiagnosticInfoBody all mask bits', function () { it('handles locale mask (0x04)', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 470); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(1); $encoder->writeByte(0x04); $encoder->writeInt32(9); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); it('handles innerStatusCode mask (0x10)', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 470); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(1); $encoder->writeByte(0x10); $encoder->writeUInt32(0x800A0000); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); }); describe('SessionService decodeCreateSessionResponse ECC ephemeral key', function () { it('reads eccServerEphemeralKey when remaining bytes exist (non-debug)', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 464); writeCreateSessionResponseBody($encoder); $encoder->writeByteString('fake-ecc-key-data'); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result['eccServerEphemeralKey'])->toBe('fake-ecc-key-data'); }); it('returns null eccServerEphemeralKey when no remaining bytes', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 464); writeCreateSessionResponseBody($encoder); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result['eccServerEphemeralKey'])->toBeNull(); }); it('catches exception when eccServerEphemeralKey read fails', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 464); writeCreateSessionResponseBody($encoder); $encoder->writeByte(0xFF); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result)->toHaveKey('eccServerEphemeralKey'); }); }); describe('SessionService readResponseHeader with AdditionalHeader', function () { it('reads additionalHeader body when encoding is 0x01 with string param', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 17537)); $encoder->writeByte(0x01); $additionalBody = new BinaryEncoder(); $additionalBody->writeInt32(1); $additionalBody->writeUInt16(0); $additionalBody->writeString('SomeKey'); $additionalBody->writeByte(12); $additionalBody->writeString('SomeValue'); $additionalBodyBytes = $additionalBody->getBuffer(); $encoder->writeInt32(strlen($additionalBodyBytes)); $encoder->writeRawBytes($additionalBodyBytes); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); it('parses ECDHKey from AdditionalParameters', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 17537)); $encoder->writeByte(0x01); $additionalBody = new BinaryEncoder(); $additionalBody->writeInt32(1); $additionalBody->writeUInt16(0); $additionalBody->writeString('ECDHKey'); $additionalBody->writeByte(22); $additionalBody->writeNodeId(NodeId::numeric(0, 17546)); $additionalBody->writeByte(0x01); $extBody = new BinaryEncoder(); $extBody->writeByteString('fake-public-key'); $extBody->writeByteString('fake-signature'); $extBodyBytes = $extBody->getBuffer(); $additionalBody->writeInt32(strlen($extBodyBytes)); $additionalBody->writeRawBytes($extBodyBytes); $additionalBodyBytes = $additionalBody->getBuffer(); $encoder->writeInt32(strlen($additionalBodyBytes)); $encoder->writeRawBytes($additionalBodyBytes); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect($session->getLastEccServerEphemeralKey())->toBe('fake-public-key'); }); }); describe('SessionService skipVariantValue coverage', function () { it('skips various variant types in AdditionalParameters', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 17537)); $encoder->writeByte(0x01); $additionalBody = new BinaryEncoder(); $additionalBody->writeInt32(10); // Boolean(1), SByte(2), UInt16(4), UInt32(6), Int64(8), Float(10), Double(11), DateTime(13), Guid(14), ByteString(15) foreach ([1, 2, 4, 6, 8, 10, 11, 13, 14, 15] as $idx => $type) { $additionalBody->writeUInt16(0); $additionalBody->writeString("p{$type}"); $additionalBody->writeByte($type); match ($type) { 1, 2, 3 => $additionalBody->writeByte(1), 4, 5 => $additionalBody->writeRawBytes(pack('v', 123)), 6, 7 => $additionalBody->writeUInt32(456), 8, 9 => $additionalBody->writeRawBytes(str_repeat("\x00", 8)), 10 => $additionalBody->writeRawBytes(pack('g', 1.0)), 11 => $additionalBody->writeRawBytes(pack('e', 2.0)), 13 => $additionalBody->writeRawBytes(str_repeat("\x00", 8)), 14 => $additionalBody->writeRawBytes(str_repeat("\x00", 16)), 15, 16 => $additionalBody->writeByteString('bytes'), }; } $additionalBodyBytes = $additionalBody->getBuffer(); $encoder->writeInt32(strlen($additionalBodyBytes)); $encoder->writeRawBytes($additionalBodyBytes); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); it('skips non-ECDHKey ExtensionObject variant (22)', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 17537)); $encoder->writeByte(0x01); $additionalBody = new BinaryEncoder(); $additionalBody->writeInt32(1); $additionalBody->writeUInt16(0); $additionalBody->writeString('OtherExtObj'); $additionalBody->writeByte(22); $additionalBody->writeNodeId(NodeId::numeric(0, 999)); $additionalBody->writeByte(0x01); $extBody = new BinaryEncoder(); $extBody->writeString('some-data'); $extBodyBytes = $extBody->getBuffer(); $additionalBody->writeInt32(strlen($extBodyBytes)); $additionalBody->writeRawBytes($extBodyBytes); $additionalBodyBytes = $additionalBody->getBuffer(); $encoder->writeInt32(strlen($additionalBodyBytes)); $encoder->writeRawBytes($additionalBodyBytes); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); }); describe('SessionService ECC secure paths', function () { beforeEach(function () { $cm = new CertificateManager(); $ms = new MessageSecurity(); $clientResult = $cm->generateSelfSignedCertificate('urn:ecc-client', 'prime256v1'); $this->clientDer = $clientResult['certDer']; $this->clientKey = $clientResult['privateKey']; $serverResult = $cm->generateSelfSignedCertificate('urn:ecc-server', 'prime256v1'); $this->serverDer = $serverResult['certDer']; $this->serverKey = $serverResult['privateKey']; $this->policy = SecurityPolicy::EccNistP256; $this->ms = $ms; $this->cm = $cm; }); it('encodeCreateSessionRequest with ECC policy includes EcdhAdditionalHeader', function () { $channel = setupEccChannelHelper($this->policy, SecurityMode::Sign, $this->clientDer, $this->clientKey, $this->serverDer, $this->serverKey, $this->ms, $this->cm); $session = new SessionService(100, 200, $channel); $msg = $session->encodeCreateSessionRequest(1, 'opc.tcp://localhost:4840'); expect(strlen($msg))->toBeGreaterThan(50); }); it('encodeActivateSessionRequest with ECC policy includes ECDSA client signature', function () { $channel = setupEccChannelHelper($this->policy, SecurityMode::Sign, $this->clientDer, $this->clientKey, $this->serverDer, $this->serverKey, $this->ms, $this->cm); $serverEphemeral = $this->ms->generateEphemeralKeyPair('prime256v1'); $serverNonceBytes = substr($serverEphemeral['publicKeyBytes'], 1); $session = new SessionService(100, 200, $channel); $msg = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 2), null, null, null, null, $serverNonceBytes, $serverNonceBytes, ); expect(strlen($msg))->toBeGreaterThan(50); }); it('encodeActivateSessionRequest with ECC and username builds ECC encrypted secret', function () { $channel = setupEccChannelHelper($this->policy, SecurityMode::SignAndEncrypt, $this->clientDer, $this->clientKey, $this->serverDer, $this->serverKey, $this->ms, $this->cm); $serverEphemeral = $this->ms->generateEphemeralKeyPair('prime256v1'); $serverNonceBytes = substr($serverEphemeral['publicKeyBytes'], 1); $session = new SessionService(100, 200, $channel); $msg = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 2), 'admin', 'password123', null, null, $serverNonceBytes, $serverNonceBytes, ); expect(strlen($msg))->toBeGreaterThan(100); }); }); function setupEccChannelHelper( SecurityPolicy $policy, SecurityMode $mode, string $clientDer, OpenSSLAsymmetricKey $clientKey, string $serverDer, OpenSSLAsymmetricKey $serverKey, MessageSecurity $ms, CertificateManager $cm, ): SecureChannel { $channel = new SecureChannel($policy, $mode, $clientDer, $clientKey, $serverDer); $channel->createOpenSecureChannelMessage(); $serverEphemeral = $ms->generateEphemeralKeyPair('prime256v1'); $serverNonceBytes = substr($serverEphemeral['publicKeyBytes'], 1); $inner = new BinaryEncoder(); $inner->writeUInt32(1); $inner->writeUInt32(1); $inner->writeNodeId(NodeId::numeric(0, 449)); $inner->writeInt64(0); $inner->writeUInt32(1); $inner->writeUInt32(0); $inner->writeByte(0); $inner->writeInt32(0); $inner->writeNodeId(NodeId::numeric(0, 0)); $inner->writeByte(0); $inner->writeUInt32(0); $inner->writeUInt32(100); $inner->writeUInt32(200); $inner->writeInt64(0); $inner->writeUInt32(3600000); $inner->writeByteString($serverNonceBytes); $secHeader = new BinaryEncoder(); $secHeader->writeString($policy->value); $secHeader->writeByteString($serverDer); $secHeader->writeByteString($cm->getThumbprint($clientDer)); $secHeaderBytes = $secHeader->getBuffer(); $coordinateSize = 32; $signatureSize = $coordinateSize * 2; $totalSize = 12 + strlen($secHeaderBytes) + strlen($inner->getBuffer()) + $signatureSize; $headerEncoder = new BinaryEncoder(); $msgHeader = new MessageHeader('OPN', 'F', $totalSize); $msgHeader->encode($headerEncoder); $headerEncoder->writeUInt32(100); $headerBytes = $headerEncoder->getBuffer(); $dataToSign = $headerBytes . $secHeaderBytes . $inner->getBuffer(); $derSig = $ms->asymmetricSign($dataToSign, $serverKey, $policy); $rawSig = $ms->ecdsaDerToRaw($derSig, $coordinateSize); $response = $headerBytes . $secHeaderBytes . $inner->getBuffer() . $rawSig; $channel->processOpenSecureChannelResponse($response); return $channel; } describe('SessionService OPCUA_ECC_DEBUG branch', function () { it('reads eccServerEphemeralKey with OPCUA_ECC_DEBUG env', function () { putenv('OPCUA_ECC_DEBUG=1'); try { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 464); writeCreateSessionResponseBody($encoder); $encoder->writeByteString('ecc-debug-key'); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result['eccServerEphemeralKey'])->toBe('ecc-debug-key'); } finally { putenv('OPCUA_ECC_DEBUG'); } }); it('handles failed readByteString in ECC_DEBUG branch', function () { putenv('OPCUA_ECC_DEBUG=1'); try { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); writeSessionResponsePrefix($encoder, 464); writeCreateSessionResponseBody($encoder); // Append truncated data that will fail readByteString $encoder->writeByte(0xFF); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result)->toHaveKey('eccServerEphemeralKey'); } finally { putenv('OPCUA_ECC_DEBUG'); } }); it('readResponseHeader logs ECC debug for AdditionalHeader', function () { putenv('OPCUA_ECC_DEBUG=1'); try { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 17537)); $encoder->writeByte(0x01); $additionalBody = new BinaryEncoder(); $additionalBody->writeInt32(1); $additionalBody->writeUInt16(0); $additionalBody->writeString('TestKey'); $additionalBody->writeByte(12); $additionalBody->writeString('TestVal'); $additionalBodyBytes = $additionalBody->getBuffer(); $encoder->writeInt32(strlen($additionalBodyBytes)); $encoder->writeRawBytes($additionalBodyBytes); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); } finally { putenv('OPCUA_ECC_DEBUG'); } }); }); describe('SessionService skipVariantValue default branch', function () { it('skips unknown variant type gracefully', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32(0); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 17537)); $encoder->writeByte(0x01); $additionalBody = new BinaryEncoder(); $additionalBody->writeInt32(1); $additionalBody->writeUInt16(0); $additionalBody->writeString('UnknownType'); $additionalBody->writeByte(0); // type 0 ? default => null $additionalBodyBytes = $additionalBody->getBuffer(); $encoder->writeInt32(strlen($additionalBodyBytes)); $encoder->writeRawBytes($additionalBodyBytes); $encoder->writeByteString('nonce'); $encoder->writeInt32(0); $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); }); describe('SessionService ECC short password padding', function () { it('buildEccEncryptedSecret adds extra block for short password', function () { $cm = new CertificateManager(); $ms = new MessageSecurity(); $clientResult = $cm->generateSelfSignedCertificate('urn:ecc-client', 'prime256v1'); $serverResult = $cm->generateSelfSignedCertificate('urn:ecc-server', 'prime256v1'); $channel = setupEccChannelHelper( SecurityPolicy::EccNistP256, SecurityMode::SignAndEncrypt, $clientResult['certDer'], $clientResult['privateKey'], $serverResult['certDer'], $serverResult['privateKey'], $ms, $cm, ); $serverEphemeral = $ms->generateEphemeralKeyPair('prime256v1'); $serverNonceBytes = substr($serverEphemeral['publicKeyBytes'], 1); $session = new SessionService(100, 200, $channel); // Very short password to trigger paddingSize += blockSize $msg = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 2), 'u', 'p', null, null, $serverNonceBytes, $serverNonceBytes, ); expect(strlen($msg))->toBeGreaterThan(100); }); });