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


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

    前言

    介绍

    NetMQ是ZeroMQ的C#移植版本,它是对标准socket接口的扩展。它提供了一种异步消息队列,多消息模式,消息过滤(订阅),对多种传输协议的无缝访问。
    当前有2个版本正在维护,版本3最新版为3.3.4,版本4最新版本为4.0.0.1。
    NetMQ.Security是基于NetMQ的上实现了TLS层。具体描述文档可以看这里

    交互过程

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

    打星号是可选项。

    支持的协议

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

    TLS协议

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

    在TCP/IP协议之上是Record协议,然后是HandShake协议,HandShake协议包括HandShake协议,Change Spec协议及Alert协议。
    2.png
    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

    20191127212134.png
    微信扫一扫二维码关注订阅号杰哥技术分享
    本文地址:https://www.cnblogs.com/Jack-Blog/p/9015783.html
    作者博客:杰哥很忙
    欢迎转载,请在明显位置给出出处及链接


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

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

  • 相关阅读:
    XCode5中新建工程后强制使用了ARC,如何去掉?
    面向对象程序的设计原则--Head First 设计模式笔记
    ios控件自定义指引
    iOS UITableViewDelegate && UITableViewDataSource 执行顺序
    awakeFromNib方法和viewDidLoad方法区别
    ios 视图的旋转及应用
    线段树模板 (刘汝佳)
    poj 3468
    hdu 2829(四边形优化 && 枚举最后一个放炸弹的地方)
    poj 3517(约瑟夫环问题)
  • 原文地址:https://www.cnblogs.com/Jack-Blog/p/9015783.html
Copyright © 2020-2023  润新知