Initial
This commit is contained in:
6
.editorconfig
Normal file
6
.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 4
|
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
**/*_
|
||||||
|
**/.#*
|
||||||
|
**/0*
|
||||||
|
**/1*
|
||||||
|
**/2*
|
||||||
|
**/3*
|
||||||
|
**/4*
|
||||||
|
**/5*
|
||||||
|
**/6*
|
||||||
|
**/7*
|
||||||
|
**/8*
|
||||||
|
**/9*
|
||||||
|
|
||||||
|
**/*.log*
|
||||||
|
**/*log
|
||||||
|
**/LOG*
|
||||||
|
|
||||||
|
cache/**
|
||||||
|
data/**
|
||||||
|
old/**
|
||||||
|
outer/**
|
||||||
|
secret/**
|
37
cmpOpenSSLClass.php
Normal file
37
cmpOpenSSLClass.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once("cmpOpenSSLTrait.php");
|
||||||
|
// require_once("../../outer/cmpUtil/cmpUtil.php");
|
||||||
|
|
||||||
|
class cmpOpenSSLClass {
|
||||||
|
use cmpOpenSSLTrait;
|
||||||
|
|
||||||
|
// use cmpUtil;
|
||||||
|
|
||||||
|
function __constructor() {
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
function e($m, $r = false) {
|
||||||
|
if(is_string($m)) {
|
||||||
|
echo "Error: $m\n";
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(get_class($m)) {
|
||||||
|
case "Exception":
|
||||||
|
case "Throwable":
|
||||||
|
echo "Error: " . $m->getMessage() . "\n";
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Error: Unknown\n";
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function d($s) {
|
||||||
|
echo "debug: $s\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// END CLASS
|
||||||
|
}
|
83
cmpOpenSSLTests.php
Normal file
83
cmpOpenSSLTests.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once("cmpOpenSSLClass.php");
|
||||||
|
|
||||||
|
$x = new cmpOpenSSLClass();
|
||||||
|
|
||||||
|
if(0) {
|
||||||
|
$out = $x->cmpOpenSslGenDh();
|
||||||
|
|
||||||
|
var_export($out);
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(1) {
|
||||||
|
$parm = array(
|
||||||
|
"days" => 3650 ,
|
||||||
|
"outfile" => "test-ca-01" ,
|
||||||
|
"commonName" => "test-ca-01" ,
|
||||||
|
"stateOrProvinceName" => "MSK" ,
|
||||||
|
"localityName" => "MSK" ,
|
||||||
|
"emailAddress" => "it@example.ru" ,
|
||||||
|
"organizationName" => "TEST" ,
|
||||||
|
"organizationalUnitName" => "TEST-OIT" ,
|
||||||
|
);
|
||||||
|
|
||||||
|
$out = array();
|
||||||
|
|
||||||
|
$x->createCA($parm, $out);
|
||||||
|
|
||||||
|
var_export($out);
|
||||||
|
|
||||||
|
//exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(1) {
|
||||||
|
$parm = array(
|
||||||
|
"days" => 3650 ,
|
||||||
|
"outfile" => "test-srv-01" ,
|
||||||
|
"commonName" => "test-srv-01" ,
|
||||||
|
"stateOrProvinceName" => "MSK" ,
|
||||||
|
"localityName" => "MSK" ,
|
||||||
|
"emailAddress" => "it@example.ru" ,
|
||||||
|
"organizationName" => "TEST" ,
|
||||||
|
"organizationalUnitName" => "TEST-OIT" ,
|
||||||
|
);
|
||||||
|
|
||||||
|
$x->loadFromFileCACrt("test-ca-01.crt");
|
||||||
|
$x->loadFromFileCAPrv("test-ca-01.prv");
|
||||||
|
|
||||||
|
$out = array();
|
||||||
|
|
||||||
|
$x->createServer($parm, $out);
|
||||||
|
|
||||||
|
var_export($out);
|
||||||
|
|
||||||
|
//exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(1) {
|
||||||
|
$parm = array(
|
||||||
|
"days" => 365 ,
|
||||||
|
"outfile" => "test-cli-cli01" ,
|
||||||
|
"commonName" => "test-cli-cli01" ,
|
||||||
|
"stateOrProvinceName" => "MSK" ,
|
||||||
|
"localityName" => "MSK" ,
|
||||||
|
"emailAddress" => "it@example.ru" ,
|
||||||
|
"organizationName" => "TEST" ,
|
||||||
|
"organizationalUnitName" => "TEST-OIT" ,
|
||||||
|
);
|
||||||
|
|
||||||
|
$x->loadFromFileCACrt("test-ca-01.crt");
|
||||||
|
$x->loadFromFileCAPrv("test-ca-01.prv");
|
||||||
|
|
||||||
|
$out = array();
|
||||||
|
|
||||||
|
$x->createClient($parm, $out);
|
||||||
|
|
||||||
|
var_export($out);
|
||||||
|
|
||||||
|
//exit;
|
||||||
|
}
|
||||||
|
|
940
cmpOpenSSLTrait.php
Normal file
940
cmpOpenSSLTrait.php
Normal file
@@ -0,0 +1,940 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
trait cmpOpenSSLTrait {
|
||||||
|
var $ver = "20240124";
|
||||||
|
|
||||||
|
var $caDN = NULL;
|
||||||
|
|
||||||
|
var $caPub = NULL;
|
||||||
|
var $caPubPEM = "";
|
||||||
|
var $caPubFile = "";
|
||||||
|
|
||||||
|
var $caPrv = NULL;
|
||||||
|
var $caPrvPEM = "";
|
||||||
|
var $caPrvFile = "";
|
||||||
|
|
||||||
|
var $caCrt = NULL;
|
||||||
|
var $caCrtPEM = "";
|
||||||
|
var $caCrtFile = "";
|
||||||
|
|
||||||
|
|
||||||
|
var $cliDN = NULL;
|
||||||
|
|
||||||
|
var $cliPub = NULL;
|
||||||
|
var $cliPubPEM = "";
|
||||||
|
var $cliPubFile = "";
|
||||||
|
|
||||||
|
var $cliPrv = NULL;
|
||||||
|
var $cliPrvPEM = "";
|
||||||
|
var $cliPrvFile = "";
|
||||||
|
|
||||||
|
var $cliCrt = NULL;
|
||||||
|
var $cliCrtPEM = "";
|
||||||
|
var $cliCrtFile = "";
|
||||||
|
|
||||||
|
var $cmpOpenSslConf = array();
|
||||||
|
|
||||||
|
function cmpOpenSslConfSetDefault() {
|
||||||
|
$claver = __CLASS__ . "-" . $this->ver;
|
||||||
|
|
||||||
|
$this->cmpOpenSslConf = array (
|
||||||
|
"GLOBAL" => array (
|
||||||
|
"oid_section" => "OIDs",
|
||||||
|
),
|
||||||
|
"OIDs" => array (
|
||||||
|
"cmpExtOid" => "1.2.3.20190828",
|
||||||
|
"certificateTemplateName" => "\${cmpExtOid}.1",
|
||||||
|
),
|
||||||
|
"ca" => array (
|
||||||
|
"default_ca" => "cmp_ca",
|
||||||
|
),
|
||||||
|
"cmp_ca" => array (
|
||||||
|
"dir" => ".",
|
||||||
|
"certs" => "\$dir",
|
||||||
|
"crl_dir" => "\$dir}",
|
||||||
|
"database" => "\$dir/index.txt",
|
||||||
|
"new_certs_dir" => "\$dir",
|
||||||
|
"serial" => "\$dir/serial",
|
||||||
|
"crl" => "\$dir/crl.pem",
|
||||||
|
"certificate" => "\$dir/ca.crt",
|
||||||
|
"private_key" => "\$dir/ca.key",
|
||||||
|
"RANDFILE" => "\$dir/.rand",
|
||||||
|
"x509_extensions" => "cmp_x509_ext_basic",
|
||||||
|
"crl_extensions" => "cmp_crl_ext",
|
||||||
|
"default_days" => 3650,
|
||||||
|
"default_crl_days" => 30,
|
||||||
|
"default_md" => "sha256",
|
||||||
|
"preserve" => "no",
|
||||||
|
"policy" => "cmp_policy_anything",
|
||||||
|
),
|
||||||
|
"cmp_policy_anything" => array (
|
||||||
|
"countryName" => "optional",
|
||||||
|
"stateOrProvinceName" => "optional",
|
||||||
|
"localityName" => "optional",
|
||||||
|
"organizationName" => "optional",
|
||||||
|
"organizationalUnitName" => "optional",
|
||||||
|
"commonName" => "supplied",
|
||||||
|
"name" => "optional",
|
||||||
|
"emailAddress" => "optional",
|
||||||
|
),
|
||||||
|
"req" => array (
|
||||||
|
"default_bits" => 2048,
|
||||||
|
"default_keyfile" => "privkey.pem",
|
||||||
|
"default_md" => "sha256",
|
||||||
|
"distinguished_name" => "cmp_dst_ext",
|
||||||
|
"x509_extensions" => "cmp_x509_ext_rsa",
|
||||||
|
"req_extensions" => "cmp_req_ext_v3",
|
||||||
|
),
|
||||||
|
"cmp_dst_ext" => array (
|
||||||
|
"commonName" => "Common Name (eg: your user, host, or server name)",
|
||||||
|
"commonName_max" => 64,
|
||||||
|
"commonName_default" => $claver . "-commonName_default",
|
||||||
|
// "certificateTemplateName" => $claver , // "-certificateTemplateName",
|
||||||
|
"certificateTemplateName_default" => $claver ,
|
||||||
|
),
|
||||||
|
"cmp_req_ext_v3" => array (
|
||||||
|
"certificateTemplateName" => "ASN1:PRINTABLESTRING:CustomUserOffline",
|
||||||
|
),
|
||||||
|
"cmp_org" => array (
|
||||||
|
"countryName" => "Country Name (2 letter code)",
|
||||||
|
"countryName_default" => "RU",
|
||||||
|
"countryName_min" => 2,
|
||||||
|
"countryName_max" => 2,
|
||||||
|
|
||||||
|
"stateOrProvinceName" => "State or Province Name (full name)",
|
||||||
|
"stateOrProvinceName_default" => $claver . "-stateOrProvinceName_default",
|
||||||
|
|
||||||
|
"localityName" => "Locality Name (eg, city)",
|
||||||
|
"localityName_default" => $claver . "-localityName_default",
|
||||||
|
|
||||||
|
"organizationName" => "Organization Name (eg, company)",
|
||||||
|
"organizationName_default" => $claver . "-organizationName_default",
|
||||||
|
|
||||||
|
"organizationalUnitName" => "Organizational Unit Name (eg, section)",
|
||||||
|
"organizationalUnitName_default" => $claver . "-organizationalUnitName_default",
|
||||||
|
|
||||||
|
"emailAddress" => "Email Address",
|
||||||
|
"emailAddress_default" => $claver . "@example.org",
|
||||||
|
"emailAddress_max" => 64,
|
||||||
|
),
|
||||||
|
"cmp_crl_ext" => array (
|
||||||
|
"authorityKeyIdentifier" => "keyid:always,issuer:always",
|
||||||
|
),
|
||||||
|
"cmp_x509_ext_basic" => array (
|
||||||
|
"basicConstraints" => "CA:FALSE",
|
||||||
|
"subjectKeyIdentifier" => "hash",
|
||||||
|
"authorityKeyIdentifier" => "keyid,issuer:always",
|
||||||
|
),
|
||||||
|
"cmp_x509_ext_rsa" => array (
|
||||||
|
"subjectKeyIdentifier" => "hash",
|
||||||
|
"authorityKeyIdentifier" => "keyid:always,issuer:always",
|
||||||
|
"basicConstraints" => "CA:true",
|
||||||
|
"keyUsage" => "cRLSign, keyCertSign",
|
||||||
|
),
|
||||||
|
"cmp_x509_ext_srv" => array (
|
||||||
|
"basicConstraints" => "CA:FALSE",
|
||||||
|
"nsCertType" => "server",
|
||||||
|
"nsComment" => "\"OpenSSL Generated Server Certificate\"",
|
||||||
|
"subjectKeyIdentifier" => "hash",
|
||||||
|
"authorityKeyIdentifier" => "keyid,issuer:always",
|
||||||
|
"extendedKeyUsage" => "serverAuth",
|
||||||
|
"keyUsage" => "digitalSignature, keyEncipherment",
|
||||||
|
),
|
||||||
|
"cmp_x509_ext_cli" => array (
|
||||||
|
"basicConstraints" => "CA:FALSE",
|
||||||
|
"nsCertType" => "client",
|
||||||
|
"nsComment" => "\"OpenSSL Generated Client Certificate\"",
|
||||||
|
"subjectKeyIdentifier" => "hash",
|
||||||
|
"authorityKeyIdentifier" => "keyid,issuer",
|
||||||
|
"keyUsage" => "critical, nonRepudiation, digitalSignature, keyEncipherment",
|
||||||
|
"extendedKeyUsage" => "clientAuth",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenSslConfTemp() {
|
||||||
|
$a = array();
|
||||||
|
|
||||||
|
if(!isset($this->cmpOpenSslConf["GLOBAL"])) {
|
||||||
|
$this->cmpOpenSslConfSetDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($this->cmpOpenSslConf["GLOBAL"])) {
|
||||||
|
throw new Exception("No GLOBAL section");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($this->cmpOpenSslConf["GLOBAL"] as $key => $val) {
|
||||||
|
$a[] = "\t" . $key . "=" . $val . "";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($this->cmpOpenSslConf as $sec => $prm) {
|
||||||
|
if($sec == "GLOBAL")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$a[] = "";
|
||||||
|
$a[] = "[" . $sec . "]";
|
||||||
|
|
||||||
|
foreach($prm as $key => $val) {
|
||||||
|
$a[] = "\t" . $key . " = " . $val . "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$a[] = "";
|
||||||
|
|
||||||
|
$t = join("\n", $a);
|
||||||
|
|
||||||
|
|
||||||
|
$rand = mt_rand(100000, 999999);
|
||||||
|
|
||||||
|
$file = "/tmp/openssl-$rand.conf";
|
||||||
|
|
||||||
|
$w = @file_put_contents($file, $t);
|
||||||
|
|
||||||
|
if(!$w) {
|
||||||
|
throw new Exception("Can't write file '$file'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenSslConfRead($file) {
|
||||||
|
$t = file_get_contents($file);
|
||||||
|
|
||||||
|
$a = explode("\n", $t);
|
||||||
|
|
||||||
|
$sec = "GLOBAL";
|
||||||
|
$key = "";
|
||||||
|
$val = "";
|
||||||
|
$obj = array();
|
||||||
|
|
||||||
|
for($i = 0; $i < count($a); $i++) {
|
||||||
|
$a[$i] = trim($a[$i], "\n \t\r");
|
||||||
|
|
||||||
|
if(!$a[$i])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(substr($a[$i], 0, 1) == "#")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$m = array();
|
||||||
|
$s = "";
|
||||||
|
|
||||||
|
if(preg_match("/^(.+)#.+$/", $a[$i], $m)) {
|
||||||
|
$s = trim($m[1], "\n \t\r");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$s = $a[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(preg_match("/^\[(.+)\]$/", $a[$i], $m)) {
|
||||||
|
$sec = trim($m[1], "\n \t\r");
|
||||||
|
|
||||||
|
if(!isset($obj[$sec])) {
|
||||||
|
$obj[$sec] = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$b = explode("=", $s, 2);
|
||||||
|
|
||||||
|
$key = trim($b[0], "\n \t\r");
|
||||||
|
$val = trim($b[1], "\n \t\r");
|
||||||
|
|
||||||
|
// echo "STR: " . $sec . " === " . $key . " === " . $val . "\n";
|
||||||
|
|
||||||
|
$obj[$sec][$key] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cmpOpenSslConf = $obj;
|
||||||
|
|
||||||
|
// var_export($obj);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenSslParm($parm, $key, $def = "") {
|
||||||
|
if(!is_array($parm))
|
||||||
|
return $def;
|
||||||
|
|
||||||
|
if(!isset($parm[$key]))
|
||||||
|
return $def;
|
||||||
|
|
||||||
|
return $parm[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenSslParmConfSection($parm, $sec, &$out) {
|
||||||
|
if(!isset($this->cmpOpenSslConf[$sec])) {
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($this->cmpOpenSslConf[$sec] as $key => $val) {
|
||||||
|
$m = array();
|
||||||
|
|
||||||
|
if(!preg_match("/^(.+)_default$/", $key, $m))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$key = $m[1];
|
||||||
|
|
||||||
|
$out[$key] = $this->cmpOpenSslParm(
|
||||||
|
$parm,
|
||||||
|
$key,
|
||||||
|
$val
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCertInfo($crt, $prv, &$out = null) {
|
||||||
|
$txtPub = "";
|
||||||
|
$txtPrv = "";
|
||||||
|
|
||||||
|
openssl_x509_export($crt , $txtPub, false );
|
||||||
|
openssl_pkey_export($prv , $txtPrv, NULL );
|
||||||
|
|
||||||
|
if(!$out)
|
||||||
|
$out = array();
|
||||||
|
|
||||||
|
$out["fingerprint"] = array(
|
||||||
|
"default" => @openssl_x509_fingerprint($crt),
|
||||||
|
"sha256" => @openssl_x509_fingerprint($crt, "sha256")
|
||||||
|
);
|
||||||
|
|
||||||
|
$out["err"] = array();
|
||||||
|
|
||||||
|
do {
|
||||||
|
$cert = @openssl_x509_parse($txtPub);
|
||||||
|
|
||||||
|
if(!$cert) {
|
||||||
|
$out["err"][] = array(__LINE__, openssl_error_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($cert["subject"])) {
|
||||||
|
$out["err"][] = array(__LINE__, "Invalid subject");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($cert["subject"]["CN"])) {
|
||||||
|
$out["err"][] = array(__LINE__, "Invalid subject CN");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($cert["extensions"])) {
|
||||||
|
$out["err"][] = array(__LINE__, "Invalid extensions");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($cert["extensions"]["authorityKeyIdentifier"])) {
|
||||||
|
$out["err"][] = array(__LINE__, "Invalid extensions authorityKeyIdentifier");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($cert["extensions"]["subjectKeyIdentifier"])) {
|
||||||
|
$out["err"][] = array(__LINE__, "Invalid extensions subjectKeyIdentifier");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while(0);
|
||||||
|
|
||||||
|
if($out["err"]) {
|
||||||
|
$this->d($out["err"]);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($out["err"]);
|
||||||
|
|
||||||
|
$nrmz = array( /* "A" => "a", "B" => "b", "C" => "c", "D" => "d", "E" => "e", "F" => "f", */ ":" => "");
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(preg_match("/^[0-9A-F:]+$/", $cert["extensions"]["authorityKeyIdentifier"])) {
|
||||||
|
$certAuth = $cert["extensions"]["authorityKeyIdentifier"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$a = explode("\n", $cert["extensions"]["authorityKeyIdentifier"]);
|
||||||
|
$b = null;
|
||||||
|
$c = array();
|
||||||
|
|
||||||
|
for($i = 0; $i < count($a); $i++) {
|
||||||
|
$b = explode(":", $a[$i], 2);
|
||||||
|
$c[$b[0]] = $b[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// $this->d($c);
|
||||||
|
|
||||||
|
if(!isset($c["keyid"]) || !$c["keyid"]) {
|
||||||
|
$out["err"][] = array(__LINE__, "Invalid authorityKeyIdentifier: '". $cert["extensions"]["authorityKeyIdentifier"] ."'");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$certAuth = strtolower($c["keyid"]);
|
||||||
|
} while(0);
|
||||||
|
|
||||||
|
$certAuth = strtr($certAuth, $nrmz);
|
||||||
|
$certAuth = strtolower($certAuth);
|
||||||
|
|
||||||
|
$certSubj = $cert["extensions"]["subjectKeyIdentifier"];
|
||||||
|
$certSubj = strtr($certSubj, $nrmz);
|
||||||
|
$certSubj = strtolower($certSubj);
|
||||||
|
|
||||||
|
$out["certAuth" ] = $certAuth;
|
||||||
|
$out["validFrom"] = date("Y-m-d H:i:s", $cert["validFrom_time_t" ]);
|
||||||
|
$out["validTo" ] = date("Y-m-d H:i:s", $cert["validTo_time_t" ]);
|
||||||
|
$out["certSubj" ] = $certSubj;
|
||||||
|
$out["certCN" ] = $cert["subject"]["CN"];
|
||||||
|
$out["public" ] = $txtPub;
|
||||||
|
$out["private" ] = $txtPrv;
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCA($parm, &$out = null) {
|
||||||
|
try {
|
||||||
|
$confFile = $this->cmpOpenSslConfTemp();
|
||||||
|
|
||||||
|
$digest_alg = $this->cmpOpenSslParm($parm, "digest_alg" , "sha256" );
|
||||||
|
$x509_extensions = $this->cmpOpenSslParm($parm, "x509_extensions" , "cmp_x509_ext_rsa" );
|
||||||
|
$days = $this->cmpOpenSslParm($parm, "days" , 365 );
|
||||||
|
$outfile = $this->cmpOpenSslParm($parm, "outfile" );
|
||||||
|
$serial = $this->cmpOpenSslParm($parm, "serial" , mt_rand(0, PHP_INT_MAX) );
|
||||||
|
|
||||||
|
$this->caDN = array();
|
||||||
|
|
||||||
|
$this->cmpOpenSslParmConfSection($parm, "cmp_org", $this->caDN);
|
||||||
|
$this->cmpOpenSslParmConfSection($parm, "cmp_dst_ext", $this->caDN);
|
||||||
|
|
||||||
|
if(!$this->caDN["commonName"]) {
|
||||||
|
throw new Exception("Empty commonName");
|
||||||
|
}
|
||||||
|
|
||||||
|
$confFile = $this->cmpOpenSslConfTemp();
|
||||||
|
|
||||||
|
$pcsrs = array(
|
||||||
|
"digest_alg" => $digest_alg ,
|
||||||
|
);
|
||||||
|
|
||||||
|
$pcsrn = array(
|
||||||
|
"config" => $confFile ,
|
||||||
|
"digest_alg" => $digest_alg ,
|
||||||
|
"x509_extensions" => $x509_extensions ,
|
||||||
|
);
|
||||||
|
|
||||||
|
$ppkey = array(
|
||||||
|
"config" => $confFile ,
|
||||||
|
"encrypt_key" => $this->cmpOpenSslParm($parm, "encrypt_key" , false ),
|
||||||
|
"private_key_type" => $this->cmpOpenSslParm($parm, "private_key_type" , OPENSSL_KEYTYPE_RSA ),
|
||||||
|
"private_key_bits" => $this->cmpOpenSslParm($parm, "private_key_bits" , 4096 ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->caPrv = openssl_pkey_new($ppkey);
|
||||||
|
|
||||||
|
if(!$this->caPrv) {
|
||||||
|
throw new Exception("openssl_pkey_new: " . openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
$csr = openssl_csr_new($this->caDN, $this->caPrv, $pcsrn);
|
||||||
|
|
||||||
|
if(!$csr) {
|
||||||
|
throw new Exception("openssl_csr_new: " . openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание самоподписанного сертификата со сроком жизни $days дней
|
||||||
|
$this->caCrt = openssl_csr_sign($csr, null, $this->caPrv, $days, $pcsrs, $serial);
|
||||||
|
|
||||||
|
if(!$this->caCrt) {
|
||||||
|
throw new Exception("openssl_csr_sign: " . openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink($confFile);
|
||||||
|
} catch(Exception|Throwable $e) {
|
||||||
|
@unlink($confFile);
|
||||||
|
|
||||||
|
$this->e($e);
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getCertInfo($this->caCrt, $this->caPrv, $out);
|
||||||
|
|
||||||
|
$txtPub = "";
|
||||||
|
$txtPrv = "";
|
||||||
|
|
||||||
|
openssl_x509_export($this->caCrt , $txtPub, false );
|
||||||
|
openssl_pkey_export($this->caPrv , $txtPrv, NULL );
|
||||||
|
|
||||||
|
if($out !== null) {
|
||||||
|
if(!$this->getCertInfo($this->caCrt, $this->caPrv, $out))
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($outfile) {
|
||||||
|
openssl_x509_export_to_file($this->caCrt , "$outfile.crt" );
|
||||||
|
openssl_pkey_export_to_file($this->caPrv , "$outfile.prv" , NULL );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->caPub = openssl_pkey_get_public($this->caCrt);
|
||||||
|
|
||||||
|
if(!$this->caPub) {
|
||||||
|
$this->e(__LINE__, "openssl_pkey_get_public: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// var_export($csrout);
|
||||||
|
// echo "\n";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromFileCACrt($file) {
|
||||||
|
// var_dump(openssl_get_cert_locations());
|
||||||
|
|
||||||
|
$this->caCrtFile = $file;
|
||||||
|
|
||||||
|
$text = @file_get_contents($file);
|
||||||
|
|
||||||
|
if($this->loadFromTextCACrt($text)) {
|
||||||
|
$this->caCrtFile = $file;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromTextCACrt($text) {
|
||||||
|
$this->caCrtFile = "";
|
||||||
|
|
||||||
|
$this->caCrtPEM = $text;
|
||||||
|
|
||||||
|
if(!$this->caCrtPEM) {
|
||||||
|
$this->e(__LINE__, "Invalid CA text");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// openssl_get_privatekey()
|
||||||
|
$this->caCrt = openssl_x509_read( $this->caCrtPEM );
|
||||||
|
|
||||||
|
if(!$this->caCrt) {
|
||||||
|
$this->e(__LINE__, "openssl_x509_read: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// openssl_x509_parse(file_get_contents($file));
|
||||||
|
|
||||||
|
$this->caPub = openssl_pkey_get_public($this->caCrt);
|
||||||
|
|
||||||
|
if(!$this->caPub) {
|
||||||
|
$this->e(__LINE__, "openssl_pkey_get_public: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pkey = openssl_pkey_get_details($this->caPub);
|
||||||
|
|
||||||
|
if(!$pkey) {
|
||||||
|
$this->e(__LINE__, "openssl_pkey_get_details: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->caPubPEM = $pkey["key"];
|
||||||
|
|
||||||
|
$this->caPub = openssl_pkey_get_public($this->caPubPEM);
|
||||||
|
|
||||||
|
if(!$this->caPub) {
|
||||||
|
$this->e(__LINE__, "openssl_pkey_get_public: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromFileCAPrv($file, $pass = NULL) {
|
||||||
|
$this->caPrvFile = $file;
|
||||||
|
|
||||||
|
$text = @file_get_contents($file);
|
||||||
|
|
||||||
|
if($this->loadFromTextCAPrv($text, $pass)) {
|
||||||
|
$this->caPrvFile = $file;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromTextCAPrv($text, $pass = NULL) {
|
||||||
|
$this->caPrvFile = "";
|
||||||
|
/*
|
||||||
|
if(@$file)
|
||||||
|
$this->caPrvPEM = @file_get_contents($file);
|
||||||
|
else
|
||||||
|
$this->caPrvPEM = "";
|
||||||
|
|
||||||
|
if(!$text) {
|
||||||
|
$this->e(__LINE__, "Invalid CA private key text");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
$this->caPrvPEM = $text;
|
||||||
|
|
||||||
|
$this->caPrv = openssl_pkey_get_private($this->caPrvPEM, $pass);
|
||||||
|
|
||||||
|
if(!$this->caPrv) {
|
||||||
|
$this->e(__LINE__, "openssl_pkey_get_private: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sign = "";
|
||||||
|
$test = "test-test";
|
||||||
|
|
||||||
|
//Вычисляем подпись
|
||||||
|
if(!openssl_sign($test, $sign, $this->caPrv, "sha1WithRSAEncryption")) {
|
||||||
|
$this->e(__LINE__, "openssl_sign: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch( openssl_verify($test, $sign, $this->caPub, OPENSSL_ALGO_SHA1) ) {
|
||||||
|
case 1:
|
||||||
|
// echo "корректна\n";
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
// echo "некорректна\n";
|
||||||
|
$this->e(__LINE__, "Incorrect CA private key");
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
case -1:
|
||||||
|
$this->e(__LINE__, openssl_error_string());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function infoCA() {
|
||||||
|
var_export(openssl_x509_parse($this->caCrt));
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// var_export(openssl_pkey_get_details($this->caPrv));
|
||||||
|
// echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createCli($parm, &$out = null) {
|
||||||
|
try {
|
||||||
|
$confFile = $this->cmpOpenSslConfTemp();
|
||||||
|
|
||||||
|
$digest_alg = $this->cmpOpenSslParm($parm, "digest_alg" , "sha256" );
|
||||||
|
$x509_extensions = $this->cmpOpenSslParm($parm, "x509_extensions" , "" );
|
||||||
|
$days = $this->cmpOpenSslParm($parm, "days" , 365 );
|
||||||
|
$outfile = $this->cmpOpenSslParm($parm, "outfile" );
|
||||||
|
$serial = $this->cmpOpenSslParm($parm, "serial" , mt_rand(0, PHP_INT_MAX) );
|
||||||
|
|
||||||
|
$this->cliDN = array();
|
||||||
|
|
||||||
|
$this->cmpOpenSslParmConfSection($parm, "cmp_org", $this->cliDN);
|
||||||
|
$this->cmpOpenSslParmConfSection($parm, "cmp_dst_ext", $this->cliDN);
|
||||||
|
|
||||||
|
if(!$this->cliDN["commonName"]) {
|
||||||
|
throw new Exception("Empty commonName");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$x509_extensions) {
|
||||||
|
throw new Exception("Empty x509_extensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pcsrn = array(
|
||||||
|
"config" => $confFile ,
|
||||||
|
"digest_alg" => $digest_alg ,
|
||||||
|
"x509_extensions" => $x509_extensions ,
|
||||||
|
);
|
||||||
|
|
||||||
|
$ppkey = array(
|
||||||
|
"config" => $confFile ,
|
||||||
|
"encrypt_key" => $this->cmpOpenSslParm($parm, "encrypt_key" , false ),
|
||||||
|
"private_key_type" => $this->cmpOpenSslParm($parm, "private_key_type" , OPENSSL_KEYTYPE_RSA ),
|
||||||
|
"private_key_bits" => $this->cmpOpenSslParm($parm, "private_key_bits" , 4096 ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->cliPrv = openssl_pkey_new($ppkey);
|
||||||
|
|
||||||
|
if(!$this->cliPrv) {
|
||||||
|
throw new Exception("openssl_csr_new: " . openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
$csr = @openssl_csr_new($this->cliDN, $this->cliPrv, $pcsrn);
|
||||||
|
|
||||||
|
if(!$csr) {
|
||||||
|
throw new Exception("openssl_csr_new: " . openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cliCrt = openssl_csr_sign($csr, $this->caCrt, $this->caPrv, $days, $pcsrn, $serial);
|
||||||
|
|
||||||
|
if(!$this->cliCrt) {
|
||||||
|
throw new Exception("openssl_csr_sign: " . openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink($confFile);
|
||||||
|
} catch(Exception|Throwable $e) {
|
||||||
|
@unlink($confFile);
|
||||||
|
|
||||||
|
$this->e($e);
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$txtPub = "";
|
||||||
|
$txtPrv = "";
|
||||||
|
|
||||||
|
openssl_x509_export($this->cliCrt, $txtPub, false );
|
||||||
|
openssl_pkey_export($this->cliPrv, $txtPrv, NULL );
|
||||||
|
|
||||||
|
if($out !== null) {
|
||||||
|
if(!$this->getCertInfo($this->cliCrt, $this->cliPrv, $out))
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($outfile) {
|
||||||
|
openssl_x509_export_to_file($this->cliCrt , "$outfile.crt" );
|
||||||
|
openssl_pkey_export_to_file($this->cliPrv , "$outfile.prv" , NULL );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cliPub = openssl_pkey_get_public($this->cliCrt);
|
||||||
|
|
||||||
|
if(!$this->cliPub) {
|
||||||
|
$this->e(__LINE__, "openssl_pkey_get_public: error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// var_export($csrout);
|
||||||
|
// echo "\n";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClient($parm = NULL, &$out = null) {
|
||||||
|
$parm["x509_extensions"] = "cmp_x509_ext_cli";
|
||||||
|
return $this->createCli($parm, $out);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createServer($parm = NULL, &$out = null) {
|
||||||
|
$parm["x509_extensions"] = "cmp_x509_ext_srv";
|
||||||
|
return $this->createCli($parm, $out);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenSslGenDh($bits = 2048) {
|
||||||
|
$a = array(
|
||||||
|
"openssl",
|
||||||
|
"dhparam",
|
||||||
|
$bits
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!method_exists($this, "cmpSysExec")) {
|
||||||
|
$this->d("Invalid method cmpSysExec");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arr = $this->cmpSysExec($a, array("return" => "outarr", "noerror" => 1));
|
||||||
|
|
||||||
|
if(0) {
|
||||||
|
var_export($arr);
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if($arr[0] != "-----BEGIN DH PARAMETERS-----") {
|
||||||
|
$this->d("Invalid first string");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lst = count($arr) - 2;
|
||||||
|
|
||||||
|
if($arr[$lst] != "-----END DH PARAMETERS-----") {
|
||||||
|
$this->d("Invalid last string");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return join("\n", $arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenVpnGenTa() {
|
||||||
|
$a = array(
|
||||||
|
"openvpn",
|
||||||
|
"--genkey",
|
||||||
|
// Valid keytype arguments are:
|
||||||
|
"secret" // Standard OpenVPN shared secret keys
|
||||||
|
// "tls-crypt" // Alias for secret
|
||||||
|
// "tls-auth" // Alias for secret
|
||||||
|
);
|
||||||
|
|
||||||
|
$arr = $this->cmpSysExec($a, array("return" => "outarr", "noerror" => 1));
|
||||||
|
|
||||||
|
if(0) {
|
||||||
|
var_export($arr);
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$idx = array();
|
||||||
|
|
||||||
|
for($i = 0; $i < count($arr); $i++) {
|
||||||
|
if($arr[$i] == "-----BEGIN OpenVPN Static key V1-----") {
|
||||||
|
$idx["bgn"] = $i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($arr[$i] == "-----END OpenVPN Static key V1-----") {
|
||||||
|
$idx["end"] = $i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($idx["bgn"]) || !isset($idx["end"])) {
|
||||||
|
$this->d("Invalid output key");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return join("\n", $arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenSslGenCrl($caCrtText, $caPrvText, $outCrlFile) {
|
||||||
|
$now = date("Y-m-d H:i:s");
|
||||||
|
$dir = "/tmp/tmp-ca-dir-" . md5("tmp-ca-dir-" . $now);
|
||||||
|
|
||||||
|
if(!mkdir($dir))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
$caCrtFile = $dir . "/ca.crt";
|
||||||
|
$caPrvFile = $dir . "/ca.prv";
|
||||||
|
$indexFile = $dir . "/index.txt";
|
||||||
|
$serialFile = $dir . "/serial";
|
||||||
|
|
||||||
|
file_put_contents($caCrtFile, $caCrtText);
|
||||||
|
file_put_contents($caPrvFile, $caPrvText);
|
||||||
|
|
||||||
|
file_put_contents($indexFile, "");
|
||||||
|
file_put_contents($serialFile, "");
|
||||||
|
|
||||||
|
$a = array(
|
||||||
|
"cd" ,
|
||||||
|
$dir ,
|
||||||
|
["asis", "&&"] ,
|
||||||
|
"openssl" ,
|
||||||
|
"ca" ,
|
||||||
|
"-config" ,
|
||||||
|
"65-openssl.cnf" ,
|
||||||
|
"-gencrl" ,
|
||||||
|
"-out" ,
|
||||||
|
$outCrlFile ,
|
||||||
|
"-cert" ,
|
||||||
|
$caCrtFile ,
|
||||||
|
"-keyfile" ,
|
||||||
|
$caPrvFile ,
|
||||||
|
);
|
||||||
|
|
||||||
|
$arr = $this->cmpSysExec($a, array(/* "return" => "outarr", "noerror" => 1 */));
|
||||||
|
|
||||||
|
unlink($caCrtFile);
|
||||||
|
unlink($caPrvFile);
|
||||||
|
unlink($indexFile);
|
||||||
|
unlink($serialFile);
|
||||||
|
|
||||||
|
if(!rmdir($dir)) {
|
||||||
|
$this->d("Can't remove directory '$dir'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(1) {
|
||||||
|
var_export($arr);
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpOpenSslAddCrl($caCrtText, $caPrvText, $outCrlFile, $sjCrtText) {
|
||||||
|
$now = date("Y-m-d H:i:s");
|
||||||
|
$dir = "/tmp/tmp-ca-dir-" . md5("tmp-ca-dir-" /* . $now . "-" . mt_rand(10000, 99999) */ );
|
||||||
|
|
||||||
|
if(!mkdir($dir)) {
|
||||||
|
;
|
||||||
|
// return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCrtFile = $dir . "/ca.crt";
|
||||||
|
$caPrvFile = $dir . "/ca.prv";
|
||||||
|
$indexFile = $dir . "/index.txt";
|
||||||
|
$serialFile= $dir . "/serial";
|
||||||
|
$sjCrtFile = $dir . "/cert.pem";
|
||||||
|
|
||||||
|
$crlFile = $dir . "/crl.pem";
|
||||||
|
|
||||||
|
if(!is_file($caCrtFile))
|
||||||
|
file_put_contents($caCrtFile, $caCrtText);
|
||||||
|
if(!is_file($caPrvFile))
|
||||||
|
file_put_contents($caPrvFile, $caPrvText);
|
||||||
|
|
||||||
|
file_put_contents($sjCrtFile, $sjCrtText);
|
||||||
|
|
||||||
|
if(!is_file($indexFile))
|
||||||
|
file_put_contents($indexFile, "");
|
||||||
|
if(!is_file($serialFile))
|
||||||
|
file_put_contents($serialFile, "");
|
||||||
|
|
||||||
|
if(is_file($outCrlFile)) {
|
||||||
|
copy($outCrlFile, $crlFile);
|
||||||
|
// $a[] = "-in";
|
||||||
|
// $a[] = $outCrlFile;
|
||||||
|
$this->d("Add IN");
|
||||||
|
}
|
||||||
|
|
||||||
|
$a = array(
|
||||||
|
"cd" ,
|
||||||
|
$dir ,
|
||||||
|
["asis", "&&"] ,
|
||||||
|
"openssl" ,
|
||||||
|
"ca" ,
|
||||||
|
"-config" ,
|
||||||
|
"65-openssl.cnf" ,
|
||||||
|
"-revoke" ,
|
||||||
|
$sjCrtFile ,
|
||||||
|
// "-out" ,
|
||||||
|
// $outCrlFile ,
|
||||||
|
"-cert" ,
|
||||||
|
$caCrtFile ,
|
||||||
|
"-keyfile" ,
|
||||||
|
$caPrvFile ,
|
||||||
|
);
|
||||||
|
|
||||||
|
// $a[] = "-in";
|
||||||
|
// $a[] = $outCrlFile;
|
||||||
|
|
||||||
|
$arr = $this->cmpSysExec($a, array(/* "return" => "outarr", "noerror" => 1 */));
|
||||||
|
|
||||||
|
if(1) {
|
||||||
|
var_export($arr);
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlink($caCrtFile);
|
||||||
|
// unlink($caPrvFile);
|
||||||
|
// unlink($sjCrtFile);
|
||||||
|
// unlink($indexFile);
|
||||||
|
// unlink($serialFile);
|
||||||
|
|
||||||
|
if(is_file($crlFile)) {
|
||||||
|
copy($crlFile, $outCrlFile);
|
||||||
|
// unlink($crlFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlink($dir . "/index.txt.attr");
|
||||||
|
// unlink($dir . "/index.txt.old");
|
||||||
|
|
||||||
|
// if(!rmdir($dir)) {
|
||||||
|
// $this->d("Can't remove directory '$dir'");
|
||||||
|
// }
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// trait
|
||||||
|
}
|
Reference in New Issue
Block a user