基于NetMQ的TLS框架NetMQ.Security的实现分析

前言

介绍

NetMQ是ZeroMQ的C#移植版本,它是对标准socket接口的扩展。它提供了一种异步消息队列,多消息模式,消息过滤(订阅),对多种传输协议的无缝访问。

当前有2个版本正在维护,版本3最新版为3.3.4,版本4最新版本为4.0.0.1。

NetMQ.Security是基于NetMQ的上实现了TLS层。具体描述文档可以看这里

交互过程

TLS连接需要进行4次握手,如下图所示。

打星号是可选项。

支持的协议

NetMQ.Security是在TLS1.2协议上的实现。

TLS协议

想要对TLS有一个了解,可以看该篇文章

在TCP/IP协议之上是Record协议,然后是HandShake协议,HandShake协议包括HandShake协议,Change Spec协议及Alert协议。



NetMQ.Security 实现了TLS中的2层子协议,Handshake协议和Record协议。Alert协议暂时不支持。

  • Handshake协议:实现了服务端和客户端之间相互验证,协商加密和MAC算法以及保密密钥,用来保护在SSL记录中发送的数据。
  • Record协议: 提供保密性和完整性。使用握手协议定义的密钥和mac值对数据进行加密及校验。
  • Alert协议:客户端和服务端之间发生错误时,向对方发送一个警告。如果是致命错误,则算法立即关闭SSL连接,双方还会先删除相关的会话号,秘密和密钥。每个警报消息共2个字节,第1个字节表示错误类型,如果是警报,则值为1,如果是致命错误,则值为2;第2个字节制定实际错误类型。

具体协议格式可以看这篇文章进行了一个归纳。

支持的算法

目前NetMQ.Security只实现了RSA + AES (128/256) + SHA (1/256)

实现

NetMQ.Security是在NetMQ数据交互之间增加了一个SecureChannel层,在创建连接之后进行TLS握手,握手完成及可对数据进行加密和解密。每个连接都有一个SecureChannel,握手完成后会保存加解密的密钥。


public delegate bool VerifyCertificateDelegate(X509Certificate2 certificate2);
public interface ISecureChannel : IDisposable
{
bool SecureChannelReady { get; }
X509Certificate2 Certificate { get; set; }
CipherSuite[] AllowedCipherSuites { get; set; }
void SetVerifyCertificate(VerifyCertificateDelegate verifyCertificate);
bool ProcessMessage(NetMQMessage incomingMessage, IList<NetMQMessage> outgoingMesssages);
NetMQMessage EncryptApplicationMessage(NetMQMessage plainMessage);
NetMQMessage DecryptApplicationMessage(NetMQMessage cipherMessage);
} public class SecureChannel : ISecureChannel
{
private HandshakeLayer m_handshakeLayer;
private RecordLayer m_recordLayer;
private readonly OutgoingMessageBag m_outgoingMessageBag;
private readonly byte[] m_protocolVersion = new byte[] { 3, 3 };
public SecureChannel(ConnectionEnd connectionEnd)
{
m_handshakeLayer = new HandshakeLayer(this, connectionEnd);
m_handshakeLayer.CipherSuiteChange += OnCipherSuiteChangeFromHandshakeLayer;
m_recordLayer = new RecordLayer(m_protocolVersion);
m_outgoingMessageBag = new OutgoingMessageBag(this);
}
...
}
  • SecureChannelReady: 是否完成握手,完成握手后续数据都进行加解密处理。
  • Certificate: X509证书,NetMQ的客户端暂时不支持证书。
  • AllowedCipherSuites: 支持的算法簇,客户端和服务端会对其进行商榷来确定最终的算法。
  • SetVerifyCertificate: 服务端接收到连接时需要加载私钥。
  • ProcessMessage: TLS握手。
  • EncryptApplicationMessage: 加密数据。
  • DecryptApplicationMessage: 解密数据。
  • HandshakeLayer: 握手层。
  • RecordLayer: 记录层。
  • OutgoingMessageBag: 握手时向对端发送的数据包。
  • m_protocolVersion: 协议版本号,源码用的是0,1。我改为了3,3,目前只支持3,3。

代码示例可以看NetMQ的开源开发者之一Doron Somech的博客,NetMQ.Security也是他在NetMQ上实现的,具体代码可以看NetMQ.Securiy,不过我下面的代码进行了一点修改,可以到这里看。

握手

第一次握手
Client Hello

TLS1.2 ClientHello的Record结构如下:

ContentType (1,handshake:22)
ProtocolVersion(2)
握手协议长度:(2)
握手协议数据
HandShakeType(ClientHello:1)
内容长度(3)
内容
TLS版本号(2)
随机数(32,4位时间+28位随机数)
SessionId长度(1)
SessionId(0到32位)
Cipher Suites长度(2)
Cipher Suites列表
压缩方法长度(1)
压缩方法
扩展长度(2)
扩展内容

括号内的数字表示字节长度,括号内,之后是对其具体值或一些解释。

public bool ProcessMessage(NetMQMessage incomingMessage, IList<NetMQMessage> outgoingMesssages)
{
ContentType contentType = ContentType.Handshake; if (incomingMessage != null)
{
...
} bool result = false; if (contentType == ContentType.Handshake)
{
result = m_handshakeLayer.ProcessMessages(incomingMessage, m_outgoingMessageBag);
...
}
else
{
ChangeSuiteChangeArrived = true;
} return (SecureChannelReady = result && ChangeSuiteChangeArrived);
}

ProcessMessage方法对即实现了TLS握手,当incomingMessage参数为null时表示客户端发送的Client Hello。在握手层进行握手处理。

public HandshakeLayer(SecureChannel secureChannel, ConnectionEnd connectionEnd)
{
// SHA256 is a class that computes the SHA-256 (SHA stands for Standard Hashing Algorithm) of it's input.
m_localHash = SHA256.Create();
m_remoteHash = SHA256.Create(); m_secureChannel = secureChannel;
SecurityParameters = new SecurityParameters
{
Entity = connectionEnd,
CompressionAlgorithm = CompressionMethod.Null,
PRFAlgorithm = PRFAlgorithm.SHA256,
CipherType = CipherType.Block
}; AllowedCipherSuites = new[]
{
CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA256,
CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256,
CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA
}; VerifyCertificate = c => c.Verify();
}

