feat: add certificate export functionality (PEM, DER, with private key, chain)
parent
2500292997
commit
1c02d6a4ab
95
README.md
95
README.md
|
|
@ -418,6 +418,99 @@ curl -H "Authorization: Bearer $TOKEN" \
|
|||
|
||||
---
|
||||
|
||||
### 📥 Export de Certificats (Authentifiés)
|
||||
|
||||
#### GET /api/v1/certificates/:id/export/pem
|
||||
Exporte un certificat au format PEM (binaire/texte).
|
||||
|
||||
**Requête :**
|
||||
```bash
|
||||
TOKEN="<your_token>"
|
||||
CERT_ID="e12e08a9-adeb-404c-a7b7-a613b77dfe66"
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
http://localhost:8080/api/v1/certificates/$CERT_ID/export/pem \
|
||||
-o certificate.pem
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
```
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDFDCCAfygAwIBAgIEAsoYhjANBg...
|
||||
...base64...
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### GET /api/v1/certificates/:id/export/der
|
||||
Exporte un certificat au format DER (binaire).
|
||||
|
||||
**Requête :**
|
||||
```bash
|
||||
TOKEN="<your_token>"
|
||||
CERT_ID="e12e08a9-adeb-404c-a7b7-a613b77dfe66"
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
http://localhost:8080/api/v1/certificates/$CERT_ID/export/der \
|
||||
-o certificate.der
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
Format binaire DER directement (données binaires).
|
||||
|
||||
---
|
||||
|
||||
#### GET /api/v1/certificates/:id/export/pem-with-key
|
||||
Exporte un certificat avec sa clé privée au format PEM (combiné).
|
||||
|
||||
**Requête :**
|
||||
```bash
|
||||
TOKEN="<your_token>"
|
||||
CERT_ID="e12e08a9-adeb-404c-a7b7-a613b77dfe66"
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
http://localhost:8080/api/v1/certificates/$CERT_ID/export/pem-with-key \
|
||||
-o certificate_with_key.pem
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
```
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDFDCCAfygAwIBAgIEAsoYhjANBg...
|
||||
...base64...
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQE...
|
||||
...base64...
|
||||
-----END PRIVATE KEY-----
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### GET /api/v1/certificates/:id/export/chain
|
||||
Exporte la chaîne de certificats (certificat + CA parent).
|
||||
|
||||
**Requête :**
|
||||
```bash
|
||||
TOKEN="<your_token>"
|
||||
CERT_ID="e12e08a9-adeb-404c-a7b7-a613b77dfe66"
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
http://localhost:8080/api/v1/certificates/$CERT_ID/export/chain \
|
||||
-o certificate_chain.pem
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
```
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDFDCCAfygAwIBAgIEAsoYhjANBg...
|
||||
...certificat feuille...
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDOTCCAiGgAwIBAgIEIlnNaD...
|
||||
...certificat CA parent...
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variables d'Environnement
|
||||
|
||||
- `JWT_SECRET_KEY` : Secret pour signer les tokens JWT (défaut: `your-secret-key-change-in-prod`)
|
||||
|
|
@ -540,10 +633,10 @@ pkiapi/
|
|||
|
||||
## Future améliorations
|
||||
|
||||
- [x] Export des certificats (PEM, DER)
|
||||
- [ ] Persistance en base de données (PostgreSQL)
|
||||
- [ ] Support OCSP (Online Certificate Status Protocol)
|
||||
- [ ] Interface web pour gérer les CAs
|
||||
- [ ] Export des certificats (PEM, DER)
|
||||
- [ ] Support des chaînes intermédiaires
|
||||
- [ ] Auditing et logging
|
||||
- [ ] Rate limiting et throttling
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -182,3 +184,153 @@ func GetCRL(c *gin.Context) {
|
|||
"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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ func RegisterRoutesWithStore(router *gin.Engine, caStore storage.CertificateStor
|
|||
v1.GET("/certificates/:id", GetCertificate)
|
||||
v1.POST("/revoke", RevokeCertificate)
|
||||
|
||||
// Endpoints Export Certificats
|
||||
v1.GET("/certificates/:id/export/pem", ExportCertificatePEM)
|
||||
v1.GET("/certificates/:id/export/der", ExportCertificateDER)
|
||||
v1.GET("/certificates/:id/export/pem-with-key", ExportCertificateWithPrivateKeyPEM)
|
||||
v1.GET("/certificates/:id/export/chain", ExportCertificateChain)
|
||||
|
||||
// Endpoints CRL
|
||||
v1.GET("/crl", GetCRL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Script de test pour la fonctionnalité d'export de certificats
|
||||
|
||||
API_URL="http://localhost:8080/api/v1"
|
||||
EXPORT_DIR="/tmp/pki_exports"
|
||||
|
||||
echo "=== PKI Certificate Export Test ==="
|
||||
echo ""
|
||||
|
||||
# Créer le répertoire d'export
|
||||
mkdir -p "$EXPORT_DIR"
|
||||
echo "[*] Répertoire d'export: $EXPORT_DIR"
|
||||
|
||||
# 1. Obtenir un token
|
||||
echo "[1] Obtention du token..."
|
||||
TOKEN_RESP=$(curl -s -X POST "$API_URL/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin"}')
|
||||
|
||||
TOKEN=$(echo $TOKEN_RESP | jq -r '.token')
|
||||
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
|
||||
echo "❌ Erreur: impossible d'obtenir le token"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Token reçu: ${TOKEN:0:50}..."
|
||||
echo ""
|
||||
|
||||
# 2. Créer un certificat
|
||||
echo "[2] Création d'un certificat..."
|
||||
CERT_RESP=$(curl -s -X POST "$API_URL/certificates" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"subject":"CN=test.example.com,O=Test,C=FR",
|
||||
"validity_days":365
|
||||
}')
|
||||
|
||||
CERT_ID=$(echo $CERT_RESP | jq -r '.certificate.id')
|
||||
if [ "$CERT_ID" = "null" ] || [ -z "$CERT_ID" ]; then
|
||||
echo "❌ Erreur: impossible de créer le certificat"
|
||||
echo "Réponse: $CERT_RESP"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Certificat créé: $CERT_ID"
|
||||
echo ""
|
||||
|
||||
# 3. Exporter en PEM
|
||||
echo "[3] Export PEM..."
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$API_URL/certificates/$CERT_ID/export/pem" \
|
||||
-o "$EXPORT_DIR/cert.pem"
|
||||
if [ -f "$EXPORT_DIR/cert.pem" ] && [ -s "$EXPORT_DIR/cert.pem" ]; then
|
||||
echo "✓ Export PEM réussi: $(stat -f%z "$EXPORT_DIR/cert.pem" 2>/dev/null || stat -c%s "$EXPORT_DIR/cert.pem") bytes"
|
||||
head -2 "$EXPORT_DIR/cert.pem"
|
||||
else
|
||||
echo "❌ Erreur: export PEM échoué"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 4. Exporter en DER
|
||||
echo "[4] Export DER..."
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$API_URL/certificates/$CERT_ID/export/der" \
|
||||
-o "$EXPORT_DIR/cert.der"
|
||||
if [ -f "$EXPORT_DIR/cert.der" ] && [ -s "$EXPORT_DIR/cert.der" ]; then
|
||||
echo "✓ Export DER réussi: $(stat -f%z "$EXPORT_DIR/cert.der" 2>/dev/null || stat -c%s "$EXPORT_DIR/cert.der") bytes"
|
||||
else
|
||||
echo "❌ Erreur: export DER échoué"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 5. Exporter avec clé privée
|
||||
echo "[5] Export PEM avec clé privée..."
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$API_URL/certificates/$CERT_ID/export/pem-with-key" \
|
||||
-o "$EXPORT_DIR/cert_with_key.pem"
|
||||
if [ -f "$EXPORT_DIR/cert_with_key.pem" ] && [ -s "$EXPORT_DIR/cert_with_key.pem" ]; then
|
||||
FILE_SIZE=$(stat -c%s "$EXPORT_DIR/cert_with_key.pem" 2>/dev/null)
|
||||
echo "✓ Export PEM+clé réussi: $FILE_SIZE bytes"
|
||||
CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" "$EXPORT_DIR/cert_with_key.pem")
|
||||
KEY_COUNT=$(grep -c "BEGIN PRIVATE KEY" "$EXPORT_DIR/cert_with_key.pem")
|
||||
echo " - Certificats trouvés: $CERT_COUNT"
|
||||
echo " - Clés privées trouvées: $KEY_COUNT"
|
||||
else
|
||||
echo "❌ Erreur: export PEM+clé échoué"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 6. Créer une CA et exporter la chaîne
|
||||
echo "[6] Création d'une CA et chaîne de certificats..."
|
||||
CA_RESP=$(curl -s -X POST "$API_URL/ca" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"subject":"CN=Test Root CA,O=Test,C=FR",
|
||||
"validity_days":3650
|
||||
}')
|
||||
|
||||
CA_ID=$(echo $CA_RESP | jq -r '.ca.id')
|
||||
if [ "$CA_ID" != "null" ] && [ -n "$CA_ID" ]; then
|
||||
echo "✓ CA créée: $CA_ID"
|
||||
|
||||
# Créer un certificat signé par la CA
|
||||
SIGNED_RESP=$(curl -s -X POST "$API_URL/certificates/sign" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"ca_id\":\"$CA_ID\",
|
||||
\"subject\":\"CN=signed.example.com,O=Test,C=FR\",
|
||||
\"validity_days\":365
|
||||
}")
|
||||
|
||||
SIGNED_ID=$(echo $SIGNED_RESP | jq -r '.certificate.id')
|
||||
if [ "$SIGNED_ID" != "null" ] && [ -n "$SIGNED_ID" ]; then
|
||||
echo "✓ Certificat signé créé: $SIGNED_ID"
|
||||
|
||||
# Exporter la chaîne
|
||||
echo "[7] Export chaîne de certificats..."
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$API_URL/certificates/$SIGNED_ID/export/chain" \
|
||||
-o "$EXPORT_DIR/cert_chain.pem"
|
||||
if [ -f "$EXPORT_DIR/cert_chain.pem" ] && [ -s "$EXPORT_DIR/cert_chain.pem" ]; then
|
||||
CHAIN_COUNT=$(grep -c "BEGIN CERTIFICATE" "$EXPORT_DIR/cert_chain.pem")
|
||||
echo "✓ Export chaîne réussi: $(stat -f%z "$EXPORT_DIR/cert_chain.pem" 2>/dev/null || stat -c%s "$EXPORT_DIR/cert_chain.pem") bytes ($CHAIN_COUNT certificats)"
|
||||
else
|
||||
echo "❌ Erreur: export chaîne échoué"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Résumé des fichiers
|
||||
echo "=== Résumé des exports ==="
|
||||
ls -lh "$EXPORT_DIR" | tail -n +2
|
||||
echo ""
|
||||
echo "✓ Test d'export complété. Fichiers disponibles dans: $EXPORT_DIR"
|
||||
Loading…
Reference in New Issue