This guide demonstrates how to implement various authentication mechanisms using the TelnetNegotiationCore AuthenticationProtocol plugin with external cryptographic libraries.
- Overview
- Kerberos V4 Authentication
- Kerberos V5 Authentication
- SRP (Secure Remote Password)
- RSA Authentication
- SSL/TLS Authentication
- Security Considerations
The AuthenticationProtocol provides a flexible callback-based system for implementing any RFC 2941-compliant authentication mechanism. This guide shows how to integrate external cryptographic libraries to implement specific authentication types.
public static class AuthType
{
public const byte NULL = 0;
public const byte KERBEROS_V4 = 1;
public const byte KERBEROS_V5 = 2;
public const byte SPX = 3;
public const byte MINK = 4;
public const byte SRP = 5;
public const byte RSA = 6;
public const byte SSL = 7;
public const byte LOKI = 10;
public const byte SSA = 11;
public const byte KEA_SJ = 12;
public const byte KEA_SJ_INTEG = 13;
public const byte DSS = 14;
public const byte NTLM = 15;
}
public static class AuthModifiers
{
// WHO
public const byte CLIENT_TO_SERVER = 0x00;
public const byte SERVER_TO_CLIENT = 0x01;
public const byte AUTH_WHO_MASK = 0x01;
// HOW
public const byte ONE_WAY = 0x00;
public const byte MUTUAL = 0x02;
public const byte AUTH_HOW_MASK = 0x02;
// ENCRYPT
public const byte ENCRYPT_OFF = 0x00;
public const byte ENCRYPT_USING_TELOPT = 0x04;
public const byte ENCRYPT_AFTER_EXCHANGE = 0x10;
public const byte ENCRYPT_RESERVED = 0x14;
public const byte ENCRYPT_MASK = 0x14;
// CREDENTIALS
public const byte INI_CRED_FWD_OFF = 0x00;
public const byte INI_CRED_FWD_ON = 0x08;
public const byte INI_CRED_FWD_MASK = 0x08;
}RFC: RFC 2942 - Telnet Authentication: Kerberos Version 4
NuGet Package: Kerberos.NET or platform-specific Kerberos libraries
using Kerberos.NET;
using Kerberos.NET.Crypto;
using Kerberos.NET.Entities;
public class KerberosV4AuthenticationHandler
{
private readonly KerberosAuthenticator _authenticator;
public KerberosV4AuthenticationHandler(string realm, byte[] serviceKey)
{
_authenticator = new KerberosAuthenticator(new KerberosValidator(serviceKey));
}
public async Task<byte[]> HandleKerberosV4RequestAsync(byte[] authData)
{
try
{
var authType = authData[0]; // Should be 1 (KERBEROS_V4)
var modifiers = authData[1];
var ticket = authData.Skip(2).ToArray();
// Validate the Kerberos ticket
var identity = await _authenticator.Authenticate(ticket);
if (identity.Name != null)
{
// Authentication successful
// Return ACCEPT status
return new byte[] { authType, modifiers, 0x00 }; // 0x00 = ACCEPT
}
else
{
// Authentication failed
return new byte[] { authType, modifiers, 0x01 }; // 0x01 = REJECT
}
}
catch (Exception ex)
{
// Log error and return rejection
return new byte[] { authData[0], authData[1], 0x01 };
}
}
}
// Usage in TelnetNegotiationCore
var kerberosHandler = new KerberosV4AuthenticationHandler("REALM", serviceKey);
var telnet = await new TelnetInterpreterBuilder()
.UseMode(TelnetInterpreter.TelnetMode.Server)
.AddPlugin<AuthenticationProtocol>()
.WithAuthenticationTypes(async () => new List<(byte, byte)>
{
(AuthType.KERBEROS_V4, AuthModifiers.CLIENT_TO_SERVER | AuthModifiers.MUTUAL)
})
.OnAuthenticationResponse(async (authData) =>
{
var authType = authData[0];
if (authType == AuthType.KERBEROS_V4)
{
var replyData = await kerberosHandler.HandleKerberosV4RequestAsync(authData);
var authPlugin = telnet.PluginManager!.GetPlugin<AuthenticationProtocol>();
await authPlugin!.SendAuthenticationReplyAsync(replyData);
}
})
.BuildAsync();using Kerberos.NET.Client;
using Kerberos.NET.Credentials;
public class KerberosV4Client
{
private readonly KerberosClient _client;
public KerberosV4Client(string realm)
{
_client = new KerberosClient();
}
public async Task<byte[]> GetKerberosTicketAsync(string username, string password, string servicePrincipal)
{
var credential = new KerberosPasswordCredential(username, password);
var ticket = await _client.GetServiceTicket(servicePrincipal, credential);
return ticket.EncodeApplication().ToArray();
}
}
// Usage in TelnetNegotiationCore
var kerberosClient = new KerberosV4Client("REALM");
var telnet = await new TelnetInterpreterBuilder()
.UseMode(TelnetInterpreter.TelnetMode.Client)
.AddPlugin<AuthenticationProtocol>()
.OnAuthenticationRequest(async (authTypePairs) =>
{
// Check if Kerberos V4 is offered
for (int i = 0; i < authTypePairs.Length; i += 2)
{
if (authTypePairs[i] == AuthType.KERBEROS_V4)
{
var modifiers = authTypePairs[i + 1];
var ticket = await kerberosClient.GetKerberosTicketAsync(
"username", "password", "telnet/server@REALM");
var response = new List<byte> { AuthType.KERBEROS_V4, modifiers };
response.AddRange(ticket);
return response.ToArray();
}
}
return null; // No supported auth type
})
.BuildAsync();RFC: RFC 2942 - Telnet Authentication: Kerberos Version 5
NuGet Package: Kerberos.NET
using Kerberos.NET;
using Kerberos.NET.Server;
public class KerberosV5AuthenticationHandler
{
private readonly KerberosAuthenticator _authenticator;
public KerberosV5AuthenticationHandler(byte[] serviceKey)
{
var validator = new KerberosValidator(serviceKey);
_authenticator = new KerberosAuthenticator(validator);
}
public async Task<byte[]> HandleKerberosV5RequestAsync(byte[] authData)
{
try
{
var authType = authData[0]; // Should be 2 (KERBEROS_V5)
var modifiers = authData[1];
var apReq = authData.Skip(2).ToArray();
// Authenticate the AP-REQ
var authenticator = await _authenticator.Authenticate(apReq);
if (authenticator != null)
{
// Generate AP-REP for mutual authentication if required
if ((modifiers & AuthModifiers.MUTUAL) != 0)
{
var apRep = authenticator.GenerateReply();
var response = new List<byte> { authType, modifiers, 0x00 }; // ACCEPT
response.AddRange(apRep);
return response.ToArray();
}
else
{
return new byte[] { authType, modifiers, 0x00 }; // ACCEPT
}
}
else
{
return new byte[] { authType, modifiers, 0x01 }; // REJECT
}
}
catch
{
return new byte[] { authData[0], authData[1], 0x01 }; // REJECT
}
}
}
// Usage similar to Kerberos V4 but with AuthType.KERBEROS_V5RFC: RFC 2944 - Telnet Authentication: SRP
NuGet Package: SecureRemotePassword or custom SRP implementation
using SecureRemotePassword;
public class SRPAuthenticationHandler
{
private readonly SrpServer _srpServer;
private readonly Dictionary<string, string> _userDatabase; // username -> verifier
private Dictionary<string, SrpEphemeral> _sessions = new();
public SRPAuthenticationHandler()
{
_srpServer = new SrpServer();
_userDatabase = new Dictionary<string, string>();
}
public void RegisterUser(string username, string verifier, string salt)
{
_userDatabase[$"{username}:{salt}"] = verifier;
}
public async Task<byte[]> HandleSRPRequestAsync(byte[] authData, string sessionId)
{
var authType = authData[0]; // Should be 5 (SRP)
var modifiers = authData[1];
var command = authData[2]; // SRP sub-command
switch (command)
{
case 0: // SRP_AUTH (client username)
var username = Encoding.UTF8.GetString(authData.Skip(3).ToArray());
// Generate server ephemeral
var salt = GetSaltForUser(username);
var verifier = _userDatabase[$"{username}:{salt}"];
var serverEphemeral = _srpServer.GenerateEphemeral(verifier);
_sessions[sessionId] = serverEphemeral;
// Send challenge with salt and server public key
var response = new List<byte> { authType, modifiers, 1 }; // SRP_CHALLENGE
response.AddRange(Encoding.UTF8.GetBytes(salt));
response.Add(0); // separator
response.AddRange(Convert.FromBase64String(serverEphemeral.Public));
return response.ToArray();
case 2: // SRP_CLIENT_AUTH (client proof)
var clientPublic = Convert.ToBase64String(authData.Skip(3).Take(256).ToArray());
var clientProof = Convert.ToBase64String(authData.Skip(259).ToArray());
var session = _sessions[sessionId];
var serverSession = _srpServer.DeriveSession(
session.Secret, clientPublic, salt, username, verifier);
if (serverSession.Proof == clientProof)
{
// Authentication successful
var acceptResponse = new List<byte> { authType, modifiers, 3 }; // SRP_ACCEPT
acceptResponse.AddRange(Convert.FromBase64String(serverSession.Proof));
return acceptResponse.ToArray();
}
else
{
return new byte[] { authType, modifiers, 4 }; // SRP_REJECT
}
default:
return new byte[] { authType, modifiers, 4 }; // SRP_REJECT
}
}
private string GetSaltForUser(string username)
{
// Retrieve salt from user database
return "user_salt_here";
}
}
// Usage with multi-round authentication
var srpHandler = new SRPAuthenticationHandler();
var telnet = await new TelnetInterpreterBuilder()
.UseMode(TelnetInterpreter.TelnetMode.Server)
.AddPlugin<AuthenticationProtocol>()
.WithAuthenticationTypes(async () => new List<(byte, byte)>
{
(AuthType.SRP, AuthModifiers.CLIENT_TO_SERVER | AuthModifiers.MUTUAL)
})
.OnAuthenticationResponse(async (authData) =>
{
if (authData[0] == AuthType.SRP)
{
var sessionId = telnet.GetHashCode().ToString();
var replyData = await srpHandler.HandleSRPRequestAsync(authData, sessionId);
var authPlugin = telnet.PluginManager!.GetPlugin<AuthenticationProtocol>();
await authPlugin!.SendAuthenticationReplyAsync(replyData);
}
})
.BuildAsync();using SecureRemotePassword;
public class SRPClient
{
private readonly SrpClient _srpClient;
private SrpEphemeral? _clientEphemeral;
public SRPClient()
{
_srpClient = new SrpClient();
}
public byte[] InitiateAuthentication(string username)
{
var response = new List<byte> { AuthType.SRP, 0, 0 }; // SRP_AUTH
response.AddRange(Encoding.UTF8.GetBytes(username));
return response.ToArray();
}
public byte[] RespondToChallenge(byte[] challengeData, string username, string password)
{
// Parse challenge (salt and server public key)
var data = challengeData.Skip(3).ToArray();
var separatorIndex = Array.IndexOf(data, (byte)0);
var salt = Encoding.UTF8.GetString(data.Take(separatorIndex).ToArray());
var serverPublic = Convert.ToBase64String(data.Skip(separatorIndex + 1).ToArray());
// Generate private key from password
var privateKey = _srpClient.DerivePrivateKey(salt, username, password);
// Generate ephemeral
_clientEphemeral = _srpClient.GenerateEphemeral(privateKey);
// Derive session
var clientSession = _srpClient.DeriveSession(
_clientEphemeral.Secret, serverPublic, salt, username, privateKey);
// Send client public key and proof
var response = new List<byte> { AuthType.SRP, 0, 2 }; // SRP_CLIENT_AUTH
response.AddRange(Convert.FromBase64String(_clientEphemeral.Public));
response.AddRange(Convert.FromBase64String(clientSession.Proof));
return response.ToArray();
}
}NuGet Package: System.Security.Cryptography (built-in)
using System.Security.Cryptography;
public class RSAAuthenticationHandler
{
private readonly RSA _rsa;
private readonly Dictionary<string, byte[]> _challenges = new();
public RSAAuthenticationHandler()
{
_rsa = RSA.Create(2048);
}
public byte[] GetPublicKey()
{
return _rsa.ExportRSAPublicKey();
}
public byte[] CreateChallenge(string sessionId)
{
var challenge = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(challenge);
}
_challenges[sessionId] = challenge;
return challenge;
}
public async Task<byte[]> HandleRSAResponseAsync(byte[] authData, string sessionId)
{
var authType = authData[0]; // Should be 6 (RSA)
var modifiers = authData[1];
var signedChallenge = authData.Skip(2).ToArray();
try
{
// Verify the signature
var challenge = _challenges[sessionId];
var isValid = _rsa.VerifyData(
challenge,
signedChallenge,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
if (isValid)
{
return new byte[] { authType, modifiers, 0x00 }; // ACCEPT
}
else
{
return new byte[] { authType, modifiers, 0x01 }; // REJECT
}
}
catch
{
return new byte[] { authType, modifiers, 0x01 }; // REJECT
}
}
}
// Usage
var rsaHandler = new RSAAuthenticationHandler();
var telnet = await new TelnetInterpreterBuilder()
.UseMode(TelnetInterpreter.TelnetMode.Server)
.AddPlugin<AuthenticationProtocol>()
.WithAuthenticationTypes(async () => new List<(byte, byte)>
{
(AuthType.RSA, AuthModifiers.CLIENT_TO_SERVER | AuthModifiers.ONE_WAY)
})
.OnAuthenticationResponse(async (authData) =>
{
if (authData[0] == AuthType.RSA)
{
var sessionId = telnet.GetHashCode().ToString();
// First, send challenge
if (authData.Length == 2) // Initial request
{
var challenge = rsaHandler.CreateChallenge(sessionId);
var challengeMsg = new List<byte> { AuthType.RSA, authData[1], 0x02 }; // CHALLENGE
challengeMsg.AddRange(challenge);
var authPlugin = telnet.PluginManager!.GetPlugin<AuthenticationProtocol>();
await authPlugin!.SendAuthenticationReplyAsync(challengeMsg.ToArray());
}
else // Response to challenge
{
var replyData = await rsaHandler.HandleRSAResponseAsync(authData, sessionId);
var authPlugin = telnet.PluginManager!.GetPlugin<AuthenticationProtocol>();
await authPlugin!.SendAuthenticationReplyAsync(replyData);
}
}
})
.BuildAsync();using System.Security.Cryptography;
public class RSAClientAuth
{
private readonly RSA _clientRsa;
public RSAClientAuth(string privateKeyPem)
{
_clientRsa = RSA.Create();
_clientRsa.ImportFromPem(privateKeyPem);
}
public byte[] SignChallenge(byte[] challenge)
{
return _clientRsa.SignData(challenge, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
// Usage
var rsaClient = new RSAClientAuth(privateKeyPem);
byte[] pendingChallenge = null;
var telnet = await new TelnetInterpreterBuilder()
.UseMode(TelnetInterpreter.TelnetMode.Client)
.AddPlugin<AuthenticationProtocol>()
.OnAuthenticationRequest(async (authTypePairs) =>
{
for (int i = 0; i < authTypePairs.Length; i += 2)
{
if (authTypePairs[i] == AuthType.RSA)
{
var modifiers = authTypePairs[i + 1];
if (pendingChallenge != null)
{
// Respond to challenge
var signature = rsaClient.SignChallenge(pendingChallenge);
var response = new List<byte> { AuthType.RSA, modifiers };
response.AddRange(signature);
pendingChallenge = null;
return response.ToArray();
}
else
{
// Initial request
return new byte[] { AuthType.RSA, modifiers };
}
}
}
return null;
})
.BuildAsync();RFC: RFC 2946 - Telnet Data Encryption Option
NuGet Package: System.Net.Security (built-in)
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
public class SSLAuthenticationHandler
{
private readonly X509Certificate2 _serverCertificate;
private SslStream? _sslStream;
public SSLAuthenticationHandler(string certPath, string certPassword)
{
_serverCertificate = new X509Certificate2(certPath, certPassword);
}
public async Task<bool> NegotiateSSLAsync(Stream networkStream)
{
try
{
_sslStream = new SslStream(networkStream, false);
await _sslStream.AuthenticateAsServerAsync(
_serverCertificate,
clientCertificateRequired: true,
enabledSslProtocols: SslProtocols.Tls12 | SslProtocols.Tls13,
checkCertificateRevocation: true);
return _sslStream.IsAuthenticated && _sslStream.IsEncrypted;
}
catch
{
return false;
}
}
public X509Certificate2? GetClientCertificate()
{
return _sslStream?.RemoteCertificate as X509Certificate2;
}
}
// Usage
var sslHandler = new SSLAuthenticationHandler("server.pfx", "password");
var telnet = await new TelnetInterpreterBuilder()
.UseMode(TelnetInterpreter.TelnetMode.Server)
.AddPlugin<AuthenticationProtocol>()
.WithAuthenticationTypes(async () => new List<(byte, byte)>
{
(AuthType.SSL, AuthModifiers.CLIENT_TO_SERVER | AuthModifiers.MUTUAL |
AuthModifiers.ENCRYPT_AFTER_EXCHANGE)
})
.OnAuthenticationResponse(async (authData) =>
{
if (authData[0] == AuthType.SSL)
{
// SSL negotiation happens at the transport level
// After successful SSL handshake, send ACCEPT
var authPlugin = telnet.PluginManager!.GetPlugin<AuthenticationProtocol>();
await authPlugin!.SendAuthenticationReplyAsync(
new byte[] { AuthType.SSL, authData[1], 0x00 }); // ACCEPT
}
})
.BuildAsync();
// Note: SSL/TLS typically requires wrapping the entire connection
// The telnet authentication is used to signal the SSL upgradeusing System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
public class SSLClientAuth
{
private readonly X509Certificate2 _clientCertificate;
private SslStream? _sslStream;
public SSLClientAuth(string certPath, string certPassword)
{
_clientCertificate = new X509Certificate2(certPath, certPassword);
}
public async Task<bool> NegotiateSSLAsync(Stream networkStream, string targetHost)
{
try
{
_sslStream = new SslStream(
networkStream,
false,
ValidateServerCertificate);
var clientCertificates = new X509Certificate2Collection(_clientCertificate);
await _sslStream.AuthenticateAsClientAsync(
targetHost,
clientCertificates,
SslProtocols.Tls12 | SslProtocols.Tls13,
checkCertificateRevocation: true);
return _sslStream.IsAuthenticated && _sslStream.IsEncrypted;
}
catch
{
return false;
}
}
private bool ValidateServerCertificate(
object sender,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
// Implement certificate validation logic
return sslPolicyErrors == SslPolicyErrors.None;
}
}-
Key Management
- Never hardcode credentials or keys in source code
- Use secure key storage (Azure Key Vault, AWS KMS, etc.)
- Rotate keys regularly
- Use strong key generation (minimum 2048-bit RSA, 256-bit symmetric)
-
Transport Security
- Always use TLS/SSL for the underlying connection when possible
- Validate certificates properly
- Use certificate pinning for critical applications
-
Credential Storage
- Store password hashes, never plaintext passwords
- Use appropriate hashing algorithms (Argon2, PBKDF2, bcrypt)
- Add salt to password hashes
- Use secure random number generators
-
Session Management
- Implement proper session timeouts
- Invalidate sessions after authentication failure
- Use secure session identifiers
- Protect against replay attacks
-
Error Handling
- Don't leak information in error messages
- Log authentication failures securely
- Implement rate limiting and account lockout
- Monitor for suspicious authentication patterns
Kerberos
- Ensure clock synchronization between client and server
- Protect service keys
- Use appropriate encryption types
- Implement ticket lifetime limits
SRP
- Use strong password requirements
- Protect verifier database
- Implement proper session key derivation
- Use appropriate group parameters (minimum 2048-bit)
RSA
- Use minimum 2048-bit keys (3072-bit recommended)
- Implement proper padding (OAEP for encryption, PSS for signatures)
- Protect private keys
- Use secure random number generation for challenges
SSL/TLS
- Use TLS 1.2 or higher
- Disable weak cipher suites
- Implement proper certificate validation
- Use certificate transparency and OCSP stapling
Ensure your implementation complies with relevant standards and regulations:
- FIPS 140-2/3 for cryptographic modules
- PCI DSS for payment applications
- HIPAA for healthcare applications
- GDPR for data protection
- SOC 2 for service organizations
Always test your authentication implementation:
- Unit tests for cryptographic operations
- Integration tests for authentication flows
- Penetration testing
- Security audits
- Fuzz testing
- Performance testing under load
- NIST Cryptographic Standards
- OWASP Authentication Cheat Sheet
- RFC 2941 - Telnet Authentication Option
- RFC 2942 - Telnet Authentication: Kerberos
- RFC 2944 - Telnet Authentication: SRP
- RFC 2946 - Telnet Data Encryption Option
The TelnetNegotiationCore AuthenticationProtocol provides a flexible framework for implementing any RFC 2941-compliant authentication mechanism. By using the callback system with appropriate external cryptographic libraries, you can implement secure authentication while keeping the core library lightweight and focused on protocol negotiation.
Remember to always:
- Use well-tested cryptographic libraries
- Follow security best practices
- Implement proper error handling
- Test thoroughly
- Get security reviews for production deployments