TLS协议算法簇的枚举值可以看这里

result = m_handshakeLayer.ProcessMessages(incomingMessage, m_outgoingMessageBag);

public bool ProcessMessages(NetMQMessage incomingMessage, OutgoingMessageBag outgoingMessages)
{
if (incomingMessage == null)
{
if (m_lastReceivedMessage == m_lastSentMessage &&
m_lastSentMessage == HandshakeType.HelloRequest &&
SecurityParameters.Entity == ConnectionEnd.Client)
{
OnHelloRequest(outgoingMessages);
return false;
}
else
{
throw new ArgumentNullException(nameof(incomingMessage));
}
} ...
}
private void OnHelloRequest(OutgoingMessageBag outgoingMessages)
{
var clientHelloMessage = new ClientHelloMessage { RandomNumber = new byte[RandomNumberLength] }; m_rng.GetBytes(clientHelloMessage.RandomNumber); SecurityParameters.ClientRandom = clientHelloMessage.RandomNumber; clientHelloMessage.CipherSuites = AllowedCipherSuites; NetMQMessage outgoingMessage = clientHelloMessage.ToNetMQMessage(); HashLocalAndRemote(outgoingMessage); outgoingMessages.AddHandshakeMessage(outgoingMessage);
m_lastSentMessage = HandshakeType.ClientHello;
}

var clientHelloMessage = new ClientHelloMessage { RandomNumber = new byte[RandomNumberLength] }; internal class ClientHelloMessage : HandshakeMessage
{
/// <summary>
/// Get or set the Random-Number that is a part of the handshake-protocol, as a byte-array.
/// </summary>
public byte[] RandomNumber { get; set; } /// <summary>
/// Get the part of the handshake-protocol that this HandshakeMessage represents
/// - in this case a ClientHello.
/// </summary>
public override HandshakeType HandshakeType => HandshakeType.ClientHello;
...
}

生成一个32字节的随机数。

private RandomNumberGenerator m_rng = new RNGCryptoServiceProvider();
m_rng.GetBytes(clientHelloMessage.RandomNumber);

RNGCryptoServiceProvider用于生成随机数。

生成支持的算法簇

NetMQMessage outgoingMessage = clientHelloMessage.ToNetMQMessage();
public override NetMQMessage ToNetMQMessage()
{
NetMQMessage message = base.ToNetMQMessage(); message.Append(RandomNumber); message.Append(BitConverter.GetBytes(CipherSuites.Length)); byte[] cipherSuitesBytes = new byte[2 * CipherSuites.Length]; int bytesIndex = 0; foreach (CipherSuite cipherSuite in CipherSuites)
{
cipherSuitesBytes[bytesIndex++] = 0;
cipherSuitesBytes[bytesIndex++] = (byte)cipherSuite;
} message.Append(cipherSuitesBytes); return message;
}
internal abstract class HandshakeMessage
{
...
public virtual NetMQMessage ToNetMQMessage()
{
NetMQMessage message = new NetMQMessage();
message.Append(new[] { (byte)HandshakeType }); return message;
}
...
}

和TLS协议进行对比,可以看出NetMQ.Security不支持SessionId。

TLS1.2算法族长度是2字节的16进制值,采用Big-Endian[1],而NetMQ.Security直接使用BitConverter.GetBytes(CipherSuites.Length)获取长度字节,而该方法返回的是四个字节Little-Endian[2]格式的十六进制值。

HashLocalAndRemote(outgoingMessage);
private void HashLocalAndRemote(NetMQMessage message)
{
HashLocal(message);
HashRemote(message);
}
private void HashLocal(NetMQMessage message)
{
Hash(m_localHash, message);
}
private static void Hash(HashAlgorithm hash, NetMQMessage message)
{
foreach (var frame in message)
{
// Access the byte-array that is the frame's buffer.
byte[] bytes = frame.ToByteArray(true); // Compute the hash value for the region of the input byte-array (bytes), starting at index 0,
// and copy the resulting hash value back into the same byte-array.
hash.TransformBlock(bytes, 0, bytes.Length, bytes, 0);
}
}

这个方法的目的是计算发送和接收的握手数据的Hash值,最后在finished包会生产一个12位的验签码,对方会校验该验签码是否一致。

将握手协议前增加记录协议的版本号和ContentType。

outgoingMessages.AddHandshakeMessage(outgoingMessage);
public void AddHandshakeMessage(NetMQMessage message)
{
m_messages.Add(m_secureChannel.InternalEncryptAndWrapMessage(ContentType.Handshake, message));
}
internal NetMQMessage InternalEncryptAndWrapMessage(ContentType contentType, NetMQMessage plainMessage)
{
NetMQMessage encryptedMessage = m_recordLayer.EncryptMessage(contentType, plainMessage);
//encryptedMessage.Push(m_protocolVersion);
encryptedMessage.Push(new[] { (byte)contentType });
encryptedMessage.Push(m_protocolVersion); return encryptedMessage;
}
public NetMQMessage EncryptMessage(ContentType contentType, NetMQMessage plainMessage)
{
if (SecurityParameters.BulkCipherAlgorithm == BulkCipherAlgorithm.Null &&
SecurityParameters.MACAlgorithm == MACAlgorithm.Null)
{
return plainMessage;
}
...
}

Client Hello是明文传输,因此无需加密数据。

NetMQ.Security Client Hello Record协议结构如下

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议数据
HandShakeType(ClientHello:1)
内容
随机数(32,4位时间+28位随机数)
Cipher Suites长度(2)
Cipher Suites列表

由于NetMQ数据包格式已经包含长度,因此NetMQ.Security实现TLS协议的时候把协议中的长度字段都去掉了。

第二次握手
Server Hello

TLS1.2 Server Hello的Record结构如下:

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议长度:(2)
握手协议数据
HandShakeType(Server Hello:)
内容长度(3)
内容
TLS版本号(2)
随机数(32,4位时间+28位随机数)
SessionId长度(1)
SessionId(0到32位)
选择的Cipher Suite(2)
压缩方法
扩展长度
扩展内容

