1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678: 679: 680: 681: 682: 683: 684: 685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704: 705: 706: 707: 708: 709: 710: 711: 712: 713: 714: 715: 716: 717: 718: 719: 720: 721: 722: 723: 724: 725: 726: 727: 728: 729: 730: 731: 732: 733: 734: 735: 736: 737: 738: 739: 740: 741: 742: 743: 744: 745: 746: 747: 748: 749: 750: 751: 752: 753: 754: 755: 756: 757: 758: 759: 760: 761: 762: 763: 764: 765: 766: 767: 768: 769: 770: 771: 772: 773: 774: 775: 776: 777: 778: 779: 780: 781: 782: 783: 784: 785: 786: 787: 788: 789: 790: 791: 792: 793: 794: 795: 796: 797: 798: 799: 800: 801: 802: 803: 804: 805: 806: 807: 808: 809: 810: 811: 812: 813: 814: 815: 816: 817: 818: 819: 820: 821: 822: 823: 824: 825: 826: 827: 828: 829: 830: 831: 832: 833: 834: 835: 836: 837: 838: 839: 840: 841: 842: 843: 844: 845: 846: 847: 848: 849: 850: 851: 852: 853: 854: 855: 856: 857: 858: 859: 860: 861: 862: 863: 864: 865: 866: 867: 868: 869: 870: 871: 872: 873: 874: 875: 876: 877: 878: 879: 880: 881: 882: 883: 884: 885: 886: 887: 888: 889: 890: 891: 892: 893: 894: 895: 896: 897: 898: 899: 900: 901: 902: 903: 904: 905: 906: 907: 908: 909: 910: 911: 912: 913: 914: 915: 916: 917: 918: 919: 920: 921: 922: 923: 924: 925: 926: 927: 928: 929: 930: 931: 932: 933: 934: 935: 936: 937: 938: 939: 940: 941: 942: 943: 944: 945: 946: 947: 948: 949: 950: 951: 952: 953: 954: 955: 956: 957: 958: 959: 960: 961: 962: 963: 964: 965: 966: 967: 968: 969: 970: 971: 972: 973: 974: 975: 976: 977: 978: 979: 980: 981: 982: 983: 984: 985: 986: 987: 988: 989: 990: 991: 992: 993: 994: 995: 996: 997: 998: 999: 1000: 1001: 1002: 1003: 1004: 1005: 1006: 1007: 1008: 1009: 1010: 1011: 1012: 1013: 1014: 1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023:
<?php
declare(strict_types=1);
use Opcenter\Contracts\Hookable;
use Opcenter\Crypto\Ssl;
use Opcenter\Http\Apache;
use Opcenter\SiteConfiguration;
class Ssl_Module extends Module_Skeleton implements Hookable
{
const DEPENDENCY_MAP = [
'apache',
'siteinfo'
];
const CRT_PATH = '/etc/httpd/conf/ssl.crt';
const KEY_PATH = '/etc/httpd/conf/ssl.key';
const CSR_PATH = '/etc/httpd/conf/ssl.csr';
const DEFAULT_CERTIFICATE_NAME = 'server';
const X509_DAYS = 1095;
const USER_RHOOK = 'letsencrypt';
const SYS_RHOOK = 'ssl';
public function __construct()
{
parent::__construct();
$this->exportedFunctions = array(
'generate_csr' => PRIVILEGE_ALL,
'generate_privatekey' => PRIVILEGE_ALL,
'get_alternative_names' => PRIVILEGE_ALL,
'has_certificate' => PRIVILEGE_SITE,
'get_certificate' => PRIVILEGE_SITE | PRIVILEGE_ADMIN,
'get_csr' => PRIVILEGE_SITE | PRIVILEGE_ADMIN,
'get_private_key' => PRIVILEGE_SITE | PRIVILEGE_ADMIN,
'get_public_key' => PRIVILEGE_SITE | PRIVILEGE_ADMIN,
'is_self_signed' => PRIVILEGE_ALL,
'key_exists' => PRIVILEGE_SITE | PRIVILEGE_ADMIN,
'parse_certificate' => PRIVILEGE_ALL,
'permitted' => PRIVILEGE_ALL,
'privkey_info' => PRIVILEGE_ALL,
'request_info' => PRIVILEGE_ALL,
'resolve_chain' => PRIVILEGE_ALL,
'sign_certificate' => PRIVILEGE_ALL,
'valid' => PRIVILEGE_ALL,
'verify_certificate_chain' => PRIVILEGE_ALL,
'verify_key' => PRIVILEGE_ALL,
'verify_x509_key' => PRIVILEGE_ALL,
'*' => PRIVILEGE_SITE,
);
}
public function cert_exists()
{
if (!IS_CLI) {
return $this->query('ssl_cert_exists');
}
$conf = $this->get_certificates();
return count($conf) > 0;
}
public function get_certificates()
{
if (!IS_CLI) {
return $this->query('ssl_get_certificates');
}
$that = $this;
$parser = static function ($config) use ($that) {
$conf = array();
$token = strtok($config, "\n \t");
while ($token !== false) {
switch (strtoupper($token)) {
case 'LISTEN':
$key = 'host';
break;
case 'SSLCERTIFICATEFILE':
$key = 'crt';
break;
case 'SSLCERTIFICATEKEYFILE':
$key = 'key';
break;
case 'SSLCERTIFICATECHAINFILE':
$key = 'chain';
break;
default:
$key = null;
break;
}
if (!is_null($key)) {
$token = trim(strtok("\t \n"));
$constant = $key === 'chain' ? 'crt' : $key;
if ($constant == 'key' || $constant == 'crt') {
if (!file_exists($token)) {
return array();
}
}
$token = $that->file_canonicalize_site($token);
$conf[$key] = basename($token);
}
$token = strtok(" \t\n");
}
if (isset($conf['chain']) && count($conf) === 1) {
return $conf;
} else {
if (!isset($conf['crt']) || !isset($conf['key'])) {
return array();
}
}
return $conf;
};
$masterconfig = glob('/etc/httpd/conf/virtual/' . $this->site . '{,.*}', GLOB_BRACE);
$sitecerts = array();
$accountaddr = (array)$this->common_get_ip_address();
foreach ($masterconfig as $config) {
$cert = array();
$site = basename($config);
if (!file_exists('/etc/httpd/conf/' . $site . '.ssl')) {
return $sitecerts;
}
$file = '/etc/httpd/conf/virtual/' . $site;
if (!file_exists($file)) {
continue;
}
$config = file_get_contents($file);
$newcert = $parser($config);
if (!$newcert) {
continue;
}
$cert = array_merge($cert, $newcert);
$sslextra = '/etc/httpd/conf/' . basename($file) . '.ssl/custom';
if (file_exists($sslextra)) {
$config = file_get_contents($sslextra);
$cert = array_merge($cert, $parser($config));
}
if (isset($cert['host'])) {
$tmp = strpos($cert['host'], ':');
if ($tmp) {
$cert['host'] = substr($cert['host'], 0, $tmp);
}
} else {
$cert['host'] = $accountaddr[0];
}
$sitecerts[] = $cert;
}
return $sitecerts;
}
public function key_exists($key = 'server.key')
{
if (!IS_CLI) {
return $this->query('ssl_key_exists', $key);
}
$name = basename($key, '.key');
if ($this->permission_level & PRIVILEGE_SITE) {
$key = $this->domain_fs_path() . self::KEY_PATH .
'/' . $name . '.key';
} else {
if ($key[0] !== '/') {
$key = self::KEY_PATH . '/' . $name;
}
}
return file_exists($key);
}
public function install($key, $cert, $chain = null)
{
if (!IS_CLI) {
return $this->query('ssl_install', $key, $cert, $chain);
}
if (!$this->permitted()) {
return error('SSL not permitted on account');
}
if (!$this->valid($cert, $key)) {
return error('certificate is not valid for given key: %s', openssl_error_string());
}
if ($this->is_self_signed($cert)) {
$chain = null;
} else if (!$chain) {
$supplemental = $this->resolve_chain($cert);
if (!$supplemental) {
return error('certificate chain is irresolvable');
}
info('downloaded chain certificates to satisfy requirement, one or more additional pathways may be missing');
$chain = join("\n", $supplemental);
} else if (!$this->verify_certificate_chain($cert, $chain)) {
return error('chain not valid for certificate');
}
$this->file_purge();
$prefix = $this->domain_fs_path();
$crtfile = $prefix . self::CRT_PATH . '/server.crt';
$keyfile = $prefix . self::KEY_PATH . '/server.key';
$this->file_shadow_buildup_backend(
$prefix . self::CSR_PATH . '/server.csr'
);
$overwrite = false;
foreach (array($crtfile, $keyfile) as $file) {
$this->file_shadow_buildup_backend($file);
$dir = dirname($file);
if (!is_dir($dir)) {
\Opcenter\Filesystem::mkdir($dir, 'root', $this->group_id, 0700);
} else if (file_exists($file)) {
$overwrite = true;
$old = file_get_contents($file);
file_put_contents($file . '-old', $old, LOCK_EX);
}
}
$this->file_purge();
file_put_contents($crtfile, $cert, LOCK_EX);
file_put_contents($keyfile, $key, LOCK_EX);
if (FILESYSTEM_TYPE !== 'xfs') {
chgrp($crtfile, $this->group_id);
chgrp($keyfile, $this->group_id);
}
chmod($crtfile, 0600);
chown($crtfile, 'root');
chmod($keyfile, 0600);
chown($keyfile, 'root');
$chainconfig = $this->_getSSLExtraConfig();
if ($chain) {
if (!file_exists(dirname($chainconfig))) {
mkdir(dirname($chainconfig), 0711);
}
file_put_contents($prefix . self::CRT_PATH . '/bundle.crt', $chain, LOCK_EX);
$chainfile = join(DIRECTORY_SEPARATOR, array($prefix, self::CRT_PATH, 'bundle.crt'));
if (file_exists($chainconfig)) {
$contents = file($chainconfig, FILE_IGNORE_NEW_LINES);
$newcontents = array();
$directive = 'SSLCertificateChainFile';
foreach ($contents as $line) {
if (0 === strpos($line, $directive)) {
continue;
}
$newcontents[] = $line;
}
$newcontents[] = $directive . ' ' . $chainfile;
file_put_contents($chainconfig, join("\n", $newcontents));
} else {
file_put_contents($chainconfig, 'SSLCertificateChainFile ' . $chainfile);
}
}
if (!$overwrite || !$this->enabled()) {
$cmd = new Util_Account_Editor($this->getAuthContext()->getAccount(), $this->getAuthContext());
$cmd->setConfig(SiteConfiguration::getModuleRemap('openssl'), 'enabled', 1);
$cmd->edit();
}
$this->file_purge();
\Util_Account_Hooks::instantiateContexted($this->getAuthContext())->run('reload', [self::USER_RHOOK]);
info('reloading web server in 2 minutes, stay tuned!');
return true;
}
public function permitted()
{
return true;
}
public function valid($cert, $pkey)
{
return openssl_x509_check_private_key($cert, $pkey);
}
public function is_self_signed($crt)
{
return Ssl::selfSigned($crt);
}
public function self_sign(string $cn, array $sans = []): bool
{
if ($this->cert_exists() && !$this->is_self_signed($this->get_certificate())) {
return error('Certificate already exists and is not self-signed');
}
return serial(function() use($cn, $sans) {
$key = $this->generate_privatekey(2048);
$csr = $this->generate_csr($key, $cn, null, null, null, null, null, null, $sans);
$crt = $this->sign_certificate($csr, $key);
return $this->install($key, $crt);
}) ?? false;
}
public function parse_certificate($crt)
{
return Ssl::parse($crt);
}
public function resolve_chain($crt)
{
$buffer = Error_Reporter::flush_buffer();
$chain = $this->_resolveChain($crt, array());
$isError = Error_Reporter::is_error();
Error_Reporter::merge_buffer($buffer);
if ($isError) {
return false;
}
return join("\n", $chain);
}
private function _resolveChain($crt, $seen)
{
if (Ssl::isDer($crt)) {
$crt = Ssl::der2Pem($crt);
}
if ($this->is_self_signed($crt)) {
return array($crt);
}
$info = $this->parse_certificate($crt);
if (!isset($info['extensions'])) {
return array();
} else if (!isset($info['extensions']['subjectKeyIdentifier'])) {
error('missing subjectKeyIdentifier fingerprint!');
}
$fingerprint = $info['extensions']['subjectKeyIdentifier'];
if (array_search($fingerprint, $seen, true)) {
return error('chain loop detected, fingerprint: %s', $fingerprint);
}
$seen[] = $fingerprint;
$extensions = $info['extensions'];
if (!isset($extensions['authorityInfoAccess'])) {
return array();
}
if (!preg_match_all(Regex::SSL_CRT_URI, $extensions['authorityInfoAccess'], $matches)) {
error("can't find URI to match in authorityInfoAccess: %s",
$extensions['authorityInfoAccess']);
return array();
}
$url = $matches['url'][0];
foreach ($matches['url'] as $candidate) {
if (false !== stripos($candidate, 'ocsp')) {
continue;
}
$url = $candidate;
}
$chainedcrt = $this->_downloadChain($url);
if (!$chainedcrt) {
error('failed to resolve chain!');
return array();
}
info("downloaded extra chain `%s'", $url);
if (Ssl::isDer($chainedcrt)) {
$chainedcrt = Ssl::der2Pem($chainedcrt);
}
return array_merge(
$this->_resolveChain($chainedcrt, $seen),
(array)$chainedcrt
);
}
private function _downloadChain($url)
{
if (extension_loaded('curl')) {
$adapter = new HTTP_Request2_Adapter_Curl();
} else {
$adapter = new HTTP_Request2_Adapter_Socket();
}
$http = new HTTP_Request2(
$url,
HTTP_Request2::METHOD_GET,
array(
'adapter' => $adapter
)
);
try {
$response = $http->send();
$code = $response->getStatus();
switch ($code) {
case 200:
break;
case 403:
return error('URL request forbidden by server');
case 404:
return error('URL not found on server');
case 302:
$newLocation = $response->getHeader('location');
return $this->_downloadChain($newLocation);
default:
return error("URL request failed, code `%d': %s",
$code, $response->getReasonPhrase());
}
$cert = $response->getBody();
} catch (HTTP_Request2_Exception $e) {
return error("fatal error retrieving URL: `%s'", $e->getMessage());
}
return $cert;
}
public function verify_certificate_chain($cert1, $cert2)
{
$resp = $this->_verify_certificate_chain_real($cert1, $cert2);
if ($resp || null === $resp) {
return (int)$resp;
}
return $this->_verify_certificate_chain_real($cert2, $cert1) ? -1 : 0;
}
private function _verify_certificate_chain_real($cert1, $cert2)
{
$icert = $this->parse_certificate($cert1);
$ichain = $this->parse_certificate($cert2);
if (!isset($ichain['extensions'])) {
return null;
}
$keyidentifier = array_get($icert, 'extensions.authorityKeyIdentifier', '');
if (0 === strncmp($keyidentifier, "keyid:", 6)) {
$keyidentifier = trim(substr($keyidentifier, 6));
}
if ($keyidentifier == $ichain['extensions']['subjectKeyIdentifier']) {
return 1;
}
return 0;
}
private function _getSSLExtraConfig()
{
return $this->web_site_config_dir() . '.ssl/custom';
}
public function enabled(): bool
{
return (bool)$this->getServiceValue(SiteConfiguration::getModuleRemap('openssl'), 'enabled');
}
public function delete($key, $crt, $chain = null)
{
if (!IS_CLI) {
return $this->query('ssl_delete', $key, $crt, $chain);
}
if (substr($key, -4) == '.crt' && substr($crt, -4) == '.key') {
$tmp = $crt;
$crt = $key;
$key = $tmp;
}
if (!$this->get_certificate($crt)) {
return error("invalid certificate `%s' specified", $crt);
} else if (!$this->get_private_key($key)) {
return error("invalid private key `%s' specified", $key);
}
if ($chain && !$this->get_certificate($chain)) {
return error("invalid certificate chain `%s' specified", $chain);
}
if (!$this->_delete_wrapper($crt)) {
return error("failed to delete certificate `%s'", $crt);
}
if (!$this->_delete_wrapper($key)) {
warn("failed to remove ssl key `%s'", $key);
}
if ($chain && !$this->_delete_wrapper($chain)) {
warn("failed to remove ssl chain certficiate `%s'", $chain);
}
$sslextra = $this->_getSSLExtraConfig();
if (file_exists($sslextra)) {
$contents = file_get_contents($sslextra);
$newconfig = array();
foreach (explode("\n", $contents) as $line) {
if (preg_match('!/' . preg_quote($chain, '!') . '$!', $line)) {
info('detected and removed certificate chain from http config');
continue;
}
$newconfig[] = $line;
}
file_put_contents($sslextra, join("\n", $newconfig));
}
$editor = new Util_Account_Editor($this->getAuthContext()->getAccount());
$editor->setConfig(SiteConfiguration::getModuleRemap('openssl'), 'enabled', 0);
$status = $editor->edit();
if (!$status) {
return error('failed to deactivate openssl on account');
}
Util_Account_Hooks::instantiateContexted($this->getAuthContext())->run('reload', [self::USER_RHOOK]);
return true;
}
public function get_certificate($name = 'server.crt')
{
if (!IS_CLI) {
return $this->query('ssl_get_certificate', $name);
}
$name = basename($name, '.crt');
if ($this->permission_level & PRIVILEGE_SITE) {
$file = $this->domain_fs_path() . self::CRT_PATH .
'/' . $name . '.crt';
} else if ($name[0] != '/') {
$file = self::CRT_PATH . $name . '.crt';
} else {
$file = $name . '.crt';
}
if (!file_exists($file)) {
return error("certificate `%s' does not exist", $name);
}
return file_get_contents($file);
}
public function get_private_key($name = 'server.key')
{
if (!IS_CLI) {
return $this->query('ssl_get_private_key', $name);
}
$name = basename($name, '.key');
if ($this->permission_level & PRIVILEGE_SITE) {
$file = $this->domain_fs_path() . self::KEY_PATH .
'/' . $name . '.key';
} else {
if ($name[0] != '/') {
$file = self::KEY_PATH . $name . '.key';
} else {
$file = $name . '.key';
}
}
if (!file_exists($file)) {
return error("private key `%s' does not exist", $name);
}
return file_get_contents($file);
}
private function _delete_wrapper($file)
{
$prefix = $this->domain_fs_path();
$ext = substr($file, -4);
switch ($ext) {
case '.key':
$folder = self::KEY_PATH;
break;
case '.csr':
$folder = self::CSR_PATH;
break;
case '.crt':
$folder = self::CRT_PATH;
break;
default:
return error("cannot delete SSL asset: unknown extension `%s'", $ext);
}
$file = join(DIRECTORY_SEPARATOR, array($prefix, $folder, $file));
if (!file_exists($file)) {
return false;
}
return unlink($file);
}
public function generate_privatekey($bits = 2048)
{
return Ssl::genkey($bits);
}
public function generate_csr(
string $privkey,
string $host,
?string $country = '',
?string $state = '',
?string $locality = '',
?string $org = '',
?string $orgunit = '',
?string $email = '',
array $san = []
) {
return Ssl::generate_csr(
$privkey, $host, $country ?? 'US', $state ?? 'GA', $locality ?? 'Atlanta', (string)$org, (string)$orgunit, (string)$email, $san
);
}
public function request_info($csr)
{
return Ssl::request_info($csr);
}
public function get_public_key($name)
{
if (!IS_CLI) {
return $this->query('ssl_get_public_key', $name);
}
$name = basename($name, '.key');
$key = $this->get_certificate($name);
if (!$key) {
return error("unable to get named certificate `%s'", $name);
}
$res = openssl_pkey_get_public($key);
$details = openssl_pkey_get_details($res);
openssl_pkey_free($res);
return $details;
}
public function order_certificates(array $certs)
{
foreach ($certs as $cert) {
}
}
public function get_csr($name)
{
if (!IS_CLI) {
return $this->query('ssl_get_csr', $name);
}
$name = basename($name, '.csr');
if ($this->permission_level & PRIVILEGE_SITE) {
$file = $this->domain_fs_path() . self::CSR_PATH .
'/' . $name . '.csr';
} else {
if ($name[0] != '/') {
$file = self::CSR_PATH . $name . '.csr';
} else {
$file = $name . '.csr';
}
}
if (!file_exists($file)) {
return error("certificate request `%s' does not exist", $name);
}
return file_get_contents($file);
}
public function sign_certificate(
$csr,
$privkey,
$days = 365,
$serial = null
) {
return Ssl::selfsign($csr, $privkey, $days, $serial);
}
public function verify_x509_key($crt, $privkey)
{
return openssl_x509_check_private_key($crt, $privkey);
}
public function verify_key($key)
{
if (!$key) {
return error('no key specified');
}
$info = $this->privkey_info($key);
if (!$info) {
return error('invalid key detected');
}
return true;
}
public function privkey_info($privkey)
{
$res = openssl_pkey_get_private($privkey);
$details = openssl_pkey_get_details($res);
return $details;
}
public function get_alternative_names($certificate): ?array
{
return Ssl::alternativeNames($certificate);
}
public function _create()
{
$this->_edit();
}
public function contains_cn(string $name): bool
{
if (!$this->cert_exists()) {
return false;
}
$certdata = $this->ssl_get_certificates();
$certdata = array_pop($certdata);
$cert = $this->ssl_get_certificate($certdata['crt']);
$sans = $this->ssl_get_alternative_names($cert);
$name = strtolower($name);
if (in_array($name, $sans, true)) {
return true;
}
$offset = 0;
while (false !== ($offset = strpos($name, '.'))) {
$name = substr($name, $offset ? $offset + 1 : 0);
if (in_array("*.${name}", $sans, true)) {
return true;
}
}
return false;
}
public function _edit()
{
$conf_new = $this->getAuthContext()->getAccount()->new;
$conf_old = $this->getAuthContext()->getAccount()->old;
$domainprefix = $this->domain_fs_path();
$renameWrapper = function ($mode) use ($domainprefix) {
$certdir = $domainprefix . self::CRT_PATH;
if ($mode === 'disable') {
foreach (glob($certdir . '/*.crt') as $cert) {
rename($cert, $cert . '-disabled');
info('disabled certificate ' . basename($cert));
}
return;
}
$pkeyfile = $domainprefix . self::KEY_PATH . '/server.key';
if (!file_exists($pkeyfile)) {
return false;
}
$pkey = file_get_contents($pkeyfile);
foreach (glob($certdir . '/*.crt-disabled') as $cert) {
$crt = file_get_contents($cert);
$file = basename($cert);
if ($file === 'server.crt' && !$this->valid($crt, $pkey)) {
info("removing dangling certificate `%s' that does not match pkey modulus", $cert);
unlink($cert);
continue;
}
rename($cert, substr($cert, 0, -9));
info('enabled certificate ' . substr(basename($cert), 0, -9));
}
};
$ssl = SiteConfiguration::getModuleRemap('openssl');
if (!$conf_new[$ssl]['enabled']) {
$renameWrapper('disable');
} else if ($conf_new[$ssl]['enabled'] && !($conf_old[$ssl]['enabled'] ?? false)) {
$renameWrapper('enable');
}
}
public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
{
return true;
}
public function _delete()
{
}
public function _create_user(string $user)
{
}
public function _delete_user(string $user)
{
}
public function _edit_user(string $userold, string $usernew, array $oldpwd)
{
}
}