added createcert and deletecert

This commit is contained in:
wt
2025-07-15 00:42:59 +07:00
parent df4cf01c87
commit 9f0a6621af
10 changed files with 415 additions and 70 deletions
+1
View File
@@ -1 +1,2 @@
scm scm
go.sum
+181
View File
@@ -0,0 +1,181 @@
package api
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net/http"
"os"
"scm/model"
"time"
"github.com/youmark/pkcs8"
)
func CreateCert(w http.ResponseWriter, r *http.Request) {
var requestData model.CreateCertificateRequest
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
http.Error(w, "invalid JSON format", http.StatusBadRequest)
return
}
caCert, caKey, err := loadCertAndKey(
fmt.Sprintf(
"%s/%s/%s.crt",
os.Getenv("CERTS_PATH"),
"root",
"TolaMironcenkoCA",
),
fmt.Sprintf(
"%s/%s/%s.key",
os.Getenv("CERTS_PATH"),
"root",
"TolaMironcenkoCA",
),
requestData.CAKeyPassword,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println("error loading certificate and key:", err)
return
}
// TODO: Implement create certificate endpoint
serverKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println("error generating server key:", err)
return
}
serverCert, err := createServerCert(caCert, caKey, serverKey, requestData)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println("error creating server certificate:", err)
return
}
_, err = os.Stat(fmt.Sprintf("%s/%s", os.Getenv("CERTS_PATH"), requestData.DNSNames[0]))
if err == nil {
http.Error(w, "certificate already exists", http.StatusConflict)
fmt.Println("certificate already exists")
return
} else if os.IsNotExist(err) {
err := os.Mkdir(fmt.Sprintf("%s/%s", os.Getenv("CERTS_PATH"), requestData.DNSNames[0]), 0755)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println("error creating directory:", err)
return
}
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println("error checking certificate directory:", err)
return
}
err = savePEM(fmt.Sprintf("%s/%s/%s.crt", os.Getenv("CERTS_PATH"), requestData.DNSNames[0], requestData.DNSNames[0]), "CERTIFICATE", serverCert)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println("error saving certificate:", err)
return
}
err = savePEM(fmt.Sprintf("%s/%s/%s.key", os.Getenv("CERTS_PATH"), requestData.DNSNames[0], requestData.DNSNames[0]), "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverKey))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println("error saving private key:", err)
return
}
fmt.Fprintf(w, "%s", `{"status": "success", "message": "Certificate created successfully"}`)
}
func createServerCert(
caCert *x509.Certificate,
caKey *rsa.PrivateKey,
serverKey *rsa.PrivateKey,
certdata model.CreateCertificateRequest,
) ([]byte, error) {
// Шаблон серверного сертификата
template := x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkix.Name{
Organization: certdata.OrganizationName,
CommonName: certdata.CommonName,
OrganizationalUnit: certdata.OrganizationUnit,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0), // 1 год
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: certdata.DNSNames,
}
// Создаем сертификат, подписанный CA
return x509.CreateCertificate(
rand.Reader,
&template,
caCert,
&serverKey.PublicKey,
caKey,
)
}
func loadCertAndKey(certFile, keyFile, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
fmt.Println(certFile, keyFile)
// Загрузка корневого сертификата
caCertPEM, err := os.ReadFile(certFile)
if err != nil {
return nil, nil, err
}
block, _ := pem.Decode(caCertPEM)
if block == nil {
return nil, nil, errors.New("failed to parse CA certificate PEM")
}
caCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
// Загрузка зашифрованного приватного ключа
keyPEM, err := os.ReadFile(keyFile)
if err != nil {
return nil, nil, err
}
block, _ = pem.Decode(keyPEM)
if block == nil {
return nil, nil, errors.New("failed to parse CA key PEM")
}
fmt.Print(block.Type)
// Декодирование ключа с паролем
decryptedKey, err := pkcs8.ParsePKCS8PrivateKey(block.Bytes, []byte(password))
if err != nil {
return nil, nil, fmt.Errorf("failed to decrypt CA key: %v", err)
}
caKey, ok := decryptedKey.(*rsa.PrivateKey)
if !ok {
return nil, nil, err
}
return caCert, caKey, nil
}
func savePEM(filename, blockType string, data []byte) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
err = pem.Encode(file, &pem.Block{
Type: blockType,
Bytes: data,
})
if err != nil {
return err
}
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"os"
"scm/model"
)
func DeleteCert(w http.ResponseWriter, r *http.Request) {
var requestData model.DeleteCertRequest
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, err := os.Stat(fmt.Sprintf("%s/%s", os.Getenv("CERTS_PATH"), requestData.Domain))
if err == nil {
err = os.RemoveAll(fmt.Sprintf("%s/%s", os.Getenv("CERTS_PATH"), requestData.Domain))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "%s", `{"status": "success"}`)
} else if !os.IsNotExist(err) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else {
http.Error(w, "Certificate not found", http.StatusNotFound)
return
}
// TODO: Implement delete certificate functionality
}
+53 -4
View File
@@ -7,6 +7,7 @@
<script src="js/bootstrap.min.js" defer></script> <script src="js/bootstrap.min.js" defer></script>
<script src="js/theme.js" defer></script> <script src="js/theme.js" defer></script>
<script src="js/certs.js" defer></script> <script src="js/certs.js" defer></script>
<script src="js/create_cert.js" defer></script>
<title>Scm</title> <title>Scm</title>
</head> </head>
<style> <style>
@@ -59,6 +60,15 @@
+ +
</button> </button>
</li> </li>
<li class="nav-item">
<button
type="button"
class="btn btn-primary"
id="downloadrootcert"
>
Download Root Certificate
</button>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -89,6 +99,37 @@
/> />
<label for="domain">Domain</label> <label for="domain">Domain</label>
</div> </div>
<div class="form-floating mb-3">
<input
type="text"
id="commonname"
class="form-control"
placeholder="commonname"
/>
<label for="commonname">Common Name</label>
</div>
<div class="form-floating mb-3">
<input
type="text"
id="organizationname"
class="form-control"
placeholder="organizationname"
/>
<label for="organizationname"
>Organization Name</label
>
</div>
<div class="form-floating mb-3">
<input
type="text"
id="organizationunit"
class="form-control"
placeholder="organizationunit"
/>
<label for="organizationunit"
>Organization Unit</label
>
</div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input <input
type="password" type="password"
@@ -107,8 +148,12 @@
> >
Close Close
</button> </button>
<button type="button" class="btn btn-primary"> <button
Save changes id="createcert"
type="button"
class="btn btn-primary"
>
Create
</button> </button>
</div> </div>
</div> </div>
@@ -127,10 +172,14 @@
></button> ></button>
</div> </div>
<div <div
class="modal-body d-flex gap-3 align-items-center justify-content-center" class="modal-body d-flex gap-3 align-items-start justify-content-between"
id="preview_ssl_content" id="preview_ssl_content"
> >
<div id="certinfo"> <div
id="certinfo"
class="border-right p-3"
style="border-right: 2px solid"
>
<h1 id="cert-title">Information</h1> <h1 id="cert-title">Information</h1>
<h2>Issuer</h2> <h2>Issuer</h2>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
+84 -66
View File
@@ -4,9 +4,9 @@ const preview = document.querySelector("#preview");
var certificates = []; var certificates = [];
const renderCards = () => { const renderCards = () => {
cards.innerHTML = ""; cards.innerHTML = "";
certificates.map((cert) => { certificates.map((cert) => {
cards.innerHTML += `<div class="card" style="width: 16rem"> cards.innerHTML += `<div class="card" style="width: 16rem">
<div class="card-body"> <div class="card-body">
<h5 class="card-title" style="text-align: center"> <h5 class="card-title" style="text-align: center">
<a href="${cert.href}">${cert.domain}</a> <a href="${cert.href}">${cert.domain}</a>
@@ -41,77 +41,95 @@ const renderCards = () => {
</button> </button>
</div> </div>
</div>`; </div>`;
}); });
}; };
const getCerts = async () => { const getCerts = () => {
fetch(window.location.origin + "/api/certs") fetch(window.location.origin + "/api/certs")
.then((data) => data.json()) .then((data) => data.json())
.then((data) => { .then((data) => {
certificates = data; certificates = data;
renderCards(); renderCards();
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching certificates:", error); console.error("Error fetching certificates:", error);
alert("Failed to fetch certificates"); alert("Failed to fetch certificates");
}); });
}; };
getCerts(); getCerts();
if (preview) { if (preview) {
preview.addEventListener("show.bs.modal", (e) => { preview.addEventListener("show.bs.modal", (e) => {
const button = e.relatedTarget; const button = e.relatedTarget;
const recipient = button.getAttribute("data-bs-whatever"); const recipient = button.getAttribute("data-bs-whatever");
// const modalBodyTextarea = preview.querySelector(".modal-body textarea"); // const modalBodyTextarea = preview.querySelector(".modal-body textarea");
const dnsnames = button.getAttribute("data-bs-dnsnames"); const dnsnames = button.getAttribute("data-bs-dnsnames");
const notafter = button.getAttribute("data-bs-notafter"); const notafter = button.getAttribute("data-bs-notafter");
const notbefore = button.getAttribute("data-bs-notbefore"); const notbefore = button.getAttribute("data-bs-notbefore");
const issuerorganizationunit = button.getAttribute( const issuerorganizationunit = button.getAttribute(
"data-bs-issuer-organizationunit", "data-bs-issuer-organizationunit",
); );
const issuerorganization = button.getAttribute( const issuerorganization = button.getAttribute(
"data-bs-issuer-organization", "data-bs-issuer-organization",
); );
const issuercommonname = button.getAttribute("data-bs-issuer-commonname"); const issuercommonname = button.getAttribute(
const subjectorganizationunit = button.getAttribute( "data-bs-issuer-commonname",
"data-bs-subject-organizationunit", );
); const subjectorganizationunit = button.getAttribute(
const subjectorganization = button.getAttribute( "data-bs-subject-organizationunit",
"data-bs-subject-organization", );
); const subjectorganization = button.getAttribute(
const subjectcommonname = button.getAttribute("data-bs-subject-commonname"); "data-bs-subject-organization",
console.log( );
dnsnames, const subjectcommonname = button.getAttribute(
notafter, "data-bs-subject-commonname",
notbefore, );
issuerorganizationunit, preview.querySelector("#raw").innerHTML = recipient.replace(
issuerorganization, /\n/g,
issuercommonname, "<br>",
); );
preview.querySelector("#raw").innerHTML = recipient.replace(/\n/g, "<br>"); preview.querySelector("#dnsNames").value = dnsnames;
preview.querySelector("#dnsNames").value = dnsnames; preview.querySelector("#notAfter").value = notafter;
preview.querySelector("#notAfter").value = notafter; preview.querySelector("#notBefore").value = notbefore;
preview.querySelector("#notBefore").value = notbefore; preview.querySelector("#issuerorganizationunit").value =
preview.querySelector("#issuerorganizationunit").value = issuerorganizationunit;
issuerorganizationunit; preview.querySelector("#issuerorganization").value = issuerorganization;
preview.querySelector("#issuerorganization").value = issuerorganization; preview.querySelector("#issuercommonname").value = issuercommonname;
preview.querySelector("#issuercommonname").value = issuercommonname; preview.querySelector("#subjectorganizationunit").value =
preview.querySelector("#subjectorganizationunit").value = subjectorganizationunit;
subjectorganizationunit; preview.querySelector("#subjectorganization").value =
preview.querySelector("#subjectorganization").value = subjectorganization; subjectorganization;
preview.querySelector("#subjectcommonname").value = subjectcommonname; preview.querySelector("#subjectcommonname").value = subjectcommonname;
}); });
} }
cards.addEventListener("click", (e) => { cards.addEventListener("click", (e) => {
if (e.target.classList.contains("delcert")) { if (e.target.classList.contains("delcert")) {
certificates.splice( if (confirm("Are you sure you want to delete this certificate?")) {
certificates.indexOf( fetch(window.location.origin + `/api/certs/delete`, {
certificates.find((item) => item.id === parseInt(e.target.id)), method: "POST",
), headers: {
1, "Content-Type": "application/json",
); },
renderCards(); body: JSON.stringify({
} domain: certificates.find((item) => item.id === e.target.id)
.domain,
}),
})
.then((data) => data.json())
.then((jsondata) => {
if (jsondata.status === "success") {
getCerts();
alert("Certificate deleted successfully");
} else {
alert("Failed to delete certificate");
}
})
.catch((error) => {
console.error(error);
alert("An error occurred while deleting the certificate");
});
}
}
}); });
+44
View File
@@ -0,0 +1,44 @@
const domain = document.querySelector("#domain");
const commonname = document.querySelector("#commonname");
const organizationname = document.querySelector("#organizationname");
const password = document.querySelector("#password");
const organizationunit = document.querySelector("#organizationunit");
const createcert = document.querySelector("#createcert");
createcert.addEventListener("click", () => {
const domainValue = domain.value;
const commonnameValue = commonname.value;
const organizationnameValue = organizationname.value;
const passwordValue = password.value;
const organizationunitValue = organizationunit.value;
fetch(window.location.origin + "/api/certs/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
dns: [domainValue],
commonname: commonnameValue,
organizationname: [organizationnameValue],
organizationunit: [organizationunitValue],
password: passwordValue,
}),
})
.then((data) => data.json())
.then((jsondata) => {
if (jsondata.status === "success") {
getCerts();
var createmodal = bootstrap.Modal.getOrCreateInstance(
document.getElementById("create_certificate"),
);
createmodal.hide();
alert("Certificate created successfully");
} else {
alert("Failed to create certificate:", jsondata.error);
}
})
.catch((error) => {
alert("An error occurred:", error);
});
});
+3
View File
@@ -5,4 +5,7 @@ go 1.24.5
require ( require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
) )
require golang.org/x/crypto v0.22.0 // indirect
+2
View File
@@ -17,6 +17,8 @@ func main() {
http.Handle("/", api.SpaHandler(http.FS(api.Frontend))) http.Handle("/", api.SpaHandler(http.FS(api.Frontend)))
http.HandleFunc("/api/certs", api.Certs) http.HandleFunc("/api/certs", api.Certs)
http.HandleFunc("/api/certs/create", api.CreateCert)
http.HandleFunc("/api/certs/delete", api.DeleteCert)
if os.Getenv("ENABLE_TLS") == "true" { if os.Getenv("ENABLE_TLS") == "true" {
e := http.ListenAndServeTLS( e := http.ListenAndServeTLS(
+9
View File
@@ -0,0 +1,9 @@
package model
type CreateCertificateRequest struct {
CommonName string `json:"commonname"`
OrganizationName []string `json:"organizationname"`
OrganizationUnit []string `json:"organizationunit"`
DNSNames []string `json:"dns"`
CAKeyPassword string `json:"password"`
}
+5
View File
@@ -0,0 +1,5 @@
package model
type DeleteCertRequest struct {
Domain string `json:"domain"`
}