Ajout export P12

main
stef 2025-12-16 00:47:22 +01:00
parent 97ec520314
commit 04f08db87d
10 changed files with 226 additions and 86 deletions

View File

@ -7,6 +7,8 @@ services:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
MONGO_INITDB_DATABASE: pki_db
ports:
- "27018:27017"
volumes:
- mongodb_data:/data/db
- ./scripts/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro

1
go.mod
View File

@ -42,4 +42,5 @@ require (
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
software.sslmate.com/src/go-pkcs12 v0.6.0 // indirect
)

2
go.sum
View File

@ -137,3 +137,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

View File

@ -1,13 +1,16 @@
package api
import (
"bytes"
"crypto/x509"
"fmt"
"net/http"
"pki-manager/internal/models"
"pki-manager/internal/repository"
"pki-manager/internal/services"
"github.com/gin-gonic/gin"
"software.sslmate.com/src/go-pkcs12"
)
type Handlers struct {
@ -334,6 +337,19 @@ func (h *Handlers) DownloadCACertificate(c *gin.Context) {
c.String(http.StatusOK, ca.Certificate)
}
func (h *Handlers) DownloadCAPrivateKey(c *gin.Context) {
id := c.Param("id")
cert, err := h.repo.GetCA(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Certificate not found"})
return
}
c.Header("Content-Type", "application/x-pem-file")
c.Header("Content-Disposition", "attachment; filename="+cert.CommonName+".key")
c.String(http.StatusOK, cert.PrivateKey)
}
func (h *Handlers) DownloadSubCACertificate(c *gin.Context) {
id := c.Param("id")
subca, err := h.repo.GetSubCA(c.Request.Context(), id)
@ -347,6 +363,19 @@ func (h *Handlers) DownloadSubCACertificate(c *gin.Context) {
c.String(http.StatusOK, subca.Certificate)
}
func (h *Handlers) DownloadSubCAPrivateKey(c *gin.Context) {
id := c.Param("id")
cert, err := h.repo.GetSubCA(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Certificate not found"})
return
}
c.Header("Content-Type", "application/x-pem-file")
c.Header("Content-Disposition", "attachment; filename="+cert.CommonName+".key")
c.String(http.StatusOK, cert.PrivateKey)
}
// Download handlers
func (h *Handlers) DownloadCertificate(c *gin.Context) {
id := c.Param("id")
@ -374,6 +403,70 @@ func (h *Handlers) DownloadPrivateKey(c *gin.Context) {
c.String(http.StatusOK, cert.PrivateKey)
}
func (h *Handlers) DownloadP12(c *gin.Context) {
id := c.Param("id")
var p12Data []byte
cert, err := h.repo.GetCertificate(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Certificate not found"})
return
}
ca, err := h.repo.GetCA(c.Request.Context(), cert.IssuerCAID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Certificate CA not found"})
return
}
// Choisir la méthode d'encodage selon la configuration
x509cert, err := h.cryptoService.ParseCertificate([]byte(cert.Certificate))
if err != nil {
message := fmt.Sprintf("Certificateformat PEM invalide %s", err)
c.JSON(http.StatusNotFound, gin.H{"error": message})
return
}
x509key, err := h.cryptoService.ParsePrivateKey([]byte(cert.PrivateKey))
if err != nil {
message := fmt.Sprintf("Keyformat PEM invalide %s", err)
c.JSON(http.StatusNotFound, gin.H{"error": message})
return
}
x509cacert, err := h.cryptoService.ParseCertificate([]byte(ca.Certificate))
if err != nil {
message := fmt.Sprintf("CA Certificateformat PEM invalide %s", err)
c.JSON(http.StatusNotFound, gin.H{"error": message})
return
}
caCerts := []*x509.Certificate{x509cacert}
p12Data, err = pkcs12.Modern.Encode(x509key, x509cert, caCerts, "password")
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "échec encodage P12"})
return
}
buffer := bytes.NewBuffer(p12Data)
securityLevel := c.DefaultQuery("security", "modern")
filename := cert.CommonName + ".p12"
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
c.Header("Content-Type", "application/x-pkcs12")
c.Header("Content-Length", fmt.Sprintf("%d", buffer.Len()))
c.Header("Content-Transfer-Encoding", "binary")
c.Header("X-Security-Level", securityLevel)
// Retourner les données
c.DataFromReader(
http.StatusOK,
int64(buffer.Len()),
"application/x-pkcs12",
buffer,
map[string]string{"Content-Disposition": fmt.Sprintf(`attachment; filename="%s"`, filename)},
)
}
// Web Interface
func (h *Handlers) ServeWebInterface(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)