服务端接收到客户端的连接请求后先会验证Record协议的参数是否合法。

public bool ProcessMessage(NetMQMessage incomingMessage, IList<NetMQMessage> outgoingMesssages)
{
ContentType contentType = ContentType.Handshake; if (incomingMessage != null)
{
// Verify that the first two frames are the content-type and the protocol-version, NetMQFrame contentTypeFrame = incomingMessage.Pop(); if (contentTypeFrame.MessageSize != 1)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidFrameLength, "wrong length for message size");
} // Verify that the content-type is either handshake, or change-cipher-suit..
contentType = (ContentType)contentTypeFrame.Buffer[0]; if (contentType != ContentType.ChangeCipherSpec && contentType != ContentType.Handshake)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidContentType, "Unknown content type");
} //标准的没有这个版本
NetMQFrame protocolVersionFrame = incomingMessage.Pop();
byte[] protocolVersionBytes = protocolVersionFrame.ToByteArray(); if (protocolVersionBytes.Length != 2)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidFrameLength, "Wrong length for protocol version frame");
} if (!protocolVersionBytes.SequenceEqual(m_protocolVersion))
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidProtocolVersion, "Wrong protocol version");
} if (ChangeSuiteChangeArrived)
{
incomingMessage = m_recordLayer.DecryptMessage(contentType, incomingMessage);
}
}
...
}

由于NetMQ.Security发送的record协议第一部分是Protorl Version(0,1),第二部分是Content Type,为了和标准的TLS格式一致,我将他们顺序换了一下,且将版本号改为(3,3)。

验证完毕后就需要对Client发来的握手数据进行处理。解析接受到的数据进行处理并生成ServerHello数据

public bool ProcessMessages(NetMQMessage incomingMessage, OutgoingMessageBag outgoingMessages)
{
if (incomingMessage == null)
{
//Client Hello请求
...
} var handshakeType = (HandshakeType)incomingMessage[0].Buffer[0]; switch (handshakeType)
{
case HandshakeType.ClientHello:
OnClientHello(incomingMessage, outgoingMessages);
break;
...
} m_lastReceivedMessage = handshakeType; return m_done;
} private void OnClientHello(NetMQMessage incomingMessage, OutgoingMessageBag outgoingMessages)
{
if (m_lastReceivedMessage != HandshakeType.HelloRequest || m_lastSentMessage != HandshakeType.HelloRequest)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeUnexpectedMessage, "Client Hello received when expecting another message");
} HashLocalAndRemote(incomingMessage); var clientHelloMessage = new ClientHelloMessage();
clientHelloMessage.SetFromNetMQMessage(incomingMessage); SecurityParameters.ClientRandom = clientHelloMessage.RandomNumber; AddServerHelloMessage(outgoingMessages, clientHelloMessage.CipherSuites); AddCertificateMessage(outgoingMessages); AddServerHelloDone(outgoingMessages);
}

获取客户端传来的随机数和支持的算法簇。

clientHelloMessage.SetFromNetMQMessage(incomingMessage);

public virtual void SetFromNetMQMessage(NetMQMessage message)
{
if (message.FrameCount == 0)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidFramesCount, "Malformed message");
} // remove the handshake type column
message.Pop();
}
public override void SetFromNetMQMessage(NetMQMessage message)
{
base.SetFromNetMQMessage(message); if (message.FrameCount != 3)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidFramesCount, "Malformed message");
} // get the random number
NetMQFrame randomNumberFrame = message.Pop();
RandomNumber = randomNumberFrame.ToByteArray(); // get the length of the cipher-suites array
NetMQFrame ciphersLengthFrame = message.Pop();
int ciphersLength = BitConverter.ToInt32(ciphersLengthFrame.Buffer, 0); // get the cipher-suites
NetMQFrame ciphersFrame = message.Pop();
CipherSuites = new CipherSuite[ciphersLength];
for (int i = 0; i < ciphersLength; i++)
{
CipherSuites[i] = (CipherSuite)ciphersFrame.Buffer[i * 2 + 1];
}
}

生成服务端握手数据


AddServerHelloMessage(outgoingMessages, clientHelloMessage.CipherSuites); private void AddServerHelloMessage(OutgoingMessageBag outgoingMessages, CipherSuite[] cipherSuites)
{
var serverHelloMessage = new ServerHelloMessage { RandomNumber = new byte[RandomNumberLength] };
m_rng.GetBytes(serverHelloMessage.RandomNumber); SecurityParameters.ServerRandom = serverHelloMessage.RandomNumber; // in case there is no match the server will return this default
serverHelloMessage.CipherSuite = CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA; foreach (var cipherSuite in cipherSuites)
{
if (AllowedCipherSuites.Contains(cipherSuite))
{
serverHelloMessage.CipherSuite = cipherSuite;
SetCipherSuite(cipherSuite);
break;
}
} NetMQMessage outgoingMessage = serverHelloMessage.ToNetMQMessage();
HashLocalAndRemote(outgoingMessage);
outgoingMessages.AddHandshakeMessage(outgoingMessage);
m_lastSentMessage = HandshakeType.ServerHello;
}

服务端默认支持的算法为TLS_RSA_WITH_AES_128_CBC_SHA,若客户端发送的算法服务端支持的话,则会设置双方商榷的最终算法。

生成Server Hello数据

public override NetMQMessage ToNetMQMessage()
{
NetMQMessage message = base.ToNetMQMessage();
message.Append(RandomNumber);
message.Append(new byte[] { 0, (byte)CipherSuite }); return message;
}
public virtual NetMQMessage ToNetMQMessage()
{
NetMQMessage message = new NetMQMessage();
message.Append(new[] { (byte)HandshakeType }); return message;
}

NetMQ.Security Server Hello Record结构如下

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议数据
HandShakeType(ServerHello:2)
内容
随机数(32,4位时间+28位随机数)
确定的Cipher Suite
Certificate

