Skip to content

Commit deacb4c

Browse files
Copilotjogibear9988
andcommitted
Add IEncryptionProvider interface and TLS/Legacy encryption support
- Create IEncryptionProvider interface abstracting the security layer - Create TlsEncryptionProvider wrapping existing OpenSSL/TLS behavior (default) - Create LegacyEncryptionProvider using HarpoS7 for older firmware - Add HarpoS7 as git submodule for legacy encryption support - Add new Connect() overload accepting IEncryptionProvider parameter - Update S7CommPlusConnection to use provider for InitSSL, channel activation, security overhead, and legitimation secrets - Backward compatible: existing Connect(address, password, ...) uses TLS by default Co-authored-by: jogibear9988 <364896+jogibear9988@users.noreply.github.com>
1 parent f324f0d commit deacb4c

File tree

8 files changed

+430
-30
lines changed

8 files changed

+430
-30
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "libs/HarpoS7"]
2+
path = libs/HarpoS7
3+
url = https://github.com/bonk-dev/HarpoS7.git

libs/HarpoS7

Submodule HarpoS7 added at af9e165
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#region License
2+
/******************************************************************************
3+
* S7CommPlusDriver
4+
*
5+
* Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de
6+
*
7+
* This file is part of S7CommPlusDriver.
8+
*
9+
* S7CommPlusDriver is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Lesser General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
/****************************************************************************/
14+
#endregion
15+
16+
namespace S7CommPlusDriver.Encryption
17+
{
18+
/// <summary>
19+
/// Abstraction for the security/encryption layer used during S7CommPlus communication.
20+
/// Two implementations exist:
21+
/// - TlsEncryptionProvider: For newer PLC firmware using TLS 1.3 (OpenSSL)
22+
/// - LegacyEncryptionProvider: For older PLC firmware using Siemens proprietary encryption (HarpoS7)
23+
/// </summary>
24+
public interface IEncryptionProvider
25+
{
26+
/// <summary>
27+
/// Whether the InitSSL request/response protocol step is needed before activation.
28+
/// TLS requires the InitSSL handshake; legacy encryption does not.
29+
/// </summary>
30+
bool RequiresInitSsl { get; }
31+
32+
/// <summary>
33+
/// Activate the security channel on the given S7Client.
34+
/// For TLS: Initializes OpenSSL and performs the TLS 1.3 handshake.
35+
/// For Legacy: No-op at this stage (authentication happens later during CreateObject processing).
36+
/// </summary>
37+
/// <param name="client">The S7Client to activate encryption on</param>
38+
/// <returns>0 on success, error code on failure</returns>
39+
int ActivateChannel(S7Client client);
40+
41+
/// <summary>
42+
/// Deactivate the security channel and release resources.
43+
/// For TLS: Deactivates TLS on the S7Client.
44+
/// For Legacy: Clears session key material.
45+
/// </summary>
46+
/// <param name="client">The S7Client to deactivate encryption on</param>
47+
void DeactivateChannel(S7Client client);
48+
49+
/// <summary>
50+
/// Get the secret used for password legitimation.
51+
/// For TLS: Returns the OMS exporter secret from SSL_export_keying_material.
52+
/// For Legacy: Returns null (legacy legitimation uses a different mechanism).
53+
/// </summary>
54+
/// <param name="client">The S7Client to get the secret from</param>
55+
/// <returns>The secret bytes, or null if not applicable</returns>
56+
byte[] GetSecretForLegitimation(S7Client client);
57+
58+
/// <summary>
59+
/// Additional overhead per PDU from the security layer, used for fragmentation calculation.
60+
/// For TLS: 22 bytes (5 byte TLS header + 17 byte GCM authentication tag).
61+
/// For Legacy: 0 bytes (no TLS encryption overhead).
62+
/// </summary>
63+
int SecurityOverheadPerPdu { get; }
64+
}
65+
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
#region License
2+
/******************************************************************************
3+
* S7CommPlusDriver
4+
*
5+
* Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de
6+
*
7+
* This file is part of S7CommPlusDriver.
8+
*
9+
* S7CommPlusDriver is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Lesser General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
/****************************************************************************/
14+
#endregion
15+
16+
#if !NET6_0
17+
18+
using System;
19+
using System.Text;
20+
using HarpoS7;
21+
using HarpoS7.Auth;
22+
using HarpoS7.Keys;
23+
using HarpoS7.PublicKeys.Impl;
24+
using HarpoS7.Utilities.Auth;
25+
using HarpoS7.Utilities.Extensions;
26+
using HarpoS7.Extensions;
27+
28+
namespace S7CommPlusDriver.Encryption
29+
{
30+
/// <summary>
31+
/// Legacy encryption provider for older PLC firmware that uses Siemens proprietary
32+
/// challenge-based authentication (pre-TLS, implemented by HarpoS7).
33+
///
34+
/// With legacy encryption:
35+
/// - No TLS/SSL is used for data encryption
36+
/// - Authentication is done via challenge-response using HarpoS7
37+
/// - Each packet includes an HMAC-SHA256 integrity digest
38+
/// - The session key is derived from the authentication handshake
39+
/// </summary>
40+
public class LegacyEncryptionProvider : IEncryptionProvider
41+
{
42+
private byte[] m_sessionKey;
43+
private byte[] m_publicKey;
44+
private EPublicKeyFamily m_publicKeyFamily;
45+
private bool m_authenticated;
46+
47+
/// <summary>
48+
/// Legacy encryption does not use the InitSSL protocol step.
49+
/// </summary>
50+
public bool RequiresInitSsl => false;
51+
52+
/// <summary>
53+
/// No TLS overhead with legacy encryption. Data is sent unencrypted with integrity digests.
54+
/// </summary>
55+
public int SecurityOverheadPerPdu => 0;
56+
57+
/// <summary>
58+
/// The session key derived from the HarpoS7 authentication handshake.
59+
/// Available after <see cref="ProcessChallengeResponse"/> completes successfully.
60+
/// </summary>
61+
public byte[] SessionKey => m_sessionKey;
62+
63+
/// <summary>
64+
/// Whether legacy authentication has been completed successfully.
65+
/// </summary>
66+
public bool IsAuthenticated => m_authenticated;
67+
68+
/// <summary>
69+
/// The public key family determined from the PLC fingerprint.
70+
/// </summary>
71+
public EPublicKeyFamily PublicKeyFamily => m_publicKeyFamily;
72+
73+
/// <summary>
74+
/// No-op for legacy encryption. The security channel is established
75+
/// during the CreateObject response processing via <see cref="ProcessChallengeResponse"/>.
76+
/// </summary>
77+
public int ActivateChannel(S7Client client)
78+
{
79+
// Legacy encryption doesn't activate TLS - authentication happens later
80+
return 0;
81+
}
82+
83+
/// <summary>
84+
/// Clears session key material.
85+
/// </summary>
86+
public void DeactivateChannel(S7Client client)
87+
{
88+
if (m_sessionKey != null)
89+
{
90+
Array.Clear(m_sessionKey, 0, m_sessionKey.Length);
91+
m_sessionKey = null;
92+
}
93+
m_authenticated = false;
94+
}
95+
96+
/// <summary>
97+
/// Returns null for legacy encryption. Legacy legitimation uses the HarpoS7
98+
/// <see cref="LegitimateScheme"/> directly with the session key.
99+
/// </summary>
100+
public byte[] GetSecretForLegitimation(S7Client client)
101+
{
102+
return null;
103+
}
104+
105+
/// <summary>
106+
/// Processes the challenge received from the PLC during the CreateObject response.
107+
/// Performs the HarpoS7 authentication and produces the encrypted key blob
108+
/// and session key.
109+
/// </summary>
110+
/// <param name="challenge">The 20-byte challenge received from the PLC</param>
111+
/// <param name="fingerprint">The public key fingerprint string from the PLC (e.g., "00:181B7B0847D11694")</param>
112+
/// <param name="keyBlob">Output: The encrypted key blob to send back to the PLC</param>
113+
/// <returns>0 on success, error code on failure</returns>
114+
public int ProcessChallengeResponse(byte[] challenge, string fingerprint, out byte[] keyBlob)
115+
{
116+
keyBlob = null;
117+
118+
try
119+
{
120+
// Determine public key family from fingerprint
121+
m_publicKeyFamily = fingerprint.ToPublicKeyFamily();
122+
123+
// Look up the public key from the default key store
124+
var store = new DefaultPublicKeyStore();
125+
m_publicKey = new byte[store.GetPublicKeyLength(fingerprint)];
126+
store.ReadPublicKey(m_publicKey.AsSpan(), fingerprint);
127+
128+
// Determine blob length based on family
129+
int blobLength = (m_publicKeyFamily == EPublicKeyFamily.PlcSim)
130+
? CommonConstants.EncryptedBlobLengthPlcSim
131+
: CommonConstants.EncryptedBlobLengthRealPlc;
132+
133+
keyBlob = new byte[blobLength];
134+
m_sessionKey = new byte[Constants.SessionKeyLength];
135+
136+
// Perform HarpoS7 authentication
137+
LegacyAuthenticationScheme.Authenticate(
138+
keyBlob.AsSpan(),
139+
m_sessionKey.AsSpan(),
140+
challenge.AsSpan(),
141+
m_publicKey.AsSpan(),
142+
m_publicKeyFamily);
143+
144+
m_authenticated = true;
145+
return 0;
146+
}
147+
catch (Exception ex)
148+
{
149+
Console.WriteLine("LegacyEncryptionProvider - ProcessChallengeResponse: Error: " + ex.Message);
150+
return S7Consts.errCliAccessDenied;
151+
}
152+
}
153+
154+
/// <summary>
155+
/// Derives the key ID from the public key for the SetMultiVariables authentication request.
156+
/// </summary>
157+
/// <returns>8-byte public key ID</returns>
158+
public byte[] GetPublicKeyId()
159+
{
160+
if (m_publicKey == null) return null;
161+
var pubKeyId = new byte[Constants.KeyIdLength];
162+
m_publicKey.DeriveKeyId(pubKeyId);
163+
return pubKeyId;
164+
}
165+
166+
/// <summary>
167+
/// Derives the key ID from the session key for the SetMultiVariables authentication request.
168+
/// </summary>
169+
/// <returns>8-byte session key ID</returns>
170+
public byte[] GetSessionKeyId()
171+
{
172+
if (m_sessionKey == null) return null;
173+
var sessionKeyId = new byte[Constants.KeyIdLength];
174+
m_sessionKey.DeriveKeyId(sessionKeyId);
175+
return sessionKeyId;
176+
}
177+
178+
/// <summary>
179+
/// Calculates the HMAC-SHA256 packet integrity digest for a given packet payload.
180+
/// This must be included with every packet in legacy authentication mode.
181+
/// </summary>
182+
/// <param name="packetData">The S7CommPlus packet data (without header/trailer)</param>
183+
/// <returns>32-byte HMAC-SHA256 digest</returns>
184+
public byte[] CalculatePacketDigest(byte[] packetData)
185+
{
186+
if (m_sessionKey == null)
187+
throw new InvalidOperationException("Session key not available. Authenticate first.");
188+
189+
var digest = new byte[HarpoS7.Integrity.HarpoPacketDigest.DigestLength];
190+
HarpoS7.Integrity.HarpoPacketDigest.CalculateDigest(
191+
digest.AsSpan(),
192+
packetData.AsSpan(),
193+
m_sessionKey.AsSpan());
194+
return digest;
195+
}
196+
197+
/// <summary>
198+
/// Solves the legitimation challenge for password-protected PLCs in legacy mode.
199+
/// </summary>
200+
/// <param name="challenge">The legitimation challenge from the PLC (20 bytes)</param>
201+
/// <param name="password">The access password</param>
202+
/// <returns>The solved legitimation blob to send back to the PLC</returns>
203+
public byte[] SolveLegitimationChallenge(byte[] challenge, string password)
204+
{
205+
if (m_sessionKey == null || m_publicKey == null)
206+
throw new InvalidOperationException("Session not authenticated. Call ProcessChallengeResponse first.");
207+
208+
if (m_publicKeyFamily == EPublicKeyFamily.PlcSim)
209+
{
210+
var blobData = new byte[CommonConstants.EncryptedLegitimationBlobLengthPlcSim];
211+
LegitimateScheme.SolveLegitimateChallengePlcSim(
212+
blobData.AsSpan(),
213+
challenge.AsSpan(),
214+
m_publicKey.AsSpan(),
215+
m_sessionKey.AsSpan(),
216+
password);
217+
return blobData;
218+
}
219+
else
220+
{
221+
var blobData = new byte[CommonConstants.EncryptedLegitimationBlobLengthRealPlc];
222+
LegitimateScheme.SolveLegitimateChallengeRealPlc(
223+
blobData.AsSpan(),
224+
challenge.AsSpan(),
225+
m_publicKey.AsSpan(),
226+
m_publicKeyFamily,
227+
m_sessionKey.AsSpan(),
228+
password);
229+
return blobData;
230+
}
231+
}
232+
}
233+
}
234+
235+
#endif
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#region License
2+
/******************************************************************************
3+
* S7CommPlusDriver
4+
*
5+
* Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de
6+
*
7+
* This file is part of S7CommPlusDriver.
8+
*
9+
* S7CommPlusDriver is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Lesser General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
/****************************************************************************/
14+
#endregion
15+
16+
namespace S7CommPlusDriver.Encryption
17+
{
18+
/// <summary>
19+
/// TLS 1.3 encryption provider for newer PLC firmware.
20+
/// This wraps the existing OpenSSL/TLS behavior and is the default provider.
21+
/// </summary>
22+
public class TlsEncryptionProvider : IEncryptionProvider
23+
{
24+
/// <summary>
25+
/// TLS requires the InitSSL request/response handshake step.
26+
/// </summary>
27+
public bool RequiresInitSsl => true;
28+
29+
/// <summary>
30+
/// TLS overhead per PDU: 5 byte TLS header + 17 byte GCM authentication tag.
31+
/// </summary>
32+
public int SecurityOverheadPerPdu => 5 + 17;
33+
34+
/// <summary>
35+
/// Activates TLS 1.3 on the S7Client via OpenSSL.
36+
/// </summary>
37+
public int ActivateChannel(S7Client client)
38+
{
39+
return client.SslActivate();
40+
}
41+
42+
/// <summary>
43+
/// Deactivates TLS on the S7Client.
44+
/// </summary>
45+
public void DeactivateChannel(S7Client client)
46+
{
47+
client.SslDeactivate();
48+
}
49+
50+
/// <summary>
51+
/// Returns the OMS exporter secret derived from TLS key material export.
52+
/// This is used for password legitimation on newer firmware.
53+
/// </summary>
54+
public byte[] GetSecretForLegitimation(S7Client client)
55+
{
56+
return client.getOMSExporterSecret();
57+
}
58+
}
59+
}

src/S7CommPlusDriver/Legitimation/Legitimation.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using S7CommPlusDriver.Legitimation;
1+
using S7CommPlusDriver.Encryption;
2+
using S7CommPlusDriver.Legitimation;
23
using System;
34
using System.Collections.Generic;
45
using System.IO;
@@ -160,8 +161,8 @@ private int legitimateNew(string password, string username = "")
160161
byte[] challengeResponse;
161162
if (omsSecret == null || omsSecret.Length != 32)
162163
{
163-
// Create oms exporter secret
164-
omsSecret = m_client.getOMSExporterSecret();
164+
// Get secret from encryption provider
165+
omsSecret = m_encryptionProvider.GetSecretForLegitimation(m_client);
165166
}
166167
// Roll key
167168
byte[] key = LegitimationCrypto.sha256(omsSecret);

0 commit comments

Comments
 (0)