View File

@ -39,6 +39,8 @@ func SetupRoutes(router *gin.Engine, repo repository.Repository, jwtSecret strin
ca.PUT("/:id", handlers.UpdateCA)
ca.DELETE("/:id", handlers.DeleteCA)
ca.GET("/:id/download/cert", handlers.DownloadCACertificate)
ca.GET("/:id/download/key", handlers.DownloadCAPrivateKey)
}
// SubCA routes
@ -50,6 +52,8 @@ func SetupRoutes(router *gin.Engine, repo repository.Repository, jwtSecret strin
subca.PUT("/:id", handlers.UpdateSubCA)
subca.DELETE("/:id", handlers.DeleteSubCA)
subca.GET("/:id/download/cert", handlers.DownloadSubCACertificate)
subca.GET("/:id/download/key", handlers.DownloadSubCAPrivateKey)
}
// Certificate routes
@ -62,6 +66,7 @@ func SetupRoutes(router *gin.Engine, repo repository.Repository, jwtSecret strin
cert.POST("/:id/revoke", handlers.RevokeCertificate)
cert.GET("/:id/download/cert", handlers.DownloadCertificate)
cert.GET("/:id/download/key", handlers.DownloadPrivateKey)
cert.GET("/:id/download/p12", handlers.DownloadP12)
}
}
}

View File