TLS1.2 Certificate的Record结构如下:

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议长度:(2)
握手协议数据
HandShakeType(Server Hello:11)
长度(3)
内容
服务器公钥列表

将服务器的证书公钥发送给客户端。

AddCertificateMessage(outgoingMessages);

private void AddCertificateMessage(OutgoingMessageBag outgoingMessages)
{
var certificateMessage = new CertificateMessage { Certificate = LocalCertificate }; NetMQMessage outgoingMessage = certificateMessage.ToNetMQMessage();
HashLocalAndRemote(outgoingMessage);
outgoingMessages.AddHandshakeMessage(outgoingMessage);
m_lastSentMessage = HandshakeType.Certificate;
}
public override NetMQMessage ToNetMQMessage()
{
NetMQMessage message = base.ToNetMQMessage(); message.Append(Certificate.Export(X509ContentType.Cert)); return message;
}

目前只支持一个证书

NetMQ.Security Certificate结构如下

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议数据
HandShakeType(Certificate:11)
内容
服务器公钥

NetMQ暂时不支持ServerKeyExchange

NetMQ暂不支持客户端证书 ,即不支持发送CertificateRequest包

ChangeCipherSpec

TLS1.2 ChangeCipherSpec结构如下

ContentType (1,handshake:20)
ProtocolVersion(2:0303)
握手协议长度:(2)
握手协议数据
ChangeCipherSpec(1)

当服务端接收到ChangeCipherSpec,表示密钥已经生成,后续操作都加密处理。

关于ChangeCipherSpec的具体解析看这里

ServerHelloDone

TLS1.2 ServerHelloDone结构如下

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议长度:(2)
握手协议数据
HandShakeType(ServerHelloDone:14)
长度(3)
AddServerHelloDone(outgoingMessages);
private void AddServerHelloDone(OutgoingMessageBag outgoingMessages)
{
var serverHelloDoneMessage = new ServerHelloDoneMessage();
NetMQMessage outgoingMessage = serverHelloDoneMessage.ToNetMQMessage();
HashLocalAndRemote(outgoingMessage);
outgoingMessages.AddHandshakeMessage(outgoingMessage);
m_lastSentMessage = HandshakeType.ServerHelloDone;
}
public virtual NetMQMessage ToNetMQMessage()
{
NetMQMessage message = new NetMQMessage();
message.Append(new[] { (byte)HandshakeType }); return message;
}

NetMQ.Security ServerHelloDone结构如下

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议数据
HandShakeType(ServerHelloDone:14)
客户端处理

接收到数据先对数据格式进行校验,然后解析出ServerHello数据。

public bool ProcessMessages(NetMQMessage incomingMessage, OutgoingMessageBag outgoingMessages)
{
if (incomingMessage == null)
{
...
} var handshakeType = (HandshakeType)incomingMessage[0].Buffer[0]; switch (handshakeType)
{
case HandshakeType.ClientHello:
OnClientHello(incomingMessage, outgoingMessages);
break;
case HandshakeType.ServerHello:
OnServerHello(incomingMessage);
case HandshakeType.Certificate:
OnCertificate(incomingMessage);
break;
case HandshakeType.ServerHelloDone:
OnServerHelloDone(incomingMessage, outgoingMessages);
break;
...
}
...
} private void OnServerHelloDone(NetMQMessage incomingMessage,
OutgoingMessageBag outgoingMessages)
{
if (m_lastReceivedMessage != HandshakeType.Certificate || m_lastSentMessage != HandshakeType.ClientHello)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeUnexpectedMessage, "Server Hello Done received when expecting another message");
} HashLocalAndRemote(incomingMessage); var serverHelloDoneMessage = new ServerHelloDoneMessage();
serverHelloDoneMessage.SetFromNetMQMessage(incomingMessage); AddClientKeyExchange(outgoingMessages); InvokeChangeCipherSuite(); AddFinished(outgoingMessages);
}

从ServerHello获取服务端生成的随机数,然后获取协商好的算法簇。

serverHelloDoneMessage.SetFromNetMQMessage(incomingMessage);
public override void SetFromNetMQMessage(NetMQMessage message)
{
base.SetFromNetMQMessage(message); if (message.FrameCount != 2)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidFramesCount, "Malformed message");
} // Get the random number
NetMQFrame randomNumberFrame = message.Pop();
RandomNumber = randomNumberFrame.ToByteArray(); // Get the cipher suite
NetMQFrame cipherSuiteFrame = message.Pop();
CipherSuite = (CipherSuite)cipherSuiteFrame.Buffer[1];
}

加载并验证服务端公钥合法性

OnCertificate(incomingMessage);
private void OnCertificate(NetMQMessage incomingMessage)
{
if (m_lastReceivedMessage != HandshakeType.ServerHello || m_lastSentMessage != HandshakeType.ClientHello)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeUnexpectedMessage, "Certificate received when expecting another message");
} HashLocalAndRemote(incomingMessage); var certificateMessage = new CertificateMessage();
certificateMessage.SetFromNetMQMessage(incomingMessage);
if (!VerifyCertificate(certificateMessage.Certificate))
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeUnexpectedMessage, "Unable to verify certificate");
} RemoteCertificate = certificateMessage.Certificate;
}

加载企业传来的公钥

certificateMessage.SetFromNetMQMessage(incomingMessage);
public override void SetFromNetMQMessage(NetMQMessage message)
{
base.SetFromNetMQMessage(message); if (message.FrameCount != 1)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidFramesCount, "Malformed message");
} NetMQFrame certificateFrame = message.Pop(); byte[] certificateBytes = certificateFrame.ToByteArray(); Certificate = new X509Certificate2();
Certificate.Import(certificateBytes);
}

对证书进行校验。客户端需要安装服务端的证书文件,由于我们使用的自签名的证书,不知道为什么一直验证失败,网上搜了下别人也有这问题,暂时不管这个问题。

if (!VerifyCertificate(certificateMessage.Certificate))
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeUnexpectedMessage, "Unable to verify certificate");
} VerifyCertificate = c => c.Verify();

暂时不验证证书。

