Add certificate issuance, CSR signing, and cert listing to web UI
- Add SignCSR RPC to v2 CA proto and regenerate; implement handleSignCSR
in CA engine and caServer gRPC layer; add SignCSR client method and
POST /pki/sign-csr web route with result display in pki.html
- Fix issuer detail cert listing: template was using map-style index on
CertSummary structs; switch to struct field access and populate
IssuedBy/IssuedAt fields from proto response
- Add certificate detail view (cert_detail.html) with GET /cert/{serial}
and GET /cert/{serial}/download routes
- Update Makefile proto target to generate both v1 and v2 protos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
|||||||
[{"lang":"en","usageCount":33}]
|
[{"lang":"en","usageCount":35}]
|
||||||
3
Makefile
3
Makefile
@@ -8,6 +8,9 @@ proto:
|
|||||||
protoc --go_out=. --go_opt=module=git.wntrmute.dev/kyle/metacrypt \
|
protoc --go_out=. --go_opt=module=git.wntrmute.dev/kyle/metacrypt \
|
||||||
--go-grpc_out=. --go-grpc_opt=module=git.wntrmute.dev/kyle/metacrypt \
|
--go-grpc_out=. --go-grpc_opt=module=git.wntrmute.dev/kyle/metacrypt \
|
||||||
proto/metacrypt/v1/*.proto
|
proto/metacrypt/v1/*.proto
|
||||||
|
protoc --go_out=. --go_opt=module=git.wntrmute.dev/kyle/metacrypt \
|
||||||
|
--go-grpc_out=. --go-grpc_opt=module=git.wntrmute.dev/kyle/metacrypt \
|
||||||
|
proto/metacrypt/v2/*.proto
|
||||||
|
|
||||||
metacrypt:
|
metacrypt:
|
||||||
go build $(LDFLAGS) -o metacrypt ./cmd/metacrypt
|
go build $(LDFLAGS) -o metacrypt ./cmd/metacrypt
|
||||||
|
|||||||
@@ -1308,6 +1308,182 @@ func (x *RenewCertResponse) GetChainPem() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SignCSRRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"`
|
||||||
|
// issuer is the name of the issuer to sign with.
|
||||||
|
Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"`
|
||||||
|
// csr_pem is the PEM-encoded PKCS#10 certificate signing request.
|
||||||
|
CsrPem []byte `protobuf:"bytes,3,opt,name=csr_pem,json=csrPem,proto3" json:"csr_pem,omitempty"`
|
||||||
|
// profile selects key usage defaults (e.g. "server", "client", "peer").
|
||||||
|
// Defaults to "server" if empty.
|
||||||
|
Profile string `protobuf:"bytes,4,opt,name=profile,proto3" json:"profile,omitempty"`
|
||||||
|
// ttl overrides the profile's default validity period (e.g. "8760h").
|
||||||
|
Ttl string `protobuf:"bytes,5,opt,name=ttl,proto3" json:"ttl,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) Reset() {
|
||||||
|
*x = SignCSRRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[22]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SignCSRRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[22]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SignCSRRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SignCSRRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_ca_proto_rawDescGZIP(), []int{22}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) GetMount() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Mount
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) GetIssuer() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Issuer
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) GetCsrPem() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.CsrPem
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) GetProfile() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Profile
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRRequest) GetTtl() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ttl
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignCSRResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"`
|
||||||
|
CommonName string `protobuf:"bytes,2,opt,name=common_name,json=commonName,proto3" json:"common_name,omitempty"`
|
||||||
|
Sans []string `protobuf:"bytes,3,rep,name=sans,proto3" json:"sans,omitempty"`
|
||||||
|
IssuedBy string `protobuf:"bytes,4,opt,name=issued_by,json=issuedBy,proto3" json:"issued_by,omitempty"`
|
||||||
|
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
|
||||||
|
// cert_pem is the signed leaf certificate. No private key is returned
|
||||||
|
// because the caller already holds it.
|
||||||
|
CertPem []byte `protobuf:"bytes,6,opt,name=cert_pem,json=certPem,proto3" json:"cert_pem,omitempty"`
|
||||||
|
// chain_pem contains the full chain: leaf + issuer + root, PEM-concatenated.
|
||||||
|
ChainPem []byte `protobuf:"bytes,7,opt,name=chain_pem,json=chainPem,proto3" json:"chain_pem,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) Reset() {
|
||||||
|
*x = SignCSRResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[23]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SignCSRResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[23]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SignCSRResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SignCSRResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_ca_proto_rawDescGZIP(), []int{23}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) GetSerial() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Serial
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) GetCommonName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CommonName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) GetSans() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Sans
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) GetIssuedBy() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.IssuedBy
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) GetExpiresAt() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.ExpiresAt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) GetCertPem() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.CertPem
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SignCSRResponse) GetChainPem() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.ChainPem
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CertRecord is the full certificate record including the PEM-encoded cert.
|
// CertRecord is the full certificate record including the PEM-encoded cert.
|
||||||
type CertRecord struct {
|
type CertRecord struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@@ -1327,7 +1503,7 @@ type CertRecord struct {
|
|||||||
|
|
||||||
func (x *CertRecord) Reset() {
|
func (x *CertRecord) Reset() {
|
||||||
*x = CertRecord{}
|
*x = CertRecord{}
|
||||||
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[22]
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[24]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1339,7 +1515,7 @@ func (x *CertRecord) String() string {
|
|||||||
func (*CertRecord) ProtoMessage() {}
|
func (*CertRecord) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CertRecord) ProtoReflect() protoreflect.Message {
|
func (x *CertRecord) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[22]
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[24]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -1352,7 +1528,7 @@ func (x *CertRecord) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CertRecord.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CertRecord.ProtoReflect.Descriptor instead.
|
||||||
func (*CertRecord) Descriptor() ([]byte, []int) {
|
func (*CertRecord) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_metacrypt_v2_ca_proto_rawDescGZIP(), []int{22}
|
return file_proto_metacrypt_v2_ca_proto_rawDescGZIP(), []int{24}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CertRecord) GetSerial() string {
|
func (x *CertRecord) GetSerial() string {
|
||||||
@@ -1435,7 +1611,7 @@ type CertSummary struct {
|
|||||||
|
|
||||||
func (x *CertSummary) Reset() {
|
func (x *CertSummary) Reset() {
|
||||||
*x = CertSummary{}
|
*x = CertSummary{}
|
||||||
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[23]
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[25]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1447,7 +1623,7 @@ func (x *CertSummary) String() string {
|
|||||||
func (*CertSummary) ProtoMessage() {}
|
func (*CertSummary) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CertSummary) ProtoReflect() protoreflect.Message {
|
func (x *CertSummary) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[23]
|
mi := &file_proto_metacrypt_v2_ca_proto_msgTypes[25]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -1460,7 +1636,7 @@ func (x *CertSummary) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CertSummary.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CertSummary.ProtoReflect.Descriptor instead.
|
||||||
func (*CertSummary) Descriptor() ([]byte, []int) {
|
func (*CertSummary) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_metacrypt_v2_ca_proto_rawDescGZIP(), []int{23}
|
return file_proto_metacrypt_v2_ca_proto_rawDescGZIP(), []int{25}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CertSummary) GetSerial() string {
|
func (x *CertSummary) GetSerial() string {
|
||||||
@@ -1606,7 +1782,23 @@ const file_proto_metacrypt_v2_ca_proto_rawDesc = "" +
|
|||||||
"expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x19\n" +
|
"expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x19\n" +
|
||||||
"\bcert_pem\x18\x06 \x01(\fR\acertPem\x12\x17\n" +
|
"\bcert_pem\x18\x06 \x01(\fR\acertPem\x12\x17\n" +
|
||||||
"\akey_pem\x18\a \x01(\fR\x06keyPem\x12\x1b\n" +
|
"\akey_pem\x18\a \x01(\fR\x06keyPem\x12\x1b\n" +
|
||||||
"\tchain_pem\x18\b \x01(\fR\bchainPem\"\xb7\x02\n" +
|
"\tchain_pem\x18\b \x01(\fR\bchainPem\"\x83\x01\n" +
|
||||||
|
"\x0eSignCSRRequest\x12\x14\n" +
|
||||||
|
"\x05mount\x18\x01 \x01(\tR\x05mount\x12\x16\n" +
|
||||||
|
"\x06issuer\x18\x02 \x01(\tR\x06issuer\x12\x17\n" +
|
||||||
|
"\acsr_pem\x18\x03 \x01(\fR\x06csrPem\x12\x18\n" +
|
||||||
|
"\aprofile\x18\x04 \x01(\tR\aprofile\x12\x10\n" +
|
||||||
|
"\x03ttl\x18\x05 \x01(\tR\x03ttl\"\xee\x01\n" +
|
||||||
|
"\x0fSignCSRResponse\x12\x16\n" +
|
||||||
|
"\x06serial\x18\x01 \x01(\tR\x06serial\x12\x1f\n" +
|
||||||
|
"\vcommon_name\x18\x02 \x01(\tR\n" +
|
||||||
|
"commonName\x12\x12\n" +
|
||||||
|
"\x04sans\x18\x03 \x03(\tR\x04sans\x12\x1b\n" +
|
||||||
|
"\tissued_by\x18\x04 \x01(\tR\bissuedBy\x129\n" +
|
||||||
|
"\n" +
|
||||||
|
"expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x19\n" +
|
||||||
|
"\bcert_pem\x18\x06 \x01(\fR\acertPem\x12\x1b\n" +
|
||||||
|
"\tchain_pem\x18\a \x01(\fR\bchainPem\"\xb7\x02\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"CertRecord\x12\x16\n" +
|
"CertRecord\x12\x16\n" +
|
||||||
"\x06serial\x18\x01 \x01(\tR\x06serial\x12\x16\n" +
|
"\x06serial\x18\x01 \x01(\tR\x06serial\x12\x16\n" +
|
||||||
@@ -1629,7 +1821,7 @@ const file_proto_metacrypt_v2_ca_proto_rawDesc = "" +
|
|||||||
"\tissued_by\x18\x05 \x01(\tR\bissuedBy\x127\n" +
|
"\tissued_by\x18\x05 \x01(\tR\bissuedBy\x127\n" +
|
||||||
"\tissued_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\bissuedAt\x129\n" +
|
"\tissued_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\bissuedAt\x129\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\x83\a\n" +
|
"expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\xcb\a\n" +
|
||||||
"\tCAService\x12O\n" +
|
"\tCAService\x12O\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"ImportRoot\x12\x1f.metacrypt.v2.ImportRootRequest\x1a .metacrypt.v2.ImportRootResponse\x12F\n" +
|
"ImportRoot\x12\x1f.metacrypt.v2.ImportRootRequest\x1a .metacrypt.v2.ImportRootResponse\x12F\n" +
|
||||||
@@ -1642,7 +1834,8 @@ const file_proto_metacrypt_v2_ca_proto_rawDesc = "" +
|
|||||||
"\tIssueCert\x12\x1e.metacrypt.v2.IssueCertRequest\x1a\x1f.metacrypt.v2.IssueCertResponse\x12F\n" +
|
"\tIssueCert\x12\x1e.metacrypt.v2.IssueCertRequest\x1a\x1f.metacrypt.v2.IssueCertResponse\x12F\n" +
|
||||||
"\aGetCert\x12\x1c.metacrypt.v2.GetCertRequest\x1a\x1d.metacrypt.v2.GetCertResponse\x12L\n" +
|
"\aGetCert\x12\x1c.metacrypt.v2.GetCertRequest\x1a\x1d.metacrypt.v2.GetCertResponse\x12L\n" +
|
||||||
"\tListCerts\x12\x1e.metacrypt.v2.ListCertsRequest\x1a\x1f.metacrypt.v2.ListCertsResponse\x12L\n" +
|
"\tListCerts\x12\x1e.metacrypt.v2.ListCertsRequest\x1a\x1f.metacrypt.v2.ListCertsResponse\x12L\n" +
|
||||||
"\tRenewCert\x12\x1e.metacrypt.v2.RenewCertRequest\x1a\x1f.metacrypt.v2.RenewCertResponseB>Z<git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2;metacryptv2b\x06proto3"
|
"\tRenewCert\x12\x1e.metacrypt.v2.RenewCertRequest\x1a\x1f.metacrypt.v2.RenewCertResponse\x12F\n" +
|
||||||
|
"\aSignCSR\x12\x1c.metacrypt.v2.SignCSRRequest\x1a\x1d.metacrypt.v2.SignCSRResponseB>Z<git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2;metacryptv2b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_proto_metacrypt_v2_ca_proto_rawDescOnce sync.Once
|
file_proto_metacrypt_v2_ca_proto_rawDescOnce sync.Once
|
||||||
@@ -1656,7 +1849,7 @@ func file_proto_metacrypt_v2_ca_proto_rawDescGZIP() []byte {
|
|||||||
return file_proto_metacrypt_v2_ca_proto_rawDescData
|
return file_proto_metacrypt_v2_ca_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_proto_metacrypt_v2_ca_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
|
var file_proto_metacrypt_v2_ca_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
|
||||||
var file_proto_metacrypt_v2_ca_proto_goTypes = []any{
|
var file_proto_metacrypt_v2_ca_proto_goTypes = []any{
|
||||||
(*ImportRootRequest)(nil), // 0: metacrypt.v2.ImportRootRequest
|
(*ImportRootRequest)(nil), // 0: metacrypt.v2.ImportRootRequest
|
||||||
(*ImportRootResponse)(nil), // 1: metacrypt.v2.ImportRootResponse
|
(*ImportRootResponse)(nil), // 1: metacrypt.v2.ImportRootResponse
|
||||||
@@ -1680,47 +1873,52 @@ var file_proto_metacrypt_v2_ca_proto_goTypes = []any{
|
|||||||
(*ListCertsResponse)(nil), // 19: metacrypt.v2.ListCertsResponse
|
(*ListCertsResponse)(nil), // 19: metacrypt.v2.ListCertsResponse
|
||||||
(*RenewCertRequest)(nil), // 20: metacrypt.v2.RenewCertRequest
|
(*RenewCertRequest)(nil), // 20: metacrypt.v2.RenewCertRequest
|
||||||
(*RenewCertResponse)(nil), // 21: metacrypt.v2.RenewCertResponse
|
(*RenewCertResponse)(nil), // 21: metacrypt.v2.RenewCertResponse
|
||||||
(*CertRecord)(nil), // 22: metacrypt.v2.CertRecord
|
(*SignCSRRequest)(nil), // 22: metacrypt.v2.SignCSRRequest
|
||||||
(*CertSummary)(nil), // 23: metacrypt.v2.CertSummary
|
(*SignCSRResponse)(nil), // 23: metacrypt.v2.SignCSRResponse
|
||||||
(*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp
|
(*CertRecord)(nil), // 24: metacrypt.v2.CertRecord
|
||||||
|
(*CertSummary)(nil), // 25: metacrypt.v2.CertSummary
|
||||||
|
(*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp
|
||||||
}
|
}
|
||||||
var file_proto_metacrypt_v2_ca_proto_depIdxs = []int32{
|
var file_proto_metacrypt_v2_ca_proto_depIdxs = []int32{
|
||||||
24, // 0: metacrypt.v2.ImportRootResponse.expires_at:type_name -> google.protobuf.Timestamp
|
26, // 0: metacrypt.v2.ImportRootResponse.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
24, // 1: metacrypt.v2.IssueCertResponse.expires_at:type_name -> google.protobuf.Timestamp
|
26, // 1: metacrypt.v2.IssueCertResponse.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
22, // 2: metacrypt.v2.GetCertResponse.cert:type_name -> metacrypt.v2.CertRecord
|
24, // 2: metacrypt.v2.GetCertResponse.cert:type_name -> metacrypt.v2.CertRecord
|
||||||
23, // 3: metacrypt.v2.ListCertsResponse.certs:type_name -> metacrypt.v2.CertSummary
|
25, // 3: metacrypt.v2.ListCertsResponse.certs:type_name -> metacrypt.v2.CertSummary
|
||||||
24, // 4: metacrypt.v2.RenewCertResponse.expires_at:type_name -> google.protobuf.Timestamp
|
26, // 4: metacrypt.v2.RenewCertResponse.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
24, // 5: metacrypt.v2.CertRecord.issued_at:type_name -> google.protobuf.Timestamp
|
26, // 5: metacrypt.v2.SignCSRResponse.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
24, // 6: metacrypt.v2.CertRecord.expires_at:type_name -> google.protobuf.Timestamp
|
26, // 6: metacrypt.v2.CertRecord.issued_at:type_name -> google.protobuf.Timestamp
|
||||||
24, // 7: metacrypt.v2.CertSummary.issued_at:type_name -> google.protobuf.Timestamp
|
26, // 7: metacrypt.v2.CertRecord.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
24, // 8: metacrypt.v2.CertSummary.expires_at:type_name -> google.protobuf.Timestamp
|
26, // 8: metacrypt.v2.CertSummary.issued_at:type_name -> google.protobuf.Timestamp
|
||||||
0, // 9: metacrypt.v2.CAService.ImportRoot:input_type -> metacrypt.v2.ImportRootRequest
|
26, // 9: metacrypt.v2.CertSummary.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
2, // 10: metacrypt.v2.CAService.GetRoot:input_type -> metacrypt.v2.GetRootRequest
|
0, // 10: metacrypt.v2.CAService.ImportRoot:input_type -> metacrypt.v2.ImportRootRequest
|
||||||
4, // 11: metacrypt.v2.CAService.CreateIssuer:input_type -> metacrypt.v2.CreateIssuerRequest
|
2, // 11: metacrypt.v2.CAService.GetRoot:input_type -> metacrypt.v2.GetRootRequest
|
||||||
6, // 12: metacrypt.v2.CAService.DeleteIssuer:input_type -> metacrypt.v2.DeleteIssuerRequest
|
4, // 12: metacrypt.v2.CAService.CreateIssuer:input_type -> metacrypt.v2.CreateIssuerRequest
|
||||||
8, // 13: metacrypt.v2.CAService.ListIssuers:input_type -> metacrypt.v2.ListIssuersRequest
|
6, // 13: metacrypt.v2.CAService.DeleteIssuer:input_type -> metacrypt.v2.DeleteIssuerRequest
|
||||||
10, // 14: metacrypt.v2.CAService.GetIssuer:input_type -> metacrypt.v2.GetIssuerRequest
|
8, // 14: metacrypt.v2.CAService.ListIssuers:input_type -> metacrypt.v2.ListIssuersRequest
|
||||||
12, // 15: metacrypt.v2.CAService.GetChain:input_type -> metacrypt.v2.CAServiceGetChainRequest
|
10, // 15: metacrypt.v2.CAService.GetIssuer:input_type -> metacrypt.v2.GetIssuerRequest
|
||||||
14, // 16: metacrypt.v2.CAService.IssueCert:input_type -> metacrypt.v2.IssueCertRequest
|
12, // 16: metacrypt.v2.CAService.GetChain:input_type -> metacrypt.v2.CAServiceGetChainRequest
|
||||||
16, // 17: metacrypt.v2.CAService.GetCert:input_type -> metacrypt.v2.GetCertRequest
|
14, // 17: metacrypt.v2.CAService.IssueCert:input_type -> metacrypt.v2.IssueCertRequest
|
||||||
18, // 18: metacrypt.v2.CAService.ListCerts:input_type -> metacrypt.v2.ListCertsRequest
|
16, // 18: metacrypt.v2.CAService.GetCert:input_type -> metacrypt.v2.GetCertRequest
|
||||||
20, // 19: metacrypt.v2.CAService.RenewCert:input_type -> metacrypt.v2.RenewCertRequest
|
18, // 19: metacrypt.v2.CAService.ListCerts:input_type -> metacrypt.v2.ListCertsRequest
|
||||||
1, // 20: metacrypt.v2.CAService.ImportRoot:output_type -> metacrypt.v2.ImportRootResponse
|
20, // 20: metacrypt.v2.CAService.RenewCert:input_type -> metacrypt.v2.RenewCertRequest
|
||||||
3, // 21: metacrypt.v2.CAService.GetRoot:output_type -> metacrypt.v2.GetRootResponse
|
22, // 21: metacrypt.v2.CAService.SignCSR:input_type -> metacrypt.v2.SignCSRRequest
|
||||||
5, // 22: metacrypt.v2.CAService.CreateIssuer:output_type -> metacrypt.v2.CreateIssuerResponse
|
1, // 22: metacrypt.v2.CAService.ImportRoot:output_type -> metacrypt.v2.ImportRootResponse
|
||||||
7, // 23: metacrypt.v2.CAService.DeleteIssuer:output_type -> metacrypt.v2.DeleteIssuerResponse
|
3, // 23: metacrypt.v2.CAService.GetRoot:output_type -> metacrypt.v2.GetRootResponse
|
||||||
9, // 24: metacrypt.v2.CAService.ListIssuers:output_type -> metacrypt.v2.ListIssuersResponse
|
5, // 24: metacrypt.v2.CAService.CreateIssuer:output_type -> metacrypt.v2.CreateIssuerResponse
|
||||||
11, // 25: metacrypt.v2.CAService.GetIssuer:output_type -> metacrypt.v2.GetIssuerResponse
|
7, // 25: metacrypt.v2.CAService.DeleteIssuer:output_type -> metacrypt.v2.DeleteIssuerResponse
|
||||||
13, // 26: metacrypt.v2.CAService.GetChain:output_type -> metacrypt.v2.CAServiceGetChainResponse
|
9, // 26: metacrypt.v2.CAService.ListIssuers:output_type -> metacrypt.v2.ListIssuersResponse
|
||||||
15, // 27: metacrypt.v2.CAService.IssueCert:output_type -> metacrypt.v2.IssueCertResponse
|
11, // 27: metacrypt.v2.CAService.GetIssuer:output_type -> metacrypt.v2.GetIssuerResponse
|
||||||
17, // 28: metacrypt.v2.CAService.GetCert:output_type -> metacrypt.v2.GetCertResponse
|
13, // 28: metacrypt.v2.CAService.GetChain:output_type -> metacrypt.v2.CAServiceGetChainResponse
|
||||||
19, // 29: metacrypt.v2.CAService.ListCerts:output_type -> metacrypt.v2.ListCertsResponse
|
15, // 29: metacrypt.v2.CAService.IssueCert:output_type -> metacrypt.v2.IssueCertResponse
|
||||||
21, // 30: metacrypt.v2.CAService.RenewCert:output_type -> metacrypt.v2.RenewCertResponse
|
17, // 30: metacrypt.v2.CAService.GetCert:output_type -> metacrypt.v2.GetCertResponse
|
||||||
20, // [20:31] is the sub-list for method output_type
|
19, // 31: metacrypt.v2.CAService.ListCerts:output_type -> metacrypt.v2.ListCertsResponse
|
||||||
9, // [9:20] is the sub-list for method input_type
|
21, // 32: metacrypt.v2.CAService.RenewCert:output_type -> metacrypt.v2.RenewCertResponse
|
||||||
9, // [9:9] is the sub-list for extension type_name
|
23, // 33: metacrypt.v2.CAService.SignCSR:output_type -> metacrypt.v2.SignCSRResponse
|
||||||
9, // [9:9] is the sub-list for extension extendee
|
22, // [22:34] is the sub-list for method output_type
|
||||||
0, // [0:9] is the sub-list for field type_name
|
10, // [10:22] is the sub-list for method input_type
|
||||||
|
10, // [10:10] is the sub-list for extension type_name
|
||||||
|
10, // [10:10] is the sub-list for extension extendee
|
||||||
|
0, // [0:10] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_proto_metacrypt_v2_ca_proto_init() }
|
func init() { file_proto_metacrypt_v2_ca_proto_init() }
|
||||||
@@ -1734,7 +1932,7 @@ func file_proto_metacrypt_v2_ca_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v2_ca_proto_rawDesc), len(file_proto_metacrypt_v2_ca_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v2_ca_proto_rawDesc), len(file_proto_metacrypt_v2_ca_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 24,
|
NumMessages: 26,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const (
|
|||||||
CAService_GetCert_FullMethodName = "/metacrypt.v2.CAService/GetCert"
|
CAService_GetCert_FullMethodName = "/metacrypt.v2.CAService/GetCert"
|
||||||
CAService_ListCerts_FullMethodName = "/metacrypt.v2.CAService/ListCerts"
|
CAService_ListCerts_FullMethodName = "/metacrypt.v2.CAService/ListCerts"
|
||||||
CAService_RenewCert_FullMethodName = "/metacrypt.v2.CAService/RenewCert"
|
CAService_RenewCert_FullMethodName = "/metacrypt.v2.CAService/RenewCert"
|
||||||
|
CAService_SignCSR_FullMethodName = "/metacrypt.v2.CAService/SignCSR"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CAServiceClient is the client API for CAService service.
|
// CAServiceClient is the client API for CAService service.
|
||||||
@@ -67,6 +68,10 @@ type CAServiceClient interface {
|
|||||||
// RenewCert renews an existing certificate, generating a new key and serial.
|
// RenewCert renews an existing certificate, generating a new key and serial.
|
||||||
// Auth required.
|
// Auth required.
|
||||||
RenewCert(ctx context.Context, in *RenewCertRequest, opts ...grpc.CallOption) (*RenewCertResponse, error)
|
RenewCert(ctx context.Context, in *RenewCertRequest, opts ...grpc.CallOption) (*RenewCertResponse, error)
|
||||||
|
// SignCSR signs an externally generated CSR with a named issuer. All Subject
|
||||||
|
// and SAN fields from the CSR are preserved exactly; profile defaults supply
|
||||||
|
// key usages and validity if not overridden. Auth required.
|
||||||
|
SignCSR(ctx context.Context, in *SignCSRRequest, opts ...grpc.CallOption) (*SignCSRResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type cAServiceClient struct {
|
type cAServiceClient struct {
|
||||||
@@ -187,6 +192,16 @@ func (c *cAServiceClient) RenewCert(ctx context.Context, in *RenewCertRequest, o
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *cAServiceClient) SignCSR(ctx context.Context, in *SignCSRRequest, opts ...grpc.CallOption) (*SignCSRResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(SignCSRResponse)
|
||||||
|
err := c.cc.Invoke(ctx, CAService_SignCSR_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CAServiceServer is the server API for CAService service.
|
// CAServiceServer is the server API for CAService service.
|
||||||
// All implementations must embed UnimplementedCAServiceServer
|
// All implementations must embed UnimplementedCAServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -222,6 +237,10 @@ type CAServiceServer interface {
|
|||||||
// RenewCert renews an existing certificate, generating a new key and serial.
|
// RenewCert renews an existing certificate, generating a new key and serial.
|
||||||
// Auth required.
|
// Auth required.
|
||||||
RenewCert(context.Context, *RenewCertRequest) (*RenewCertResponse, error)
|
RenewCert(context.Context, *RenewCertRequest) (*RenewCertResponse, error)
|
||||||
|
// SignCSR signs an externally generated CSR with a named issuer. All Subject
|
||||||
|
// and SAN fields from the CSR are preserved exactly; profile defaults supply
|
||||||
|
// key usages and validity if not overridden. Auth required.
|
||||||
|
SignCSR(context.Context, *SignCSRRequest) (*SignCSRResponse, error)
|
||||||
mustEmbedUnimplementedCAServiceServer()
|
mustEmbedUnimplementedCAServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +284,9 @@ func (UnimplementedCAServiceServer) ListCerts(context.Context, *ListCertsRequest
|
|||||||
func (UnimplementedCAServiceServer) RenewCert(context.Context, *RenewCertRequest) (*RenewCertResponse, error) {
|
func (UnimplementedCAServiceServer) RenewCert(context.Context, *RenewCertRequest) (*RenewCertResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method RenewCert not implemented")
|
return nil, status.Error(codes.Unimplemented, "method RenewCert not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedCAServiceServer) SignCSR(context.Context, *SignCSRRequest) (*SignCSRResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method SignCSR not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedCAServiceServer) mustEmbedUnimplementedCAServiceServer() {}
|
func (UnimplementedCAServiceServer) mustEmbedUnimplementedCAServiceServer() {}
|
||||||
func (UnimplementedCAServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedCAServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -484,6 +506,24 @@ func _CAService_RenewCert_Handler(srv interface{}, ctx context.Context, dec func
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _CAService_SignCSR_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SignCSRRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(CAServiceServer).SignCSR(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: CAService_SignCSR_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(CAServiceServer).SignCSR(ctx, req.(*SignCSRRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// CAService_ServiceDesc is the grpc.ServiceDesc for CAService service.
|
// CAService_ServiceDesc is the grpc.ServiceDesc for CAService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -535,6 +575,10 @@ var CAService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "RenewCert",
|
MethodName: "RenewCert",
|
||||||
Handler: _CAService_RenewCert_Handler,
|
Handler: _CAService_RenewCert_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SignCSR",
|
||||||
|
Handler: _CAService_SignCSR_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "proto/metacrypt/v2/ca.proto",
|
Metadata: "proto/metacrypt/v2/ca.proto",
|
||||||
|
|||||||
@@ -300,6 +300,8 @@ func (e *CAEngine) HandleRequest(ctx context.Context, req *engine.Request) (*eng
|
|||||||
return e.handleListCerts(ctx, req)
|
return e.handleListCerts(ctx, req)
|
||||||
case "renew":
|
case "renew":
|
||||||
return e.handleRenew(ctx, req)
|
return e.handleRenew(ctx, req)
|
||||||
|
case "sign-csr":
|
||||||
|
return e.handleSignCSR(ctx, req)
|
||||||
case "import-root":
|
case "import-root":
|
||||||
return e.handleImportRoot(ctx, req)
|
return e.handleImportRoot(ctx, req)
|
||||||
default:
|
default:
|
||||||
@@ -1014,6 +1016,109 @@ func (e *CAEngine) handleRenew(ctx context.Context, req *engine.Request) (*engin
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *CAEngine) handleSignCSR(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
issuerName, _ := req.Data["issuer"].(string)
|
||||||
|
if issuerName == "" {
|
||||||
|
issuerName = req.Path
|
||||||
|
}
|
||||||
|
if issuerName == "" {
|
||||||
|
return nil, fmt.Errorf("ca: issuer name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
csrPEM, _ := req.Data["csr_pem"].(string)
|
||||||
|
if csrPEM == "" {
|
||||||
|
return nil, fmt.Errorf("ca: csr_pem is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode([]byte(csrPEM))
|
||||||
|
if block == nil || block.Type != "CERTIFICATE REQUEST" {
|
||||||
|
return nil, fmt.Errorf("ca: invalid CSR PEM")
|
||||||
|
}
|
||||||
|
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ca: parse CSR: %w", err)
|
||||||
|
}
|
||||||
|
if err := csr.CheckSignature(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ca: invalid CSR signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profileName, _ := req.Data["profile"].(string)
|
||||||
|
if profileName == "" {
|
||||||
|
profileName = "server"
|
||||||
|
}
|
||||||
|
profile, ok := GetProfile(profileName)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnknownProfile, profileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := req.Data["ttl"].(string); ok && v != "" {
|
||||||
|
profile.Expiry = v
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
if e.rootCert == nil {
|
||||||
|
return nil, ErrSealed
|
||||||
|
}
|
||||||
|
|
||||||
|
is, ok := e.issuers[issuerName]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrIssuerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
leafCert, err := profile.SignRequest(is.cert, csr, is.key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ca: sign CSR: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw})
|
||||||
|
|
||||||
|
var chainPEM []byte
|
||||||
|
chainPEM = append(chainPEM, leafCertPEM...)
|
||||||
|
chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: is.cert.Raw})...)
|
||||||
|
chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: e.rootCert.Raw})...)
|
||||||
|
|
||||||
|
serial := fmt.Sprintf("%x", leafCert.SerialNumber)
|
||||||
|
cn := csr.Subject.CommonName
|
||||||
|
allSANs := append(leafCert.DNSNames, ipStrings(leafCert.IPAddresses)...)
|
||||||
|
|
||||||
|
record := &CertRecord{
|
||||||
|
Serial: serial,
|
||||||
|
Issuer: issuerName,
|
||||||
|
CN: cn,
|
||||||
|
SANs: allSANs,
|
||||||
|
Profile: profileName,
|
||||||
|
CertPEM: string(leafCertPEM),
|
||||||
|
IssuedBy: req.CallerInfo.Username,
|
||||||
|
IssuedAt: time.Now(),
|
||||||
|
ExpiresAt: leafCert.NotAfter,
|
||||||
|
}
|
||||||
|
recordData, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ca: marshal cert record: %w", err)
|
||||||
|
}
|
||||||
|
if err := e.barrier.Put(ctx, e.mountPath+"certs/"+serial+".json", recordData); err != nil {
|
||||||
|
return nil, fmt.Errorf("ca: store cert record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &engine.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"serial": serial,
|
||||||
|
"cert_pem": string(leafCertPEM),
|
||||||
|
"chain_pem": string(chainPEM),
|
||||||
|
"cn": cn,
|
||||||
|
"sans": allSANs,
|
||||||
|
"issued_by": req.CallerInfo.Username,
|
||||||
|
"expires_at": leafCert.NotAfter.Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
func defaultCAConfig() *CAConfig {
|
func defaultCAConfig() *CAConfig {
|
||||||
|
|||||||
@@ -371,6 +371,55 @@ func (cs *caServer) RenewCert(ctx context.Context, req *pb.RenewCertRequest) (*p
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cs *caServer) SignCSR(ctx context.Context, req *pb.SignCSRRequest) (*pb.SignCSRResponse, error) {
|
||||||
|
if req.Mount == "" || req.Issuer == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and issuer are required")
|
||||||
|
}
|
||||||
|
if len(req.CsrPem) == 0 {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "csr_pem is required")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"issuer": req.Issuer,
|
||||||
|
"csr_pem": string(req.CsrPem),
|
||||||
|
}
|
||||||
|
if req.Profile != "" {
|
||||||
|
data["profile"] = req.Profile
|
||||||
|
}
|
||||||
|
if req.Ttl != "" {
|
||||||
|
data["ttl"] = req.Ttl
|
||||||
|
}
|
||||||
|
resp, err := cs.caHandleRequest(ctx, req.Mount, "sign-csr", &engine.Request{
|
||||||
|
Operation: "sign-csr",
|
||||||
|
CallerInfo: cs.callerInfo(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serial, _ := resp.Data["serial"].(string)
|
||||||
|
cn, _ := resp.Data["cn"].(string)
|
||||||
|
issuedBy, _ := resp.Data["issued_by"].(string)
|
||||||
|
certPEM, _ := resp.Data["cert_pem"].(string)
|
||||||
|
chainPEM, _ := resp.Data["chain_pem"].(string)
|
||||||
|
sans := toStringSliceFromInterface(resp.Data["sans"])
|
||||||
|
var expiresAt *timestamppb.Timestamp
|
||||||
|
if s, ok := resp.Data["expires_at"].(string); ok {
|
||||||
|
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||||
|
expiresAt = timestamppb.New(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cs.s.logger.Info("audit: CSR signed", "mount", req.Mount, "issuer", req.Issuer, "cn", cn, "serial", serial, "username", callerUsername(ctx))
|
||||||
|
return &pb.SignCSRResponse{
|
||||||
|
Serial: serial,
|
||||||
|
CommonName: cn,
|
||||||
|
Sans: sans,
|
||||||
|
IssuedBy: issuedBy,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
CertPem: []byte(certPEM),
|
||||||
|
ChainPem: []byte(chainPEM),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
|
|
||||||
func certRecordFromData(d map[string]interface{}) *pb.CertRecord {
|
func certRecordFromData(d map[string]interface{}) *pb.CertRecord {
|
||||||
|
|||||||
@@ -278,6 +278,87 @@ func (c *VaultClient) IssueCert(ctx context.Context, token string, req IssueCert
|
|||||||
return issued, nil
|
return issued, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignCSRRequest holds parameters for signing an external CSR.
|
||||||
|
type SignCSRRequest struct {
|
||||||
|
Mount string
|
||||||
|
Issuer string
|
||||||
|
CSRPEM string
|
||||||
|
Profile string
|
||||||
|
TTL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignedCert holds the result of signing a CSR.
|
||||||
|
type SignedCert struct {
|
||||||
|
Serial string
|
||||||
|
CertPEM string
|
||||||
|
ChainPEM string
|
||||||
|
ExpiresAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignCSR signs an externally generated CSR with the named issuer.
|
||||||
|
func (c *VaultClient) SignCSR(ctx context.Context, token string, req SignCSRRequest) (*SignedCert, error) {
|
||||||
|
resp, err := c.ca.SignCSR(withToken(ctx, token), &pb.SignCSRRequest{
|
||||||
|
Mount: req.Mount,
|
||||||
|
Issuer: req.Issuer,
|
||||||
|
CsrPem: []byte(req.CSRPEM),
|
||||||
|
Profile: req.Profile,
|
||||||
|
Ttl: req.TTL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sc := &SignedCert{
|
||||||
|
Serial: resp.Serial,
|
||||||
|
CertPEM: string(resp.CertPem),
|
||||||
|
ChainPEM: string(resp.ChainPem),
|
||||||
|
}
|
||||||
|
if resp.ExpiresAt != nil {
|
||||||
|
sc.ExpiresAt = resp.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||||
|
}
|
||||||
|
return sc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertDetail holds the full certificate record for the detail view.
|
||||||
|
type CertDetail struct {
|
||||||
|
Serial string
|
||||||
|
Issuer string
|
||||||
|
CommonName string
|
||||||
|
SANs []string
|
||||||
|
Profile string
|
||||||
|
IssuedBy string
|
||||||
|
IssuedAt string
|
||||||
|
ExpiresAt string
|
||||||
|
CertPEM string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCert retrieves a full certificate record by serial number.
|
||||||
|
func (c *VaultClient) GetCert(ctx context.Context, token, mount, serial string) (*CertDetail, error) {
|
||||||
|
resp, err := c.ca.GetCert(withToken(ctx, token), &pb.GetCertRequest{Mount: mount, Serial: serial})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rec := resp.GetCert()
|
||||||
|
if rec == nil {
|
||||||
|
return nil, fmt.Errorf("cert not found")
|
||||||
|
}
|
||||||
|
cd := &CertDetail{
|
||||||
|
Serial: rec.Serial,
|
||||||
|
Issuer: rec.Issuer,
|
||||||
|
CommonName: rec.CommonName,
|
||||||
|
SANs: rec.Sans,
|
||||||
|
Profile: rec.Profile,
|
||||||
|
IssuedBy: rec.IssuedBy,
|
||||||
|
CertPEM: string(rec.CertPem),
|
||||||
|
}
|
||||||
|
if rec.IssuedAt != nil {
|
||||||
|
cd.IssuedAt = rec.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||||
|
}
|
||||||
|
if rec.ExpiresAt != nil {
|
||||||
|
cd.ExpiresAt = rec.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||||
|
}
|
||||||
|
return cd, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CertSummary holds lightweight certificate metadata for list views.
|
// CertSummary holds lightweight certificate metadata for list views.
|
||||||
type CertSummary struct {
|
type CertSummary struct {
|
||||||
Serial string
|
Serial string
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package webserver
|
package webserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -39,6 +41,8 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
|
|||||||
r.Post("/create-issuer", ws.requireAuth(ws.handleCreateIssuer))
|
r.Post("/create-issuer", ws.requireAuth(ws.handleCreateIssuer))
|
||||||
r.Post("/issue", ws.requireAuth(ws.handleIssueCert))
|
r.Post("/issue", ws.requireAuth(ws.handleIssueCert))
|
||||||
r.Get("/issuer/{issuer}", ws.requireAuth(ws.handleIssuerDetail))
|
r.Get("/issuer/{issuer}", ws.requireAuth(ws.handleIssuerDetail))
|
||||||
|
r.Get("/cert/{serial}", ws.requireAuth(ws.handleCertDetail))
|
||||||
|
r.Get("/cert/{serial}/download", ws.requireAuth(ws.handleCertDownload))
|
||||||
r.Get("/{issuer}", ws.requireAuth(ws.handlePKIIssuer))
|
r.Get("/{issuer}", ws.requireAuth(ws.handlePKIIssuer))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -526,12 +530,140 @@ func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-render the PKI page with the issued certificate displayed.
|
// Stream a tgz archive containing the private key (PKCS8) and certificate.
|
||||||
|
filename := issuedCert.Serial + ".tgz"
|
||||||
|
w.Header().Set("Content-Type", "application/gzip")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||||
|
|
||||||
|
gw := gzip.NewWriter(w)
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
|
||||||
|
writeTarFile := func(name string, data []byte) error {
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: name,
|
||||||
|
Mode: 0600,
|
||||||
|
Size: int64(len(data)),
|
||||||
|
ModTime: time.Now(),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := tw.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeTarFile("key.pem", []byte(issuedCert.KeyPEM)); err != nil {
|
||||||
|
ws.logger.Error("write key to tgz", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := writeTarFile("cert.pem", []byte(issuedCert.CertPEM)); err != nil {
|
||||||
|
ws.logger.Error("write cert to tgz", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tw.Close()
|
||||||
|
_ = gw.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleCertDetail(w http.ResponseWriter, r *http.Request) {
|
||||||
|
info := tokenInfoFromContext(r.Context())
|
||||||
|
token := extractCookie(r)
|
||||||
|
|
||||||
|
mountName, err := ws.findCAMount(r, token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serial := chi.URLParam(r, "serial")
|
||||||
|
cert, err := ws.vault.GetCert(r.Context(), token, mountName, serial)
|
||||||
|
if err != nil {
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() == codes.NotFound {
|
||||||
|
http.Error(w, "certificate not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.renderTemplate(w, "cert_detail.html", map[string]interface{}{
|
||||||
|
"Username": info.Username,
|
||||||
|
"IsAdmin": info.IsAdmin,
|
||||||
|
"MountName": mountName,
|
||||||
|
"Cert": cert,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleCertDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := extractCookie(r)
|
||||||
|
|
||||||
|
mountName, err := ws.findCAMount(r, token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serial := chi.URLParam(r, "serial")
|
||||||
|
cert, err := ws.vault.GetCert(r.Context(), token, mountName, serial)
|
||||||
|
if err != nil {
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() == codes.NotFound {
|
||||||
|
http.Error(w, "certificate not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+serial+".pem\"")
|
||||||
|
_, _ = w.Write([]byte(cert.CertPEM))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleSignCSR(w http.ResponseWriter, r *http.Request) {
|
||||||
|
info := tokenInfoFromContext(r.Context())
|
||||||
|
token := extractCookie(r)
|
||||||
|
|
||||||
|
mountName, err := ws.findCAMount(r, token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||||
|
_ = r.ParseForm()
|
||||||
|
|
||||||
|
issuer := r.FormValue("issuer")
|
||||||
|
if issuer == "" {
|
||||||
|
ws.renderPKIWithError(w, r, mountName, info, "Issuer is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
csrPEM := r.FormValue("csr_pem")
|
||||||
|
if csrPEM == "" {
|
||||||
|
ws.renderPKIWithError(w, r, mountName, info, "CSR PEM is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := SignCSRRequest{
|
||||||
|
Mount: mountName,
|
||||||
|
Issuer: issuer,
|
||||||
|
CSRPEM: csrPEM,
|
||||||
|
Profile: r.FormValue("profile"),
|
||||||
|
TTL: r.FormValue("ttl"),
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := ws.vault.SignCSR(r.Context(), token, req)
|
||||||
|
if err != nil {
|
||||||
|
ws.renderPKIWithError(w, r, mountName, info, grpcMessage(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"Username": info.Username,
|
"Username": info.Username,
|
||||||
"IsAdmin": info.IsAdmin,
|
"IsAdmin": info.IsAdmin,
|
||||||
"MountName": mountName,
|
"MountName": mountName,
|
||||||
"IssuedCert": issuedCert,
|
"SignedCert": signed,
|
||||||
}
|
}
|
||||||
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
|
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
|
||||||
if cert, err := parsePEMCert(rootPEM); err == nil {
|
if cert, err := parsePEMCert(rootPEM); err == nil {
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ service CAService {
|
|||||||
// RenewCert renews an existing certificate, generating a new key and serial.
|
// RenewCert renews an existing certificate, generating a new key and serial.
|
||||||
// Auth required.
|
// Auth required.
|
||||||
rpc RenewCert(RenewCertRequest) returns (RenewCertResponse);
|
rpc RenewCert(RenewCertRequest) returns (RenewCertResponse);
|
||||||
|
|
||||||
|
// SignCSR signs an externally generated CSR with a named issuer. All Subject
|
||||||
|
// and SAN fields from the CSR are preserved exactly; profile defaults supply
|
||||||
|
// key usages and validity if not overridden. Auth required.
|
||||||
|
rpc SignCSR(SignCSRRequest) returns (SignCSRResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ImportRoot ---
|
// --- ImportRoot ---
|
||||||
@@ -217,6 +222,34 @@ message RenewCertResponse {
|
|||||||
bytes chain_pem = 8;
|
bytes chain_pem = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SignCSR ---
|
||||||
|
|
||||||
|
message SignCSRRequest {
|
||||||
|
string mount = 1;
|
||||||
|
// issuer is the name of the issuer to sign with.
|
||||||
|
string issuer = 2;
|
||||||
|
// csr_pem is the PEM-encoded PKCS#10 certificate signing request.
|
||||||
|
bytes csr_pem = 3;
|
||||||
|
// profile selects key usage defaults (e.g. "server", "client", "peer").
|
||||||
|
// Defaults to "server" if empty.
|
||||||
|
string profile = 4;
|
||||||
|
// ttl overrides the profile's default validity period (e.g. "8760h").
|
||||||
|
string ttl = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignCSRResponse {
|
||||||
|
string serial = 1;
|
||||||
|
string common_name = 2;
|
||||||
|
repeated string sans = 3;
|
||||||
|
string issued_by = 4;
|
||||||
|
google.protobuf.Timestamp expires_at = 5;
|
||||||
|
// cert_pem is the signed leaf certificate. No private key is returned
|
||||||
|
// because the caller already holds it.
|
||||||
|
bytes cert_pem = 6;
|
||||||
|
// chain_pem contains the full chain: leaf + issuer + root, PEM-concatenated.
|
||||||
|
bytes chain_pem = 7;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Shared message types ---
|
// --- Shared message types ---
|
||||||
|
|
||||||
// CertRecord is the full certificate record including the PEM-encoded cert.
|
// CertRecord is the full certificate record including the PEM-encoded cert.
|
||||||
|
|||||||
36
web/templates/cert_detail.html
Normal file
36
web/templates/cert_detail.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{{define "title"}} - Certificate: {{.Cert.Serial}}{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Certificate: {{.Cert.CommonName}}</h2>
|
||||||
|
<div class="page-meta">
|
||||||
|
<a href="/pki/issuer/{{.Cert.Issuer}}">← Issuer: {{.Cert.Issuer}}</a>
|
||||||
|
 · 
|
||||||
|
<a href="/pki/cert/{{.Cert.Serial}}/download">Download Certificate (PEM)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Certificate Details</div>
|
||||||
|
<table class="kv-table">
|
||||||
|
<tbody>
|
||||||
|
<tr><th>Serial</th><td><code>{{.Cert.Serial}}</code></td></tr>
|
||||||
|
<tr><th>Common Name</th><td>{{.Cert.CommonName}}</td></tr>
|
||||||
|
<tr><th>Issuer</th><td>{{.Cert.Issuer}}</td></tr>
|
||||||
|
<tr><th>Profile</th><td>{{.Cert.Profile}}</td></tr>
|
||||||
|
{{if .Cert.SANs}}
|
||||||
|
<tr><th>SANs</th><td>{{range $i, $san := .Cert.SANs}}{{if $i}}, {{end}}{{$san}}{{end}}</td></tr>
|
||||||
|
{{end}}
|
||||||
|
<tr><th>Issued By</th><td>{{.Cert.IssuedBy}}</td></tr>
|
||||||
|
<tr><th>Issued At</th><td>{{.Cert.IssuedAt}}</td></tr>
|
||||||
|
<tr><th>Expires At</th><td>{{.Cert.ExpiresAt}}</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Certificate PEM</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea rows="12" class="pem-input" readonly>{{.Cert.CertPEM}}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .Certs}}
|
{{range .Certs}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.CommonName}}</td>
|
<td><a href="/pki/cert/{{.Serial}}">{{.CommonName}}</a></td>
|
||||||
<td>{{.Profile}}</td>
|
<td>{{.Profile}}</td>
|
||||||
<td><code>{{.Serial}}</code></td>
|
<td><code>{{.Serial}}</code></td>
|
||||||
<td>{{.IssuedBy}}</td>
|
<td>{{.IssuedBy}}</td>
|
||||||
|
|||||||
@@ -98,31 +98,6 @@
|
|||||||
{{if and .HasRoot .Issuers}}
|
{{if and .HasRoot .Issuers}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Issue Certificate</div>
|
<div class="card-title">Issue Certificate</div>
|
||||||
{{if .IssuedCert}}
|
|
||||||
<div class="success">
|
|
||||||
<p><strong>Certificate issued successfully.</strong></p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Serial</label>
|
|
||||||
<input type="text" class="pem-input" value="{{index .IssuedCert "serial"}}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Expires</label>
|
|
||||||
<input type="text" value="{{index .IssuedCert "expires_at"}}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Certificate PEM</label>
|
|
||||||
<textarea rows="8" class="pem-input" readonly>{{index .IssuedCert "cert_pem"}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Private Key PEM</label>
|
|
||||||
<textarea rows="8" class="pem-input" readonly>{{index .IssuedCert "key_pem"}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Chain PEM</label>
|
|
||||||
<textarea rows="8" class="pem-input" readonly>{{index .IssuedCert "chain_pem"}}</textarea>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<form method="post" action="/pki/issue">
|
<form method="post" action="/pki/issue">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -196,6 +171,53 @@
|
|||||||
<button type="submit">Issue Certificate</button>
|
<button type="submit">Issue Certificate</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if and .HasRoot .Issuers}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Sign CSR</div>
|
||||||
|
{{if .SignedCert}}
|
||||||
|
<div class="success">
|
||||||
|
<p>CSR signed successfully. Serial: <code>{{.SignedCert.Serial}}</code> — Expires: {{.SignedCert.ExpiresAt}}</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Certificate PEM</label>
|
||||||
|
<textarea rows="8" class="pem-input" readonly>{{.SignedCert.CertPEM}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Chain PEM</label>
|
||||||
|
<textarea rows="8" class="pem-input" readonly>{{.SignedCert.ChainPEM}}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<form method="post" action="/pki/sign-csr">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sign_issuer">Issuer</label>
|
||||||
|
<select id="sign_issuer" name="issuer" required>
|
||||||
|
<option value="">— select issuer —</option>
|
||||||
|
{{range .Issuers}}<option value="{{.}}">{{.}}</option>{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sign_profile">Profile (key usage defaults)</label>
|
||||||
|
<select id="sign_profile" name="profile">
|
||||||
|
<option value="server">server (default)</option>
|
||||||
|
<option value="client">client</option>
|
||||||
|
<option value="peer">peer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sign_ttl">TTL (optional)</label>
|
||||||
|
<input type="text" id="sign_ttl" name="ttl" placeholder="2160h">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sign_csr_pem">CSR (PEM)</label>
|
||||||
|
<textarea id="sign_csr_pem" name="csr_pem" rows="8" class="pem-input" placeholder="-----BEGIN CERTIFICATE REQUEST-----" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Sign CSR</button>
|
||||||
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user