@ -13,6 +13,8 @@ type CA struct {
Country string `json:"country" bson:"country"`
Province string `json:"province" bson:"province"`
Locality string `json:"locality" bson:"locality"`
StreetAddress string `json:"street_address" bson:"street_address"`
PostalCode string `json:"postal_code" bson:"postal_code"`
Email string `json:"email" bson:"email"`
PrivateKey string `json:"private_key,omitempty" bson:"private_key"`
Certificate string `json:"certificate" bson:"certificate"`
@ -32,6 +34,8 @@ type CreateCARequest struct {
Country string `json:"country" binding:"required"`
Province string `json:"province"`
Locality string `json:"locality"`
StreetAddress string `json:"street_address"`
PostalCode string `json:"postal_code"`
Email string `json:"email" binding:"omitempty,email"` // omitempty permet les chaînes vides
KeySize int `json:"key_size" binding:"required,min=2048"`
ValidYears int `json:"valid_years" binding:"required,min=1,max=20"`

View File

@ -7,7 +7,7 @@ import (
type Certificate struct {
ID string `json:"id" bson:"_id"`
CommonName string `json:"common_name" bson:"common_name"`
Subject string `json:"subject" bson:"subject"`
// Subject string `json:"subject" bson:"subject"`
DNSNames []string `json:"dns_names" bson:"dns_names"`
IPAddresses []string `json:"ip_addresses" bson:"ip_addresses"`
Type string `json:"type" bson:"type"` // "server" or "client"

View File

@ -38,10 +38,12 @@ func (s *CryptoService) GenerateRootCA(req models.CreateCARequest) (*models.CA,
subject := pkix.Name{
CommonName: req.CommonName,
Organization: []string{req.Organization},
OrganizationalUnit []string{req.OrganizationalUnit},
OrganizationalUnit: []string{req.OrganizationalUnit},
Country: []string{req.Country},
Province: []string{req.Province},
Locality: []string{req.Locality},
StreetAddress: []string{req.StreetAddress},
PostalCode: []string{req.PostalCode},
}
// Ajouter l'email seulement s'il est fourni
@ -93,6 +95,8 @@ func (s *CryptoService) GenerateRootCA(req models.CreateCARequest) (*models.CA,
Country: req.Country,
Province: req.Province,
Locality: req.Locality,
StreetAddress: req.StreetAddress,
PostalCode: req.PostalCode,
Email: req.Email,
PrivateKey: string(privPEM),
Certificate: string(certPEM),
@ -139,7 +143,7 @@ func (s *CryptoService) GenerateSubCA(req models.CreateSubCARequest, parentCA *m
subject := pkix.Name{
CommonName: req.CommonName,
Organization: []string{req.Organization},
OrganizationalUnit []string{req.OrganizationalUnit},
OrganizationalUnit: []string{req.OrganizationalUnit},
Country: []string{req.Country},
Province: []string{req.Province},
Locality: []string{req.Locality},
@ -245,7 +249,6 @@ func (s *CryptoService) GenerateCertificate(req models.CreateCertificateRequest,
default:
return nil, fmt.Errorf("unsupported issuer type")
}
// Generate certificate private key
priv, err := rsa.GenerateKey(rand.Reader, req.KeySize)
if err != nil {
@ -258,11 +261,19 @@ func (s *CryptoService) GenerateCertificate(req models.CreateCertificateRequest,
return nil, fmt.Errorf("failed to generate serial number: %v", err)
}
subject := pkix.Name{
CommonName: req.CommonName,
Organization: []string{issuerCert.Subject.Organization[0]},
OrganizationalUnit: []string{issuerCert.Subject.OrganizationalUnit[0]},
Country: []string{issuerCert.Subject.Country[0]},
Province: []string{issuerCert.Subject.Province[0]},
Locality: []string{issuerCert.Subject.Locality[0]},
StreetAddress: []string{issuerCert.Subject.StreetAddress[0]},
PostalCode: []string{issuerCert.Subject.PostalCode[0]},
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: req.CommonName,
},
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, req.ValidDays),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
@ -293,7 +304,6 @@ func (s *CryptoService) GenerateCertificate(req models.CreateCertificateRequest,
cert := &models.Certificate{
ID: fmt.Sprintf("cert_%d", time.Now().UnixNano()),
CommonName: req.CommonName,
Subject: template.Subject.String(),
DNSNames: req.DNSNames,
IPAddresses: req.IPAddresses,
Type: req.Type,
@ -329,3 +339,30 @@ func (s *CryptoService) parseIPs(ips []string) []net.IP {
}
return parsedIPs
}
func (s *CryptoService) ParseCertificate(data []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, fmt.Errorf("format PEM invalide")
}
return x509.ParseCertificate(block.Bytes)
}
func (s *CryptoService) ParsePrivateKey(data []byte) (interface{}, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, fmt.Errorf("format PEM invalide")
}
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
return key, nil
}
if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil {
return key, nil
}
return nil, fmt.Errorf("format de clé non supporté")
}

View File

@ -593,9 +593,12 @@ async fetchSubCAs() {
name: data.name,
common_name: data.common_name,
organization: data.organization,
organization_unit: data.organization_unit,
country: data.country,
province: data.province || '',
locality: data.locality || '',
street_address: data.street_address || '',
postal_code: data.postal_code || '',
email: data.email || '',
key_size: parseInt(data.key_size) || 4096,
valid_years: parseInt(data.valid_years) || 10,
@ -632,6 +635,7 @@ async fetchSubCAs() {
name: data.name,
common_name: data.common_name,
organization: data.organization,
organization_unit: data.organization_unit,
email: data.email || '',
country: data.country,
province: data.province || '',
@ -785,10 +789,16 @@ async fetchSubCAs() {
<h3>${ca.name}</h3>
<p><strong>Common Name:</strong> ${ca.common_name}</p>
<p><strong>Organization:</strong> ${ca.organization}</p>
<p><strong>Organizational Unit:</strong> ${ca.organization_unit}</p>
<p><strong>StreetAddress:</strong> ${ca.street_address}</p>
<p><strong>PostalCode:</strong> ${ca.postal_code}</p>
<p><strong>Valid From:</strong> ${new Date(ca.valid_from).toLocaleString()}</p>
<p><strong>Valid To:</strong> ${new Date(ca.valid_to).toLocaleString()}</p>
<p><strong>Serial:</strong> ${ca.serial_number}</p>
<div class="button-group">
<button class="btn" onclick="window.open('${this.apiBase}/cas/${id}/download/cert')">Download Certificate</button>
<button class="btn" onclick="window.open('${this.apiBase}/cas/${id}/download/key')">Download Private Key</button>
</div>
`);
} catch (error) {
this.showError('Failed to load CA details');
@ -804,7 +814,9 @@ async fetchSubCAs() {
this.showModal('Sub CA Details', `
<h3>${subca.name}</h3>
<p><strong>Common Name:</strong> ${subca.common_name}</p>
<p><strong>Subject:</strong> ${subca.subject}</p>
<p><strong>Organization:</strong> ${subca.organization}</p>
<p><strong>Organizational Unit:</strong> ${subca.organization_unit}</p>
<p><strong>Valid From:</strong> ${new Date(subca.valid_from).toLocaleString()}</p>
<p><strong>Valid To:</strong> ${new Date(subca.valid_to).toLocaleString()}</p>
<p><strong>Serial:</strong> ${subca.serial_number}</p>
@ -818,12 +830,15 @@ async fetchSubCAs() {
async viewCertificate(id) {
try {
const response = await fetch(`${this.apiBase}/certificates/${id}`);
if (!response.ok) throw new Error('Not found');
const cert = await response.json();
this.showModal('Certificate Details', `
<h3>${cert.common_name}</h3>
<p><strong>Type:</strong> ${cert.type}</p>
<p><strong>SANs:</strong> ${cert.dns_names}
<p><strong>IP Address:</strong> ${cert.ip_adresses}
<p><strong>Valid From:</strong> ${new Date(cert.valid_from).toLocaleString()}</p>
<p><strong>Valid To:</strong> ${new Date(cert.valid_to).toLocaleString()}</p>
<p><strong>Status:</strong> ${cert.revoked ? 'Revoked' : new Date(cert.valid_to) > new Date() ? 'Valid' : 'Expired'}</p>
@ -831,6 +846,7 @@ async fetchSubCAs() {
<div class="button-group">
<button class="btn" onclick="window.open('${this.apiBase}/certificates/${id}/download/cert')">Download Certificate</button>
<button class="btn" onclick="window.open('${this.apiBase}/certificates/${id}/download/key')">Download Private Key</button>
<button class="btn" onclick="window.open('${this.apiBase}/certificates/${id}/download/p12')">Download P12</button>
</div>
`);
} catch (error) {

View File

@ -5,41 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PKI Manager</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css">
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-shield-alt"></i> PKI Manager</h1>
<h1><i class="fas fa-shield-alt"></i>ZEN6 PKI Manager - WIP</h1>
<p>Manage your Certificate Authority infrastructure</p>
<div style="margin-bottom: 10px;">
<button class="btn" onclick="pki.debugData()" style="background: #38a169;">
<i class="fas fa-bug"></i> Debug Data
</button>
<button onclick="testDropdown()" style="padding: 8px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
Test Dropdown
</button>
<script>
function testDropdown() {
console.log('=== TEST DROPDOWN ===');
const dropdown = document.getElementById('issuerCA');
console.log('Dropdown:', dropdown);
console.log('Parent:', dropdown?.parentElement);
console.log('All selects:', document.querySelectorAll('select'));
// Simuler le remplissage
if (dropdown) {
dropdown.innerHTML = '';
const option = document.createElement('option');
option.value = 'test';
option.textContent = 'Test Option';
dropdown.appendChild(option);
console.log('Test option added');
}
}
</script>
</div>
<nav>
<button class="nav-btn" data-tab="dashboard">
<i class="fas fa-home"></i> Dashboard
@ -136,13 +108,21 @@
<label for="caLocality">Locality/City</label>
<input type="text" id="caLocality" name="locality">
</div>
<div class="form-group">
<label for="caStreetAddress">StreetAddress/City</label>
<input type="text" id="caStreetAddress" name="street_address">
</div>
<div class="form-group">
<label for="caPostalCode">PostalCode</label>
<input type="text" id="caPostalCode" name="postal_code">
</div>
<div class="form-group">
<label for="caEmail">Email</label>
<input type="email" id="caEmail" name="email" required>
<input type="email" id="caEmail" name="email">
</div>
<div class="form-group">
<label for="caKeySize">Key Size</label>
<select id="caKeySize" name="key_size" required>
<select id="caKeySize" name="key_sizeq" required>
<option value="2048">2048 bits</option>
<option value="4096" selected>4096 bits</option>
</select>