public void SetVerifyCertificate(VerifyCertificateDelegate verifyCertificate)
{
m_handshakeLayer.VerifyCertificate = verifyCertificate;
}
//客户端的SecureChannel对象设置证书校验始终返回true
m_clientSecureChannel.SetVerifyCertificate(c => true);

客户端处理ServerDone

OnServerHelloDone(incomingMessage, outgoingMessages);
private void OnServerHelloDone(NetMQMessage incomingMessage,
OutgoingMessageBag outgoingMessages)
{
if (m_lastReceivedMessage != HandshakeType.Certificate || m_lastSentMessage != HandshakeType.ClientHello)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeUnexpectedMessage, "Server Hello Done received when expecting another message");
} HashLocalAndRemote(incomingMessage); var serverHelloDoneMessage = new ServerHelloDoneMessage();
serverHelloDoneMessage.SetFromNetMQMessage(incomingMessage); AddClientKeyExchange(outgoingMessages); InvokeChangeCipherSuite(); AddFinished(outgoingMessages);
}
第三次握手
ClientKeyExchange

TLS1.2 ClientKeyExchange的Record结构如下:

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议长度:(2)
握手协议数据
HandShakeType(ClientKeyExchange:16)
长度(3)
内容
密钥长度(3)
密钥

接收到服务端的ServerHelloDone之后,客户端需要继续处理第三次握手。

TLS1.2协议提到,若客户端没有发送证书,那在ServerHelloDone之后必须是ClientKeyExchange。

客户端生成一个48位的随机数称之为pre-master key,该随机数需要使用服务端的公钥进行加密,保证只有服务端才能解密出来。至此三个随机数就产生了,这三个随机数即生成对称加密密钥用于后续数据的加密,具体为什么使用三个随机数这篇文章讲的很清楚了。

AddClientKeyExchange(outgoingMessages);

private void AddClientKeyExchange(OutgoingMessageBag outgoingMessages)
{
var clientKeyExchangeMessage = new ClientKeyExchangeMessage(); var premasterSecret = new byte[ClientKeyExchangeMessage.PreMasterSecretLength];
m_rng.GetBytes(premasterSecret); var rsa = RemoteCertificate.PublicKey.Key as RSACryptoServiceProvider;
clientKeyExchangeMessage.EncryptedPreMasterSecret = rsa.Encrypt(premasterSecret, false); GenerateMasterSecret(premasterSecret); NetMQMessage outgoingMessage = clientKeyExchangeMessage.ToNetMQMessage();
HashLocalAndRemote(outgoingMessage);
outgoingMessages.AddHandshakeMessage(outgoingMessage);
m_lastSentMessage = HandshakeType.ClientKeyExchange;
}
private void GenerateMasterSecret(byte[] preMasterSecret)
{
var seed = new byte[RandomNumberLength*2]; Buffer.BlockCopy(SecurityParameters.ClientRandom, 0, seed, 0, RandomNumberLength);
Buffer.BlockCopy(SecurityParameters.ServerRandom, 0, seed, RandomNumberLength, RandomNumberLength); SecurityParameters.MasterSecret =
PRF.Get(preMasterSecret, MasterSecretLabel, seed, MasterSecretLength); Array.Clear(preMasterSecret, 0, preMasterSecret.Length);
}
private static byte[] PRF(byte[] secret, string label, byte[] seed, int iterations)
{
byte[] ls = new byte[label.Length + seed.Length]; Buffer.BlockCopy(Encoding.ASCII.GetBytes(label), 0, ls, 0, label.Length);
Buffer.BlockCopy(seed, 0, ls, label.Length, seed.Length); return PHash(secret, ls, iterations);
} private static byte[] PHash(byte[] secret, byte[] seed, int iterations)
{
using (HMACSHA256 hmac = new HMACSHA256(secret))
{
byte[][] a = new byte[iterations + 1][]; a[0] = seed; for (int i = 0; i < iterations; i++)
{
a[i + 1] = hmac.ComputeHash(a[i]);
} byte[] prf = new byte[iterations * 32]; byte[] buffer = new byte[32 + seed.Length];
Buffer.BlockCopy(seed, 0, buffer, 32, seed.Length); for (int i = 0; i < iterations; i++)
{
Buffer.BlockCopy(a[i + 1], 0, buffer, 0, 32); byte[] hash = hmac.ComputeHash(buffer); Buffer.BlockCopy(hash, 0, prf, 32 * i, 32);
} return prf;
}
}

关于HMAC算法可以看这里

NetMQ.Security 使用的是RSA加密算法,还有Diffie-Hellman算法可以看这里

将加密后的PreMasterSecret发送给服务端

NetMQMessage outgoingMessage = clientKeyExchangeMessage.ToNetMQMessage();
public override NetMQMessage ToNetMQMessage()
{
NetMQMessage message = base.ToNetMQMessage();
message.Append(EncryptedPreMasterSecret); return message;
}

NetMQ.Security ClientKeyExchange的Record结构如下:

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议数据
HandShakeType(ClientKeyExchange:16)
内容
密钥
key计算

关于key计算的算法在这里,下面是具体的实现。

