This commit is contained in:
net909
2023-11-24 22:45:30 +08:00
parent 3df2bc15e2
commit e9b08fdbf2
20 changed files with 2360 additions and 138 deletions
+107
View File
@@ -0,0 +1,107 @@
<?php
/**
* CSR生成与查看
*/
namespace plugin\dev\csr;
use app\Plugin;
use Exception;
error_reporting(0);
class App extends Plugin
{
private static $key_type = ['RSA', 'DSA', 'DH', 'EC'];
public function index()
{
return $this->view();
}
public function query(){
$csr = input('post.csr');
if(!$csr) return msg('error','no csr');
$subject_info = openssl_csr_get_subject($csr);
if(!$subject_info){
return msg('error', 'CSR文件错误');
}
$pkey_detail = openssl_pkey_get_details(openssl_csr_get_public_key($csr));
$info = [
'通用名称(CN)' => $this->format($subject_info['CN']),
'国家(C)' => $this->format($subject_info['C']),
'省份(S)' => $this->format($subject_info['ST']),
'城市(L)' => $this->format($subject_info['L']),
'组织(O)' => $this->format($subject_info['O']),
'部门(OU)' => $this->format($subject_info['OU']),
'密钥类型' => isset(self::$key_type[$pkey_detail['type']]) ? self::$key_type[$pkey_detail['type']] : '未知',
'密钥强度' => $pkey_detail['bits'],
];
return msg('ok','success',['info'=>$info]);
}
private function format($mixed){
if(is_array($mixed)){
return $mixed[array_key_first($mixed)];
}
return $mixed;
}
public function generate(){
$commonName = input('post.commonName', null, 'trim');//通用名
$countryName = input('post.countryName', null, 'trim');//国家
$stateOrProvinceName = input('post.stateOrProvinceName', null, 'trim');//省份
$localityName = input('post.localityName', null, 'trim');//城市
$organizationName = input('post.organizationName', null, 'trim');//组织
$organizationalUnitName = input('post.organizationalUnitName', null, 'trim');//部门
$subjectAltName = input('post.subjectAltName', null, 'trim');//备用名
$emailAddress = input('post.emailAddress', null, 'trim');//邮箱
$private_key_type = input('post.private_key_type/d');//密钥算法
$private_key_bits = input('post.private_key_bits/d');//密钥长度
$curve_name = input('post.curve_name');//曲线名称
$digest_alg = input('post.digest_alg');//签名算法
$pass_phrase = input('post.pass_phrase', null, 'trim');//私钥密码
//生成公私钥
$config = [
"private_key_bits" => $private_key_bits,
"private_key_type" => $private_key_type,
"curve_name" => $curve_name
];
$privkey = openssl_pkey_new($config);
if(!$privkey){
return msg('error', '私钥生成失败'.openssl_error_string());
}
$privateKey = '';
openssl_pkey_export($privkey, $privateKey, $pass_phrase?$pass_phrase:null, $config);
$pubKey = openssl_pkey_get_details($privkey);
$publicKey = $pubKey['key'];
//生成CSR
$dn = array(
"countryName" => $countryName,
"stateOrProvinceName" => $stateOrProvinceName,
"localityName" => $localityName,
"organizationName" => $organizationName,
"organizationalUnitName" => $organizationalUnitName,
"commonName" => $commonName,
"emailAddress" => $emailAddress,
"subjectAltName" => $subjectAltName
);
$dn = array_filter($dn);
$csr = openssl_csr_new($dn, $privkey, array('digest_alg' => $digest_alg));
if(!$csr){
return msg('error', 'CSR生成失败'.openssl_error_string());
}
openssl_csr_export($csr, $csrout);
return msg('ok', 'success', ['public_key'=>$publicKey, 'private_key'=>$privateKey, 'csr'=>$csrout]);
}
}
+382
View File
@@ -0,0 +1,382 @@
{extend name="common/plugin_layout" /}
{block name="title"}{$plugin.title} - {:config_get('title')}{/block}
{block name="main"}
<style>
.query-title{text-align: right; }
@media (min-width: 576px) { .query-title{width: 150px;} }
</style>
<div class="container-xl" id="app">
<div class="col-sm-12 col-md-10 col-xl-8 center-block">
<div class="card card-preview">
<div class="card-inner mt-3">
<div class="nya-title nk-ibx-action-item progress-rating">
<span class="nk-menu-text font-weight-bold">CSR生成与查看</span>
</div>
<ul class="nav nav-tabs">
<li class="nav-item"><a class="nav-link" @click="show=0" :class="{'active':show===0}" href="javascript:">CSR查看</a></li>
<li class="nav-item"><a class="nav-link" @click="show=1" :class="{'active':show===1}" href="javascript:">CSR生成</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="tabItem1" :class="{'active':show===0}">
<div class="form-group mt-3">
<div class="form-control-wrap">
<div class="custom-control custom-radio mr-3">
<input type="radio" class="custom-control-input" v-model="inputtype" id="customRadio1" :value="0">
<label class="custom-control-label" for="customRadio1">粘贴文件内容</label>
</div>
<div class="custom-control custom-radio mr-3">
<input type="radio" class="custom-control-input" v-model="inputtype" id="customRadio2" :value="1">
<label class="custom-control-label" for="customRadio2">上传文件</label>
</div>
</div>
</div>
<div class="form-group" v-show="inputtype==0">
<label class="form-label">CSR请求文件:</label>
<div class="form-control-wrap">
<textarea class="form-control" rows="6" v-model="csr" placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
</div>
</div>
<div class="form-group" v-show="inputtype==1">
<label class="form-label">CSR请求文件:</label>
<div class="form-control-wrap">
<div class="custom-file">
<input type="file" class="custom-file-input" id="customFile1" accept=".csr" required @change="handleFile">
<label class="custom-file-label" for="customFile1">点击选择文件</label>
</div>
</div>
</div>
<button class="btn btn-dim btn-outline-primary btn-block card-link mb-3" @click="query">
查看CSR信息
</button>
</div>
<div class="tab-pane" id="tabItem2" :class="{'active':show===1}">
<h6 class="text-info">主题信息(以下均为选填)</h6>
<div class="row">
<div class="form-group col-md-4 col-6">
<label class="form-label">通用名(CN)</label>
<div class="form-control-wrap">
<input type="text" v-model="input.commonName" placeholder="站点的域名" class="form-control">
</div>
</div>
<div class="form-group col-md-4 col-6">
<label class="form-label">组织(O)</label>
<div class="form-control-wrap">
<input type="text" v-model="input.organizationName" placeholder="机构名称,例如:Tencent" class="form-control">
</div>
</div>
<div class="form-group col-md-4 col-6">
<label class="form-label">部门(OU)</label>
<div class="form-control-wrap">
<input type="text" v-model="input.organizationalUnitName" placeholder="部门名称" class="form-control">
</div>
</div>
<div class="form-group col-md-4 col-6">
<label class="form-label">国家(C)</label>
<div class="form-control-wrap">
<input type="text" v-model="input.countryName" placeholder="国家简称,例如:CN" class="form-control">
</div>
</div>
<div class="form-group col-md-4 col-6">
<label class="form-label">省份(S)</label>
<div class="form-control-wrap">
<input type="text" v-model="input.stateOrProvinceName" placeholder="省级地区,例如:Guangzhou" class="form-control">
</div>
</div>
<div class="form-group col-md-4 col-6">
<label class="form-label">城市(L)</label>
<div class="form-control-wrap">
<input type="text" v-model="input.localityName" placeholder="市级地区,例如:Shenzhen" class="form-control">
</div>
</div>
<div class="form-group col-md-8 col-6">
<label class="form-label">备用名</label>
<div class="form-control-wrap">
<input type="text" v-model="input.subjectAltName" placeholder="备用域名,多个用,分隔" class="form-control">
</div>
</div>
<div class="form-group col-md-4 col-6">
<label class="form-label">邮箱</label>
<div class="form-control-wrap">
<input type="text" v-model="input.emailAddress" placeholder="邮箱" class="form-control">
</div>
</div>
</div>
<h6 class="text-info">密钥信息</h6>
<div class="row">
<div class="form-group col-6">
<label class="form-label">密钥算法</label>
<select class="form-control" v-model="input.private_key_type">
<option v-for="v,index in private_key_type" :value="index">{{v}}</option>
</select>
</div>
<div class="form-group col-6" v-if="input.private_key_type!=3">
<label class="form-label">密钥长度</label>
<select class="form-control" v-model="input.private_key_bits">
<option v-for="v in private_key_bits" :value="v">{{v}} bit</option>
</select>
</div>
<div class="form-group col-6" v-if="input.private_key_type==3">
<label class="form-label">曲线名称</label>
<select class="form-control" v-model="input.curve_name">
<option v-for="v in curve_name" :value="v">{{v}}</option>
</select>
</div>
<div class="form-group col-6">
<label class="form-label">签名算法</label>
<select class="form-control" v-model="input.digest_alg">
<option v-for="v in digest_alg" :value="v">{{v}}</option>
</select>
</div>
<div class="form-group col-6">
<label class="form-label">私钥密码</label>
<div class="form-control-wrap">
<input type="text" v-model="input.pass_phrase" placeholder="可不设置" class="form-control">
</div>
</div>
</div>
<button class="btn btn-dim btn-outline-primary btn-block card-link mb-3" @click="generate">
生成CSR文件
</button>
</div>
</div>
</div>
</div>
<div class="card card-preview" v-show="show==0&&showresult" style="display:none">
<div class="card-inner mt-3">
<div class="nya-title nk-ibx-action-item progress-rating">
<span class="nk-menu-text font-weight-bold">查询结果</span>
</div>
<div class="table-responsive mb-2">
<table class="table table-hover table-bordered">
<tbody>
<tr v-for="(item,index) in info"><td class="query-title">{{index}}</td><td>{{item}}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card card-preview" v-show="show==1&&showresult2" style="display:none">
<div class="card-inner mt-3">
<div class="nya-title nk-ibx-action-item progress-rating">
<span class="nk-menu-text font-weight-bold">生成结果</span>
</div>
<div class="table-responsive mb-2">
<div class="form-group">
<label class="form-label">CSRPEM格式) <em class="icon ni ni-download" title="下载保存" v-show="result.csr" @click="download('csr')"></em></label>
<div class="form-control-wrap">
<textarea class="form-control" rows="6" v-model="result.csr"></textarea>
</div>
</div>
<div class="d-flex flex-wrap">
<div class="form-group flex-fill mr-2">
<label class="form-label">公钥 <em class="icon ni ni-download" title="下载保存" v-show="result.public_key" @click="download('public_key')"></em></label>
<div class="form-control-wrap">
<textarea class="form-control" rows="8" v-model="result.public_key"></textarea>
</div>
</div>
<div class="form-group flex-fill">
<label class="form-label">私钥 <em class="icon ni ni-download" title="下载保存" v-show="result.private_key" @click="download('private_key')"></em></label>
<div class="form-control-wrap">
<textarea class="form-control" rows="8" v-model="result.private_key"></textarea>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">公私钥格式</label>
<select class="form-control" v-model="result.key_type">
<option value="pem">PEM格式</option>
<option value="base64">Base64无换行</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdn_cdnjs}vue/2.6.14/vue.min.js"></script>
<script src="{$cdn_cdnjs}FileSaver.js/2.0.5/FileSaver.min.js"></script>
<script>
function pemToBase64(pem) {
var lines = pem.split('\n');
var encoded = '';
for (var i = 0; i < lines.length; i++) {
if (lines[i].trim().length > 0 && lines[i].indexOf('-----BEGIN') === -1 && lines[i].indexOf('-----END') === -1) {
encoded += lines[i].trim();
}
}
return encoded;
}
function base64ToPem(base64Key, type) {
var pemHeader = '-----BEGIN ' + type + '-----\n';
var pemFooter = '\n-----END ' + type + '-----\n';
var chunkSize = 64;
var base64KeyWithBreaks = base64Key.match(new RegExp('.{1,' + chunkSize + '}', 'g')).join('\n');
var pemKey = pemHeader + base64KeyWithBreaks + pemFooter;
return pemKey;
}
new Vue({
el: '#app',
data: {
show: 0,
inputtype: 0,
csr: '',
showresult: false,
showresult2: false,
info: [],
private_key_type: [
'RSA',
'DSA',
'DH',
'ECC'
],
private_key_bits: [512,1024,2048,4096],
curve_name: [
'prime256v1',
'secp256k1',
'secp384r1',
'secp521r1',
],
digest_alg: [
'md5',
'sha1',
'sha224',
'sha256',
'sha384',
'sha512',
],
input: {
commonName: '',
countryName: '',
stateOrProvinceName: '',
localityName: '',
organizationName: '',
organizationalUnitName: '',
subjectAltName: '',
emailAddress: '',
private_key_type: 0,
private_key_bits: 2048,
curve_name: 'prime256v1',
digest_alg: 'sha256',
pass_phrase: '',
},
result:{
csr: '',
key_type: 'pem',
public_key: '',
private_key: '',
}
},
watch: {
"input.private_key_type"(newVal) {
if(newVal == 2){
this.private_key_bits = [512,1024,2048]
}else{
this.private_key_bits = [512,1024,2048,4096]
}
},
"result.key_type"(newVal) {
if(this.result.public_key != ''){
if(newVal == 'pem'){
this.result.public_key = base64ToPem(this.result.public_key, 'PUBLIC KEY')
}else{
this.result.public_key = pemToBase64(this.result.public_key)
}
}
if(this.result.private_key != ''){
if(newVal == 'pem'){
this.result.private_key = base64ToPem(this.result.private_key, this.input.pass_phrase ? 'ENCRYPTED PRIVATE KEY' : 'PRIVATE KEY')
}else{
this.result.private_key = pemToBase64(this.result.private_key)
}
}
},
},
methods: {
handleFile(event) {
var file = event.target.files[0];
layer.load(0, {shade:0.1});
var that = this;
that.csr = '';
var reader = new FileReader();
reader.onload = () => {
layer.closeAll();
var csr = reader.result;
if(csr.indexOf('BEGIN CERTIFICATE REQUEST') == -1){
layer.alert('证书文件错误', {icon: 5});return;
}
that.csr = csr;
};
reader.onerror = () => {
layer.closeAll();
layer.alert('证书文件读取失败', {icon: 5});return;
};
reader.readAsText(file);
},
query() {
if(this.csr == ''){
layer.alert('证书内容不能为空');return;
}
layer.load(0, {shade:0.1});
var that=this;
$.ajax({
url: '/api/{$plugin.alias}/query',
type: 'post',
dataType: 'json',
data: {csr: that.csr},
cache: false,
success: function (data) {
layer.closeAll();
if(data.status=='ok'){
that.info = data.data.info;
that.showresult = true;
}else{
layer.alert(data.message, {icon: 5});
}
},
error: function () {
layer.closeAll();
layer.msg('服务器错误', {icon: 5});
}
});
},
generate() {
layer.load(0, {shade:0.1});
var that=this;
$.ajax({
url: '/api/{$plugin.alias}/generate',
type: 'post',
dataType: 'json',
data: {...that.input},
cache: false,
success: function (data) {
layer.closeAll();
if(data.status=='ok'){
that.result.csr = data.data.csr;
that.result.public_key = data.data.public_key;
that.result.private_key = data.data.private_key;
that.showresult2 = true;
}else{
layer.alert(data.message, {icon: 5});
}
},
error: function () {
layer.closeAll();
layer.msg('服务器错误', {icon: 5});
}
});
},
download(name) {
var content = this.result[name]
if(!content) return;
var ext = this.result.key_type == 'base64' ? '.txt' : '.pem';
if(name == 'csr') ext = '.csr';
var filename = name + ext
var blob = new Blob([content], {type: "application/force-download"});
saveAs(blob, filename);
}
},
})
</script>
{/block}