feat: PKI API Go avec persistance MongoDB et abstraction storage

- API REST complète pour gestion Infrastructure à Clé Publique
- Authentification JWT sur tous les endpoints (sauf login)
- Hiérarchie de certificats (Root CA, Sub-CA, End Certificates)
- Abstraction de stockage avec MemoryStore et MongoStore
- Configuration centralisée via variables d'environnement
- Support déploiement Docker Compose avec MongoDB
- Tests unitaires pour sérialisation des clés RSA
- Documentation complète avec exemples API
This commit is contained in:
zen6
2025-12-06 23:11:50 +01:00
commit ddece00272
23 changed files with 2382 additions and 0 deletions

236
internal/pki/certificate.go Normal file
View File

@@ -0,0 +1,236 @@
package pki
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"time"
)
// Certificate représente un certificat X.509
type Certificate struct {
ID string
Subject string
Issuer string
NotBefore time.Time
NotAfter time.Time
PublicKey *rsa.PublicKey
PrivateKey *rsa.PrivateKey // Stocké pour les CAs
Cert *x509.Certificate
Revoked bool
IsCA bool // True si c'est une autorité de certification
}
// GenerateCertificate crée un nouveau certificat auto-signé
func GenerateCertificate(subject string, validityDays int) (*Certificate, error) {
// Générer une clé RSA
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// Créer le certificat
serialNumber, _ := rand.Int(rand.Reader, big.NewInt(1000000))
notBefore := time.Now()
notAfter := notBefore.AddDate(0, 0, validityDays)
cert := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: subject,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
}
// Auto-signer le certificat
certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, err
}
parsedCert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
return &Certificate{
Subject: subject,
Issuer: subject,
NotBefore: notBefore,
NotAfter: notAfter,
PublicKey: &privateKey.PublicKey,
PrivateKey: privateKey,
Cert: parsedCert,
Revoked: false,
IsCA: false,
}, nil
}
// GenerateCA crée une nouvelle autorité de certification
func GenerateCA(subject string, validityDays int) (*Certificate, error) {
// Générer une clé RSA
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// Créer le certificat CA
serialNumber, _ := rand.Int(rand.Reader, big.NewInt(1000000000))
notBefore := time.Now()
notAfter := notBefore.AddDate(0, 0, validityDays)
cert := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: subject,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: -1, // Pas de limite de chaîne
}
// Auto-signer le certificat CA
certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, err
}
parsedCert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
return &Certificate{
Subject: subject,
Issuer: subject,
NotBefore: notBefore,
NotAfter: notAfter,
PublicKey: &privateKey.PublicKey,
PrivateKey: privateKey,
Cert: parsedCert,
Revoked: false,
IsCA: true,
}, nil
}
// SignCertificate signe un certificat avec une CA
func SignCertificate(caCert *Certificate, subject string, validityDays int) (*Certificate, error) {
if !caCert.IsCA {
return nil, ErrNotACA
}
if caCert.PrivateKey == nil {
return nil, ErrMissingPrivateKey
}
// Générer une clé RSA pour le nouveau certificat
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// Créer le certificat à signer
serialNumber, _ := rand.Int(rand.Reader, big.NewInt(1000000000))
notBefore := time.Now()
notAfter := notBefore.AddDate(0, 0, validityDays)
cert := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: subject,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
Issuer: caCert.Cert.Subject,
}
// Signer le certificat avec la clé privée de la CA
certBytes, err := x509.CreateCertificate(rand.Reader, cert, caCert.Cert, &privateKey.PublicKey, caCert.PrivateKey)
if err != nil {
return nil, err
}
parsedCert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
return &Certificate{
Subject: subject,
Issuer: caCert.Subject,
NotBefore: notBefore,
NotAfter: notAfter,
PublicKey: &privateKey.PublicKey,
PrivateKey: privateKey,
Cert: parsedCert,
Revoked: false,
IsCA: false,
}, nil
}
// SignSubCA signe une CA intermédiaire avec une CA parent
func SignSubCA(parentCA *Certificate, subject string, validityDays int) (*Certificate, error) {
if !parentCA.IsCA {
return nil, ErrNotACA
}
if parentCA.PrivateKey == nil {
return nil, ErrMissingPrivateKey
}
// Générer une clé RSA pour la sub-CA
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// Créer le certificat CA intermédiaire
serialNumber, _ := rand.Int(rand.Reader, big.NewInt(1000000000))
notBefore := time.Now()
notAfter := notBefore.AddDate(0, 0, validityDays)
subCA := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: subject,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0, // Limite la chaîne : cette CA ne peut pas signer d'autres CAs
Issuer: parentCA.Cert.Subject,
}
// Signer le certificat CA avec la clé privée du parent
certBytes, err := x509.CreateCertificate(rand.Reader, subCA, parentCA.Cert, &privateKey.PublicKey, parentCA.PrivateKey)
if err != nil {
return nil, err
}
parsedCert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
return &Certificate{
Subject: subject,
Issuer: parentCA.Subject,
NotBefore: notBefore,
NotAfter: notAfter,
PublicKey: &privateKey.PublicKey,
PrivateKey: privateKey,
Cert: parsedCert,
Revoked: false,
IsCA: true,
}, nil
}

8
internal/pki/errors.go Normal file
View File

@@ -0,0 +1,8 @@
package pki
import "errors"
var (
ErrNotACA = errors.New("n'est pas une autorité de certification")
ErrMissingPrivateKey = errors.New("clé privée manquante")
)