InvokeChangeCipherSuite();
private void InvokeChangeCipherSuite()
{
CipherSuiteChange?.Invoke(this, EventArgs.Empty);
}
private void OnCipherSuiteChangeFromHandshakeLayer(object sender, EventArgs e)
{
NetMQMessage changeCipherMessage = new NetMQMessage();
changeCipherMessage.Append(new byte[] { 1 }); m_outgoingMessageBag.AddCipherChangeMessage(changeCipherMessage); m_recordLayer.SecurityParameters = m_handshakeLayer.SecurityParameters; m_recordLayer.InitalizeCipherSuite();
} public void InitalizeCipherSuite()
{
byte[] clientMAC;
byte[] serverMAC;
byte[] clientEncryptionKey;
byte[] serverEncryptionKey; GenerateKeys(out clientMAC, out serverMAC, out clientEncryptionKey, out serverEncryptionKey); if (SecurityParameters.BulkCipherAlgorithm == BulkCipherAlgorithm.AES)
{
m_decryptionBulkAlgorithm = new AesCryptoServiceProvider
{
Padding = PaddingMode.None,
KeySize = SecurityParameters.EncKeyLength*8,
BlockSize = SecurityParameters.BlockLength*8
}; m_encryptionBulkAlgorithm = new AesCryptoServiceProvider
{
Padding = PaddingMode.None,
KeySize = SecurityParameters.EncKeyLength*8,
BlockSize = SecurityParameters.BlockLength*8
}; if (SecurityParameters.Entity == ConnectionEnd.Client)
{
m_encryptionBulkAlgorithm.Key = clientEncryptionKey;
m_decryptionBulkAlgorithm.Key = serverEncryptionKey;
}
else
{
m_decryptionBulkAlgorithm.Key = clientEncryptionKey;
m_encryptionBulkAlgorithm.Key = serverEncryptionKey;
}
}
else
{
m_decryptionBulkAlgorithm = m_encryptionBulkAlgorithm = null;
} if (SecurityParameters.MACAlgorithm == MACAlgorithm.HMACSha1)
{
if (SecurityParameters.Entity == ConnectionEnd.Client)
{
m_encryptionHMAC = new HMACSHA1(clientMAC);
m_decryptionHMAC = new HMACSHA1(serverMAC);
}
else
{
m_encryptionHMAC = new HMACSHA1(serverMAC);
m_decryptionHMAC = new HMACSHA1(clientMAC);
}
}
else if (SecurityParameters.MACAlgorithm == MACAlgorithm.HMACSha256)
{
if (SecurityParameters.Entity == ConnectionEnd.Client)
{
m_encryptionHMAC = new HMACSHA256(clientMAC);
m_decryptionHMAC = new HMACSHA256(serverMAC);
}
else
{
m_encryptionHMAC = new HMACSHA256(serverMAC);
m_decryptionHMAC = new HMACSHA256(clientMAC);
}
}
else
{
m_encryptionHMAC = m_decryptionHMAC = null;
}
}
private void GenerateKeys(
out byte[] clientMAC, out byte[] serverMAC,
out byte[] clientEncryptionKey, out byte[] serverEncryptionKey)
{
byte[] seed = new byte[HandshakeLayer.RandomNumberLength * 2]; Buffer.BlockCopy(SecurityParameters.ServerRandom, 0, seed, 0, HandshakeLayer.RandomNumberLength);
Buffer.BlockCopy(SecurityParameters.ClientRandom, 0, seed,
HandshakeLayer.RandomNumberLength, HandshakeLayer.RandomNumberLength); int length = (SecurityParameters.FixedIVLength +
SecurityParameters.EncKeyLength + SecurityParameters.MACKeyLength) * 2; if (length > 0)
{
byte[] keyBlock = PRF.Get(SecurityParameters.MasterSecret,
KeyExpansion, seed, length); clientMAC = new byte[SecurityParameters.MACKeyLength];
Buffer.BlockCopy(keyBlock, 0, clientMAC, 0, SecurityParameters.MACKeyLength);
int pos = SecurityParameters.MACKeyLength; serverMAC = new byte[SecurityParameters.MACKeyLength];
Buffer.BlockCopy(keyBlock, pos, serverMAC, 0, SecurityParameters.MACKeyLength);
pos += SecurityParameters.MACKeyLength; clientEncryptionKey = new byte[SecurityParameters.EncKeyLength];
Buffer.BlockCopy(keyBlock, pos, clientEncryptionKey, 0, SecurityParameters.EncKeyLength);
pos += SecurityParameters.EncKeyLength; serverEncryptionKey = new byte[SecurityParameters.EncKeyLength];
Buffer.BlockCopy(keyBlock, pos, serverEncryptionKey, 0, SecurityParameters.EncKeyLength);
}
else
{
clientMAC = serverMAC = clientEncryptionKey = serverEncryptionKey = null;
}
}
ClientFinish

TLS1.2 ClientFinish的Record结构如下:

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议长度:(2)
握手协议数据
HandShakeType(finished:20)
VerifyData
private void AddFinished(OutgoingMessageBag outgoingMessages)
{
m_localHash.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0); byte[] seed = m_localHash.Hash;
#if NET40
m_localHash.Dispose();
#endif
m_localHash = null; var label = SecurityParameters.Entity == ConnectionEnd.Server ? ServerFinishedLabel : ClientFinshedLabel; var finishedMessage = new FinishedMessage
{
VerifyData = PRF.Get(SecurityParameters.MasterSecret, label, seed, FinishedMessage.VerifyDataLength)
}; NetMQMessage outgoingMessage = finishedMessage.ToNetMQMessage();
outgoingMessages.AddHandshakeMessage(outgoingMessage);
m_lastSentMessage = HandshakeType.Finished; if (SecurityParameters.Entity == ConnectionEnd.Client)
{
HashRemote(outgoingMessage);
}
}
NetMQMessage outgoingMessage = finishedMessage.ToNetMQMessage();
public override NetMQMessage ToNetMQMessage()
{
NetMQMessage message = base.ToNetMQMessage();
message.Append(VerifyData); return message;
}

客户端和服务端会对对方发来的VerifyData进行校验。

关于finished报文的信息可以看这里

NetMQ.Security ClientFinish的Record结构如下:

ContentType (1,handshake:22)
ProtocolVersion(2:0303)
握手协议数据
HandShakeType(finished:20)
VerifyData
服务端接收客户端finished

服务端和客户端的结构一致。

