368 lines
9.9 KiB
Go
368 lines
9.9 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/stef/pkiapi/internal/pki"
|
|
"github.com/stef/pkiapi/internal/storage"
|
|
)
|
|
|
|
// CreateCertificateRequest représente la requête de création de certificat
|
|
type CreateCertificateRequest struct {
|
|
Subject string `json:"subject" binding:"required"`
|
|
ValidityDays int `json:"validity_days" binding:"required,min=1,max=3650"`
|
|
}
|
|
|
|
// CertificateResponse représente la réponse avec les données du certificat
|
|
type CertificateResponse struct {
|
|
ID string `json:"id"`
|
|
Subject string `json:"subject"`
|
|
Issuer string `json:"issuer"`
|
|
NotBefore string `json:"not_before"`
|
|
NotAfter string `json:"not_after"`
|
|
SerialNumber string `json:"serial_number"`
|
|
Certificate string `json:"certificate"` // Base64 encoded
|
|
PrivateKey string `json:"private_key,omitempty"` // Base64 encoded (optional)
|
|
Revoked bool `json:"revoked"`
|
|
}
|
|
|
|
// encodePrivateKey encode une clé privée en format base64 PKCS#8 PEM
|
|
func encodePrivateKey(privateKey interface{}) (string, error) {
|
|
if privateKey == nil {
|
|
return "", nil
|
|
}
|
|
|
|
privKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(privKeyDER), nil
|
|
}
|
|
|
|
// certificateStore est un store global pour les certificats
|
|
var certificateStore storage.CertificateStore
|
|
|
|
// InitCertificateStore initialise le store pour les certificats
|
|
func InitCertificateStore(store storage.CertificateStore) {
|
|
certificateStore = store
|
|
}
|
|
|
|
// CreateCertificate crée un nouveau certificat auto-signé
|
|
func CreateCertificate(c *gin.Context) {
|
|
var req CreateCertificateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Générer le certificat
|
|
cert, err := pki.GenerateCertificate(req.Subject, req.ValidityDays)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur génération certificat"})
|
|
return
|
|
}
|
|
|
|
// Générer un ID unique
|
|
certID := uuid.New().String()
|
|
cert.ID = certID
|
|
|
|
// Sauvegarder le certificat
|
|
if err := certificateStore.SaveCertificate(certID, cert); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur sauvegarde certificat"})
|
|
return
|
|
}
|
|
|
|
// Récupérer l'utilisateur depuis le contexte JWT
|
|
userID, _ := c.Get("user_id")
|
|
|
|
// Retourner la réponse
|
|
response := CertificateResponse{
|
|
ID: certID,
|
|
Subject: cert.Subject,
|
|
Issuer: cert.Issuer,
|
|
NotBefore: cert.NotBefore.Format("2006-01-02T15:04:05Z"),
|
|
NotAfter: cert.NotAfter.Format("2006-01-02T15:04:05Z"),
|
|
Revoked: false,
|
|
}
|
|
|
|
if cert.Cert != nil {
|
|
response.SerialNumber = cert.Cert.SerialNumber.String()
|
|
response.Certificate = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
|
}
|
|
|
|
// Ajouter la clé privée
|
|
if cert.PrivateKey != nil {
|
|
privKeyBase64, err := encodePrivateKey(cert.PrivateKey)
|
|
if err == nil {
|
|
response.PrivateKey = privKeyBase64
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"certificate": response,
|
|
"created_by": userID,
|
|
})
|
|
}
|
|
|
|
// ListCertificates retourne tous les certificats
|
|
func ListCertificates(c *gin.Context) {
|
|
certs := certificateStore.ListCertificates()
|
|
|
|
var responses []CertificateResponse
|
|
for _, cert := range certs {
|
|
response := CertificateResponse{
|
|
ID: cert.ID,
|
|
Subject: cert.Subject,
|
|
Issuer: cert.Issuer,
|
|
NotBefore: cert.NotBefore.Format("2006-01-02T15:04:05Z"),
|
|
NotAfter: cert.NotAfter.Format("2006-01-02T15:04:05Z"),
|
|
Revoked: cert.Revoked,
|
|
}
|
|
if cert.Cert != nil {
|
|
response.SerialNumber = cert.Cert.SerialNumber.String()
|
|
}
|
|
responses = append(responses, response)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"certificates": responses})
|
|
}
|
|
|
|
// GetCertificate retourne un certificat par ID
|
|
func GetCertificate(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
cert, err := certificateStore.GetCertificate(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificat non trouvé"})
|
|
return
|
|
}
|
|
|
|
response := CertificateResponse{
|
|
ID: cert.ID,
|
|
Subject: cert.Subject,
|
|
Issuer: cert.Issuer,
|
|
NotBefore: cert.NotBefore.Format("2006-01-02T15:04:05Z"),
|
|
NotAfter: cert.NotAfter.Format("2006-01-02T15:04:05Z"),
|
|
Revoked: cert.Revoked,
|
|
}
|
|
|
|
if cert.Cert != nil {
|
|
response.SerialNumber = cert.Cert.SerialNumber.String()
|
|
response.Certificate = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
|
}
|
|
|
|
// Ajouter la clé privée si disponible
|
|
if cert.PrivateKey != nil {
|
|
privKeyBase64, err := encodePrivateKey(cert.PrivateKey)
|
|
if err == nil {
|
|
response.PrivateKey = privKeyBase64
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"certificate": response})
|
|
}
|
|
|
|
// RevokeCertificate révoque un certificat
|
|
func RevokeCertificate(c *gin.Context) {
|
|
var req struct {
|
|
CertificateID string `json:"certificate_id" binding:"required"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
cert, err := certificateStore.GetCertificate(req.CertificateID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificat non trouvé"})
|
|
return
|
|
}
|
|
|
|
cert.Revoked = true
|
|
certificateStore.SaveCertificate(req.CertificateID, cert)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "certificat révoqué",
|
|
"id": req.CertificateID,
|
|
"reason": req.Reason,
|
|
})
|
|
}
|
|
|
|
// GetCRL retourne la liste de révocation
|
|
func GetCRL(c *gin.Context) {
|
|
certs := certificateStore.ListCertificates()
|
|
|
|
var revokedCerts []gin.H
|
|
for _, cert := range certs {
|
|
if cert.Revoked && cert.Cert != nil {
|
|
revokedCerts = append(revokedCerts, gin.H{
|
|
"serial_number": cert.Cert.SerialNumber.String(),
|
|
"subject": cert.Subject,
|
|
})
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"crl": revokedCerts,
|
|
"version": 1,
|
|
})
|
|
}
|
|
|
|
// ExportCertificatePEM retourne un certificat au format PEM
|
|
func ExportCertificatePEM(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
cert, err := certificateStore.GetCertificate(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificat non trouvé"})
|
|
return
|
|
}
|
|
|
|
if cert.Cert == nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "certificat non disponible"})
|
|
return
|
|
}
|
|
|
|
// Convertir le certificat en format PEM
|
|
pemCert := &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Cert.Raw,
|
|
}
|
|
pemData := pem.EncodeToMemory(pemCert)
|
|
|
|
// Retourner en tant que fichier téléchargeable
|
|
filename := "certificate_" + id + ".pem"
|
|
c.Header("Content-Type", "application/x-pem-file")
|
|
c.Header("Content-Disposition", "attachment; filename="+filename)
|
|
c.String(http.StatusOK, string(pemData))
|
|
}
|
|
|
|
// ExportCertificateDER retourne un certificat au format DER
|
|
func ExportCertificateDER(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
cert, err := certificateStore.GetCertificate(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificat non trouvé"})
|
|
return
|
|
}
|
|
|
|
if cert.Cert == nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "certificat non disponible"})
|
|
return
|
|
}
|
|
|
|
// Retourner en tant que fichier binaire DER
|
|
filename := "certificate_" + id + ".der"
|
|
c.Header("Content-Type", "application/pkix-cert")
|
|
c.Header("Content-Disposition", "attachment; filename="+filename)
|
|
c.Data(http.StatusOK, "application/pkix-cert", cert.Cert.Raw)
|
|
}
|
|
|
|
// ExportCertificateWithPrivateKeyPEM retourne un certificat avec sa clé privée au format PEM
|
|
func ExportCertificateWithPrivateKeyPEM(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
cert, err := certificateStore.GetCertificate(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificat non trouvé"})
|
|
return
|
|
}
|
|
|
|
if cert.Cert == nil || cert.PrivateKey == nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "certificat ou clé privée non disponible"})
|
|
return
|
|
}
|
|
|
|
// Convertir le certificat en PEM
|
|
pemCert := &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Cert.Raw,
|
|
}
|
|
certPEM := pem.EncodeToMemory(pemCert)
|
|
|
|
// Convertir la clé privée en PKCS#8 DER
|
|
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur sérialisation clé privée"})
|
|
return
|
|
}
|
|
|
|
// Convertir en PEM
|
|
pemKey := &pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: privateKeyDER,
|
|
}
|
|
keyPEM := pem.EncodeToMemory(pemKey)
|
|
|
|
// Combiner certificat + clé privée
|
|
combined := string(certPEM) + string(keyPEM)
|
|
|
|
// Retourner en tant que fichier téléchargeable
|
|
filename := "certificate_" + id + "_with_key.pem"
|
|
c.Header("Content-Type", "application/x-pem-file")
|
|
c.Header("Content-Disposition", "attachment; filename="+filename)
|
|
c.String(http.StatusOK, combined)
|
|
}
|
|
|
|
// ExportCertificateChain retourne une chaîne de certificats (certificat + CA parent)
|
|
func ExportCertificateChain(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
cert, err := certificateStore.GetCertificate(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificat non trouvé"})
|
|
return
|
|
}
|
|
|
|
if cert.Cert == nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "certificat non disponible"})
|
|
return
|
|
}
|
|
|
|
// Commencer par le certificat lui-même
|
|
var chain []*pem.Block
|
|
|
|
pemCert := &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Cert.Raw,
|
|
}
|
|
chain = append(chain, pemCert)
|
|
|
|
// Essayer de trouver le certificat parent (issuer)
|
|
// En cherchant par l'issuer DN
|
|
if cert.Issuer != cert.Subject {
|
|
allCerts := certificateStore.ListCertificates()
|
|
for _, parentCert := range allCerts {
|
|
if parentCert.Cert != nil && parentCert.Subject == cert.Issuer {
|
|
parentPEM := &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: parentCert.Cert.Raw,
|
|
}
|
|
chain = append(chain, parentPEM)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Encoder tous les certificats en PEM
|
|
var chainPEM string
|
|
for _, block := range chain {
|
|
chainPEM += string(pem.EncodeToMemory(block))
|
|
}
|
|
|
|
// Retourner en tant que fichier téléchargeable
|
|
filename := "certificate_chain_" + id + ".pem"
|
|
c.Header("Content-Type", "application/x-pem-file")
|
|
c.Header("Content-Disposition", "attachment; filename="+filename)
|
|
c.String(http.StatusOK, chainPEM)
|
|
}
|