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:
7
internal/storage/errors.go
Normal file
7
internal/storage/errors.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package storage
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("certificat non trouvé")
|
||||
)
|
||||
18
internal/storage/interface.go
Normal file
18
internal/storage/interface.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package storage
|
||||
|
||||
import "github.com/stef/pkiapi/internal/pki"
|
||||
|
||||
// CertificateStore définit l'interface pour le stockage des certificats
|
||||
type CertificateStore interface {
|
||||
// SaveCertificate sauvegarde ou met à jour un certificat
|
||||
SaveCertificate(id string, cert *pki.Certificate) error
|
||||
|
||||
// GetCertificate récupère un certificat par ID
|
||||
GetCertificate(id string) (*pki.Certificate, error)
|
||||
|
||||
// ListCertificates retourne tous les certificats
|
||||
ListCertificates() []*pki.Certificate
|
||||
|
||||
// Close ferme la connexion au store
|
||||
Close() error
|
||||
}
|
||||
232
internal/storage/mongo.go
Normal file
232
internal/storage/mongo.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/stef/pkiapi/internal/pki"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// MongoStore gère le stockage des certificats dans MongoDB
|
||||
type MongoStore struct {
|
||||
client *mongo.Client
|
||||
collection *mongo.Collection
|
||||
}
|
||||
|
||||
// CertificateDoc représente un certificat stocké dans MongoDB
|
||||
type CertificateDoc struct {
|
||||
ID string `bson:"_id"`
|
||||
Subject string `bson:"subject"`
|
||||
Issuer string `bson:"issuer"`
|
||||
NotBefore time.Time `bson:"not_before"`
|
||||
NotAfter time.Time `bson:"not_after"`
|
||||
IsCA bool `bson:"is_ca"`
|
||||
Revoked bool `bson:"revoked"`
|
||||
Cert string `bson:"cert"` // Base64 encoded certificate
|
||||
PrivateKey string `bson:"private_key"` // Base64 encoded private key (only for CAs)
|
||||
CreatedAt time.Time `bson:"created_at"`
|
||||
}
|
||||
|
||||
// NewMongoStore crée une connexion MongoDB et retourne un MongoStore
|
||||
func NewMongoStore(mongoURI string, dbName string) (*MongoStore, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Tester la connexion
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := client.Ping(ctx, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collection := client.Database(dbName).Collection("certificates")
|
||||
|
||||
// Créer un index sur l'ID
|
||||
indexModel := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "_id", Value: 1}},
|
||||
}
|
||||
_, err = collection.Indexes().CreateOne(ctx, indexModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MongoStore{
|
||||
client: client,
|
||||
collection: collection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SaveCertificate sauvegarde un certificat dans MongoDB
|
||||
func (m *MongoStore) SaveCertificate(id string, cert *pki.Certificate) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
doc := CertificateDoc{
|
||||
ID: id,
|
||||
Subject: cert.Subject,
|
||||
Issuer: cert.Issuer,
|
||||
NotBefore: cert.NotBefore,
|
||||
NotAfter: cert.NotAfter,
|
||||
IsCA: cert.IsCA,
|
||||
Revoked: cert.Revoked,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Encoder le certificat en base64
|
||||
if cert.Cert != nil {
|
||||
doc.Cert = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
||||
}
|
||||
|
||||
// Encoder la clé privée en base64 (seulement pour les CAs)
|
||||
if cert.PrivateKey != nil && cert.IsCA {
|
||||
privKeyBytes, err := marshalPrivateKey(cert.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
doc.PrivateKey = base64.StdEncoding.EncodeToString(privKeyBytes)
|
||||
}
|
||||
|
||||
_, err := m.collection.UpdateOne(
|
||||
ctx,
|
||||
bson.M{"_id": id},
|
||||
bson.M{"$set": doc},
|
||||
options.Update().SetUpsert(true),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCertificate récupère un certificat depuis MongoDB
|
||||
func (m *MongoStore) GetCertificate(id string) (*pki.Certificate, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var doc CertificateDoc
|
||||
err := m.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&doc)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Décoder le certificat
|
||||
cert := &pki.Certificate{
|
||||
ID: doc.ID,
|
||||
Subject: doc.Subject,
|
||||
Issuer: doc.Issuer,
|
||||
NotBefore: doc.NotBefore,
|
||||
NotAfter: doc.NotAfter,
|
||||
IsCA: doc.IsCA,
|
||||
Revoked: doc.Revoked,
|
||||
}
|
||||
|
||||
// Décoder le certificat X.509
|
||||
if doc.Cert != "" {
|
||||
certBytes, err := base64.StdEncoding.DecodeString(doc.Cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedCert, err := parseCertificate(certBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Cert = parsedCert
|
||||
}
|
||||
|
||||
// Décoder la clé privée
|
||||
if doc.PrivateKey != "" {
|
||||
privKeyBytes, err := base64.StdEncoding.DecodeString(doc.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privKey, err := unmarshalPrivateKey(privKeyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Type assertion pour convertir interface{} en *rsa.PrivateKey
|
||||
rsaKey, ok := privKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
// Si ce n'est pas une clé RSA, on la laisse nil (CAs sans clé privée)
|
||||
cert.PrivateKey = nil
|
||||
} else {
|
||||
cert.PrivateKey = rsaKey
|
||||
}
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// ListCertificates retourne tous les certificats depuis MongoDB
|
||||
func (m *MongoStore) ListCertificates() []*pki.Certificate {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := m.collection.Find(ctx, bson.M{})
|
||||
if err != nil {
|
||||
return []*pki.Certificate{}
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var certs []*pki.Certificate
|
||||
if err = cursor.All(ctx, &certs); err != nil {
|
||||
return []*pki.Certificate{}
|
||||
}
|
||||
|
||||
// Reconvertir les documents en certificats
|
||||
var results []*pki.Certificate
|
||||
if err = cursor.All(ctx, &[]CertificateDoc{}); err != nil {
|
||||
return []*pki.Certificate{}
|
||||
}
|
||||
|
||||
cursor, _ = m.collection.Find(ctx, bson.M{})
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
for cursor.Next(ctx) {
|
||||
var doc CertificateDoc
|
||||
if err := cursor.Decode(&doc); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cert := &pki.Certificate{
|
||||
ID: doc.ID,
|
||||
Subject: doc.Subject,
|
||||
Issuer: doc.Issuer,
|
||||
NotBefore: doc.NotBefore,
|
||||
NotAfter: doc.NotAfter,
|
||||
IsCA: doc.IsCA,
|
||||
Revoked: doc.Revoked,
|
||||
}
|
||||
|
||||
// Décoder le certificat X.509
|
||||
if doc.Cert != "" {
|
||||
certBytes, _ := base64.StdEncoding.DecodeString(doc.Cert)
|
||||
if parsedCert, _ := parseCertificate(certBytes); parsedCert != nil {
|
||||
cert.Cert = parsedCert
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, cert)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Close ferme la connexion MongoDB
|
||||
func (m *MongoStore) Close() error {
|
||||
if m.client != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return m.client.Disconnect(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
66
internal/storage/mongo_test.go
Normal file
66
internal/storage/mongo_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
|
||||
"github.com/stef/pkiapi/internal/pki"
|
||||
)
|
||||
|
||||
// TestStorageConfiguration teste que la config est correctement chargée
|
||||
func TestStorageConfiguration(t *testing.T) {
|
||||
// Test MemoryStore
|
||||
memStore := NewMemoryStore()
|
||||
if memStore == nil {
|
||||
t.Fatalf("MemoryStore nil")
|
||||
}
|
||||
|
||||
testCert := &pki.Certificate{
|
||||
ID: "test-123",
|
||||
Subject: "CN=test.example.com",
|
||||
Issuer: "CN=test.example.com",
|
||||
IsCA: false,
|
||||
}
|
||||
|
||||
memStore.SaveCertificate("test-123", testCert)
|
||||
retrieved, err := memStore.GetCertificate("test-123")
|
||||
if err != nil {
|
||||
t.Fatalf("Erreur récupération certificat: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Subject != "CN=test.example.com" {
|
||||
t.Fatalf("Certificate Subject mismatch")
|
||||
}
|
||||
|
||||
t.Log("✓ MemoryStore OK")
|
||||
memStore.Close()
|
||||
}
|
||||
|
||||
// TestKeyMarshalling teste la sérialisation des clés RSA
|
||||
func TestKeyMarshalling(t *testing.T) {
|
||||
// Créer une clé RSA de test
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("Erreur génération clé: %v", err)
|
||||
}
|
||||
|
||||
// Test marshalling privée clé
|
||||
marshalledKey, err := marshalPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Erreur marshalling clé: %v", err)
|
||||
}
|
||||
|
||||
// Test unmarshalling clé privée
|
||||
unmarshalledKey, err := unmarshalPrivateKey(marshalledKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Erreur unmarshalling clé: %v", err)
|
||||
}
|
||||
|
||||
_, ok := unmarshalledKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
t.Fatalf("Type assertion failed, expected *rsa.PrivateKey")
|
||||
}
|
||||
|
||||
t.Log("✓ Sérialisation/désérialisation des clés RSA OK")
|
||||
}
|
||||
61
internal/storage/store.go
Normal file
61
internal/storage/store.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/stef/pkiapi/internal/pki"
|
||||
)
|
||||
|
||||
// MemoryStore gère le stockage en mémoire des certificats
|
||||
type MemoryStore struct {
|
||||
mu sync.RWMutex
|
||||
certificates map[string]*pki.Certificate
|
||||
}
|
||||
|
||||
// NewMemoryStore crée une nouvelle instance de MemoryStore
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
return &MemoryStore{
|
||||
certificates: make(map[string]*pki.Certificate),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveCertificate sauvegarde un certificat
|
||||
func (s *MemoryStore) SaveCertificate(id string, cert *pki.Certificate) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.certificates[id] = cert
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCertificate récupère un certificat par ID
|
||||
func (s *MemoryStore) GetCertificate(id string) (*pki.Certificate, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
cert, exists := s.certificates[id]
|
||||
if !exists {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// ListCertificates retourne tous les certificats
|
||||
func (s *MemoryStore) ListCertificates() []*pki.Certificate {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
certs := make([]*pki.Certificate, 0, len(s.certificates))
|
||||
for _, cert := range s.certificates {
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
return certs
|
||||
}
|
||||
|
||||
// Close ferme le store (ne fait rien pour le store mémoire)
|
||||
func (s *MemoryStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: NewStore utilise maintenant NewMemoryStore
|
||||
// Conservé pour compatibilité
|
||||
func NewStore() CertificateStore {
|
||||
return NewMemoryStore()
|
||||
}
|
||||
37
internal/storage/util.go
Normal file
37
internal/storage/util.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
)
|
||||
|
||||
// marshalPrivateKey encode une clé privée en bytes
|
||||
func marshalPrivateKey(privateKey interface{}) ([]byte, error) {
|
||||
privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKeyBytes, nil
|
||||
}
|
||||
|
||||
// unmarshalPrivateKey décode une clé privée depuis bytes
|
||||
func unmarshalPrivateKey(privKeyBytes []byte) (interface{}, error) {
|
||||
return x509.ParsePKCS8PrivateKey(privKeyBytes)
|
||||
}
|
||||
|
||||
// parseCertificate parse un certificat X.509
|
||||
func parseCertificate(certBytes []byte) (*x509.Certificate, error) {
|
||||
// Essayer de parser en DER
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err == nil {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// Essayer de parser en PEM
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block != nil {
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
Reference in New Issue
Block a user