OnFinished(incomingMessage, outgoingMessages);
private void OnFinished(NetMQMessage incomingMessage, OutgoingMessageBag outgoingMessages)
{
if (
(SecurityParameters.Entity == ConnectionEnd.Client &&
(!m_secureChannel.ChangeSuiteChangeArrived ||
m_lastReceivedMessage != HandshakeType.ServerHelloDone || m_lastSentMessage != HandshakeType.Finished)) ||
(SecurityParameters.Entity == ConnectionEnd.Server &&
(!m_secureChannel.ChangeSuiteChangeArrived ||
m_lastReceivedMessage != HandshakeType.ClientKeyExchange || m_lastSentMessage != HandshakeType.ServerHelloDone)))
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeUnexpectedMessage, "Finished received when expecting another message");
} if (SecurityParameters.Entity == ConnectionEnd.Server)
{
HashLocal(incomingMessage);
} var finishedMessage = new FinishedMessage();
finishedMessage.SetFromNetMQMessage(incomingMessage); m_remoteHash.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0); byte[] seed = m_remoteHash.Hash; #if NET40
m_remoteHash.Dispose();
#else
m_remoteHash.Clear();
#endif
m_remoteHash = null; var label = SecurityParameters.Entity == ConnectionEnd.Client ? ServerFinishedLabel : ClientFinshedLabel; var verifyData = PRF.Get(SecurityParameters.MasterSecret, label, seed, FinishedMessage.VerifyDataLength); if (!verifyData.SequenceEqual(finishedMessage.VerifyData))
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.HandshakeVerifyData, "peer verify data wrong");
} if (SecurityParameters.Entity == ConnectionEnd.Server)
{
AddFinished(outgoingMessages);
} m_done = true;
}

m_remoteHash.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0);会初始化一个32字节的字节数组当作seed

finishedMessage.SetFromNetMQMessage(incomingMessage);
public override void SetFromNetMQMessage(NetMQMessage message)
{
base.SetFromNetMQMessage(message); if (message.FrameCount != 1)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.InvalidFramesCount, "Malformed message");
} NetMQFrame verifyDataFrame = message.Pop(); VerifyData = verifyDataFrame.ToByteArray();
}
第四次握手
ServerFinish
AddFinished(outgoingMessages);
private void AddFinished(OutgoingMessageBag outgoingMessages)
{
m_localHash.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0); byte[] seed = m_localHash.Hash;
#if NET4
m_localHash.Dispose();
#endif
m_localHash = null; var label = SecurityParameters.Entity == ConnectionEnd.Server ? ServerFinishedLabel : ClientFinshedLabel; var finishedMessage = new FinishedMessage
{
VerifyData = PRF.Get(SecurityParameters.MasterSecret, label, seed, FinishedMessage.VerifyDataLength)
}; NetMQMessage outgoingMessage = finishedMessage.ToNetMQMessage();
outgoingMessages.AddHandshakeMessage(outgoingMessage);
m_lastSentMessage = HandshakeType.Finished; if (SecurityParameters.Entity == ConnectionEnd.Client)
{
HashRemote(outgoingMessage);
}
}
客户端接收服务端Finished

处理和上面服务端处理一致。只是不再发送finished包。至此,四次握手完成,后面就是对数据进行加密和解密。

数据传输

ApplicationData

TLS1.2 ApplicationData的Record结构如下:

ContentType (1,ApplicationData:23)
ProtocolVersion(2:0303)
长度:(2)
加密数据
向量长度(1)
向量
SeqNum(8)
加密数据列表
加密数据长度(2)
加密数据

数据上层也是Record协议。

NetMQ.Security ApplicationData的Record结构如下:

ContentType (1,ApplicationData:23)
ProtocolVersion(2:0303)
加密数据
向量
SeqNum
加密数据列表
数据加密
public NetMQMessage EncryptApplicationMessage([NotNull] NetMQMessage plainMessage)
{
...
return InternalEncryptAndWrapMessage(ContentType.ApplicationData, plainMessage);
}
public NetMQMessage EncryptMessage(ContentType contentType, NetMQMessage plainMessage)
{
...
NetMQMessage cipherMessage = new NetMQMessage(); using (var encryptor = m_encryptionBulkAlgorithm.CreateEncryptor())
{
ulong seqNum = GetAndIncreaseSequneceNumber();
byte[] seqNumBytes = BitConverter.GetBytes(seqNum); var iv = GenerateIV(encryptor, seqNumBytes);
cipherMessage.Append(iv); // including the frame number in the message to make sure the frames are not reordered
int frameIndex = 0; // the first frame is the sequence number and the number of frames to make sure frames was not removed
byte[] frameBytes = new byte[12];
Buffer.BlockCopy(seqNumBytes, 0, frameBytes, 0, 8);
Buffer.BlockCopy(BitConverter.GetBytes(plainMessage.FrameCount), 0, frameBytes, 8, 4); byte[] cipherSeqNumBytes = EncryptBytes(encryptor, contentType, seqNum, frameIndex, frameBytes);
cipherMessage.Append(cipherSeqNumBytes); frameIndex++; foreach (NetMQFrame plainFrame in plainMessage)
{
byte[] cipherBytes = EncryptBytes(encryptor, contentType, seqNum, frameIndex, plainFrame.ToByteArray());
cipherMessage.Append(cipherBytes); frameIndex++;
} return cipherMessage;
}
}
数据解密
public NetMQMessage DecryptApplicationMessage([NotNull] NetMQMessage cipherMessage)
{
...
return m_recordLayer.DecryptMessage(ContentType.ApplicationData, cipherMessage);
}
public NetMQMessage DecryptMessage(ContentType contentType, NetMQMessage cipherMessage)
{
...
NetMQFrame ivFrame = cipherMessage.Pop(); m_decryptionBulkAlgorithm.IV = ivFrame.ToByteArray(); using (var decryptor = m_decryptionBulkAlgorithm.CreateDecryptor())
{
NetMQMessage plainMessage = new NetMQMessage(); NetMQFrame seqNumFrame = cipherMessage.Pop(); byte[] frameBytes;
byte[] seqNumMAC;
byte[] padding; DecryptBytes(decryptor, seqNumFrame.ToByteArray(), out frameBytes, out seqNumMAC, out padding); ulong seqNum = BitConverter.ToUInt64(frameBytes, 0);
int frameCount = BitConverter.ToInt32(frameBytes, 8); int frameIndex = 0; ValidateBytes(contentType, seqNum, frameIndex, frameBytes, seqNumMAC, padding); if (CheckReplayAttack(seqNum))
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.ReplayAttack,
"Message already handled or very old message, might be under replay attack");
} if (frameCount != cipherMessage.FrameCount)
{
throw new NetMQSecurityException(NetMQSecurityErrorCode.EncryptedFramesMissing, "Frames was removed from the encrypted message");
} frameIndex++; foreach (NetMQFrame cipherFrame in cipherMessage)
{
byte[] data;
byte[] mac; DecryptBytes(decryptor, cipherFrame.ToByteArray(), out data, out mac, out padding);
ValidateBytes(contentType, seqNum, frameIndex, data, mac, padding); frameIndex++; plainMessage.Append(data);
} return plainMessage;
}
}

