Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env php
- <?php
- /*
- MYSQL TABLE:
- CREATE TABLE IF NOT EXISTS `sites` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `active` enum('0','1') COLLATE utf8_unicode_ci DEFAULT NULL,
- `name` varchar(256) COLLATE utf8_unicode_ci NOT NULL,
- `webroot` varchar(256) COLLATE utf8_unicode_ci NOT NULL,
- `domains` longtext COLLATE utf8_unicode_ci NOT NULL,
- `nsupdate` varchar(256) COLLATE utf8_unicode_ci NOT NULL,
- `renewdays` int(11) NOT NULL DEFAULT '30',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
- */
- $le_key = '/usr/local/sbin/account_key.pem';
- $le_email = 'youremailhere';
- $rootca = file_get_contents('https://letsencrypt.org/certs/isrgrootx1.pem');
- $sslroot = '/etc/ssl/web/';
- $db = new mysqli('localhost', 'user', 'pass', 'db');
- class LE {
- private
- $directory='https://acme-v01.api.letsencrypt.org/directory', // live
- //$directory='https://acme-staging.api.letsencrypt.org/directory', // staging
- $resources=null,
- $nonce='',
- $header, // JOSE Header
- $account_key;
- public
- $thumbprint,
- $acme_path='.well-known/acme-challenge/';
- public function __construct($account_key_pem)
- {
- // load account key
- if (false===($this->account_key=openssl_pkey_get_private('file://'.$account_key_pem))){
- throw new Exception(
- 'Could not load account key: '.$account_key_pem."\n".
- openssl_error_string()
- );
- }
- // get account key details
- if (false===($details=openssl_pkey_get_details($this->account_key))){
- throw new Exception(
- 'Could not get account key details: '.$account_key_pem."\n".
- openssl_error_string()
- );
- }
- // JOSE Header - RFC7515
- $this->header=array(
- 'alg'=>'RS256',
- 'jwk'=>array( // JSON Web Key
- 'e'=>$this->base64url($details['rsa']['e']), // public exponent
- 'kty'=>'RSA',
- 'n'=>$this->base64url($details['rsa']['n']) // public modulus
- )
- );
- // JSON Web Key (JWK) Thumbprint - RFC7638
- $this->thumbprint=$this->base64url(
- hash(
- 'sha256',
- json_encode($this->header['jwk']),
- true
- )
- );
- }
- public function __destruct(){
- if ($this->account_key) {
- openssl_pkey_free($this->account_key);
- }
- }
- private function init(){
- $ret=$this->http_request($this->directory); // Read ACME Directory
- $this->resources=$ret['body']; // store resources for later use
- $this->nonce=$ret['headers']['replay-nonce']; // capture first replay-nonce
- }
- // Encapsulate $payload into JSON Web Signature (JWS) - RFC7515
- private function jws_encapsulate($payload){
- $protected=$this->header;
- $protected['nonce']=$this->nonce; // replay-nonce
- $protected64=$this->base64url(json_encode($protected));
- $payload64=$this->base64url(json_encode($payload));
- if (false===openssl_sign(
- $protected64.'.'.$payload64,
- $signature,
- $this->account_key,
- OPENSSL_ALGO_SHA256
- )){
- throw new Exception(
- 'Failed to sign payload !'."\n".
- openssl_error_string()
- );
- }
- return array(
- 'header'=>$this->header,
- 'protected'=>$protected64,
- 'payload'=>$payload64,
- 'signature'=>$this->base64url($signature)
- );
- }
- // RFC7515 - Appendix C
- final public function base64url($data){
- return rtrim(strtr(base64_encode($data),'+/','-_'),'=');
- }
- final public function request($type,$payload=array(),$url=null,$raw=false,$accept=null){
- if ($this->resources===null){
- $this->init(); // read AMCE directory and get first replay-nonce
- }
- $data=json_encode(
- $this->jws_encapsulate(
- array_merge(
- $payload,
- array('resource'=>$type)
- )
- )
- );
- $ret=$this->http_request($url===null?$this->resources[$type]:$url,$data,$raw,$accept);
- $this->nonce=$ret['headers']['replay-nonce']; // capture replay-nonce
- return $ret;
- }
- final public function http_request($url,$data=null,$raw=false,$accept=null){
- $ctx=stream_context_create(
- array(
- 'http'=>array(
- 'header'=>$data===null?'':'Content-Type: application/json',
- 'method'=>$data===null?'GET':'POST',
- 'user_agent'=>'CertLE (PHP LE Client)',
- 'ignore_errors'=>true,
- 'timeout'=>60,
- 'content'=>$data
- )
- )
- );
- $body=@file_get_contents($url,false,$ctx);
- if ($body===false){
- throw new Exception('request error: '.$url);
- }
- list(,$code,$status)=explode(' ',reset($http_response_header),3);
- $headers=array_reduce( // parse http headers into array
- array_slice($http_response_header,1),
- function($carry,$item){
- list($k,$v)=explode(':',$item,2);
- $k=strtolower(trim($k));
- $v=trim($v);
- if ($k==='link'){ // parse Link Headers
- if (preg_match("/<(.*)>\\s*;\\s*rel=\"(.*)\"/",$v,$matches)){
- $carry['link'][$matches[2]]=$matches[1];
- }
- }else{
- $carry[$k]=$v;
- }
- return $carry;
- },
- array()
- );
- if (!$raw) {
- if ($body==''){
- $json='';
- }else{
- $json=json_decode($body,true);
- }
- }else{
- $json=null;
- }
- if (is_array($json)){
- if ($accept!='409' && isset($json['detail'])) {
- throw new Exception($json['detail']);
- }
- if (isset($json['error']) && is_array($json['error']) && isset($json['error']['detail'])) {
- var_dump($json);
- throw new Exception($json['error']['detail']);
- }
- }
- if ( ($code!=$accept) && ($code[0]!='2') ){
- throw new Exception('request failed: '.$code.' ['.$status.']: '.$url);
- }
- if (!$raw) {
- if ($json===null) {
- throw new Exception('json_decode failed: '.print_r($headers,true).$body);
- }else{
- $body=$json;
- }
- }
- $ret=array(
- 'code'=>$code,
- 'status'=>$status,
- 'headers'=>$headers,
- 'body'=>$body
- );
- //print_r($ret);
- return $ret;
- }
- final public function write_challenge($docroot,$challenge){
- echo ' * Write challenge files'.PHP_EOL;
- if (!is_dir($docroot)){
- throw new Exception('docroot does not exist: '.$docroot);
- }
- @mkdir($docroot.$this->acme_path,0755,true);
- if (!is_dir($docroot.$this->acme_path)){
- throw new Exception('failed to create acme challenge directory: '.$docroot.$this->acme_path);
- }
- $keyAuthorization=$challenge['token'].'.'.$this->thumbprint;
- if (false===@file_put_contents($docroot.$this->acme_path.$challenge['token'],$keyAuthorization)){
- throw new Exception('failed to create challenge file: '.$docroot.$this->acme_path.$challenge['token']);
- }
- file_put_contents($docroot.$this->acme_path.'.htaccess', 'RewriteEngine Off'.PHP_EOL.'Allow from all'.PHP_EOL);
- }
- final public function remove_challenge($docroot,$challenge){
- echo ' * Remove challenge files'.PHP_EOL;
- unlink($docroot.$this->acme_path.$challenge['token']);
- unlink($docroot.$this->acme_path.'.htaccess');
- @rmdir($docroot.$this->acme_path);
- @rmdir($docroot.dirname($this->acme_path));
- }
- final public function pem2der($pem) {
- return base64_decode(
- implode(
- '',
- array_slice(
- array_map('trim',explode("\n",trim($pem))),
- 1,
- -1
- )
- )
- );
- }
- final public function der2pem($der) {
- return "-----BEGIN CERTIFICATE-----\n".
- chunk_split(base64_encode($der),64,"\n").
- "-----END CERTIFICATE-----\n";
- }
- final public function generate_csr($domain_key_pem,$domains){
- if (false===($domain_key=openssl_pkey_get_private('file://'.$domain_key_pem))){
- throw new Exception(
- 'Could not load domain key: '.$domain_key_pem."\n".
- openssl_error_string()
- );
- }
- if (false===($fn=tempnam("/tmp", "CNF_"))){
- throw new Exception('Failed to create temp file !');
- }
- if (false===@file_put_contents($fn,
- 'HOME = .'."\n".
- 'RANDFILE=$ENV::HOME/.rnd'."\n".
- '[req]'."\n".
- 'distinguished_name=req_distinguished_name'."\n".
- '[req_distinguished_name]'."\n".
- '[v3_req]'."\n".
- '[v3_ca]'."\n".
- '[SAN]'."\n".
- 'subjectAltName='.
- implode(',',array_map(function($domain){
- return 'DNS:'.$domain;
- },$domains)).
- "\n"
- )){
- throw new Exception('Failed to write tmp file: '.$fn);
- }
- $dn=array('commonName'=>reset($domains));
- $csr=openssl_csr_new($dn,$domain_key,array(
- 'config'=>$fn,
- 'req_extensions'=>'SAN',
- 'digest_alg'=>'sha512'
- ));
- unlink($fn);
- openssl_pkey_free($domain_key);
- if (!$csr) {
- throw new Exception(
- 'Could not generate CSR !'."\n".
- openssl_error_string()
- );
- }
- if (false===openssl_csr_export($csr,$out)){
- throw new Exception(
- 'Could not export CSR !'."\n".
- openssl_error_string()
- );
- }
- return $out;
- }
- }
- function nsupdate($site, $domain, $challenge) {
- // prepare command
- $file = tempnam(sys_get_temp_dir(), uniqid());
- $zone = implode('.',array_slice(explode('.', $domain),-2));
- $data = 'server localhost'.PHP_EOL.'zone '.$zone.PHP_EOL.'update delete _acme-challenge.'.$domain.'. TXT'.PHP_EOL.'update add _acme-challenge.'.$domain.'. 60 IN TXT "'.$challenge.'"'.PHP_EOL.'send'.PHP_EOL;
- file_put_contents($file, $data);
- passthru('/usr/bin/nsupdate -k '.$site['nsupdate'].' '.$file);
- unlink($file);
- return;
- }
- if (!file_exists($le_key))
- {
- // Create the private key for the account
- $file = tempnam(sys_get_temp_dir(), uniqid());
- $data = 'HOME = .'.PHP_EOL.'RANDFILE=\$ENV::HOME/.rnd'.PHP_EOL.'[v3_ca]'.PHP_EOL;
- file_put_contents($file, $data);
- $config = array(
- 'config'=> $file,
- 'private_key_bits' => 4096,
- 'private_key_type' => OPENSSL_KEYTYPE_RSA
- );
- openssl_pkey_export(openssl_pkey_new($config), $le_key);
- unlink($file);
- // Register with LetsEncrypt
- $le = new LE($le_key);
- $data = array('contact'=>array('mailto:'.$le_email));
- $ret = $le->request('new-reg',$data,null,false,409);
- switch($ret['code'])
- {
- case 409: // account already registered
- $reg = $ret['headers']['location'];
- $ret = $le->request('reg',$data,$reg);
- break;
- case 201: // account created
- $reg = $ret['headers']['location'];
- break;
- default:
- die('register error: '.$ret['body']['detail']);
- break;
- }
- if ( !isset($ret['body']['agreement']) ){
- $data['agreement'] = $ret['headers']['link']['terms-of-service'];
- $ret = $le->request('reg', $data, $reg);
- }
- }
- else
- $le = new LE($le_key);
- $query = $db->query("SELECT * FROM `sites` WHERE `active`='1'");
- while ($site = $query->fetch_array())
- {
- echo 'Checking '.$site['name'].': '.PHP_EOL;
- if (!file_exists($sslroot.$site['name']))
- {
- mkdir($sslroot.$site['name']);
- echo ' - Created folder for ssl'.PHP_EOL;
- }
- $csrfile = $sslroot.$site['name'].'/server.csr';
- $crtfile = $sslroot.$site['name'].'/server.crt';
- $pemfile = $sslroot.$site['name'].'/server.pem';
- $domains = preg_split("/[\s,;\n]+/", $site['domains']);
- // echo ' - Domains in DB: '.implode(' ',$domains).PHP_EOL;
- if (file_exists($crtfile))
- {
- echo ' - Reading existing cert (CRT)'.PHP_EOL;
- $data = openssl_x509_parse(file_get_contents($crtfile));
- preg_match_all('/DNS:([\w\.]+)/', $data['extensions']['subjectAltName'], $cert_sni);
- // echo ' - Domains in CRT: '.implode(' ',$cert_sni[1]).PHP_EOL;
- $diff1 = array_diff($domains, $cert_sni[1]);
- $diff2 = array_diff($cert_sni[1], $domains);
- $renew = $data['validTo_time_t'] - ($site['renewdays']*86400);
- if (!empty($diff1) || !empty($diff2))
- {
- echo ' - Regenerating because domain list differs:'.PHP_EOL;
- echo ' * Added : '.implode(' ',$diff1).PHP_EOL;
- echo ' * Removed: '.implode(' ',$diff2).PHP_EOL;
- if (file_exists($csrfile))
- unlink($csrfile);
- }
- else if (time() < $renew)
- {
- echo ' * Valid : '.date('d-m-Y H:i:s', $data['validTo_time_t']).PHP_EOL;
- echo ' * Renewal: '.date('d-m-Y', $renew).PHP_EOL;
- continue;
- }
- }
- if (!file_exists($pemfile))
- {
- echo ' - Creating private key (PEM)'.PHP_EOL;
- openssl_pkey_export(openssl_pkey_new(), $pem);
- file_put_contents($pemfile, $pem);
- }
- foreach($domains AS $d)
- {
- $domain = trim($d);
- echo ' - Validating '.$domain.PHP_EOL;
- $ret = $le->request('new-authz',array('identifier'=>array('type'=>'dns', 'value'=>$domain )));
- if ($ret['code'] != 201)
- {
- echo ' E new-authz failed'.PHP_EOL;
- var_dump($ret);
- continue(2);
- }
- $validated = false;
- $challenge = false;
- foreach($ret['body']['challenges'] AS $ch)
- {
- if ($ch['status'] == 'valid')
- {
- $validated = true;
- break;
- }
- if ($ch['type']===(!empty($site['nsupdate']) ? 'dns-01' : 'http-01'))
- $challenge = $ch;
- }
- if (!$validated)
- {
- if (!$challenge)
- {
- echo ' E No challenge found'.PHP_EOL;
- continue(2);
- }
- echo ' - Preparing validation'.PHP_EOL;
- if (!empty($site['nsupdate']))
- {
- $auth = $le->base64url(hash('sha256', $challenge['token'].'.'.$le->thumbprint, true));
- $data = dns_get_record('_acme-challenge.'.$domain, DNS_TXT);
- if ($data[0]['txt'] != $auth)
- {
- echo ' * Setting up DNS TXT records'.PHP_EOL;
- nsupdate($site, $domain, $auth);
- // Confirm updates on all
- $zone = implode('.',array_slice(explode('.', $domain),-2));
- $data = dns_get_record($zone, DNS_NS);
- $ok = array();
- while (true)
- {
- foreach($data AS $server)
- {
- if (in_array($server['target'], $ok))
- continue;
- $test = exec('host -t TXT _acme-challenge.'.$domain.' '.$server['target']);
- echo $server['target'].': '.$test.PHP_EOL;
- if (strstr($test, $auth))
- $ok[] = $server['target'];
- }
- if (count($ok) == count($data))
- break;
- else
- sleep(5);
- }
- }
- }
- else
- $le->write_challenge($site['webroot'], $challenge);
- $ret = $le->request('challenge', array('keyAuthorization'=>$challenge['token'].'.'.$le->thumbprint), $challenge['uri']);
- if ($ret['code'] != 202)
- {
- echo ' E Unexpected http status code: '.$ret['code'].PHP_EOL;
- if (empty($site['nsupdate']))
- $le->remove_challenge($site['webroot'],$challenge);
- continue(2);
- }
- sleep(3); // waiting for ACME-Server to verify challenge
- // poll
- $tries=10;
- $delay=2;
- do
- {
- $ret = $le->http_request($challenge['uri']);
- if ($ret['body']['status']==='valid')
- break;
- echo '.';
- sleep($delay); // still waiting..
- $delay=min($delay*2,32);
- if (--$tries==0)
- {
- echo ' E Failed to verify challenge after 10 tries !'.PHP_EOL;
- if (empty($site['nsupdate']))
- $le->remove_challenge($site['webroot'], $challenge);
- continue(3);
- }
- } while($ret['body']['status']==='pending');
- if (empty($site['nsupdate']))
- $le->remove_challenge($site['webroot'],$challenge);
- if ($ret['body']['status']!=='valid')
- {
- echo ' E Challenge failed'.PHP_EOL;
- continue(2);
- }
- }
- else
- echo ' * Already validated'.PHP_EOL;
- }
- $csrfile = $sslroot.$site['name'].'/server.csr';
- if (!file_exists($sslroot.$site['name'].'/server.csr'))
- {
- echo ' - Generating signing request (CSR)'.PHP_EOL;
- $csr = $le->generate_csr($pemfile, $domains);
- file_put_contents($csrfile, $csr);
- }
- else
- $csr = file_get_contents($csrfile);
- echo ' - Requesting certificate (CRT)'.PHP_EOL;
- $ret = $le->request('new-cert',array('csr'=>$le->base64url($le->pem2der($csr))),null,true);
- if ($ret['code']!=201)
- { // HTTP: Created
- echo ' E Unexpected http status code: '.$ret['code'].PHP_EOL;
- continue;
- }
- if ($ret['headers']['content-type']!='application/pkix-cert')
- {
- echo ' E Unexpected content-type: '.$ret['headers']['content-type'].PHP_EOL;
- continue;
- }
- $crt = $le->der2pem($ret['body']);
- echo ' - Requesting Intermediate CA Certificate (SUB)'.PHP_EOL;
- $ret = $le->http_request($ret['headers']['link']['up'],null,true);
- if ($ret['code']!=200)
- {
- echo ' E Unexpected http status code: '.$ret['code'].PHP_EOL;
- continue;
- }
- if ($ret['headers']['content-type']!='application/pkix-cert')
- {
- echo ' E Unexpected content-type: '.$ret['headers']['content-type'].PHP_EOL;
- continue;
- }
- if ($rootca)
- file_put_contents($sslroot.$site['name'].'/server.ca', $rootca);
- file_put_contents($sslroot.$site['name'].'/server.crt', $crt);
- file_put_contents($sslroot.$site['name'].'/server.sub', $le->der2pem($ret['body']));
- file_put_contents($sslroot.$site['name'].'/server.all', $crt.PHP_EOL.$le->der2pem($ret['body']).($rootca ? PHP_EOL.$rootca : ''));
- echo ' - Succeeded'.PHP_EOL;
- }
- passthru('/usr/sbin/apache2ctl restart');
- ?>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement