├── .gitignore ├── examples ├── html │ └── index.html ├── pem │ ├── ecdsa_key.pem │ ├── ecdsa_crt.pem │ ├── crt.pem │ └── key.pem ├── tls-client.php ├── tls-server-https.php └── tls-server-echo.php ├── src ├── Exceptions │ ├── TLSException.php │ └── TLSAlertException.php ├── DataConverterInterface.php ├── TLSContext.php ├── Handshake │ ├── ServerHelloDone.php │ ├── ServerKeyExchange.php │ ├── HelloRequest.php │ ├── HandshakeFactory.php │ ├── HandshakeType.php │ ├── Certificate.php │ ├── HandshakeAbstract.php │ ├── Finished.php │ ├── ServerHello.php │ ├── ClientKeyExchange.php │ └── ClientHello.php ├── Content │ ├── ApplicationData.php │ ├── ChangeCipherSpec.php │ ├── Alert.php │ ├── ContentAbstract.php │ ├── ClientContent.php │ └── ServerContent.php ├── ProtocolAbstract.php ├── Buffer.php ├── Extensions │ ├── ExtensionAbstract.php │ ├── TLSExtensions.php │ ├── SignatureAlgorithm.php │ └── Curve.php ├── ContentType.php ├── Debug.php ├── Record │ ├── CipherRecordAbstract.php │ ├── Record.php │ ├── AEADCipherRecord.php │ └── BlockCipherRecord.php ├── EcDSA.php ├── X509.php ├── Config.php ├── TLS.php ├── ConnectionDuplex.php ├── AEADGcm.php ├── Prf.php ├── EcDH.php ├── Core.php └── CipherSuites.php ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /examples/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | TLS sample 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Exceptions/TLSException.php: -------------------------------------------------------------------------------- 1 | isServer(), $config); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Handshake/ServerHelloDone.php: -------------------------------------------------------------------------------- 1 | msgType = 14; 19 | $this->length = 0; 20 | 21 | return $this->getBinHeader(); 22 | } 23 | 24 | public function debugInfo() 25 | { 26 | return "[HandshakeType::ServerHelloDone]\n"; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/Content/ApplicationData.php: -------------------------------------------------------------------------------- 1 | core = $core; 15 | } 16 | 17 | public function encode($data) 18 | { 19 | $this->core->getBufferIn()->append($data); 20 | } 21 | 22 | public function decode(){} 23 | 24 | public function debugInfo() 25 | { 26 | return "[ApplicationData]\n" 27 | . "Data Length: " . $this->core->getBufferIn()->length(); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rnaga/php-tls", 3 | "description": "PHP TLS library", 4 | "type": "library", 5 | "homepage": "https://github.com/rnaga/phptls", 6 | "keywords": ["tls", "tlsv1.1", "tlsv1.2", "non-blocking"], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Ryohei Nagatsuka", 11 | "role": "Author" 12 | }], 13 | "autoload": { 14 | 15 | "psr-4": { 16 | "PTLS\\": "src/" 17 | } 18 | }, 19 | 20 | "require": { 21 | "php": ">=7.0.0", 22 | "ext-gmp": "*", 23 | "mdanter/ecc" : "*", 24 | "spomky-labs/php-aes-gcm": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ProtocolAbstract.php: -------------------------------------------------------------------------------- 1 | $property; 19 | 20 | return $default; 21 | } 22 | 23 | /** 24 | * Set properties 25 | */ 26 | public function set($property, $value) 27 | { 28 | $this->$property = $value; 29 | return $this; 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/Buffer.php: -------------------------------------------------------------------------------- 1 | buffer = $data; 15 | return $this; 16 | } 17 | 18 | public function append($data) 19 | { 20 | $this->buffer .= $data; 21 | return $this; 22 | } 23 | 24 | public function flush() 25 | { 26 | $data = $this->buffer; 27 | $this->buffer = null; 28 | return $data; 29 | } 30 | 31 | public function get() 32 | { 33 | return $this->buffer; 34 | } 35 | 36 | public function length() 37 | { 38 | return strlen($this->buffer); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /examples/pem/ecdsa_crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICCTCCAY6gAwIBAgIJAK5X4ktWKaalMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYT 3 | AlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29t 4 | cGFueSBMdGQwHhcNMTcwMjAxMjI0ODIyWhcNMTkwMjAxMjI0ODIyWjBCMQswCQYD 5 | VQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0 6 | IENvbXBhbnkgTHRkMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMdF/Txq184X0ZQ3I 7 | CQar7hOb1H0qbYlnkKnFWhaIIdN/cnWevSarh2j/BBlVdjYcUdENSQlTcHdtIF7M 8 | ID5fseTVFwNeaqJ7GDVO9vpnwrlvtDzXLMkKIv65q5CzuqlRo1AwTjAdBgNVHQ4E 9 | FgQUlmkDsDte1HlJrGveMr/TCIPdMPgwHwYDVR0jBBgwFoAUlmkDsDte1HlJrGve 10 | Mr/TCIPdMPgwDAYDVR0TBAUwAwEB/zAKBggqhkjOPQQDAgNpADBmAjEA85L/6aUY 11 | BeVeHYKlvRklIZ4JCNnDicSU/r5QpME0I5iYzqsBocPjrhAVrTPk4D6OAjEAhiA4 12 | 84FJrR3xCjQ4c03KLq5atOBNEju/6tfDqhQWArZ0FzDgfiTrFcGVgzzHKQ/Q 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /src/Extensions/ExtensionAbstract.php: -------------------------------------------------------------------------------- 1 | core = $core; 16 | } 17 | 18 | protected function decodeHeader() 19 | { 20 | // MsgType 21 | $header = Core::_pack('C', 0 ) 22 | . Core::_pack('C', $this->extType) 23 | // Length 24 | . Core::_pack( 'n', $this->length ); 25 | 26 | return $header; 27 | } 28 | 29 | abstract public function onEncodeClientHello($type, $data); 30 | abstract public function onDecodeClientHello(); 31 | abstract public function onDecodeServerHello(); 32 | } 33 | -------------------------------------------------------------------------------- /src/Handshake/ServerKeyExchange.php: -------------------------------------------------------------------------------- 1 | core; 17 | $extensions = $core->extensions; 18 | 19 | $this->encodeHeader($data); 20 | 21 | if( $core->cipherSuite->isECDHEEnabled() ) 22 | { 23 | $extensions->call('Curve', 'encodeServerKeyExchange', null, $data); 24 | } 25 | } 26 | 27 | public function decode() 28 | { 29 | // Extensions\Curve::decodeServerKeyExchange 30 | } 31 | 32 | public function debugInfo() 33 | { 34 | return "[HandshakeType::ServerKeyExchange]\n" 35 | . "Lengh: " . $this->length . "\n"; 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/Content/ChangeCipherSpec.php: -------------------------------------------------------------------------------- 1 | core; 19 | 20 | $data = $this->encodeHeader($data); 21 | 22 | if( $core->isServer ) 23 | { 24 | throw new TLSAlertException(Alert::create(Alert::UNEXPECTED_MESSAGE), 25 | "Server received Hello Request"); 26 | } 27 | else 28 | { 29 | // We don't re-negotiate 30 | throw new TLSAlertException(Alert::create(Alert::NO_RENEGOTIATION), "No renegotiation"); 31 | } 32 | } 33 | 34 | public function decode(){} 35 | 36 | public function debugInfo() 37 | { 38 | return "[HandshakeType::HelloRequest]\n"; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/Exceptions/TLSAlertException.php: -------------------------------------------------------------------------------- 1 | output = null; 17 | $this->alert = $alert; 18 | $message = $this->alert->toString() . " " . $message; 19 | parent::__construct($message, $alert->getDescCode()); 20 | } 21 | 22 | public function setOutput(Core $core) 23 | { 24 | $alert = $this->alert; 25 | 26 | if( $alert->fromPeer() ) return; 27 | 28 | $recordOut = $core->getOutDuplex()->getRecord(); 29 | 30 | $payload = $alert->decode(); 31 | 32 | $this->output = $recordOut->set('contentType', ContentType::ALERT) 33 | ->set('payload', $payload ) 34 | ->decode(); 35 | } 36 | 37 | public function decode() 38 | { 39 | return $this->output; 40 | } 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ryohei Nagatsuka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP TLS 2 | 3 | TLS library written in PHP. 4 | 5 | Features: 6 | - TLSv1.1 and TLSv1.2 7 | - ECDHE(secp256r1, secp384r1) 8 | - Signature Alogorithm(TLSv1.2) 9 | - AEAD(GCM) 10 | - ECDSA 11 | 12 | Supported Cipher Suite: 13 | - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 14 | - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 15 | - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 16 | - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA 17 | - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 18 | - TLS_RSA_WITH_AES_256_GCM_SHA384 19 | - TLS_RSA_WITH_AES_128_GCM_SHA256 20 | - TLS_RSA_WITH_AES_256_CBC_SHA256 21 | - TLS_RSA_WITH_AES_256_CBC_SHA 22 | - TLS_RSA_WITH_AES_128_CBC_SHA256 23 | - TLS_RSA_WITH_AES_128_CBC_SHA 24 | 25 | Usage: 26 | 27 | ```php 28 | // Create a TLS Engine 29 | $tls = TLSContext::createTLS(TLSContext::getServerConfig([])); 30 | 31 | // Receive raw data from a client 32 | $data = stream_socket_recvfrom($clientSocket); 33 | 34 | // Pass raw data to TLS Engine for conversion 35 | $tls->encode($data); 36 | 37 | // Get the plaintext from TLS Engine 38 | $in = $tls->input(); 39 | 40 | // Convert plaintext into TLS format 41 | $out = $tls->output("Hello World")->decode(); 42 | 43 | // Send the output to a client 44 | stream_socket_sendto($clientSocket, $out); 45 | 46 | ``` 47 | -------------------------------------------------------------------------------- /examples/pem/crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDVzCCAj+gAwIBAgIJAP17HQcpth1uMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV 3 | BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg 4 | Q29tcGFueSBMdGQwHhcNMTcwMjAzMTkxNDUxWhcNMTgwMjAzMTkxNDUxWjBCMQsw 5 | CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh 6 | dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 7 | 48GDyN+/Fnx4DLriXWOYLeq6HxpbUn/rspTZhqmKtglGmeHbpIeeqwvjcljb0QoP 8 | /dF7tTObGrqa98qCT6imyJGIbpBn38giULD6GzoYRPRF1mPvJr4okRAaKEG6WGOv 9 | zkTgmA1fPcRgX4IA/5WN/W2Q6YmpgY/L618KD9zT8oJygcVBUL9OCMW22QfqG9e3 10 | CjtM+33MeNOnW5jhLRY/HvNIce9EIzO3986pp3LFd2P79haydyC+J/LeaM6UuUSc 11 | mbkSHMRuOIk5L6NzbxilJlG1dql9Q1rB+07w2+9jXwqEJrlscx44dzI6HB22nnNn 12 | IuTjsEhnKuoiDOOIB8mQWwIDAQABo1AwTjAdBgNVHQ4EFgQUq0MjrIYJfWZI/J/m 13 | 2ZFprZ4GFuEwHwYDVR0jBBgwFoAUq0MjrIYJfWZI/J/m2ZFprZ4GFuEwDAYDVR0T 14 | BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAq2PCRt1rvw8Uw1+XD7seRk+Jguqv 15 | bke0yu/xs/OUmL7iCOO6G2bBF8JH92zeyfBC664nQ3zjDMvOIJ2N2pWYxeqJ1SbY 16 | b57PS4gVJkSqpjCpxmQabijLzAcvzzvum8mT6bUo+skAIavscCbzeZyPJbpWlkJM 17 | l/bUt5maxrRB0NAIhl0syH8nT8umrvnXHlvTh3YLIglrhGEyS5JtbJJ0RdUwaQ8U 18 | 8uUsyQ3b0RRGIQhoxoLZJYg1uTwnq8G9B0YcJ1jMb3onnelIiBzzCDuQkK8Y4XfS 19 | gT6TNVUB/X0P/QhyQzOmgXYQ9qzqV+eSSdrUSNc2ktlz1NjeutpYPI4kvw== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /src/Handshake/HandshakeFactory.php: -------------------------------------------------------------------------------- 1 | core = $core; 12 | } 13 | 14 | public function getProtocolVersion() 15 | { 16 | list($vMajor, $vMinor) = $core->getVersion(); 17 | return "1." . ($vMinor - 1); 18 | } 19 | 20 | public function getCertificates() 21 | { 22 | $crtDers = $this->core->getCrtDers(); 23 | 24 | if( !count( $crtDers ) ) return ''; 25 | 26 | $output = []; 27 | 28 | foreach( $crtDers as $der ) 29 | { 30 | $output[] = X509::crtDerToPem($der); 31 | } 32 | 33 | return implode("\n", $output) . "\n"; 34 | } 35 | 36 | public function getPrivateKey() 37 | { 38 | if( !$this->core->isServer ) 39 | return; 40 | 41 | return $this->core->getConfig('private_key'); 42 | } 43 | 44 | public function getUsingCipherSuite() 45 | { 46 | if( is_null( $this->core->cipherSuite ) ) 47 | return; 48 | 49 | return $this->core->cipherSuite->debugInfo(); 50 | } 51 | 52 | public function getSessionID() 53 | { 54 | return $this->core->getSessionID(); 55 | } 56 | 57 | public function getMasterSecret() 58 | { 59 | return $this->core->getMasterSecret(); 60 | } 61 | 62 | public function getRecordStatus() 63 | { 64 | $recordIn = $this->core->getInDuplex()->getRecord(); 65 | return "=================RecordStatus===================\n" 66 | . $recordIn->debugInfo() 67 | . "\n================================================\n"; 68 | } 69 | 70 | } 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /examples/pem/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA48GDyN+/Fnx4DLriXWOYLeq6HxpbUn/rspTZhqmKtglGmeHb 3 | pIeeqwvjcljb0QoP/dF7tTObGrqa98qCT6imyJGIbpBn38giULD6GzoYRPRF1mPv 4 | Jr4okRAaKEG6WGOvzkTgmA1fPcRgX4IA/5WN/W2Q6YmpgY/L618KD9zT8oJygcVB 5 | UL9OCMW22QfqG9e3CjtM+33MeNOnW5jhLRY/HvNIce9EIzO3986pp3LFd2P79hay 6 | dyC+J/LeaM6UuUScmbkSHMRuOIk5L6NzbxilJlG1dql9Q1rB+07w2+9jXwqEJrls 7 | cx44dzI6HB22nnNnIuTjsEhnKuoiDOOIB8mQWwIDAQABAoIBAA64l1gIfkVpziik 8 | e7UlltSA837HKVfvxMBKNrUpiDmz58hSfOvpiCvuHMlrTOn6CIaTX7eamk/j99hE 9 | Up/rerxEC9l5o/0m5nDov4G7A46Ra2IsG20ZpW2a6NXpSa4k41wlv46Ubq+DXedO 10 | c9oRRJg42MD/kOE69idEgVX9JAXsoVO9FJkITU9eyePsFx+U87xzOt2HIzV4qIM5 11 | 066PgU6A5jAHQq9P9f0Q6oc9egBrIneOjiC2dms8FgEF3mGseQ5CVQjK7FZFNFI8 12 | luAapdviJQ7/DXeN4NweQri+aQEU/KGgPARvx9NZpisU7f0NBxi6OcI8izmhYfgk 13 | bYh6t0ECgYEA/A9s3jWyU4tI2rxU/xvGUf5gSFZQ7PyAISL1FjtjRIJG9MJEE2Xu 14 | q3U+ttFfRZCZsIxMGV6B5+hSdmpF5A3MoYEVCZwuelA9ByXhUPEf7LLbsahUwcBW 15 | ijrI0S3ML6958qsfne+fy8nxWx7CiL2mA0GHs9XbKd8Zp/1Mlt4BC7MCgYEA51DX 16 | CYj8o/fNClyjvwYbWN/r+yvxmk/axwOO091xPABgnw1gFnnoJJ8nTP6wEgnGArq2 17 | MLW20kc194Tym2yvqEHF604Qr+g0WTu+MyFLRq5x2TD0AmzQ8cTtqFJzWmT9PyJY 18 | 6/cOe/VxnDnYVaUnENu5YWGpGhzhbDCQEnfZdLkCgYEApep1bBOGbY26iBj+Deq3 19 | cZNyx0rh/Az9PlKnZ1nyLs9ea5BQhUBMiVokzOwmvUDAmcDP+scF1aRMW5v6o3Id 20 | 55VzRp8izOpyMXlSxhfiPslA6cF3AQ5dKKInO+HcjOsB56WSq0BnPBqSn7swrBfB 21 | 5lTFQbAckoRDfBu0xX4ezBcCgYAPzUDzYM1JcjsqkwuImCU0HA/l16mojf33DHIs 22 | XPtISwpOsz42KtvF7K1loBxtDwfc1d5uU2uKz9yK7SUZowIY721S1sFjEuzIj8E9 23 | gu++g1o5qRNyOvbHHVnS3tcMfMukDXefnv/5TdoV/wmdSCv0Bd6THXXM1946YtLk 24 | wBU12QKBgQDBbUNLaadxXbD0kt0hF00+GtwLqyvc0/BBVe76ZyvhWuy5Peo0tO82 25 | JEhNmOyqvjwyjOl3Bb42jv/I4JMChMPERNln2JvJIlUuSU2w+DxABHuKKI1Q4pUU 26 | KMXv0nXXeBAESldUEDbAOsdeRbcRF4TaprCyncd1XVeF+/c80+N21w== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/Record/CipherRecordAbstract.php: -------------------------------------------------------------------------------- 1 | maxLength = self::MAX_CIPHER_LENGTH; 23 | } 24 | 25 | /** 26 | * @Override 27 | */ 28 | public function get($property, $default = null) 29 | { 30 | if( $property == 'length' ) 31 | { 32 | return 5 + $this->encLength; 33 | } 34 | 35 | return parent::get($property); 36 | } 37 | 38 | protected function getSeq() 39 | { 40 | if( is_null( $this->seq ) ) 41 | { 42 | $this->seq = self::getZeroSeq(); 43 | } 44 | 45 | return implode('', $this->seq ); 46 | } 47 | 48 | protected function incrementSeq() 49 | { 50 | if( is_null( $this->seq ) ) 51 | { 52 | $this->seq = $this->getZeroSeq(); 53 | } 54 | 55 | for( $i = 7; $i >= 0; $i--) 56 | { 57 | $num = Core::_unpack('C', $this->seq[$i]) + 1; 58 | $this->seq[$i] = Core::_pack('C', $num ); 59 | 60 | if( $num%256 > 0 ) break; 61 | } 62 | } 63 | 64 | protected static function getZeroSeq() 65 | { 66 | $seq = []; 67 | for($i = 0; $i < 8; $i++) 68 | $seq[$i] = Core::_pack('C', 0); 69 | 70 | return $seq; 71 | } 72 | 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/Handshake/Certificate.php: -------------------------------------------------------------------------------- 1 | core; 17 | 18 | $data = $this->encodeHeader($data); 19 | 20 | $crtsLength = Core::_unpack('N', $data[0] . $data[1] . $data[2] . 0x00 ) >> 8; 21 | $crtsData = substr( $data, 3, $crtsLength ); 22 | 23 | for( $i = 0; $i < $crtsLength; ) 24 | { 25 | $crtLength = Core::_unpack('n', $crtsData[$i+1] . $crtsData[$i+2] ); 26 | if( 0 >= (int)$crtLength ) break; 27 | 28 | $crtDers[] = substr($crtsData, $i+3, $crtLength); 29 | 30 | $i += $crtLength + 3; 31 | } 32 | 33 | $core->setCrtDers($crtDers); 34 | } 35 | 36 | public function decode() 37 | { 38 | $core = $this->core; 39 | $crtDers = $core->getCrtDers(); 40 | 41 | $crtData = ''; 42 | 43 | foreach( $crtDers as $crtDer ) 44 | { 45 | $crtLength = strlen($crtDer); 46 | 47 | // Cert Length 48 | $crtData .= Core::_pack('C', 0x00 ) 49 | . Core::_pack('n', $crtLength ) 50 | . $crtDer; 51 | } 52 | 53 | $data = Core::_pack('C', 0x00 ) 54 | . Core::_pack('n', strlen($crtData)) 55 | . $crtData; 56 | 57 | $this->msgType = HandshakeType::CERTIFICATE; 58 | $this->length = strlen($data); 59 | 60 | return $this->getBinHeader() . $data; 61 | } 62 | 63 | public function debugInfo() 64 | { 65 | $core = $this->core; 66 | $crtDers = $core->getCrtDers(); 67 | 68 | return "[HandshakeType::Certificate]\n" 69 | . "Lengh: " . $this->length . "\n" 70 | . "Number of Certificates: " . count($crtDers) . "\n"; 71 | } 72 | } 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/EcDSA.php: -------------------------------------------------------------------------------- 1 | adapter = EccFactory::getAdapter(); 29 | $this->pemSerializer = new PemPrivateKeySerializer(new DerPrivateKeySerializer($this->adapter)); 30 | $this->privateKey = $this->pemSerializer->parse($privateKeyPem); 31 | $this->gen = $this->privateKey->getPoint(); 32 | } 33 | 34 | public function getPrivateKey() 35 | { 36 | return $this->privateKey; 37 | } 38 | 39 | public function getGenerator() 40 | { 41 | return $this->gen; 42 | } 43 | 44 | public function getSignature($dataSign, $hashAlgo) 45 | { 46 | $signer = new Signer($this->adapter); 47 | $privateKey = $this->getPrivateKey(); 48 | 49 | $hash = $signer->hashData($this->gen, $hashAlgo, $dataSign); 50 | 51 | // $random = RandomGeneratorFactory::getRandomGenerator(); 52 | $random = RandomGeneratorFactory::getHmacRandomGenerator($privateKey, $hash, $hashAlgo); 53 | 54 | $randomK = $random->generate($this->gen->getOrder()); 55 | 56 | $signature = $signer->sign($privateKey, $hash, $randomK); 57 | 58 | $serializer = new DerSignatureSerializer(); 59 | $serializedSig = $serializer->serialize($signature); 60 | 61 | return $serializedSig; 62 | } 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/X509.php: -------------------------------------------------------------------------------- 1 | isServer = $isServer; 19 | $this->config = []; 20 | 21 | if( $isServer ) 22 | $this->encodeServerConfig($arrConfig); 23 | else 24 | $this->encodeClientConfig($arrConfig); 25 | } 26 | 27 | private function encodeClientConfig(array $arrConfig) 28 | { 29 | // Setting up TLS version 30 | if( isset( $arrConfig['version'] ) ) 31 | $this->config['version'] = $arrConfig['version']; 32 | } 33 | 34 | private function encodeServerConfig(array $arrConfig) 35 | { 36 | if( !isset( $arrConfig['key_pair_files'] ) ) 37 | throw new TLSException("No keyPairFiles"); 38 | 39 | $keyPairFiles = $arrConfig['key_pair_files']; 40 | 41 | if( !isset( $keyPairFiles['cert']) || !isset( $keyPairFiles['key'] ) ) 42 | throw new TLSException("Invalid keyPair"); 43 | 44 | $pemCrtFiles = $keyPairFiles['cert']; 45 | $pemPriFile = $keyPairFiles['key'][0]; 46 | $pemPriPassCode = $keyPairFiles['key'][1]; 47 | 48 | $pemPrivate = file_get_contents($pemPriFile); 49 | 50 | $this->config['crt_ders'] = X509::crtFilePemToDer($pemCrtFiles); 51 | 52 | $this->config['private_key_pem'] = $pemPrivate; 53 | 54 | // Check for ECDSA 55 | if( EcDSA::isValidPrivateKey($pemPrivate) ) 56 | { 57 | $this->config['is_ecdsa'] = true; 58 | // Get a ECDSA instance for Signature Algorithm 59 | $this->config['ecdsa'] = new EcDSA($pemPrivate); 60 | } 61 | else // RSA 62 | { 63 | $this->config['is_ecdsa'] = false; 64 | $this->config['private_key'] = X509::getPrivateKey($pemPrivate, $pemPriPassCode); 65 | } 66 | } 67 | 68 | public function get($key, $default = null) 69 | { 70 | return ( isset($this->config[$key] ) ) ? $this->config[$key] : $default; 71 | } 72 | 73 | public function isServer() 74 | { 75 | return $this->isServer; 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /examples/tls-client.php: -------------------------------------------------------------------------------- 1 | isHandshaked() ) 37 | { 38 | $out = $tls->decode(); 39 | 40 | if( strlen( $out ) > 0 ) 41 | $w = stream_socket_sendto($socket, $out); 42 | } 43 | else 44 | { 45 | if( !$requestSent ) 46 | { 47 | $out = $tls->output($httpRequest)->decode(); 48 | stream_socket_sendto($socket, $out); 49 | $requestSent = true; 50 | } 51 | 52 | $response .= $tls->input(); 53 | 54 | if( $tls->isClosed()) 55 | break; 56 | } 57 | 58 | $read = [$socket]; 59 | $write = $except = []; 60 | 61 | // Wait for a server to send data 62 | $n = stream_select($read, $write, $except, 60); 63 | 64 | // Receive raw data from a server 65 | $data = stream_socket_recvfrom($socket, 16384); 66 | 67 | if( $data == "" ) 68 | { 69 | echo "Disconnted\n"; 70 | break; 71 | } 72 | 73 | try 74 | { 75 | // Calling encode method to 76 | $tls->encode($data); 77 | } 78 | catch(TLSAlertException $e) 79 | { 80 | 81 | echo "Alert: " . $e->getMessage() . "\n"; 82 | 83 | if( strlen( $out = $e->decode() ) ) 84 | stream_socket_sendto($socket, $out); 85 | 86 | break; 87 | } 88 | 89 | //echo $debug->getRecordStatus(); 90 | } 91 | 92 | stream_socket_shutdown( $socket, STREAM_SHUT_WR ); 93 | 94 | echo "Received content length: " . strlen($response) . "\n"; 95 | echo $response; 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/Handshake/HandshakeAbstract.php: -------------------------------------------------------------------------------- 1 | core = $core; 18 | } 19 | 20 | public function encodeHeader($data) 21 | { 22 | // https://tools.ietf.org/html/rfc5246#section-7.4 23 | $this->msgType = $msgType = Core::_unpack( 'C', $data[0] ); 24 | $this->length = $length = Core::_unpack( 'N', $data[1] . $data[2] . $data[3] . 0x00 ) >> 8; 25 | 26 | $data = substr($data, 4, $length); 27 | 28 | $this->payload = $data; 29 | 30 | if( $this->length != strlen($data) ) 31 | throw new TLSAlertException(Alert::create(Alert::ILLEGAL_PARAMETER), "Invalid Handshake payload: " . $this->length); 32 | 33 | return $data; 34 | } 35 | 36 | // @Override 37 | public function get($property, $default = null) 38 | { 39 | if( $property == 'length' ) 40 | return $this->length + 4; 41 | 42 | parent::get($property, $default); 43 | } 44 | 45 | public function getBinHeader() 46 | { 47 | // MsgType 48 | $header = Core::_pack('C', $this->msgType) 49 | // Length 50 | . Core::_pack('C', 0x00 ) 51 | . Core::_pack( 'n', $this->length ); 52 | 53 | return $header; 54 | } 55 | 56 | public function setMsgType($msgType) 57 | { 58 | $this->msgType = $msgType; 59 | } 60 | 61 | /** 62 | * for Client Hello and Server Hello 63 | */ 64 | protected function encodeExtensions($data) 65 | { 66 | $extensions = []; 67 | 68 | for( $j = 0; $j < strlen($data); ) 69 | { 70 | $extType = Core::_unpack( 'n', $data[$j] . $data[$j+1] ); 71 | $extDataLen = Core::_unpack( 'n', $data[$j+2] . $data[$j+3] ); 72 | 73 | if( 0 == $extDataLen ) 74 | { 75 | $j += 2 + 2; 76 | continue; 77 | } 78 | 79 | $extData = substr( $data, $j+4, $extDataLen ); 80 | 81 | $j += 2 + 2 + $extDataLen; 82 | 83 | $extensions[] = ['type' => $extType, 'data' => $extData]; 84 | } 85 | 86 | return $extensions; 87 | } 88 | } 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/TLS.php: -------------------------------------------------------------------------------- 1 | core = new Core($isServer, $config); 16 | $this->bufferOut = new Buffer(); 17 | } 18 | 19 | public function isHandshaked() 20 | { 21 | return $this->core->isHandshaked; 22 | } 23 | 24 | public function isClosed() 25 | { 26 | return $this->core->isClosed; 27 | } 28 | 29 | public function input() 30 | { 31 | $core = $this->core; 32 | return $core->getBufferIn()->flush(); 33 | } 34 | 35 | public function encode($data) 36 | { 37 | $core = $this->core; 38 | 39 | $in = $core->getInDuplex(); 40 | 41 | try{ 42 | $in->encodeRecord($data); 43 | } 44 | catch(TLSAlertException $e) 45 | { 46 | // Set output if any 47 | $e->setOutput($core); 48 | 49 | // Re-throw so that the upper layer can catch it 50 | throw $e; 51 | } 52 | } 53 | 54 | public function decode() 55 | { 56 | $core = $this->core; 57 | $coreBufferOut = $core->getBufferOut(); 58 | 59 | $out = ''; 60 | 61 | if( $coreBufferOut->length() > 0 ) 62 | $out = $coreBufferOut->flush(); 63 | 64 | if( !$core->isHandshaked ) 65 | return $out; 66 | 67 | $payload = $this->bufferOut->flush(); 68 | 69 | if( 0 >= strlen($payload) ) 70 | return $out; 71 | 72 | $connOut = $core->getOutDuplex(); 73 | 74 | $out .= $connOut->decodeRecord($payload); 75 | 76 | return $out; 77 | } 78 | 79 | public function output($data, $isAppend = false) 80 | { 81 | $core = $this->core; 82 | $bufferOut = $this->bufferOut; 83 | 84 | if( !$core->isHandshaked ) 85 | throw new TLSException('Handshake is not done'); 86 | 87 | if( $isAppend ) 88 | $bufferOut->append($data); 89 | else 90 | $bufferOut->set($data); 91 | 92 | return $this; 93 | } 94 | 95 | public function append($data) 96 | { 97 | return $this->setOutput($data, true); 98 | } 99 | 100 | public function getDebug() 101 | { 102 | return new Debug($this->core); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ConnectionDuplex.php: -------------------------------------------------------------------------------- 1 | core = $core; 25 | $this->record = new Record($this); 26 | $this->isCipherChanged = false; 27 | } 28 | 29 | public function getCore() 30 | { 31 | return $this->core; 32 | } 33 | 34 | /** 35 | * Switch over to cipher record 36 | */ 37 | public function cipherChanged() 38 | { 39 | $core = $this->core; 40 | 41 | if( $core->cipherSuite->getCipherType() == CipherSuites::CIPHER_TYPE_AEAD ) 42 | $this->cipherRecord = new AEADCipherRecord($this); 43 | else 44 | $this->cipherRecord = new BlockCipherRecord($this); 45 | 46 | $this->isCipherChanged = true; 47 | return $this->cipherRecord; 48 | } 49 | 50 | /** 51 | * Set secret keys needed for encryption 52 | */ 53 | public function setSecretKeys(array $secretKeys) 54 | { 55 | $this->MAC = $secretKeys['MAC']; 56 | $this->IV = $secretKeys['IV']; 57 | $this->Key = $secretKeys['Key']; 58 | } 59 | 60 | public function getRecord() 61 | { 62 | if( $this->isCipherChanged ) 63 | $record = $this->cipherRecord; 64 | else 65 | $record = $this->record; 66 | 67 | return $record; 68 | } 69 | 70 | public function getContentType() 71 | { 72 | $record = $this->getRecord(); 73 | return $record->contentType; 74 | } 75 | 76 | public function encodeRecord($data) 77 | { 78 | while( !is_null($data) && strlen($data) > 0 ) 79 | { 80 | $strlen = strlen($data); 81 | 82 | $record = $this->getRecord(); 83 | $record->encode($data); 84 | $data = $record->get('dataRest'); 85 | 86 | if( $strlen == strlen($data) ) 87 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Failed on encodeRecord"); 88 | } 89 | } 90 | 91 | public function decodeRecord($data) 92 | { 93 | $core = $this->core; 94 | 95 | if(!$core->isHandshaked) 96 | throw new TLSException("Handshake is not finished"); 97 | 98 | if( 0 >= strlen($data) ) 99 | throw new TLSException("Empty output"); 100 | 101 | $record = $this->getRecord(); 102 | 103 | $record->set('contentType', ContentType::APPLICATION_DATA) 104 | ->set('payload', $data ); 105 | 106 | return $record->decode(); 107 | } 108 | } 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/Content/Alert.php: -------------------------------------------------------------------------------- 1 | descCode = $descCode; 51 | $alert->level = $level; 52 | $alert->fromPeer = false; 53 | 54 | return $alert; 55 | } 56 | 57 | public static function getConst($value) 58 | { 59 | $class = new \ReflectionClass(__CLASS__); 60 | $constants = array_flip($class->getConstants()); 61 | 62 | return $constants[$value]; 63 | } 64 | 65 | public function getDescCode() 66 | { 67 | return $this->descCode; 68 | } 69 | 70 | public function fromPeer() 71 | { 72 | return $this->fromPeer; 73 | } 74 | 75 | public function encode($data) 76 | { 77 | $this->level = Core::_unpack('C', $data[0]); 78 | $this->descCode = Core::_unpack('C', $data[1]); 79 | 80 | // We got alert message from peer 81 | $this->fromPeer = true; 82 | } 83 | 84 | public function decode() 85 | { 86 | return Core::_pack('C', $this->level) 87 | . Core::_pack('C', $this->descCode); 88 | } 89 | 90 | public function toString() 91 | { 92 | $desc = $this->getConst($this->descCode); 93 | $msg = $desc . " " . $this->descCode; 94 | 95 | return $msg; 96 | } 97 | 98 | public function debugInfo(){ $this->toString(); } 99 | } 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/Handshake/Finished.php: -------------------------------------------------------------------------------- 1 | core; 21 | 22 | $protoVersion = $core->getProtocolVersion(); 23 | 24 | $finishedLabel = ( $isServer ) ? "server finished" : "client finished"; 25 | $prf = $core->prf; 26 | 27 | /* 28 | * [TLS 1.1] 29 | * https://www.ietf.org/rfc/rfc2246.txt 7.4.9 30 | * verify_data 31 | * PRF(master_secret, finished_label, MD5(handshake_messages) + 32 | * SHA-1(handshake_messages)) [0..11]; 33 | */ 34 | if( $protoVersion == 31 ) 35 | { 36 | $seedHash = md5($handshakeMessages, true) . sha1($handshakeMessages, true); 37 | } 38 | /* 39 | * [TLS 1.2] 40 | * 7.4.0 https://tools.ietf.org/html/rfc5246 41 | * verify_data 42 | * PRF(master_secret, finished_label, Hash(handshake_messages)) 43 | * [0..verify_data_length-1]; 44 | */ 45 | else // 1.2 46 | { 47 | $cipherSuite = $core->cipherSuite; 48 | $seedHash = hash($cipherSuite->getHashAlogV33(), $handshakeMessages, true); 49 | } 50 | 51 | $masterSecret = $core->getMasterSecret(); 52 | 53 | $verifyData = $prf->prf(self::PRF_LENGTH, $masterSecret, $finishedLabel, $seedHash); 54 | 55 | return $verifyData; 56 | } 57 | 58 | public function encode($data) 59 | { 60 | $core = $this->core; 61 | 62 | $data = $this->encodeHeader($data); 63 | 64 | /* 65 | * https://tools.ietf.org/html/rfc5246#section-7.4.9 66 | * 67 | * Note that this 68 | * representation has the same encoding as with previous versions. 69 | * Future cipher suites MAY specify other lengths but such length 70 | * MUST be at least 12 bytes. 71 | */ 72 | $this->verifyData = substr($data, 0, $this->length); 73 | 74 | // Get all handshakeMessages excluding this message 75 | $handshakeMessages = $core->getHandshakeMessages(1); 76 | 77 | // Get verify data 78 | $verifyData = $this->getVerifyData($core->isServer ^ true, $handshakeMessages); 79 | 80 | if( $this->verifyData != $verifyData ) 81 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), 82 | "Handshake Finished: verifyData mismatched:" . base64_encode( $this->verifyData ) . "<=>" . base64_encode( $verifyData )); 83 | } 84 | 85 | public function decode() 86 | { 87 | $core = $this->core; 88 | 89 | $handshakeMessages = $core->getHandshakeMessages(); 90 | 91 | $verifyData = $this->getVerifyData($core->isServer, $handshakeMessages); 92 | 93 | $this->msgType = 20; 94 | $this->length = strlen($verifyData); 95 | return $this->getBinHeader() . $verifyData; 96 | } 97 | 98 | public function debugInfo() 99 | { 100 | /* 101 | * struct { 102 | * opaque verify_data[verify_data_length]; 103 | * } Finished; 104 | */ 105 | return "[HandshakeType::Finished]\n" 106 | . "Verify Data: " . base64_encode($this->verifyData) . "\n"; 107 | } 108 | } 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/Extensions/TLSExtensions.php: -------------------------------------------------------------------------------- 1 | 'Curve', 19 | self::TYPE_EC_POINT_FORMATS => 'Curve', 20 | self::TYPE_SIGNATURE_ALGORITHM => 'SignatureAlgorithm', 21 | ]; 22 | 23 | private $core; 24 | private $instances; 25 | 26 | public function __construct(Core $core) 27 | { 28 | $this->core = $core; 29 | $this->instances = []; 30 | 31 | foreach(self::$supportedList as $type => $className) 32 | { 33 | if( isset( $this->instances[$className] ) ) 34 | continue; 35 | 36 | $this->instances[$className] = $this->getExtension($type); 37 | } 38 | } 39 | 40 | private function getExtension($type) 41 | { 42 | switch($type) 43 | { 44 | case self::TYPE_ELLIPTIC_CURVES: 45 | case self::TYPE_EC_POINT_FORMATS: 46 | return new Curve($this->core); 47 | case self::TYPE_SIGNATURE_ALGORITHM: 48 | return new SignatureAlgorithm($this->core); 49 | } 50 | 51 | return null; 52 | } 53 | 54 | private function onEncode(string $method, array $extensions) 55 | { 56 | // $extensions[] = ['type' => $extType, 'data' => $extData]; 57 | foreach( $extensions as $extension ) 58 | { 59 | if( !isset( $extension['type'] ) || !isset( $extension['data'] ) ) 60 | throw new Exception("Invalid Extension Paramenter"); 61 | 62 | $type = $extension['type']; 63 | $data = $extension['data']; 64 | 65 | if( array_key_exists( $type, self::$supportedList ) ) 66 | { 67 | $className = self::$supportedList[$type]; 68 | 69 | if( !isset( $this->instances[$className] ) ) 70 | $this->instances[$className] = $this->getExtension($type); 71 | 72 | $ins = $this->instances[$className]; 73 | 74 | if( !$ins instanceof ExtensionAbstract ) 75 | throw new Exception("Not ExtensionAbstract"); 76 | 77 | [$ins, $method]($type, $data); 78 | } 79 | } 80 | } 81 | 82 | private function onDecode(string $method) 83 | { 84 | $out = ''; 85 | 86 | if( !count( $this->instances ) ) 87 | { 88 | return $out; 89 | } 90 | 91 | foreach( $this->instances as $className => $ins ) 92 | { 93 | $out .= [$ins, $method](); 94 | } 95 | 96 | return $out; 97 | } 98 | 99 | public function __call(string $method, array $args) 100 | { 101 | if( false !== strpos($method, 'onEncode') ) 102 | { 103 | return $this->onEncode($method, $args[0]); 104 | } 105 | 106 | if( false !== strpos($method, 'onDecode') ) 107 | { 108 | return $this->onDecode($method); 109 | } 110 | } 111 | 112 | /** 113 | * API call 114 | */ 115 | public function call($className, $method, $default, ...$args) 116 | { 117 | if( isset( $this->instances[$className] ) && 118 | is_callable( [$this->instances[$className], $method] ) ) 119 | { 120 | return [$this->instances[$className], $method](...$args); 121 | } 122 | 123 | return $default; 124 | } 125 | } 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/Content/ContentAbstract.php: -------------------------------------------------------------------------------- 1 | core = $core; 23 | $this->content = 24 | $this->appData = null; 25 | } 26 | 27 | /** 28 | * https://tools.ietf.org/html/rfc5246#section-6.2.1 29 | * 30 | * enum { 31 | * change_cipher_spec(20), alert(21), handshake(22), 32 | * application_data(23), (255) 33 | * } ContentType; 34 | */ 35 | public function encodeContent($contentType, $payload, $record) 36 | { 37 | $core = $this->core; 38 | 39 | switch($contentType) 40 | { 41 | case ContentType::HANDSHAKE: 42 | 43 | // Count handshake for later to create finished message 44 | $core->countHandshakeMessages($payload); 45 | 46 | $this->encodeHandshake($payload); 47 | break; 48 | 49 | case ContentType::CHANGE_CIPHER_SPEC: 50 | $this->encodeChangeCipherSpec($payload); 51 | $record->cipherChanged(); 52 | break; 53 | 54 | case ContentType::ALERT: 55 | $this->encodeAlert($payload); 56 | break; 57 | 58 | case ContentType::APPLICATION_DATA: 59 | $this->encodeApplicationData($payload); 60 | break; 61 | 62 | /* 63 | * https://tools.ietf.org/html/rfc5246#section-6 64 | * 65 | * If a TLS implementation receives an unexpected record type, it MUST send an 66 | * unexpected_message alert. 67 | */ 68 | default: 69 | throw new TLSAlertException(Alert::create(Alert::UNEXPECTED_MESSAGE), "Unknow Content Type: $contentType"); 70 | } 71 | } 72 | 73 | protected function decodeContent($payload, int $contentType) 74 | { 75 | $core = $this->core; 76 | 77 | $recordOut = $core->getOutDuplex()->getRecord(); 78 | 79 | $out = $recordOut->set('contentType', $contentType) 80 | ->set('payload', $payload ) 81 | ->decode(); 82 | 83 | return $out; 84 | } 85 | 86 | public function encodeChangeCipherSpec($data) 87 | { 88 | $core = $this->core; 89 | 90 | if( $this->expectedHandshakeType != HandshakeType::FINISHED || $core->isHandshaked ) 91 | throw new TLSException("Invalid message"); 92 | 93 | $changeCipherSpec = new ChangeCipherSpec(); 94 | $changeCipherSpec->encode($data); 95 | 96 | $this->content = $changeCipherSpec; 97 | } 98 | 99 | public function encodeApplicationData($data) 100 | { 101 | $core = $this->core; 102 | 103 | if( !$core->isHandshaked ) 104 | throw new TLSException("Handshake Imcomplete"); 105 | 106 | if( is_null( $this->appData ) ) 107 | $this->appData = new ApplicationData($this->core); 108 | 109 | $this->appData->encode($data); 110 | } 111 | 112 | public function encodeAlert($data) 113 | { 114 | $core = $this->core; 115 | 116 | $alert = new Alert(); 117 | $alert->encode($data); 118 | 119 | $this->content = $alert; 120 | 121 | $core->isClosed = true; 122 | 123 | if( $alert->getDescCode() != Alert::CLOSE_NOTIFY ) 124 | throw new TLSAlertException($alert, "Alert received from peer"); 125 | } 126 | 127 | public function debugInfo() 128 | { 129 | if( is_null( $this->content ) ) return; 130 | return $this->content->debugInfo(); 131 | } 132 | } 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /examples/tls-server-https.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'cert' => ['pem/crt.pem'], 17 | 'key' => ['pem/key.pem', 'test'] 18 | ] 19 | ]); 20 | */ 21 | 22 | // ECDSA 23 | $config = TLSContext::getServerConfig([ 24 | 'key_pair_files' => [ 25 | 'cert' => ['pem/ecdsa_crt.pem'], 26 | 'key' => ['pem/ecdsa_key.pem', ''] 27 | ] 28 | ]); 29 | 30 | 31 | $tlsClients = []; 32 | 33 | // Create a tcp server socket 34 | $server = stream_socket_server("tcp://0.0.0.0:443", $errno, $errstr); 35 | 36 | // Non-blocking mode 37 | stream_set_blocking( $server, 0 ); 38 | 39 | $sockets = [$server]; 40 | 41 | $index = 1; 42 | 43 | $closeSocket = function($clientSocket){ 44 | global $sockets, $tlsClients; 45 | 46 | list($tls, $index) = $tlsClients[(int)$clientSocket]; 47 | unset( $tlsClients[(int)$clientSocket] ); 48 | unset( $sockets[$index] ); 49 | stream_socket_shutdown( $clientSocket, STREAM_SHUT_WR ); 50 | }; 51 | 52 | while(1) 53 | { 54 | $readSockets = array_values($sockets); 55 | $write = $except = null; 56 | 57 | echo "Waiting...\n"; 58 | $n = stream_select( $readSockets, $write, $except, 60); 59 | 60 | if( $n > 0 ) 61 | { 62 | foreach( $readSockets as $readSocket ) 63 | { 64 | if( $server === $readSocket ) 65 | { 66 | // Accept a new client 67 | $clientSocket = stream_socket_accept( $server ); 68 | 69 | echo "Accept: $clientSocket\n"; 70 | 71 | // Non-blocking mode 72 | stream_set_blocking( $clientSocket, 0 ); 73 | 74 | // Create a TLS Engine 75 | $tls = TLSContext::createTLS($config); 76 | 77 | // Store it to an array 78 | $tlsClients[(int)$clientSocket] = [$tls, $index]; 79 | 80 | $sockets[$index++] = $clientSocket; 81 | } 82 | else 83 | { 84 | $clientSocket = $readSocket; 85 | 86 | // Get the TLS Engine 87 | list($tls, $index) = $tlsClients[(int)$clientSocket]; 88 | 89 | // Receive raw data from a client 90 | $data = stream_socket_recvfrom($clientSocket, 16384); 91 | 92 | if( 0 >= strlen($data) ) 93 | { 94 | echo "Disconnted\n"; 95 | $closeSocket($clientSocket); 96 | break; 97 | } 98 | 99 | try 100 | { 101 | $tls->encode($data); 102 | } 103 | catch(TLSAlertException $e) 104 | { 105 | echo "Alert: " . $e->getMessage() . "\n"; 106 | 107 | if( strlen($out = $e->decode()) ) 108 | stream_socket_sendto($clientSocket, $out); 109 | } 110 | 111 | // Get any buffer from TLS Engine and send it to the client 112 | if( strlen($out = $tls->decode()) ) 113 | stream_socket_sendto($clientSocket, $out); 114 | 115 | // Handshake is done, start sending/receiving own data 116 | if( $tls->isHandshaked() ) 117 | { 118 | echo "Finished handshaking for $clientSocket\n"; 119 | 120 | $in = $tls->input(); 121 | 122 | $content = file_get_contents('html/index.html'); 123 | 124 | $response = "HTTP/2.0 200 OK\r\nContent-Length: " . strlen($content) . "\r\n\r\n" 125 | . $content; 126 | 127 | $out = $tls->output($response)->decode(); 128 | stream_socket_sendto($clientSocket, $out); 129 | $closeSocket($clientSocket); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/AEADGcm.php: -------------------------------------------------------------------------------- 1 | = 0 ) ? true : false; 24 | } 25 | 26 | /** 27 | * https://github.com/bukka/php-crypto 28 | * 29 | * Objective PHP binding of OpenSSL Crypto library 30 | * 31 | */ 32 | private static function useSO() 33 | { 34 | return class_exists('\Crypto\Cipher') ? true : false; 35 | } 36 | 37 | private static function bitLen($password) 38 | { 39 | // 128(16), 192(24) or 256(32) 40 | $l = strlen($password); 41 | 42 | if( $l != 16 && $l != 24 && $l != 32 ) 43 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Invalid gcm key length: $l"); 44 | 45 | return $l * 8; 46 | } 47 | 48 | private static function getMethod($password) 49 | { 50 | return "aes-" . self::bitLen($password) . "-gcm"; 51 | } 52 | 53 | public static function encrypt($data, $password, $IV, $AAD) 54 | { 55 | if( self::useOpenSSL() ) 56 | { 57 | $method = self::getMethod($password); 58 | $encrypt = openssl_encrypt($data, $method, $password, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $IV, $tag, $AAD); 59 | } 60 | else if( self::useSO() ) 61 | { 62 | try{ 63 | $cipher = \Crypto\Cipher::aes(\Crypto\Cipher::MODE_GCM, self::bitLen($password)); 64 | $cipher->setAAD($AAD); 65 | $encrypt = $cipher->encrypt($data, $password, $IV); 66 | $tag = $cipher->getTag(); 67 | }catch(\Exception $e){ 68 | //echo $e->getMessage(); 69 | return false; 70 | } 71 | } 72 | else 73 | { 74 | try{ 75 | list($encrypt, $tag) = AESGCM::encrypt($password, $IV, $data, $AAD); 76 | }catch(\Exception $e){ 77 | //echo $e->getMessage(); 78 | return false; 79 | } 80 | } 81 | 82 | return $encrypt . $tag; 83 | } 84 | 85 | public static function decrypt($encData, $password, $IV, $AAD) 86 | { 87 | /* 88 | * https://tools.ietf.org/html/rfc5116#section-5.1 89 | * 90 | * An authentication tag with a length of 16 octets (128 91 | * bits) is used. The AEAD_AES_128_GCM ciphertext is formed by 92 | * appending the authentication tag provided as an output to the GCM 93 | * encryption operation to the ciphertext that is output by that 94 | * operation. 95 | * 96 | * ciphertext is exactly 16 octets longer than its 97 | * corresponding plaintext. 98 | */ 99 | if( strlen($encData) < self::TAG_LEN ) 100 | return false; 101 | 102 | // Get the tag appended to cipher text 103 | $tag = substr($encData, strlen($encData) - self::TAG_LEN, self::TAG_LEN); 104 | 105 | // Resize the cipher text 106 | $encData = substr($encData, 0, strlen($encData) - self::TAG_LEN); 107 | 108 | if( self::useOpenSSL() ) 109 | { 110 | $method = self::getMethod($password); 111 | $data = openssl_decrypt($encData, $method, $password, OPENSSL_RAW_DATA, $IV, $tag, $AAD); 112 | } 113 | else if( self::useSO() ) 114 | { 115 | try{ 116 | $cipher = \Crypto\Cipher::aes(\Crypto\Cipher::MODE_GCM, self::bitLen($password)); 117 | $cipher->setTag($tag); 118 | $cipher->setAAD($AAD); 119 | $data = $cipher->decrypt($encData, $password, $IV); 120 | }catch(\Exception $e){ 121 | return false; 122 | } 123 | } 124 | else 125 | { 126 | try{ 127 | $data = AESGCM::decrypt($password, $IV, $encData, $AAD, $tag); 128 | }catch(\Exception $e){ 129 | //echo $e->getMessage(); 130 | return false; 131 | } 132 | } 133 | 134 | return $data; 135 | } 136 | } 137 | 138 | 139 | -------------------------------------------------------------------------------- /examples/tls-server-echo.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'cert' => ['pem/ecdsa_crt.pem'], // RSA => pem/crt.pem 39 | 'key' => ['pem/ecdsa_key.pem', 'test'] // RSA => pem/key.pem 40 | ] 41 | ]); 42 | 43 | $tlsClients = []; 44 | 45 | // Create a tcp server socket 46 | $server = stream_socket_server("tcp://0.0.0.0:443", $errno, $errstr); 47 | 48 | // Non-blocking mode 49 | stream_set_blocking( $server, 0 ); 50 | 51 | $sockets = [$server]; 52 | 53 | $index = 1; 54 | 55 | $closeSocket = function($clientSocket){ 56 | global $sockets, $tlsClients; 57 | 58 | list($tls, $index) = $tlsClients[(int)$clientSocket]; 59 | unset( $tlsClients[(int)$clientSocket] ); 60 | unset( $sockets[$index] ); 61 | stream_socket_shutdown( $clientSocket, STREAM_SHUT_WR ); 62 | }; 63 | 64 | 65 | while(1) 66 | { 67 | $readSockets = array_values($sockets); 68 | $write = $except = null; 69 | 70 | echo "Waiting...\n"; 71 | $n = stream_select( $readSockets, $write, $except, 60); 72 | 73 | if( $n > 0 ) 74 | { 75 | foreach( $readSockets as $readSocket ) 76 | { 77 | if( $server === $readSocket ) 78 | { 79 | // Accept a new client 80 | $clientSocket = stream_socket_accept( $server ); 81 | 82 | // Non-blocking mode 83 | stream_set_blocking( $clientSocket, 0 ); 84 | 85 | // Create a TLS Engine 86 | $tls = TLSContext::createTLS($config); 87 | 88 | // Store it to an array 89 | $tlsClients[(int)$clientSocket] = [$tls, $index]; 90 | 91 | $sockets[$index++] = $clientSocket; 92 | } 93 | else 94 | { 95 | $clientSocket = $readSocket; 96 | 97 | // Get the TLS Engine 98 | list($tls, $index) = $tlsClients[(int)$clientSocket]; 99 | 100 | // Receive raw data from a client 101 | $data = stream_socket_recvfrom($clientSocket, 16384); 102 | 103 | if( 0 >= strlen($data) ) 104 | { 105 | echo "Disconnted\n"; 106 | $closeSocket($clientSocket); 107 | break; 108 | } 109 | 110 | try 111 | { 112 | // Convert TLS data 113 | $tls->encode($data); 114 | } 115 | catch(TLSAlertException $e) 116 | { 117 | echo "Alert: " . $e->getMessage() . "\n"; 118 | 119 | if( strlen($out = $e->decode()) ) 120 | stream_socket_sendto($clientSocket, $out); 121 | } 122 | 123 | // Get any buffer from TLS Engine and send it to the client 124 | if( strlen($out = $tls->decode()) ) 125 | stream_socket_sendto($clientSocket, $out); 126 | 127 | // Handshake is done, start sending/receiving own data 128 | if( $tls->isHandshaked() ) 129 | { 130 | // Receive data from a client 131 | $in = $tls->input(); 132 | 133 | echo "received from $clientSocket: $in"; 134 | 135 | // Conver output into TLS format 136 | $out = $tls->output($in)->decode(); 137 | stream_socket_sendto($clientSocket, $out); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/Content/ClientContent.php: -------------------------------------------------------------------------------- 1 | getOutDuplex()->getRecord(); 18 | $bufferOut = $core->getBufferOut(); 19 | 20 | // =========================================== 21 | // Send Client Hello 22 | // =========================================== 23 | $clientHello = HandshakeFactory::getInstance($core, HandshakeType::CLIENT_HELLO); 24 | $payload = $clientHello->decode(); 25 | 26 | $bufferOut->set( $this->decodeContent($payload, ContentType::HANDSHAKE) ); 27 | 28 | $this->expectedHandshakeType = HandshakeType::SERVER_HELLO; 29 | } 30 | 31 | public function encodeHandshake($payload) 32 | { 33 | $core = $this->core; 34 | 35 | // Incomming Record 36 | $recordIn = $core->getInDuplex()->getRecord(); 37 | 38 | // Outgoing Record 39 | $recordOut = $core->getOutDuplex()->getRecord(); 40 | 41 | // Buffer to send 42 | $bufferOut = $core->getBufferOut(); 43 | 44 | // Extension 45 | $extensions = $core->extensions; 46 | 47 | if( $core->isHandshaked ) 48 | throw new TLSAlertException(Alert::create(Alert::UNEXPECTED_MESSAGE), 49 | "Handshake message received after handshake is complete"); 50 | /* 51 | * https://tools.ietf.org/html/rfc5246#section-7.4 52 | * 53 | * Get Handshake Msg type 54 | */ 55 | $handshakeType = Core::_unpack('C', $payload[0] ); 56 | 57 | if( $this->expectedHandshakeType != $handshakeType ) 58 | throw new TLSAlertException(Alert::create(Alert::UNEXPECTED_MESSAGE), 59 | "Unexpected handshake message: $handshakeType <=> " . $this->expectedHandshakeType); 60 | 61 | $handshake = HandshakeFactory::getInstance($core, $handshakeType); 62 | 63 | $handshake->encode($payload); 64 | 65 | $this->content = $handshake; 66 | 67 | switch($this->expectedHandshakeType) 68 | { 69 | case HandshakeType::SERVER_HELLO: 70 | $this->expectedHandshakeType = HandshakeType::CERTIFICATE; 71 | break; 72 | 73 | case HandshakeType::CERTIFICATE: 74 | 75 | if( $core->cipherSuite->isECDHEEnabled() ) 76 | $this->expectedHandshakeType = HandshakeType::SERVER_KEY_EXCHANGE; 77 | else 78 | $this->expectedHandshakeType = HandshakeType::SERVER_HELLO_DONE; 79 | break; 80 | 81 | case HandshakeType::SERVER_KEY_EXCHANGE: 82 | $this->expectedHandshakeType = HandshakeType::SERVER_HELLO_DONE; 83 | break; 84 | 85 | case HandshakeType::SERVER_HELLO_DONE: 86 | 87 | // =========================================== 88 | // Send Client Key Exchange 89 | // =========================================== 90 | $clientKeyExchange = HandshakeFactory::getInstance($core, HandshakeType::CLIENT_KEY_EXCHANGE); 91 | $bufferOut->set( $this->decodeContent($clientKeyExchange->decode(), ContentType::HANDSHAKE) ); 92 | 93 | // =========================================== 94 | // Send Change Cipher Spec 95 | // =========================================== 96 | $changeCipherSpec = new ChangeCipherSpec(); 97 | 98 | $bufferOut->append( $this->decodeContent($changeCipherSpec->decode(), ContentType::CHANGE_CIPHER_SPEC ) ); 99 | 100 | // Enable encryption 101 | $recordOut->cipherChanged(); 102 | 103 | // =========================================== 104 | // Send Client finished 105 | // =========================================== 106 | $clientFinished = HandShakeFactory::getInstance($core, HandshakeType::FINISHED); 107 | 108 | $bufferOut->append( $this->decodeContent($clientFinished->decode(), ContentType::HANDSHAKE) ); 109 | 110 | $this->expectedHandshakeType = HandshakeType::FINISHED; 111 | break; 112 | 113 | case HandshakeType::FINISHED: 114 | $core->isHandshaked = true; 115 | break; 116 | 117 | } 118 | 119 | if( strlen($payload) > $handshake->get('length') ) 120 | { 121 | $payload = substr($payload, $handshake->get('length')); 122 | $this->encodeHandshake($payload); 123 | } 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/Prf.php: -------------------------------------------------------------------------------- 1 | core = $core; 17 | } 18 | 19 | public function prf($length, $secret, $label, $seed) 20 | { 21 | $core = $this->core; 22 | $protoVersion = $core->getProtocolVersion(); 23 | 24 | if( $protoVersion == 31 ) 25 | { 26 | return $this->prf31($length, $secret, $label, $seed); 27 | } 28 | else 29 | { 30 | return $this->prf32($length, $secret, $label, $seed); 31 | } 32 | } 33 | 34 | /** 35 | * Generate master secret from premaster 36 | */ 37 | public function getMaster($preMaster, $clientRandom, $serverRandom) 38 | { 39 | $masterSecretLength = 48; 40 | $seed = $clientRandom . $serverRandom; 41 | 42 | $masterSecret = $this->prf($masterSecretLength, $preMaster, "master secret", $seed); 43 | return $masterSecret; 44 | } 45 | 46 | 47 | /** 48 | * Generate secret keys 49 | */ 50 | public function getKeys($masterSecret, $clientRandom, $serverRandom) 51 | { 52 | $core = $this->core; 53 | $cipherSuite = $core->cipherSuite; 54 | 55 | $macLen = $cipherSuite->getMACLen(); 56 | $keyLen = $cipherSuite->getKeyLen(); 57 | $ivLen = $cipherSuite->getIVLen(); 58 | 59 | $seed = $serverRandom . $clientRandom; 60 | 61 | /* 62 | * https://tools.ietf.org/html/rfc5246#section-6.3 63 | * 64 | * client_write_MAC_key[SecurityParameters.mac_key_length] 65 | * server_write_MAC_key[SecurityParameters.mac_key_length] 66 | * client_write_key[SecurityParameters.enc_key_length] 67 | * server_write_key[SecurityParameters.enc_key_length] 68 | * client_write_IV[SecurityParameters.fixed_iv_length] 69 | * server_write_IV[SecurityParameters.fixed_iv_length] 70 | */ 71 | $offset = 0; 72 | $length = 2*$macLen + 2*$keyLen + 2*$ivLen; 73 | $keys = $this->prf($length, $masterSecret, "key expansion", $seed); 74 | 75 | $clientMAC = substr($keys, $offset, $macLen); 76 | $offset += $macLen; 77 | 78 | $serverMAC = substr($keys, $offset, $macLen); 79 | $offset += $macLen; 80 | 81 | $clientKey = substr($keys, $offset, $keyLen); 82 | $offset += $keyLen; 83 | 84 | $serverKey = substr($keys, $offset, $keyLen); 85 | $offset += $keyLen; 86 | 87 | $clientIV = substr($keys, $offset, $ivLen); 88 | $offset += $ivLen; 89 | 90 | $serverIV = substr($keys, $offset, $ivLen); 91 | 92 | return [ 93 | 'client' => ['MAC' => $clientMAC, 'Key' => $clientKey, 'IV' => $clientIV], 94 | 'server' => ['MAC' => $serverMAC, 'Key' => $serverKey, 'IV' => $serverIV], 95 | ]; 96 | } 97 | 98 | /** 99 | * For TLS1.1 100 | */ 101 | public function prf31($length, $secret, $label, $seed) 102 | { 103 | $labelAndSeed = $label . $seed; 104 | 105 | $LS1 = substr($secret, 0, ceil(strlen($secret))/2); 106 | $LS2 = substr($secret, ceil(strlen($secret)/2)); 107 | 108 | $md5 = $this->pHash($length, $LS1, $labelAndSeed, "md5"); 109 | $sha1 = $this->pHash($length, $LS2, $labelAndSeed, "sha1"); 110 | 111 | $result = []; 112 | for( $i = 0; $i < strlen($sha1); $i++) 113 | $result[$i] = ( $md5[$i] ) ^ ( $sha1[$i] ); 114 | 115 | return implode("", $result); 116 | } 117 | 118 | /** 119 | * For TLS1.2 120 | */ 121 | public function prf32($length, $secret, $label, $seed) 122 | { 123 | $core = $this->core; 124 | $cipherSuite = $core->cipherSuite; 125 | 126 | $labelAndSeed = $label . $seed; 127 | $hash = $this->pHash($length, $secret, $labelAndSeed, $cipherSuite->getHashAlogV33()); 128 | return $hash; 129 | } 130 | 131 | /** 132 | * https://tools.ietf.org/html/rfc5246#section-5 133 | * 134 | * HMAC and the Pseudorandom Function 135 | */ 136 | public function pHash($length, $secret, $seed, $hashType) 137 | { 138 | $j = 0; 139 | 140 | $A = hash_hmac($hashType, $seed, $secret, true); 141 | 142 | $result = null; 143 | while( $j < $length ) 144 | { 145 | $b = hash_hmac($hashType, $A . $seed, $secret, true); 146 | 147 | $blen = strlen($b); 148 | 149 | if( $j+$blen > $length ) 150 | $result .= substr($b, 0, $length - $j); 151 | else 152 | $result .= $b; 153 | 154 | $A = hash_hmac($hashType, $A, $secret, true); 155 | 156 | $j += $blen; 157 | } 158 | 159 | return $result; 160 | } 161 | } 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/Record/Record.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 32 | $this->encodeBuffer = new Buffer(); 33 | $this->maxLength = self::MAX_LENGTH; 34 | } 35 | 36 | public function getCore() 37 | { 38 | return $this->conn->getCore(); 39 | } 40 | 41 | public function getConn() 42 | { 43 | return $this->conn; 44 | } 45 | 46 | /** 47 | * Delegation to ConnectionDuplex::cipherChanged() 48 | */ 49 | public function cipherChanged() 50 | { 51 | return $this->conn->cipherChanged(); 52 | } 53 | 54 | protected function encodeHeader($data) 55 | { 56 | $data = $this->encodeBuffer->flush() . $data; 57 | 58 | $this->contentType = Core::_unpack( 'C', $data[0] ); 59 | 60 | $vMajor = Core::_unpack( 'C', $data[1] ); 61 | $vMinor = Core::_unpack( 'C', $data[2] ); 62 | 63 | $this->length = Core::_unpack( 'n', $data[3] . $data[4] ); 64 | 65 | if( $this->length > $this->maxLength )//|| strlen($data) > self::MAX_BUFFER_LENGTH ) 66 | { 67 | /* 68 | * A TLSCiphertext record was received that had a length more than 69 | * 2^14+2048 bytes, or a record decrypted to a TLSCompressed record 70 | * with more than 2^14+1024 bytes. 71 | */ 72 | throw new TLSAlertException(Alert::create(Alert::RECORD_OVERFLOW), "Exceed max length of payload: " . strlen($data) ); 73 | } 74 | 75 | if( $this->length > strlen( $data ) ) 76 | { 77 | $this->encodeBuffer->set($data); 78 | return false; 79 | } 80 | 81 | $this->payload = substr($data, 5, $this->length); 82 | $this->dataRest = substr($data, 5 + $this->length); 83 | 84 | return true; 85 | } 86 | 87 | protected function encodeContent() 88 | { 89 | $core = $this->getCore(); 90 | $content = $core->content; 91 | 92 | $content->encodeContent($this->contentType, $this->payload, $this); 93 | } 94 | 95 | public function encode($data) 96 | { 97 | $this->reset(); 98 | 99 | if( !$this->encodeHeader($data) ) 100 | return; 101 | 102 | $this->encodeContent(); 103 | } 104 | 105 | /** 106 | * @Override 107 | */ 108 | public function get($property, $default = null) 109 | { 110 | if( $property == 'length' ) 111 | { 112 | return 5 + $this->length; 113 | } 114 | 115 | return parent::get($property); 116 | } 117 | 118 | /** 119 | * @Override 120 | */ 121 | public function set($property, $value) 122 | { 123 | parent::set($property, $value); 124 | 125 | if( $property == 'payload' ) 126 | { 127 | $this->length = strlen($this->payload); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | public function reset() 134 | { 135 | $this->contentType = 136 | $this->dataRest = 137 | $this->payload = null; 138 | 139 | $this->length = -1; 140 | } 141 | 142 | public function decode() 143 | { 144 | $core = $this->getCore(); 145 | 146 | list($vMajor, $vMinor) = $core->getVersion(); 147 | 148 | // type 149 | $data = Core::_pack('C', $this->contentType) 150 | . Core::_pack('C', $vMajor) 151 | . Core::_pack('C', $vMinor) 152 | . Core::_pack('n', $this->length) 153 | . $this->payload; 154 | 155 | // Handshake 156 | if( $this->contentType == ContentType::HANDSHAKE && !$this->conn->isCipherChanged ) 157 | $core->countHandshakeMessages($this->payload); 158 | 159 | $this->reset(); 160 | 161 | return $data; 162 | } 163 | 164 | public function debugInfo() 165 | { 166 | $core = $this->getCore(); 167 | 168 | $outputs[] = "ContentType: " . ContentType::getString($this->contentType); 169 | $outputs[] = "Length: " . $this->length; 170 | $outputs[] = "Received Payload: " . strlen($this->payload); 171 | 172 | $r = "[Record Protocol]\n" . implode("\n", $outputs) . "\n" 173 | . "[Content]\n" . $core->content->debugInfo(); 174 | 175 | return $r; 176 | } 177 | } 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/Record/AEADCipherRecord.php: -------------------------------------------------------------------------------- 1 | payload; 38 | 39 | $conn = $this->conn; 40 | $core = $conn->getCore(); 41 | 42 | $cipherSuite = $core->cipherSuite; 43 | $sharedKey = $conn->Key; 44 | 45 | $nonceImplicit = $conn->IV; 46 | 47 | // 16 => tag length 48 | $gcmHeaderLen = self::nonceExplicitLen + 16; 49 | $rawPayloadLen = strlen($this->payload); 50 | 51 | if( $rawPayloadLen < $gcmHeaderLen ) 52 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), "GCM payload too short"); 53 | 54 | $nonceExplicit = substr($this->payload, 0, self::nonceExplicitLen); 55 | 56 | $aad = $this->getAAD($rawPayloadLen - $gcmHeaderLen); 57 | 58 | // Copy payload over to encPayload 59 | $this->encPayload = $this->payload; 60 | $this->encLength = $this->length; 61 | 62 | $nonce = $nonceImplicit . $nonceExplicit; 63 | $encData = substr($this->encPayload, self::nonceExplicitLen); 64 | 65 | $data = $cipherSuite->gcmDecrypt($encData, $sharedKey, $nonce, $aad); 66 | 67 | // If the decryption fails, a fatal bad_record_mac alert MUST be generated 68 | if( false === $data ) 69 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), "Cipher gcm decryption failed"); 70 | 71 | // Re-set the length 72 | $this->length = strlen($data); 73 | 74 | // Set Payload 75 | $this->payload = $payload = substr($data, 0, $this->length); 76 | 77 | $this->incrementSeq(); 78 | 79 | $content = $core->content; 80 | 81 | $content->encodeContent($this->contentType, $this->payload, $this); 82 | } 83 | 84 | /** 85 | * @Override 86 | */ 87 | public function decode() 88 | { 89 | $conn = $this->conn; 90 | $core = $conn->getCore(); 91 | 92 | $cipherSuite = $core->cipherSuite; 93 | 94 | $nonceImplicit = $conn->IV; // 4 bytes 95 | $sharedKey = $conn->Key; 96 | 97 | $aad = $this->getAAD($this->length); 98 | 99 | $nonceExplicit = $this->getSeq(); // 8 bytes 100 | 101 | /* 102 | * https://tools.ietf.org/html/rfc5288 page 2 103 | * 104 | * struct { 105 | * opaque salt[4]; 106 | * opaque nonce_explicit[8]; 107 | * } GCMNonce; 108 | */ 109 | $nonce = $nonceImplicit . $nonceExplicit; 110 | 111 | $encData = $cipherSuite->gcmEncrypt($this->payload, $sharedKey, $nonce, $aad); 112 | 113 | if( false === $encData ) 114 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), "Cipher gcm encryption failed"); 115 | 116 | $this->incrementSeq(); 117 | 118 | if( $this->contentType == ContentType::HANDSHAKE ) 119 | $core->countHandshakeMessages($this->payload); 120 | 121 | $payload = $nonceExplicit . $encData; 122 | 123 | $this->set('payload', $payload ); 124 | 125 | return parent::decode(); 126 | } 127 | 128 | /** 129 | * Additional Authentication Data 130 | */ 131 | public function getAAD($length) 132 | { 133 | $conn = $this->conn; 134 | $core = $conn->getCore(); 135 | $cipherSuite = $core->cipherSuite; 136 | 137 | list($vMajor, $vMinor) = $core->getVersion(); 138 | 139 | if( is_null( $this->seq ) ) 140 | { 141 | $this->seq = self::getZeroSeq(); 142 | } 143 | 144 | $contentType = Core::_pack( 'C', $this->contentType ); 145 | $major = Core::_pack( 'C', $vMajor ); 146 | $minor = Core::_pack( 'C', $vMinor ); 147 | 148 | $length = Core::_pack('n', $length); 149 | 150 | /* 151 | * https://tools.ietf.org/html/rfc5246#section-6.2.3.3 152 | * 153 | * additional_data = seq_num + TLSCompressed.type + 154 | * TLSCompressed.version + TLSCompressed.length; 155 | * 156 | */ 157 | $concat = implode('', $this->seq ) 158 | . $contentType 159 | . $major 160 | . $minor 161 | . $length; 162 | 163 | return $concat; 164 | } 165 | 166 | } 167 | 168 | 169 | -------------------------------------------------------------------------------- /src/Handshake/ServerHello.php: -------------------------------------------------------------------------------- 1 | core; 25 | $connIn = $core->getInDuplex(); 26 | 27 | $data = $this->encodeHeader($data); 28 | 29 | $vMajor = Core::_unpack('C', $data[0] ); 30 | $vMinor = Core::_unpack('C', $data[1] ); 31 | 32 | // Server Random 33 | $random = substr( $data, 2, 32 ); 34 | 35 | $connIn->random = $random; 36 | 37 | // Session ID 38 | $sessionLength = Core::_unpack( 'C', $data[34] ); 39 | 40 | $data = substr($data, 35); 41 | 42 | // SessionID if > 0 43 | if( $sessionLength > 0 ) 44 | { 45 | $sessionID = substr( $data, 35, $sessionLength); 46 | $core->setSessionID($sessionID); 47 | $data = substr($data, $sessionLength); 48 | } 49 | 50 | $cipherID = [Core::_unpack('C', $data[0] ), Core::_unpack('C', $data[1] )]; 51 | 52 | $cipherSuite = new CipherSuites($cipherID); 53 | 54 | if( is_null($cipherSuite ) ) 55 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "cipherSuite is null"); 56 | 57 | $core->cipherSuite = $cipherSuite; 58 | 59 | // Cipher Suite 60 | $core->setCompressionMethod(Core::_unpack( 'C', $data[2] )); 61 | 62 | // Extensions 63 | if( strlen($data) < 5 ) 64 | return; 65 | 66 | $extLength = Core::_unpack( 'n', $data[3] . $data[4] ); 67 | $data = substr( $data, 5, $extLength ); 68 | 69 | $this->requestedExtensions = $extensions = $this->encodeExtensions($data); 70 | 71 | $core->extensions->onEncodeServerHello($extensions); 72 | 73 | } 74 | 75 | public function decode() 76 | { 77 | $core = $this->core; 78 | 79 | $extensions = $core->extensions; 80 | $connOut = $core->getOutDuplex(); 81 | $sessionID = $core->getSessionID(); 82 | 83 | list($vMajor, $vMinor) = $core->getVersion(); 84 | 85 | // Set server random 86 | $connOut->random = Core::getRandom(32); 87 | 88 | $sessionLength = strlen($sessionID); 89 | 90 | $data = Core::_pack('C', $vMajor) 91 | . Core::_pack('C', $vMinor) 92 | . $connOut->random 93 | . Core::_pack('C', $sessionLength ); 94 | 95 | if( $sessionLength > 0 ) 96 | { 97 | $data .= $sessionID; 98 | } 99 | 100 | $cipherSuite = $core->cipherSuite; 101 | list( $cipher1, $cipher2 ) = $cipherSuite->getID(); 102 | 103 | $data .= Core::_pack('C', $cipher1 ) 104 | . Core::_pack('C', $cipher2 ); 105 | 106 | // Compression method length 107 | $data .= Core::_pack('C', 0x00); 108 | 109 | $extData = $extensions->onDecodeServerHello(); 110 | 111 | if( strlen($extData) > 0 ) 112 | $data .= Core::_pack('n', strlen($extData) ) . $extData; 113 | 114 | $this->msgType = 2; 115 | $this->length = strlen($data); 116 | 117 | return $this->getBinHeader() . $data; 118 | } 119 | 120 | public function debugInfo() 121 | { 122 | /* 123 | * struct { 124 | * ProtocolVersion server_version; 125 | * Random random; 126 | * SessionID session_id; 127 | * CipherSuite cipher_suite; 128 | * CompressionMethod compression_method; 129 | * select (extensions_present) { 130 | * case false: 131 | * struct {}; 132 | * case true: 133 | * Extension extensions<0..2^16-1>; 134 | * }; 135 | * } ServerHello; 136 | */ 137 | $core = $this->core; 138 | $connIn = $this->core->getInDuplex(); 139 | 140 | $protoVersion = $core->getProtocolVersion(); 141 | $sessionID = base64_encode($core->getSessionID()); 142 | $cipherSuite = $core->cipherSuite->debugInfo(); 143 | 144 | $extensions = []; 145 | 146 | // ['type' => $extType, 'data' => $extData] 147 | foreach( $this->requestedExtensions as $value ) 148 | { 149 | $extensions[]= "Type: " . dechex($value['type']) 150 | . ' Data Length: ' . strlen($value['data'] ); 151 | } 152 | 153 | return "[HandshakeType::ServerHello]\n" 154 | . "Lengh: " . $this->length . "\n" 155 | . "Protocol Version: $protoVersion \n" 156 | . "Session ID: $sessionID\n" 157 | . "[Extensions]\n" 158 | . implode("\n", $extensions) . "\n" 159 | . $cipherSuite; 160 | } 161 | } 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /src/Content/ServerContent.php: -------------------------------------------------------------------------------- 1 | core = $core; 16 | $this->expectedHandshakeType = HandshakeType::CLIENT_HELLO; 17 | } 18 | 19 | public function encodeHandshake($payload) 20 | { 21 | $core = $this->core; 22 | 23 | // Incomming Record 24 | $recordIn = $core->getInDuplex()->getRecord(); 25 | 26 | // Outgoing Record 27 | $recordOut = $core->getOutDuplex()->getRecord(); 28 | 29 | // Buffer to send 30 | $bufferOut = $core->getBufferOut(); 31 | 32 | // Extension 33 | $extensions = $core->extensions; 34 | 35 | /* 36 | * https://tools.ietf.org/html/rfc5246#section-7.4 37 | * 38 | * Get Handshake Msg type 39 | */ 40 | $handshakeType = Core::_unpack('C', $payload[0] ); 41 | 42 | if( $core->isHandshaked ) 43 | { 44 | throw new TLSAlertException(Alert::create(Alert::UNEXPECTED_MESSAGE), 45 | "Handshake message received after handshake is complete: $handshakeType"); 46 | } 47 | 48 | if( $this->expectedHandshakeType != $handshakeType ) 49 | throw new TLSAlertException(Alert::create(Alert::UNEXPECTED_MESSAGE), "Unexpected handshake message"); 50 | 51 | $handshake = HandshakeFactory::getInstance($core, $handshakeType); 52 | 53 | $handshake->encode($payload); 54 | 55 | $this->content = $handshake; 56 | 57 | // Set the response into bufferOut if any 58 | switch($this->expectedHandshakeType) 59 | { 60 | case HandshakeType::CLIENT_HELLO: 61 | 62 | // =========================================== 63 | // Send Server Hello 64 | // =========================================== 65 | $serverHello = HandshakeFactory::getInstance($core, HandshakeType::SERVER_HELLO); 66 | 67 | $bufferOut->set( $this->decodeContent($serverHello->decode(), ContentType::HANDSHAKE) ); 68 | 69 | // =========================================== 70 | // Send Certificate 71 | // =========================================== 72 | $certificate = HandshakeFactory::getInstance($core, HandshakeType::CERTIFICATE); 73 | 74 | $bufferOut->append( $this->decodeContent($certificate->decode(), ContentType::HANDSHAKE) ); 75 | 76 | // =========================================== 77 | // Send Server Key Exchange 78 | // =========================================== 79 | if( $core->cipherSuite->isECDHEEnabled() ) 80 | { 81 | $curveOut = $extensions->call('Curve', 'decodeServerKeyExchange', null); 82 | $bufferOut->append( $this->decodeContent($curveOut, ContentType::HANDSHAKE) ); 83 | } 84 | 85 | // =========================================== 86 | // Send Server Hello Done 87 | // =========================================== 88 | $serverHelloDone = HandshakeFactory::getInstance($core, HandshakeType::SERVER_HELLO_DONE); 89 | 90 | $bufferOut->append( $this->decodeContent($serverHelloDone->decode(), ContentType::HANDSHAKE) ); 91 | 92 | // Update state 93 | $this->expectedHandshakeType = HandshakeType::CLIENT_KEY_EXCHANGE; 94 | 95 | break; 96 | 97 | case HandshakeType::CLIENT_KEY_EXCHANGE: 98 | $this->expectedHandshakeType = HandshakeType::FINISHED; 99 | break; 100 | 101 | case HandshakeType::FINISHED: 102 | 103 | // =========================================== 104 | // Send Change Cipher Spec 105 | // =========================================== 106 | $changeCipherSpec = new ChangeCipherSpec(); 107 | 108 | $bufferOut->set( $this->decodeContent( $changeCipherSpec->decode(), ContentType::CHANGE_CIPHER_SPEC) ); 109 | 110 | // Enable encryption 111 | $recordOut->cipherChanged(); 112 | 113 | // =========================================== 114 | // Send Server finished 115 | // =========================================== 116 | $serverFinished = HandShakeFactory::getInstance($core, HandshakeType::FINISHED); 117 | 118 | $bufferOut->append( $this->decodeContent( $serverFinished->decode(), ContentType::HANDSHAKE) ); 119 | 120 | $core->isHandshaked = true; 121 | 122 | break; 123 | } 124 | 125 | if( strlen($payload) > $handshake->get('length') ) 126 | { 127 | $payload = substr($payload, $handshake->get('length')); 128 | $this->encodeHandshake($payload); 129 | } 130 | 131 | } 132 | 133 | } 134 | 135 | -------------------------------------------------------------------------------- /src/Handshake/ClientKeyExchange.php: -------------------------------------------------------------------------------- 1 | core; 20 | $prf = $core->prf; 21 | 22 | // https://tools.ietf.org/html/rfc5246#section-8.1 23 | // Get a master secret from premaster 24 | $clientRandom = $connClient->random; 25 | $serverRandom = $connServer->random; 26 | 27 | $masterSecret = $prf->getMaster($preMaster, $clientRandom, $serverRandom); 28 | $secretKeys = $prf->getKeys($masterSecret, $clientRandom, $serverRandom); 29 | 30 | $core->setMasterSecret($masterSecret); 31 | 32 | // Set Secret keys 33 | $connClient->setSecretKeys($secretKeys['client']); 34 | $connServer->setSecretKeys($secretKeys['server']); 35 | } 36 | 37 | public function encode($data) 38 | { 39 | $core = $this->core; 40 | $extensions = $core->extensions; 41 | 42 | // Client 43 | $connIn = $core->getInDuplex(); 44 | 45 | // Server 46 | $connOut = $core->getOutDuplex(); 47 | 48 | $data = $this->encodeHeader($data); 49 | 50 | // ECDHE 51 | if( $core->cipherSuite->isECDHEEnabled() ) 52 | { 53 | $publicKeyLen = Core::_unpack( 'C', $data[0] ); 54 | $publicKey = substr($data, 1, $publicKeyLen ); 55 | 56 | $preMaster = $extensions->call('Curve', 'calculatePreMaster', null, $publicKey); 57 | } 58 | // RSA 59 | else 60 | { 61 | // https://tools.ietf.org/html/rfc5246#section-7.4.7.1 62 | // Get a Premaster Secret 63 | $preMasterLen = Core::_unpack( 'n', $data[0] . $data[1] ); 64 | 65 | $encPreMaster = substr($data, 2, $preMasterLen ); 66 | 67 | $privateKey = $core->getConfig('private_key'); 68 | openssl_private_decrypt($encPreMaster, $preMaster, $privateKey); 69 | 70 | $vMajor = Core::_unpack('C', $preMaster[0]); 71 | $vMinor = Core::_unpack('C', $preMaster[1]); 72 | 73 | list($vMajor2, $vMinor2) = $core->getVersion(); 74 | 75 | if( $vMajor != $vMajor2 || $vMinor != $vMinor ) 76 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), 77 | "Invalid protocol version in PreMaster $vMajor <=> $vMajor2, $vMinor <=> $vMinor2"); 78 | } 79 | 80 | $this->setKeys($preMaster, $connOut, $connIn); 81 | } 82 | 83 | public function decode() 84 | { 85 | $core = $this->core; 86 | 87 | list($vMajor, $vMinor) = $core->getVersion(); 88 | 89 | // Client 90 | $connOut = $core->getOutDuplex(); 91 | 92 | // Server 93 | $connIn = $core->getInDuplex(); 94 | 95 | // ECDHE 96 | if( $core->cipherSuite->isECDHEEnabled() ) 97 | { 98 | $extensions = $core->extensions; 99 | $data = $extensions->call('Curve', 'decodeClientKeyExchange', ''); 100 | 101 | $preMaster = $extensions->call('Curve', 'getPremaster', null); 102 | } 103 | // RSA 104 | else 105 | { 106 | $preMaster = Core::_pack('C', $vMajor) 107 | . Core::_pack('C', $vMinor) 108 | . Core::getRandom(46); 109 | 110 | $crtDers = $core->getCrtDers(); 111 | $publicKey = X509::getPublicKey($crtDers); 112 | 113 | openssl_public_encrypt($preMaster, $encPreMaster, $publicKey); 114 | 115 | $data = Core::_pack('n', strlen($encPreMaster) ) 116 | . $encPreMaster; 117 | 118 | } 119 | 120 | // Set Master Secret, IV and MAC 121 | $this->setKeys($preMaster, $connIn, $connOut); 122 | 123 | $this->msgType = HandshakeType::CLIENT_KEY_EXCHANGE; 124 | $this->length = strlen($data); 125 | 126 | return $this->getBinHeader() . $data; 127 | } 128 | 129 | public function debugInfo() 130 | { 131 | $connOut = $core->getOutDuplex(); 132 | $connIn = $core->getInDuplex(); 133 | 134 | foreach(['OUT' => $connOut, 'IN' => $connIn] as $key => $conn ) 135 | { 136 | $arr[$key] = [ 137 | 'Random' => base64_encode($conn->random), 138 | 'CipherChanged' => (($conn->isCipherChanged) ? 'True' : 'False' ), 139 | 'Key' => ('MAC: ' . base64_encode($connOut->MAC) 140 | . 'IV: ' . base64_encode($connOut->IV) 141 | . 'MasterKEY: ' . base64_encode($connOut->Key)), 142 | ]; 143 | } 144 | 145 | $output = 'IN: ' . explode("\n", $arr['IN']) . "\n" 146 | . 'OUT: ' . explode("\n", $arr['OUT']) . "\n"; 147 | 148 | return "[HandshakeType::ClientKeyExchange]\n" 149 | . "Lengh: " . $this->length . "\n" 150 | . $output; 151 | } 152 | 153 | } 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /src/EcDH.php: -------------------------------------------------------------------------------- 1 | secp256r1 34 | * self::TYPE_SECP384R1 => secp384r1 35 | * 36 | */ 37 | class EcDH 38 | { 39 | const TYPE_SECP256R1 = 23; 40 | const TYPE_SECP384R1 = 24; 41 | 42 | public static $typeList = [ 43 | self::TYPE_SECP256R1, 44 | self::TYPE_SECP384R1, 45 | ]; 46 | 47 | private $ecdh; 48 | private $curve; 49 | private $gen; 50 | private $privateKey; 51 | private $publicKey; 52 | private $adapter; 53 | 54 | public static function isSupported(int $type) 55 | { 56 | switch($type) 57 | { 58 | case self::TYPE_SECP256R1: 59 | case self::TYPE_SECP384R1: 60 | return true; 61 | } 62 | 63 | return false; 64 | } 65 | 66 | function __construct($type) 67 | { 68 | $this->type = $type; 69 | 70 | $this->ecdh = 71 | $this->curve = 72 | $this->gen = 73 | $this->privateKey = 74 | $this->publicKey = 75 | $this->adapter = null; 76 | } 77 | 78 | private function getGenerator() 79 | { 80 | if( !is_null( $this->gen ) ) 81 | return $this->gen; 82 | 83 | switch($this->type) 84 | { 85 | case self::TYPE_SECP256R1: 86 | $gen = EccFactory::getSecgCurves()->generator256r1(); 87 | break; 88 | case self::TYPE_SECP384R1: 89 | $gen = EccFactory::getSecgCurves()->generator384r1(); 90 | break; 91 | 92 | default: return null; 93 | } 94 | 95 | $this->gen = $gen; 96 | return $this->gen; 97 | 98 | } 99 | 100 | private function getCurve() 101 | { 102 | if( !is_null( $this->curve ) ) 103 | return $this->curve; 104 | 105 | switch($this->type) 106 | { 107 | case self::TYPE_SECP256R1: 108 | $curve = EccFactory::getSecgCurves()->curve256r1(); 109 | break; 110 | case self::TYPE_SECP384R1: 111 | $curve = EccFactory::getSecgCurves()->curve384r1(); 112 | break; 113 | 114 | default: return null; 115 | } 116 | 117 | $this->curve = $curve; 118 | return $this->curve; 119 | } 120 | 121 | private function getAdapter() 122 | { 123 | if( !is_null( $this->adapter ) ) 124 | return $this->adapter; 125 | 126 | $this->adapter = EccFactory::getAdapter(); 127 | return $this->adapter; 128 | } 129 | 130 | private function getEcdh() 131 | { 132 | if( !is_null( $this->ecdh ) ) 133 | return $this->ecdh; 134 | 135 | $adapter = $this->getAdapter(); 136 | 137 | $this->ecdh = new MdanterEcDH($adapter); 138 | return $this->ecdh; 139 | } 140 | 141 | public function getPrivateKey() 142 | { 143 | if( !is_null( $this->privateKey ) ) 144 | return $this->privateKey; 145 | 146 | $gen = $this->getGenerator();; 147 | 148 | $this->privateKey = $gen->createPrivateKey(); 149 | 150 | return $this->privateKey; 151 | } 152 | 153 | public function createPrivateKey() 154 | { 155 | $this->getPrivateKey(); 156 | return $this; 157 | } 158 | 159 | public function getPublicKey() 160 | { 161 | $privateKey = $this->getPrivateKey(); 162 | 163 | $this->publicKey = $publicKey = $privateKey->getPublicKey(); 164 | 165 | $publicPoint = $publicKey->getPoint(); 166 | 167 | // Convert to binary - Uncompressed 168 | $publicKeyBin = Core::_pack('C', 0x04) 169 | . gmp_export($publicPoint->getX(), 1, GMP_BIG_ENDIAN) 170 | . gmp_export($publicPoint->getY(), 1, GMP_BIG_ENDIAN); 171 | 172 | return $publicKeyBin; 173 | } 174 | 175 | public function calculateSharedKey($publicKeyBin) 176 | { 177 | $length = strlen($publicKeyBin) - 1; 178 | 179 | if( $length % 2 != 0 ) 180 | return; 181 | 182 | $half = $length/2; 183 | 184 | $x = substr($publicKeyBin, 1, $half); 185 | $gmpX = gmp_import($x, 1); 186 | 187 | $y = substr($publicKeyBin, $half+1); 188 | $gmpY = gmp_import($y, 1); 189 | 190 | $curve = $this->getCurve(); 191 | $adapter = $this->getAdapter(); 192 | $gen = $this->getGenerator(); 193 | $ecdh = $this->getEcdh(); 194 | 195 | $point = $curve->getPoint($gmpX, $gmpY); 196 | 197 | $privateKey = $this->getPrivateKey(); 198 | $publicKey = new PublicKey($adapter, $gen, $point); 199 | 200 | $ecdh->setSenderKey($privateKey); 201 | $ecdh->setRecipientKey($publicKey); 202 | 203 | $sharedKey = $ecdh->calculateSharedKey(); 204 | 205 | return gmp_export($sharedKey); 206 | } 207 | } 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /src/Handshake/ClientHello.php: -------------------------------------------------------------------------------- 1 | core; 31 | $connIn = $core->getInDuplex(); 32 | 33 | $data = $this->encodeHeader($data); 34 | 35 | // https://tools.ietf.org/html/rfc5246#section-7.4.1.2 36 | $vMajor = $major = Core::_unpack( 'C', $data[0] ); 37 | $vMinor = $minor = Core::_unpack( 'C', $data[1] ); 38 | 39 | // Set TLS Version 40 | $core->setVersion($vMajor, $vMinor); 41 | 42 | // Get and set Client Random 43 | $random = substr( $data, 2, 32 ); 44 | 45 | $connIn->random = $random; 46 | 47 | $sessionLength = Core::_unpack( 'C', $data[34] ); 48 | 49 | $data = substr($data, 35); 50 | 51 | // SessionID if > 0 52 | if( $sessionLength > 0 ) 53 | { 54 | $sessionID = substr( $data, 0, $sessionLength); 55 | $core->setSessionID($sessionID); 56 | $data = substr($data, $sessionLength); 57 | } 58 | 59 | $cipherLength = Core::_unpack( 'n', $data[0] . $data[1] ); 60 | 61 | $data = substr($data, 2); 62 | 63 | $cipherIDs = []; 64 | 65 | // https://github.com/pornin/TestSSLServer/blob/master/Src/CipherSuite.cs 66 | for( $i = 0; $i < $cipherLength; $i += 2 ) 67 | { 68 | // https://tools.ietf.org/html/rfc5246#section-7.4.1.2 69 | $cipher1 = Core::_unpack( 'C', $data[$i] ); 70 | $cipher2 = Core::_unpack( 'C', $data[$i+1] ); 71 | 72 | $cipherIDs[] = [$cipher1 , $cipher2]; 73 | } 74 | 75 | $this->requestCipherIDs = $cipherIDs; 76 | 77 | $data = substr($data, $cipherLength); 78 | 79 | $compressionLength = Core::_unpack( 'C', $data[0] ); 80 | $compressionMethod = Core::_unpack( 'C', $data[1] ); 81 | 82 | if( $compressionMethod != 0x00 ) 83 | throw new TLSAlertException(Alert::create(Alert::HANDSHAKE_FAILURE), "compressionMethod is not null"); 84 | 85 | $core->setCompressionMethod($compressionMethod); 86 | 87 | // Extensions 88 | $extLength = Core::_unpack( 'n', $data[2] . $data[3] ); 89 | 90 | $data = substr( $data, 4, $extLength ); 91 | 92 | $this->requestedExtensions = $extensions = $this->encodeExtensions($data); 93 | 94 | $core->extensions->onEncodeClientHello($extensions); 95 | 96 | $cipherID = CipherSuites::pickCipherID($core, $cipherIDs); 97 | 98 | if( is_null( $cipherID ) ) 99 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Cipher Suite not found"); 100 | 101 | $core->cipherSuite = new CipherSuites($cipherID); 102 | } 103 | 104 | public function decode() 105 | { 106 | $core = $this->core; 107 | $connOut = $core->getOutDuplex(); 108 | 109 | list($vMajor, $vMinor) = $core->getVersion(); 110 | 111 | // Set client random 112 | $connOut->random = Core::getRandom(32); 113 | 114 | // Set TLS Version 115 | $data = Core::_pack('C', $vMajor ) . Core::_pack('C', $vMinor); 116 | 117 | // Client Random 118 | $data .= $connOut->random; 119 | 120 | // Session ID - no session 121 | $data .= Core::_pack('C', 0x00); 122 | 123 | // Cipher Suite 124 | $cipherSuiteList = CipherSuites::decodeCipherList(); 125 | 126 | $data .= Core::_pack('n', strlen($cipherSuiteList) ) . $cipherSuiteList; 127 | 128 | // Compression method 129 | $data .= Core::_pack('C', 0x01 ) . Core::_pack('C', $core->getCompressionMethod()); 130 | 131 | // Extension Length 132 | //$data .= Core::_pack('n', 0x00); 133 | $extensionData = $core->extensions->onDecodeClientHello(); 134 | $data .= Core::_pack('n', strlen($extensionData)) . $extensionData; 135 | 136 | $this->msgType = HandshakeType::CLIENT_HELLO; 137 | $this->length = strlen($data); 138 | return $this->getBinHeader() . $data; 139 | } 140 | 141 | public function debugInfo() 142 | { 143 | /* 144 | * struct { 145 | * ProtocolVersion client_version; 146 | * Random random; 147 | * SessionID session_id; 148 | * CipherSuite cipher_suites<2..2^16-2>; 149 | * CompressionMethod compression_methods<1..2^8-1>; 150 | * select (extensions_present) { 151 | * case false: 152 | * struct {}; 153 | * case true: 154 | * Extension extensions<0..2^16-1>; 155 | * }; 156 | * } ClientHello; 157 | */ 158 | $core = $this->core; 159 | $connIn = $this->core->getInDuplex(); 160 | 161 | $protoVersion = $core->getProtocolVersion(); 162 | $sessionID = base64_encode($core->getSessionID()); 163 | $compressionMethod = $core->getCompressionMethod(); 164 | 165 | $cipherSuites = []; 166 | 167 | // [$cipher1 , $cipher2] 168 | foreach( $this->requestCipherIDs as $value ) 169 | { 170 | $cipherSuites[] = "0x" . dechex($value[0]) . dechex($value[1]); 171 | } 172 | 173 | $extensions = []; 174 | 175 | // ['type' => $extType, 'data' => $extData] 176 | foreach( $this->requestedExtensions as $value ) 177 | { 178 | $extensions[]= "Type: " . dechex($value['type']) 179 | . ' Data Length: ' . strlen($value['data'] ); 180 | } 181 | 182 | return "[HandshakeType::ServerHello]\n" 183 | . "Lengh: " . $this->length . "\n" 184 | . "Protocol Version: $protoVersion \n" 185 | . "Session ID: $sessionID \n" 186 | . "[CipherSuites]\n" 187 | . implode("\n", $cipherSuites) . "\n" 188 | . "[Extensions]\n" 189 | . implode("\n", $extensions); 190 | } 191 | 192 | } 193 | 194 | 195 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /src/Record/BlockCipherRecord.php: -------------------------------------------------------------------------------- 1 | maxLength = self::MAX_CIPHER_LENGTH; 35 | } 36 | 37 | /** 38 | * @Override 39 | */ 40 | public function get($property, $default = null) 41 | { 42 | if( $property == 'length' ) 43 | { 44 | return 5 + $this->encLength; 45 | } 46 | 47 | return parent::get($property); 48 | } 49 | 50 | /** 51 | * @Override 52 | */ 53 | protected function encodeContent() 54 | { 55 | $payload = $this->payload; 56 | 57 | $conn = $this->conn; 58 | $core = $conn->getCore(); 59 | 60 | $cipherSuite = $core->cipherSuite; 61 | 62 | $sharedKey = $conn->Key; 63 | $ivLen = $cipherSuite->getIVLen(); 64 | $macLen = $cipherSuite->getMACLen(); 65 | 66 | // Copy payload over to encPayload 67 | $this->encPayload = $this->payload; 68 | $this->encLength = $this->length; 69 | 70 | $IV = substr($this->encPayload, 0, $ivLen); 71 | 72 | $data = $cipherSuite->blockDecrypt($this->encPayload, $sharedKey, $IV); 73 | 74 | // If the decryption fails, a fatal bad_record_mac alert MUST be generated 75 | if( false === $data ) 76 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), "Cipher block decryption failed"); 77 | 78 | // padding length - https://tools.ietf.org/html/rfc5246#section-6.2.3.2 79 | $paddingLength = Core::_unpack('C', $data[strlen($data)-1]); 80 | 81 | // Re-set the length 82 | $this->length = strlen($data) - $ivLen - $macLen - $paddingLength - 1; 83 | 84 | // Set Payload 85 | $this->payload = $payload = substr($data, $ivLen, $this->length); 86 | 87 | if( strlen($this->payload) != $this->length ) 88 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), "Invalid block cipher length"); 89 | 90 | // MAC to verify 91 | $MAC = substr($data, $ivLen + $this->length, $macLen); 92 | $MAC2 = $this->calculateMAC(); 93 | 94 | if( $MAC != $MAC2 ) 95 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), 96 | "Mismatch MAC Record " . base64_encode($MAC) . "<=>" . base64_encode($MAC2)); 97 | 98 | $this->incrementSeq(); 99 | 100 | $content = $core->content; 101 | 102 | $content->encodeContent($this->contentType, $this->payload, $this); 103 | } 104 | 105 | /** 106 | * @Override 107 | */ 108 | public function decode() 109 | { 110 | $conn = $this->conn; 111 | $core = $conn->getCore(); 112 | 113 | $cipherSuite = $core->cipherSuite; 114 | 115 | $sharedKey = $conn->Key; 116 | $ivLen = $cipherSuite->getIVLen(); 117 | $macLen = $cipherSuite->getMACLen(); 118 | 119 | $MAC = $this->calculateMAC(); 120 | 121 | $IV = Core::getRandom($ivLen); 122 | 123 | $data = $this->payload . $MAC; 124 | 125 | // Calculate and append padding 126 | $fpd = function($l, $bz){ 127 | return (($l+$bz) - ($l%$bz)) - $l; 128 | }; 129 | 130 | $paddingLength = $fpd( strlen($this->payload . $MAC) + 1, $ivLen); 131 | 132 | $data .= Core::_pack('C', $paddingLength); 133 | 134 | $encData = $cipherSuite->blockEncrypt($data, $sharedKey, $IV); 135 | 136 | if( false === $encData ) 137 | throw new TLSAlertException(Alert::create(Alert::BAD_RECORD_MAC), "Cipher block encryption failed"); 138 | 139 | $encData = $IV . $encData; 140 | 141 | $this->incrementSeq(); 142 | 143 | if( $this->contentType == ContentType::HANDSHAKE ) 144 | $core->countHandshakeMessages($this->payload); 145 | 146 | $this->set('payload', $encData ); 147 | 148 | return parent::decode(); 149 | } 150 | 151 | private function incrementSeq() 152 | { 153 | if( is_null( $this->seq ) ) 154 | { 155 | $this->seq = $this->getZeroSeq(); 156 | } 157 | 158 | for( $i = 7; $i >= 0; $i--) 159 | { 160 | $num = Core::_unpack('C', $this->seq[$i]) + 1; 161 | $this->seq[$i] = Core::_pack('C', $num ); 162 | 163 | if( $num%256 > 0 ) break; 164 | } 165 | } 166 | 167 | private static function getZeroSeq() 168 | { 169 | $seq = []; 170 | for($i = 0; $i < 8; $i++) 171 | $seq[$i] = Core::_pack('C', 0); 172 | 173 | return $seq; 174 | } 175 | 176 | public function calculateMAC() 177 | { 178 | $conn = $this->conn; 179 | $core = $conn->getCore(); 180 | $cipherSuite = $core->cipherSuite; 181 | 182 | list($vMajor, $vMinor) = $core->getVersion(); 183 | 184 | if( is_null( $this->seq ) ) 185 | { 186 | $this->seq = self::getZeroSeq(); 187 | } 188 | 189 | $secretMAC = $conn->MAC; 190 | 191 | $contentType = Core::_pack( 'C', $this->contentType ); 192 | $major = Core::_pack( 'C', $vMajor ); 193 | $minor = Core::_pack( 'C', $vMinor ); 194 | 195 | $length = Core::_pack('n', strlen($this->payload)); 196 | 197 | /* 198 | * https://tools.ietf.org/html/rfc5246#section-6.2.3.1 199 | * 200 | * The MAC is generated as: 201 | * 202 | * MAC(MAC_write_key, seq_num + 203 | * TLSCompressed.type + 204 | * TLSCompressed.version + 205 | * TLSCompressed.length + 206 | * TLSCompressed.fragment); 207 | */ 208 | $concat = implode('', $this->seq ) 209 | . $contentType 210 | . $major 211 | . $minor 212 | . $length 213 | . $this->payload; 214 | 215 | //$macStr = $cipherSuite->hashHmac($concat, $secretMAC, false ); 216 | $mac = $cipherSuite->hashHmac($concat, $secretMAC ); 217 | 218 | return $mac; 219 | 220 | } 221 | 222 | } 223 | 224 | 225 | -------------------------------------------------------------------------------- /src/Extensions/SignatureAlgorithm.php: -------------------------------------------------------------------------------- 1 | ; 31 | */ 32 | class SignatureAlgorithm extends ExtensionAbstract 33 | { 34 | const TYPE_DEFAULT_RSA = 0x0201; //sha1 rsa 35 | 36 | const TYPE_SHA512_RSA = 0x0601; 37 | const TYPE_SHA384_RSA = 0x0501; 38 | const TYPE_SHA256_RSA = 0x0401; 39 | 40 | const TYPE_SHA512_ECDSA = 0x0603; 41 | const TYPE_SHA384_ECDSA = 0x0503; 42 | const TYPE_SHA256_ECDSA = 0x0403; 43 | 44 | const TYPE_SIGNATURE_RSA = 1; 45 | const TYPE_SIGNATURE_ECDSA = 3; 46 | 47 | private static $supportedAlgorithmList = [ 48 | self::TYPE_SHA512_RSA, 49 | self::TYPE_SHA384_RSA, 50 | self::TYPE_SHA256_RSA, 51 | self::TYPE_SHA512_ECDSA, 52 | self::TYPE_SHA384_ECDSA, 53 | self::TYPE_SHA256_ECDSA, 54 | ]; 55 | 56 | private $core; 57 | private $algorithm; 58 | 59 | // Used for server 60 | private $availSigAlgo; 61 | 62 | public function __construct(Core $core) 63 | { 64 | $this->core = $core; 65 | $this->algorithm = null; 66 | 67 | if( $core->isServer ) 68 | { 69 | // Set available signature algorithm 70 | $this->availSigAlgo = $core->getConfig('is_ecdsa') 71 | ? self::TYPE_SIGNATURE_ECDSA : self::TYPE_SIGNATURE_RSA; 72 | } 73 | } 74 | 75 | public function onEncodeClientHello($type, $data) 76 | { 77 | $core = $this->core; 78 | 79 | if( $type != TLSExtensions::TYPE_SIGNATURE_ALGORITHM ) 80 | return; 81 | 82 | $protoVersion = $core->getProtocolVersion(); 83 | 84 | /* 85 | * Note: this extension is not meaningful for TLS versions prior to 1.2. 86 | * Clients MUST NOT offer it if they are offering prior versions. 87 | * However, even if clients do offer it, the rules specified in [TLSEXT] 88 | * require servers to ignore extensions they do not understand. 89 | */ 90 | if( $protoVersion < 32 ) 91 | return; 92 | 93 | $length = Core::_unpack('n', $data[0] . $data[1]); 94 | $data = substr($data, 2); 95 | 96 | for( $i = 0; $i < $length; $i += 2 ) 97 | { 98 | $hash = Core::_unpack('C', $data[$i]); 99 | $sig = Core::_unpack('C', $data[$i+1]); 100 | 101 | $algorithm = $hash << 8 | $sig; 102 | 103 | if( in_array( $algorithm, self::$supportedAlgorithmList ) && $this->availSigAlgo == $sig ) 104 | { 105 | $this->algorithm = $algorithm; 106 | break; 107 | } 108 | } 109 | } 110 | 111 | public function onDecodeClientHello() 112 | { 113 | $sigData = ''; 114 | 115 | foreach(self::$supportedAlgorithmList as $algorithm) 116 | { 117 | $sigData .= Core::_pack('C', $algorithm >> 8) . Core::_pack('C', $algorithm & 0x00ff); 118 | } 119 | 120 | $sigData = Core::_pack('n', strlen($sigData) ) . $sigData; 121 | 122 | $this->extType = TLSExtensions::TYPE_SIGNATURE_ALGORITHM; 123 | $this->length = strlen($sigData); 124 | 125 | $data = $this->decodeHeader() . $sigData; 126 | 127 | return $data; 128 | } 129 | 130 | public function onDecodeServerHello(){} 131 | 132 | public function getAlgorithm() 133 | { 134 | $algorithm = is_null($this->algorithm) ? self::TYPE_DEFAULT_RSA : $this->algorithm; 135 | return [$algorithm >> 8, $algorithm & 0x00ff]; 136 | } 137 | 138 | /** 139 | * Our version of md5sha1 signature as openssl doesn't support it 140 | */ 141 | public function getSignatureMD5Sha1($dataSign, &$signature, $privateKey) 142 | { 143 | $hash = md5($dataSign, true) . sha1($dataSign, true); 144 | openssl_private_encrypt($hash, $signature, $privateKey); 145 | } 146 | 147 | public function getSignature($dataSign) 148 | { 149 | $core = $this->core; 150 | $privateKey = $core->getConfig('private_key'); 151 | $isECDSA = $core->getConfig('is_ecdsa'); 152 | 153 | if( $isECDSA ) 154 | $ecdsa = $core->getConfig('ecdsa'); 155 | 156 | $protoVersion = $core->getProtocolVersion(); 157 | 158 | /* 159 | * https://www.ietf.org/rfc/rfc2246.txt page 40 160 | * 161 | * select (SignatureAlgorithm) 162 | * { case anonymous: struct { }; 163 | * case rsa: 164 | * digitally-signed struct { 165 | * opaque md5_hash[16]; 166 | * opaque sha_hash[20]; 167 | * }; 168 | * case dsa: 169 | * digitally-signed struct { 170 | * opaque sha_hash[20]; 171 | * }; 172 | * } Signature; 173 | */ 174 | if( $protoVersion < 32 ) 175 | { 176 | $this->getSignatureMD5Sha1($dataSign, $signature, $privateKey); 177 | return $signature; 178 | } 179 | 180 | /* 181 | * https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1 182 | * 183 | * If the client does not send the signature_algorithms extension, the 184 | * server MUST do the following: 185 | * 186 | * - If the negotiated key exchange algorithm is one of (RSA, DHE_RSA, 187 | * DH_RSA, RSA_PSK, ECDH_RSA, ECDHE_RSA), behave as if client had 188 | * sent the value {sha1,rsa}. 189 | * 190 | * - If the negotiated key exchange algorithm is one of (ECDH_ECDSA, 191 | * ECDHE_ECDSA), behave as if the client had sent value {sha1,ecdsa}. 192 | */ 193 | if( is_null( $this->algorithm ) ) 194 | { 195 | if( $isECDSA ) 196 | $signature = $ecdsa->getSignature($dataSign, 'sha1'); 197 | else 198 | openssl_sign($dataSign, $signature, $privateKey, OPENSSL_ALGO_SHA1); 199 | 200 | return $signature; 201 | } 202 | 203 | switch( $this->algorithm ) 204 | { 205 | case self::TYPE_SHA512_RSA: 206 | openssl_sign($dataSign, $signature, $privateKey, OPENSSL_ALGO_SHA512); 207 | break; 208 | case self::TYPE_SHA384_RSA: 209 | openssl_sign($dataSign, $signature, $privateKey, OPENSSL_ALGO_SHA384); 210 | break; 211 | case self::TYPE_SHA256_RSA: 212 | openssl_sign($dataSign, $signature, $privateKey, OPENSSL_ALGO_SHA256); 213 | break; 214 | case self::TYPE_SHA512_ECDSA: 215 | $signature = $ecdsa->getSignature($dataSign, 'sha512'); 216 | break; 217 | case self::TYPE_SHA384_ECDSA: 218 | $signature = $ecdsa->getSignature($dataSign, 'sha384'); 219 | break; 220 | case self::TYPE_SHA256_ECDSA: 221 | $signature = $ecdsa->getSignature($dataSign, 'sha256'); 222 | break; 223 | 224 | } 225 | 226 | return $signature; 227 | } 228 | } 229 | 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /src/Core.php: -------------------------------------------------------------------------------- 1 | config = $config; 114 | 115 | $this->isHandshaked = false; 116 | $this->isClosed = false; 117 | 118 | $this->isServer = $isServer; 119 | 120 | $this->server = new ConnectionDuplex($this); 121 | $this->client = new ConnectionDuplex($this); 122 | 123 | $this->handshakeMessages = ''; 124 | 125 | $this->bufferIn = new Buffer(); 126 | $this->bufferOut = new Buffer(); 127 | 128 | $this->extensions = new TLSExtensions($this); 129 | 130 | // Compression Method - uncompressed 131 | $this->compressionMethod = 0; 132 | 133 | // Prf 134 | $this->prf = new Prf($this); 135 | 136 | $this->handshakeMessages = []; 137 | 138 | if( $isServer ) 139 | { 140 | $this->crtDers = $this->config->get('crt_ders'); 141 | $this->content = new ServerContent($this); 142 | } 143 | else // Client 144 | { 145 | $this->content = new ClientContent($this); 146 | $this->setVersion(3, $config->get('version', $this->vMinorDefault)); 147 | } 148 | } 149 | 150 | /** 151 | * Getter and Setter 152 | */ 153 | public function __call(string $name, array $args) 154 | { 155 | if( strlen($name) < 3 ) 156 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Core::$name too short"); 157 | 158 | $properties = ['bufferIn', 'bufferOut', 'sessionID', 'compressionMethod', 'masterSecret', 'crtDers']; 159 | 160 | $getOrSet = substr($name, 0, 3); 161 | 162 | if( $getOrSet != 'get' && $getOrSet != 'set' ) 163 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Core::$name not exist"); 164 | 165 | $property = substr($name, 3); 166 | $property[0] = strtolower($property[0]); 167 | 168 | if( !in_array($property, $properties) ) 169 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Core::$property not exist"); 170 | 171 | // Getter 172 | if( $getOrSet == 'get' ) 173 | return $this->$property; 174 | 175 | if( !isset( $args[0] ) ) 176 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "No args set for Core::$property"); 177 | 178 | // Setter 179 | $this->$property = $args[0]; 180 | } 181 | 182 | public function getOutDuplex() 183 | { 184 | return ($this->isServer ) ? $this->server : $this->client; 185 | } 186 | 187 | public function getInDuplex() 188 | { 189 | return ($this->isServer ) ? $this->client : $this->server; 190 | } 191 | 192 | public function getProtocolVersion() 193 | { 194 | return $this->protocolVersion; 195 | } 196 | 197 | public function getVersion() 198 | { 199 | if( $this->vMajor == 0 || $this->vMinor == 0 ) 200 | return [$this->vMajorDefault, $this->vMinorDefault]; 201 | 202 | return [$this->vMajor, $this->vMinor]; 203 | } 204 | 205 | public function setVersion($vMajor, $vMinor) 206 | { 207 | if( $vMajor != 3 || ( $vMinor > 3 && $vMinor < 2 ) ) 208 | throw new TLSAlertException(Alert::create(Alert::PROTOCOL_VERSION), "Unsupported Protocol $vMajor:$vMinor"); 209 | 210 | if( !is_null( $this->protocolVersion ) ) 211 | return; 212 | 213 | $this->vMajor = $vMajor; 214 | $this->vMinor = $vMinor; 215 | 216 | // TLS1.2 217 | if( $this->vMinor == 3 ) 218 | $this->protocolVersion = 32; 219 | // TLS1.1 220 | else 221 | $this->protocolVersion = 31; 222 | } 223 | 224 | /** 225 | * All Handshake messages must be recorded 226 | */ 227 | public function countHandshakeMessages($msg) 228 | { 229 | if( $this->isHandshaked ) 230 | return; 231 | 232 | $this->handshakeMessages[] = $msg; 233 | } 234 | 235 | public function getHandshakeMessages($sub = 0) 236 | { 237 | if( $sub != 0 ) 238 | $msg = implode('', array_slice($this->handshakeMessages, 0, count($this->handshakeMessages) - $sub)); 239 | else 240 | $msg = implode('', $this->handshakeMessages); 241 | 242 | return $msg; 243 | } 244 | 245 | public function getConfig($key) 246 | { 247 | return $this->config->get($key); 248 | } 249 | 250 | /** 251 | * Generate client/server random, IV, PreMaster for RSA Key Exchange 252 | */ 253 | public static function getRandom($length) 254 | { 255 | $random = openssl_random_pseudo_bytes($length, $strong); 256 | 257 | if( true !== $strong ) 258 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Random byte not strong"); 259 | 260 | return $random; 261 | } 262 | 263 | public static function _unpack( $f, $d ) 264 | { 265 | $r = unpack( $f, $d ); 266 | 267 | if( !is_array($r) || !isset($r[1]) ) 268 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "unpack failed. format: $f, data: $d"); 269 | 270 | return $r[1]; 271 | } 272 | 273 | public static function _pack( $f, $d ) 274 | { 275 | return pack( $f, $d ); 276 | } 277 | } 278 | 279 | 280 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /src/Extensions/Curve.php: -------------------------------------------------------------------------------- 1 | core = $core; 38 | $this->isUncompressed = false; 39 | } 40 | 41 | public function isEnabled() 42 | { 43 | return $this->isUncompressed && !is_null( $this->namedCurveType ); 44 | } 45 | 46 | public function onEncodeClientHello($type, $data) 47 | { 48 | $core = $this->core; 49 | 50 | switch($type) 51 | { 52 | /* 53 | * elliptic_curves(10) 54 | * 55 | * https://tools.ietf.org/html/rfc4492#section-5.1.1 56 | * 57 | * struct { 58 | * NamedCurve elliptic_curve_list<1..2^16-1> 59 | * } EllipticCurveList; 60 | */ 61 | case TLSExtensions::TYPE_ELLIPTIC_CURVES: 62 | $length = Core::_unpack('n', $data[0] . $data[1]); 63 | $data = substr($data, 2); 64 | 65 | for( $i = 0; $i < $length; $i += 2 ) 66 | { 67 | $namedCurveType = Core::_unpack('n', $data[$i] . $data[$i+1]); 68 | 69 | if( Ecdh::isSupported($namedCurveType) ) 70 | { 71 | $this->namedCurveType = $namedCurveType; 72 | break; 73 | } 74 | } 75 | 76 | break; 77 | 78 | case TLSExtensions::TYPE_EC_POINT_FORMATS: 79 | $this->encodeEcPointFormat($data); 80 | break; 81 | } 82 | } 83 | 84 | /** 85 | * ec_point_formats(11) 86 | * 87 | * https://tools.ietf.org/html/rfc4492#section-5.1.2 88 | * 89 | * enum { uncompressed (0), ansiX962_compressed_prime (1), 90 | * ansiX962_compressed_char2 (2), reserved (248..255) 91 | * } ECPointFormat; 92 | * 93 | * struct { 94 | * ECPointFormat ec_point_format_list<1..2^8-1> 95 | * } ECPointFormatList; 96 | * 97 | * We ONLY support uncompressed(0) 98 | */ 99 | private function encodeEcPointFormat($data) 100 | { 101 | $length = Core::_unpack('C', $data[0]); 102 | $data = substr($data, 1); 103 | 104 | for( $i = 0; $i < $length; $i++ ) 105 | { 106 | $format = Core::_unpack('C', $data[$i]); 107 | if( $format == 0 ) 108 | { 109 | $this->isUncompressed = true; 110 | break; 111 | } 112 | } 113 | } 114 | 115 | public function onEncodeServerHello($type, $data) 116 | { 117 | $core = $this->core; 118 | 119 | if( $type != TLSExtensions::TYPE_EC_POINT_FORMATS ) 120 | return; 121 | 122 | $this->encodeEcPointFormat($data); 123 | } 124 | 125 | private function decodeEcPointFormat() 126 | { 127 | // ec_point_format - uncompressed 128 | $data = Core::_pack('C', 1) . Core::_pack('C', 0); 129 | 130 | $this->extType = TLSExtensions::TYPE_EC_POINT_FORMATS; 131 | $this->length = strlen($data); 132 | 133 | return $this->decodeHeader() . $data; 134 | } 135 | 136 | public function onDecodeClientHello() 137 | { 138 | // ec_point_format 139 | $data = $this->decodeEcPointFormat(); 140 | 141 | // elliptic curves 142 | $namedCurveTypes = ''; 143 | 144 | foreach(EcDH::$typeList as $namedCurveType) 145 | { 146 | $namedCurveTypes .= Core::_pack('n', $namedCurveType); 147 | } 148 | 149 | $namedCurveData = Core::_pack('n', strlen($namedCurveTypes)) . $namedCurveTypes; 150 | 151 | $this->extType = TLSExtensions::TYPE_ELLIPTIC_CURVES; 152 | $this->length = strlen($namedCurveData); 153 | 154 | $data .= $this->decodeHeader() . $namedCurveData; 155 | 156 | return $data; 157 | } 158 | 159 | public function onDecodeServerHello() 160 | { 161 | if( !$this->isEnabled() ) 162 | return; 163 | 164 | return $this->decodeEcPointFormat(); 165 | } 166 | 167 | public function encodeServerKeyExchange($data) 168 | { 169 | // Must turn true when onEncodeServerHello is called 170 | //if( !$this->isUncompressed ) 171 | // return; 172 | 173 | $core = $this->core; 174 | $hs = HandShakeFactory::getInstance($core, HandshakeType::SERVER_KEY_EXCHANGE); 175 | 176 | $data = $hs->encodeHeader($data); 177 | 178 | $length = $hs->get('length'); 179 | 180 | $curveType = Core::_unpack('C', $data[0]); 181 | 182 | if( $curveType != 0x03 ) 183 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), 184 | "Not named curve type: " + $curveType); 185 | 186 | $namedCurveType = Core::_unpack('n', $data[1] . $data[2] ); 187 | 188 | if( !EcDH::isSupported($namedCurveType) ) 189 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), 190 | "Unknow named curve: " + $namedCurveType); 191 | 192 | $this->namedCurveType = $namedCurveType; 193 | 194 | $this->ecdh = new EcDH($this->namedCurveType); 195 | 196 | $publicKeyLen = Core::_unpack('C', $data[3] ); 197 | $data = substr($data, 4); 198 | 199 | $publicKeyBin = substr($data, 0, $publicKeyLen); 200 | 201 | // Calculate and set premaster 202 | $this->calculatePremaster($publicKeyBin); 203 | 204 | // TODO verify signature 205 | } 206 | 207 | public function decodeClientKeyExchange() 208 | { 209 | $core = $this->core; 210 | $publicKey = $this->getSenderPublicKey(); 211 | $data = Core::_pack('C', strlen($publicKey) ) . $publicKey; 212 | 213 | return $data; 214 | } 215 | 216 | public function decodeServerKeyExchange() 217 | { 218 | $core = $this->core; 219 | $extensions = $core->extensions; 220 | 221 | $protoVersion = $core->getProtocolVersion(); 222 | 223 | /* 224 | * ECCurveType 225 | * 226 | * We only support named curves, which is 0x03 227 | * 228 | * enum { explicit_prime (1), explicit_char2 (2), 229 | * named_curve (3), reserved(248..255) } ECCurveType; 230 | */ 231 | $data = Core::_pack('C', 0x03); 232 | 233 | // Named curve type 234 | $data .= Core::_pack('n', $this->namedCurveType); 235 | 236 | // ECDH Public Key 237 | $this->ecdh = new EcDH($this->namedCurveType); 238 | $dataPublicKey = $this->ecdh->getPublicKey(); 239 | 240 | $data .= Core::_pack('C', strlen($dataPublicKey)) . $dataPublicKey; 241 | 242 | /* 243 | * Signature 244 | * 245 | * https://tools.ietf.org/html/rfc4492 Page 19 246 | * signed_params: A hash of the params, with the signature appropriate 247 | * to that hash applied. The private key corresponding to the 248 | * certified public key in the server's Certificate message is used 249 | * for signing. 250 | * 251 | * ServerKeyExchange.signed_params.sha_hash 252 | * SHA(ClientHello.random + ServerHello.random + 253 | * ServerKeyExchange.params); 254 | */ 255 | $connIn = $core->getInDuplex(); 256 | $connOut = $core->getOutDuplex(); 257 | 258 | $dataSign = $connIn->random . $connOut->random . $data; 259 | 260 | $signature = $extensions->call('SignatureAlgorithm', 'getSignature', null, $dataSign); 261 | 262 | if( $protoVersion >= 32 ) 263 | { 264 | // Signature Hash Alogorithm 265 | // [null, null] never happens 266 | list( $hash, $sig ) = $extensions->call('SignatureAlgorithm', 'getAlgorithm', [null, null]); 267 | $data .= Core::_pack('C', $hash) . Core::_pack('C', $sig); 268 | } 269 | 270 | // Append signature 271 | $data .= Core::_pack('n', strlen($signature)) . $signature; 272 | 273 | $hs = HandShakeFactory::getInstance($core, HandshakeType::SERVER_KEY_EXCHANGE); 274 | 275 | $hs->setMsgType(HandshakeType::SERVER_KEY_EXCHANGE); 276 | $hs->set('length', strlen($data)); 277 | 278 | return $hs->getBinHeader() . $data; 279 | } 280 | 281 | /** 282 | * https://tools.ietf.org/html/rfc4492#section-2 283 | * Called from HandshakeClientKeyExchange 284 | */ 285 | public function calculatePremaster($publicKeyBin) 286 | { 287 | $ecdh = $this->ecdh; 288 | $sharedKey = $ecdh->calculateSharedKey($publicKeyBin); 289 | 290 | $this->preMaster = $sharedKey; 291 | 292 | return $sharedKey; 293 | } 294 | 295 | public function getPremaster() 296 | { 297 | return $this->preMaster; 298 | } 299 | 300 | public function getSenderPublicKey() 301 | { 302 | $ecdh = $this->ecdh; 303 | return $ecdh->getPublicKey(); 304 | } 305 | } 306 | 307 | 308 | -------------------------------------------------------------------------------- /src/CipherSuites.php: -------------------------------------------------------------------------------- 1 | 26 | ['cipher_type' => self::CIPHER_TYPE_AEAD, 'crypto_method' => 'AES-128-GCM', 'mac_len' => 0, 'iv_len' => 4, 'key_len' => 16, 'mac' => 'sha256'], 27 | 28 | self::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => 29 | ['cipher_type' => self::CIPHER_TYPE_AEAD, 'crypto_method' => 'AES-128-GCM', 'mac_len' => 0, 'iv_len' => 4, 'key_len' => 16, 'mac' => 'sha256'], 30 | 31 | self::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => 32 | ['cipher_type' => self::CIPHER_TYPE_AEAD, 'crypto_method' => 'AES-256-GCM', 'mac_len' => 0, 'iv_len' => 4, 'key_len' => 32, 'mac' => 'sha384'], 33 | 34 | self::TLS_RSA_WITH_AES_128_GCM_SHA256 => 35 | ['cipher_type' => self::CIPHER_TYPE_AEAD, 'crypto_method' => 'AES-128-GCM', 'mac_len' => 0, 'iv_len' => 4, 'key_len' => 16, 'mac' => 'sha256'], 36 | 37 | self::TLS_RSA_WITH_AES_256_GCM_SHA384 => 38 | ['cipher_type' => self::CIPHER_TYPE_AEAD, 'crypto_method' => 'AES-256-GCM', 'mac_len' => 0, 'iv_len' => 4, 'key_len' => 32, 'mac' => 'sha384'], 39 | 40 | self::TLS_RSA_WITH_AES_256_CBC_SHA => 41 | ['cipher_type' => self::CIPHER_TYPE_BLOCK, 'crypto_method' => 'AES-256-CBC', 'mac_len' => 20, 'iv_len' => 16, 'key_len' => 32, 'mac' => 'sha1'], 42 | 43 | self::TLS_RSA_WITH_AES_128_CBC_SHA => 44 | ['cipher_type' => self::CIPHER_TYPE_BLOCK, 'crypto_method' => 'AES-128-CBC', 'mac_len' => 20, 'iv_len' => 16, 'key_len' => 16, 'mac' => 'sha1'], 45 | 46 | self::TLS_RSA_WITH_AES_128_CBC_SHA256 => 47 | ['cipher_type' => self::CIPHER_TYPE_BLOCK, 'crypto_method' => 'AES-128-CBC', 'mac_len' => 32, 'iv_len' => 16, 'key_len' => 16, 'mac' => 'sha256'], 48 | 49 | self::TLS_RSA_WITH_AES_256_CBC_SHA256 => 50 | ['cipher_type' => self::CIPHER_TYPE_BLOCK, 'crypto_method' => 'AES-256-CBC', 'mac_len' => 32, 'iv_len' => 16, 'key_len' => 32, 'mac' => 'sha256'], 51 | 52 | self::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA => 53 | ['cipher_type' => self::CIPHER_TYPE_BLOCK, 'crypto_method' => 'AES-128-CBC', 'mac_len' => 20, 'iv_len' => 16, 'key_len' => 16, 'mac' => 'sha1'], 54 | 55 | self::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA => 56 | ['cipher_type' => self::CIPHER_TYPE_BLOCK, 'crypto_method' => 'AES-256-CBC', 'mac_len' => 20, 'iv_len' => 16, 'key_len' => 32, 'mac' => 'sha1'], 57 | ]; 58 | 59 | public static $enabledCipherSuites = [ 60 | self::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 61 | self::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 62 | self::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 63 | self::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 64 | self::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 65 | self::TLS_RSA_WITH_AES_256_GCM_SHA384, 66 | self::TLS_RSA_WITH_AES_128_GCM_SHA256, 67 | self::TLS_RSA_WITH_AES_256_CBC_SHA256, 68 | self::TLS_RSA_WITH_AES_256_CBC_SHA, 69 | self::TLS_RSA_WITH_AES_128_CBC_SHA256, 70 | self::TLS_RSA_WITH_AES_128_CBC_SHA, 71 | ]; 72 | 73 | const CIPHER_TYPE_STREAM = 1; 74 | const CIPHER_TYPE_BLOCK = 2; 75 | const CIPHER_TYPE_AEAD = 3; 76 | 77 | // Authentication algorithm 78 | const AU_TYPE_RSA = 1; 79 | const AU_TYPE_ECDSA = 2; 80 | 81 | private $macLen; 82 | private $ivLen; 83 | private $keyLen; 84 | private $macAlgorithm; 85 | private $cryptoMethod; 86 | private $cipherType; 87 | private $cipherID; 88 | 89 | public function __construct(array $arr) 90 | { 91 | $cipherID = $arr[0] << 8 | $arr[1]; 92 | 93 | if( !array_key_exists($cipherID, self::$cipherList) ) 94 | throw new TLSAlertException(Alert::create(Alert::INTERNAL_ERROR), "Failed to initiate CipherSuite. cipherID: $cipherID"); 95 | 96 | $cipherSuite = self::$cipherList[$cipherID]; 97 | 98 | $this->cipherID = $cipherID; 99 | 100 | $this->cipherType = $cipherSuite['cipher_type']; 101 | $this->ivLen = $cipherSuite['iv_len']; 102 | $this->keyLen = $cipherSuite['key_len']; 103 | $this->macLen = $cipherSuite['mac_len']; 104 | $this->cryptoMethod = $cipherSuite['crypto_method']; 105 | $this->macAlgorithm = $cipherSuite['mac']; 106 | } 107 | 108 | public function isECDHEEnabled() 109 | { 110 | return self::isECDHE($this->cipherID); 111 | } 112 | 113 | public static function isGCM($cipherID) 114 | { 115 | switch($cipherID) 116 | { 117 | case self::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: 118 | case self::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: 119 | case self::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: 120 | case self::TLS_RSA_WITH_AES_128_GCM_SHA256: 121 | case self::TLS_RSA_WITH_AES_256_GCM_SHA384: 122 | return true; 123 | } 124 | 125 | return false; 126 | } 127 | 128 | public static function isECDHE($cipherID) 129 | { 130 | switch($cipherID) 131 | { 132 | case self::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: 133 | case self::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: 134 | case self::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: 135 | case self::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: 136 | case self::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: 137 | return true; 138 | } 139 | 140 | return false; 141 | } 142 | 143 | public static function isECDSA($cipherID) 144 | { 145 | switch($cipherID) 146 | { 147 | case self::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: 148 | return true; 149 | } 150 | 151 | return false; 152 | } 153 | 154 | /** 155 | * Called by the server 156 | */ 157 | public static function pickCipherID(Core $core, array $arr) 158 | { 159 | $extensions = $core->extensions; 160 | 161 | $servAUType = $core->getConfig('is_ecdsa') ? self::AU_TYPE_ECDSA : self::AU_TYPE_RSA; 162 | 163 | foreach( $arr as $val) 164 | { 165 | list($cipher1, $cipher2) = $val; 166 | 167 | $cipherID = $cipher1 << 8 | $cipher2; 168 | 169 | $auType = self::isECDSA($cipherID) ? self::AU_TYPE_ECDSA : self::AU_TYPE_RSA; 170 | 171 | // Check for Authentication algorithm 172 | if( $servAUType !== $auType ) 173 | continue; 174 | 175 | if( in_array($cipherID, self::$enabledCipherSuites ) ) 176 | { 177 | // Check for ECDHE 178 | if( self::isECDHE($cipherID) && true !== $extensions->call('Curve', 'isEnabled', false) ) 179 | continue; 180 | else 181 | return [$cipher1, $cipher2]; 182 | } 183 | } 184 | 185 | return null; 186 | } 187 | 188 | public static function decodeCipherList() 189 | { 190 | $data = ''; 191 | 192 | foreach(self::$enabledCipherSuites as $val) 193 | { 194 | $data .= Core::_pack('C', $val >> 8) . Core::_pack('C', $val & 0x00ff); 195 | } 196 | 197 | return $data; 198 | } 199 | 200 | private function getProperty($property) 201 | { 202 | if( !property_exists($this, $property) ) 203 | return; 204 | 205 | return $this->$property; 206 | } 207 | 208 | public function getID() 209 | { 210 | $cipherID = $this->getProperty('cipherID'); 211 | return [$cipherID >> 8, $cipherID & 0x00ff]; 212 | } 213 | 214 | /** 215 | * 216 | * https://tools.ietf.org/html/rfc5288 Page 2 217 | * 218 | * The Pseudo Random Function (PRF) algorithms SHALL be as follows: 219 | * 220 | * For cipher suites ending with _SHA256, the PRF is the TLS PRF 221 | * [RFC5246] with SHA-256 as the hash function. 222 | * 223 | * For cipher suites ending with _SHA384, the PRF is the TLS PRF 224 | * [RFC5246] with SHA-384 as the hash function. 225 | * 226 | * This is also used by Finished message in Handshake 227 | */ 228 | public function getHashAlogV33() 229 | { 230 | if( self::isGCM($this->cipherID) ) 231 | return $this->macAlgorithm; 232 | 233 | return 'sha256'; 234 | } 235 | 236 | public function getKeyLen() 237 | { 238 | return $this->getProperty('keyLen'); 239 | } 240 | 241 | public function getIVLen() 242 | { 243 | return $this->getProperty('ivLen'); 244 | } 245 | 246 | public function getMACLen() 247 | { 248 | return $this->getProperty('macLen'); 249 | } 250 | 251 | public function getCipherType() 252 | { 253 | return $this->getProperty('cipherType'); 254 | } 255 | 256 | public function blockDecrypt($encPayload, $sharedKey, $IV) 257 | { 258 | $data = openssl_decrypt($encPayload, $this->cryptoMethod, $sharedKey, OPENSSL_ZERO_PADDING|OPENSSL_RAW_DATA, $IV); 259 | return $data; 260 | } 261 | 262 | public function blockEncrypt($payload, $sharedKey, $IV) 263 | { 264 | $encData = openssl_encrypt($payload, $this->cryptoMethod, $sharedKey, OPENSSL_RAW_DATA, $IV); 265 | return $encData; 266 | } 267 | 268 | public function hashHmac($data, $secretMac, $binary = true) 269 | { 270 | return hash_hmac($this->macAlgorithm, $data, $secretMac, $binary ); 271 | } 272 | 273 | public function gcmEncrypt($payload, $sharedKey, $nonce, $aad) 274 | { 275 | return AEADGcm::encrypt($payload, $sharedKey, $nonce, $aad); 276 | } 277 | 278 | public function gcmDecrypt($encData, $sharedKey, $nonce, $aad) 279 | { 280 | return AEADGcm::decrypt($encData, $sharedKey, $nonce, $aad); 281 | } 282 | 283 | public function debugInfo() 284 | { 285 | $class = new \ReflectionClass(__CLASS__); 286 | $constants = array_flip($class->getConstants()); 287 | 288 | $outputs[] = 'Cipher ID: ' . $constants[$this->cipherID]; 289 | $outputs[] = 'MAC Length: ' . $this->macLen; 290 | $outputs[] = 'IV Length: ' . $this->ivLen; 291 | $outputs[] = 'MAC Algorithm: ' . $this->macAlgorithm; 292 | 293 | $r = "[CipherSuite]\n" 294 | . implode("\n", $outputs); 295 | 296 | return $r; 297 | } 298 | 299 | } 300 | 301 | 302 | 303 | --------------------------------------------------------------------------------