NetMQ.Security目前只支持AES加密解密。

相关文献

  1. Overview of SSL/TLS Encryption
  2. SSL/TLS协议运行机制的概述
  3. 图解SSL/TLS协议
  4. The Transport Layer Security (TLS) Protocol Version 1.2
  5. TLS/SSL报文格式探究
  6. Traffic Analysis of an ssl/tls session



微信扫一扫二维码关注订阅号杰哥技术分享

本文地址:https://www.cnblogs.com/Jack-Blog/p/9015783.html

作者博客:杰哥很忙

欢迎转载,请在明显位置给出出处及链接


  1. Big-Endian:将高序字节存储在起始地址(高位编址) ↩︎

  2. Little-Endian:将低序字节存储在起始地址(低位编址) ↩︎

基于NetMQ的TLS框架NetMQ.Security的实现分析的更多相关文章

  1. 基于LoadRunner构建接口测试框架

    基于LoadRunner构建接口测试框架 http://www.docin.com/p-775544153.html

  2. 8个强大的基于Bootstrap的CSS框架

    做过前端开发的小伙伴们应该对Bootstrap不会陌生,它是由Twitter推出的开源CSS框架,其中包含了很多Web前端开发的工具包和应用组件.当然,和jQuery一样,Bootstrap同时也是一 ...

  3. 基于cocos2d-x的游戏框架设计——李成

    视频:http://v.youku.com/v_show/id_XMzc5ODUyMTI4.html?f=17330006 网易科技讯 3月31日,第四届CocoaChina开发者大会暨Cocos2d ...

  4. 基于MEF的插件框架之总体设计

    基于MEF的插件框架之总体设计 1.MEF框架简介 MEF的全称是Managed Extensibility Framework(MEF),其是.net4.0的组成部分,在3.5上也可以使用.熟悉ja ...

  5. 【百度地图开发之二】基于Fragment的地图框架的使用

    写在前面的话: [百度地图开发之二]基于Fragment的地图框架的使用(博客地址:http://blog.csdn.net/developer_jiangqq),转载请注明. Author:hmji ...

  6. 基于maven的ssm框架整合

    基于maven的ssm框架整合 第一步:通过maven建立一个web项目.                第二步:pom文件导入jar包                              (1 ...

  7. DIY一些基于netty的开源框架

    几款基于netty的开源框架,有益于对netty的理解和学习! 基于netty的http server框架 https://github.com/TogetherOS/cicada 基于netty的即 ...

  8. NET Core微服务之路:自己动手实现Rpc服务框架,基于DotEasy.Rpc服务框架的介绍和集成

    本篇内容属于非实用性(拿来即用)介绍,如对框架设计没兴趣的朋友,请略过. 快一个月没有写博文了,最近忙着两件事;    一:阅读刘墉先生的<说话的魅力>,以一种微妙的,你我大家都会经常遇见 ...

  9. Vulcan 基于Meteor的APollO框架 , grapesjs 用于可视化生成Html 页面

    Vulcan 基于Meteor的APollO框架 :http://vulcanjs.org/ grapesjs 用于可视化生成Html    http://grapesjs.com/

随机推荐

  1. CSS、j&#39;s单行、多行文本溢出显示省略号

    在项目中,由于实际描述文字过多,导致初始页面纵向长度过长,也使得余下信息利用率降低:所以在文字过多的时候,初始化限制行数是有必要的 1. CSS单行文本溢出,显示省略号 <div style=& ...

  2. EUI Scroller实现图片轮播 组件 ItemScroller

    一 自定义组件如下 /** * 文 件 名:ItemScroll.ts * 功 能: 滚动组件 * 内 容: 自定义组件,支持多张图片水平(垂直)切换滚动 * * Example: * 1. 从自定义 ...

  3. linux服务器并发与tcmalloc

    前一天使用pmap查看服务器中自己开发的游戏服务的内存使用情况,发现其中数据存储服务的内存占用率非常高,截图如下. 从截图中可以看出来,分配了大量的64MB左右的内存空间,因为对自己的服务比较了解,知 ...

  4. FLASH和EEPROM的最大区别

    源:http://www.cnblogs.com/bingoo/p/3551753.html FLASH和EEPROM的最大区别是FLASH按扇区操作,EEPROM则按字节操作,二者寻址方法不同,存储 ...

  5. Myeclipse中隐藏jar包

    在package explorer的右上角有一个向下的小三角 点击选择Filter 在打开的对话框中 第一个选框中打上对勾 文字框中填上 *.jar 然后点击OK就行了 多个隐藏内容之间用逗号隔开 如 ...

  6. T-SQL DISTINCT子句 去重复

    语法 以下是DISTINCT关键字的基本语法,用于删除重复记录. SELECT DISTINCT column1, column2,.....columnN FROM table_name WHERE ...

  7. 【bzoj 1076】【SCOI2008】奖励关

    1076: [SCOI2008]奖励关 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 1602  Solved: 891[Submit][Status ...

  8. Android提交数据到服务器的两种方式四种方法

    本帖最后由 yanghe123 于 2012-6-7 09:58 编辑 Android应用开发中,会经常要提交数据到服务器和从服务器得到数据,本文主要是给出了利用http协议采用HttpClient方 ...

  9. wampserver实现外网访问

    1.打开运行WampServer3.0.4,鼠标移到wampserver上去,单击右键,出来个wamp Settings, 按照如图所示,选择Menu item : Online / Offline. ...

  10. N个苹果分给M个人,有多少种分法

    每次分配一个苹果出去,然后再分配N-1个苹果.这里有个注意的地方就是,分那1个苹果的时候,假设还有N个苹果,不是从第一个人开始分,而是从N+1个苹果分配的位置开始,不然的话会产生重复的解.所以i=p不 ...