Compare commits
10 Commits
ecd36f186c
...
7a05e3afe4
| Author | SHA1 | Date |
|---|---|---|
|
|
7a05e3afe4 | |
|
|
d9e9db06ab | |
|
|
1e52006b46 | |
|
|
028f768299 | |
|
|
3cb1bb4c47 | |
|
|
c7427dae28 | |
|
|
4a06ec52ca | |
|
|
98adf5e971 | |
|
|
1c02d6a4ab | |
|
|
2500292997 |
399
README.md
399
README.md
|
|
@ -9,6 +9,8 @@ API Go complète pour gérer une Infrastructure à Clé Publique (PKI) avec hié
|
||||||
- ✅ **Signature de certificats** : Certificats auto-signés ou signés par une CA
|
- ✅ **Signature de certificats** : Certificats auto-signés ou signés par une CA
|
||||||
- ✅ **Gestion des révocations** : Révocation et CRL (Certificate Revocation List)
|
- ✅ **Gestion des révocations** : Révocation et CRL (Certificate Revocation List)
|
||||||
- ✅ **Stockage pluggable** : MemoryStore (développement) ou MongoDB (production)
|
- ✅ **Stockage pluggable** : MemoryStore (développement) ou MongoDB (production)
|
||||||
|
- ✅ **Export de certificats** : PEM, DER, avec clé privée, chaîne complète
|
||||||
|
- ✅ **Clés privées** : Stockage et récupération sécurisés pour tous les certificats (JSON + export fichier)
|
||||||
- ✅ **Cryptographie** : X.509, RSA 2048-bit, signatures HS256 pour JWT
|
- ✅ **Cryptographie** : X.509, RSA 2048-bit, signatures HS256 pour JWT
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
@ -36,7 +38,9 @@ pkiapi/
|
||||||
│ ├── mongo.go # MongoStore (persistance)
|
│ ├── mongo.go # MongoStore (persistance)
|
||||||
│ ├── util.go # Helpers sérialisation
|
│ ├── util.go # Helpers sérialisation
|
||||||
│ └── errors.go # Erreurs storage
|
│ └── errors.go # Erreurs storage
|
||||||
└── go.mod
|
├── tests/ # Scripts de test
|
||||||
|
├── go.mod
|
||||||
|
└── docker-compose.yaml # Orchestration services
|
||||||
```
|
```
|
||||||
|
|
||||||
## Démarrage rapide
|
## Démarrage rapide
|
||||||
|
|
@ -58,13 +62,11 @@ export PORT=8080
|
||||||
# Serveur lancé sur http://localhost:8080
|
# Serveur lancé sur http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
**Mode production (MongoDB):**
|
**Mode production (MongoDB via Docker Compose):**
|
||||||
```bash
|
```bash
|
||||||
export STORAGE_TYPE=mongodb
|
docker compose up -d --build
|
||||||
export MONGO_URI=mongodb://mongodb-server:27017
|
# L'API est disponible sur http://localhost:8080
|
||||||
export MONGO_DB=pkiapi-prod
|
# MongoDB est disponible sur mongodb://localhost:27017
|
||||||
export JWT_SECRET_KEY=super-secret-key
|
|
||||||
./pkiapi
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Obtenir un token JWT
|
### 3. Obtenir un token JWT
|
||||||
|
|
@ -110,34 +112,6 @@ curl -X POST http://localhost:8080/api/v1/login \
|
||||||
|
|
||||||
### 🔑 Autorités de Certification (Authentifiés)
|
### 🔑 Autorités de Certification (Authentifiés)
|
||||||
|
|
||||||
#### GET /api/v1/ca
|
|
||||||
Liste toutes les autorités de certification.
|
|
||||||
|
|
||||||
**Requête :**
|
|
||||||
```bash
|
|
||||||
TOKEN="<your_token>"
|
|
||||||
curl -H "Authorization: Bearer $TOKEN" \
|
|
||||||
http://localhost:8080/api/v1/ca
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cas": [
|
|
||||||
{
|
|
||||||
"id": "16de28da-f25e-49cd-81de-a929d34dfe08",
|
|
||||||
"subject": "CN=Root CA,O=Example,C=FR",
|
|
||||||
"issuer": "CN=Root CA,O=Example,C=FR",
|
|
||||||
"not_before": "2025-12-06T22:52:48Z",
|
|
||||||
"not_after": "2035-12-04T22:52:48Z",
|
|
||||||
"serial_number": "574847517"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### POST /api/v1/ca
|
#### POST /api/v1/ca
|
||||||
Crée une nouvelle autorité de certification auto-signée.
|
Crée une nouvelle autorité de certification auto-signée.
|
||||||
|
|
||||||
|
|
@ -163,6 +137,7 @@ curl -X POST http://localhost:8080/api/v1/ca \
|
||||||
"not_after": "2035-12-04T21:45:01Z",
|
"not_after": "2035-12-04T21:45:01Z",
|
||||||
"serial_number": "546965196",
|
"serial_number": "546965196",
|
||||||
"certificate": "MIIC5zCCAc+gAwIBAgIDCkUz...",
|
"certificate": "MIIC5zCCAc+gAwIBAgIDCkUz...",
|
||||||
|
"private_key": "MIIEwAIBADANBgkqhkiG9w0BAQE...",
|
||||||
"is_ca": true
|
"is_ca": true
|
||||||
},
|
},
|
||||||
"created_by": "admin"
|
"created_by": "admin"
|
||||||
|
|
@ -172,7 +147,7 @@ curl -X POST http://localhost:8080/api/v1/ca \
|
||||||
---
|
---
|
||||||
|
|
||||||
#### GET /api/v1/ca/:id
|
#### GET /api/v1/ca/:id
|
||||||
Récupère une autorité de certification par ID.
|
Récupère une autorité de certification par ID (avec clé privée).
|
||||||
|
|
||||||
**Requête :**
|
**Requête :**
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -192,6 +167,7 @@ curl -H "Authorization: Bearer $TOKEN" \
|
||||||
"not_after": "2035-12-04T21:45:01Z",
|
"not_after": "2035-12-04T21:45:01Z",
|
||||||
"serial_number": "546965196",
|
"serial_number": "546965196",
|
||||||
"certificate": "MIIC5zCCAc+gAwIBAgIDCkUz...",
|
"certificate": "MIIC5zCCAc+gAwIBAgIDCkUz...",
|
||||||
|
"private_key": "MIIEwAIBADANBgkqhkiG9w0BAQE...",
|
||||||
"is_ca": true
|
"is_ca": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,58 +192,12 @@ curl -X POST http://localhost:8080/api/v1/ca/sign \
|
||||||
}"
|
}"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ca": {
|
|
||||||
"id": "b2350d39-53c2-469a-802c-acc39707e352",
|
|
||||||
"subject": "CN=Intermediate CA,O=Example Inc,C=FR",
|
|
||||||
"not_before": "2025-12-06T21:45:09Z",
|
|
||||||
"not_after": "2030-12-05T21:45:09Z",
|
|
||||||
"serial_number": "576310632",
|
|
||||||
"certificate": "MIIDOTCCAiGgAwIBAgIEIlnNaD...",
|
|
||||||
"is_ca": true
|
|
||||||
},
|
|
||||||
"created_by": "admin",
|
|
||||||
"signed_by": "ff3ac5c5-08d1-401b-9e83-f18eda4c538b"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📜 Certificats (Authentifiés)
|
### 📜 Certificats (Authentifiés)
|
||||||
|
|
||||||
#### GET /api/v1/certificates
|
|
||||||
Liste tous les certificats.
|
|
||||||
|
|
||||||
**Requête :**
|
|
||||||
```bash
|
|
||||||
TOKEN="<your_token>"
|
|
||||||
curl -H "Authorization: Bearer $TOKEN" \
|
|
||||||
http://localhost:8080/api/v1/certificates
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"certificates": [
|
|
||||||
{
|
|
||||||
"id": "e12e08a9-adeb-404c-a7b7-a613b77dfe66",
|
|
||||||
"subject": "CN=server.example.com,O=Example Inc,C=FR",
|
|
||||||
"issuer": "CN=Intermediate CA,O=Example Inc,C=FR",
|
|
||||||
"not_before": "2025-12-06T21:45:09Z",
|
|
||||||
"not_after": "2026-12-06T21:45:09Z",
|
|
||||||
"serial_number": "46798982",
|
|
||||||
"revoked": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### POST /api/v1/certificates
|
#### POST /api/v1/certificates
|
||||||
Crée un certificat auto-signé.
|
Crée un certificat auto-signé avec clé privée.
|
||||||
|
|
||||||
**Requête :**
|
**Requête :**
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -292,6 +222,7 @@ curl -X POST http://localhost:8080/api/v1/certificates \
|
||||||
"not_after": "2026-12-06T21:41:38Z",
|
"not_after": "2026-12-06T21:41:38Z",
|
||||||
"serial_number": "673075",
|
"serial_number": "673075",
|
||||||
"certificate": "MIIC5zCCAc+gAwIBAgIDCkUzMA0GCSq...",
|
"certificate": "MIIC5zCCAc+gAwIBAgIDCkUzMA0GCSq...",
|
||||||
|
"private_key": "MIIEwAIBADANBgkqhkiG9w0BAQE...",
|
||||||
"revoked": false
|
"revoked": false
|
||||||
},
|
},
|
||||||
"created_by": "admin"
|
"created_by": "admin"
|
||||||
|
|
@ -300,45 +231,8 @@ curl -X POST http://localhost:8080/api/v1/certificates \
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### POST /api/v1/certificates/sign
|
|
||||||
Signe un certificat avec une CA.
|
|
||||||
|
|
||||||
**Requête :**
|
|
||||||
```bash
|
|
||||||
TOKEN="<your_token>"
|
|
||||||
CA_ID="b2350d39-53c2-469a-802c-acc39707e352"
|
|
||||||
curl -X POST http://localhost:8080/api/v1/certificates/sign \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"ca_id\": \"$CA_ID\",
|
|
||||||
\"subject\": \"CN=server.example.com,O=Example Inc,C=FR\",
|
|
||||||
\"validity_days\": 365
|
|
||||||
}"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"certificate": {
|
|
||||||
"id": "e12e08a9-adeb-404c-a7b7-a613b77dfe66",
|
|
||||||
"subject": "CN=server.example.com,O=Example Inc,C=FR",
|
|
||||||
"issuer": "CN=Intermediate CA,O=Example Inc,C=FR",
|
|
||||||
"not_before": "2025-12-06T21:45:09Z",
|
|
||||||
"not_after": "2026-12-06T21:45:09Z",
|
|
||||||
"serial_number": "46798982",
|
|
||||||
"certificate": "MIIDFDCCAfygAwIBAgIEAsoYhjANBg...",
|
|
||||||
"revoked": false
|
|
||||||
},
|
|
||||||
"created_by": "admin",
|
|
||||||
"signed_by": "b2350d39-53c2-469a-802c-acc39707e352"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### GET /api/v1/certificates/:id
|
#### GET /api/v1/certificates/:id
|
||||||
Récupère un certificat par ID.
|
Récupère un certificat par ID (avec clé privée encodée en base64).
|
||||||
|
|
||||||
**Requête :**
|
**Requête :**
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -359,6 +253,7 @@ curl -H "Authorization: Bearer $TOKEN" \
|
||||||
"not_after": "2026-12-06T21:45:09Z",
|
"not_after": "2026-12-06T21:45:09Z",
|
||||||
"serial_number": "46798982",
|
"serial_number": "46798982",
|
||||||
"certificate": "MIIDFDCCAfygAwIBAgIEAsoYhjANBg...",
|
"certificate": "MIIDFDCCAfygAwIBAgIEAsoYhjANBg...",
|
||||||
|
"private_key": "MIIEwAIBADANBgkqhkiG9w0BAQE...",
|
||||||
"revoked": false
|
"revoked": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -366,6 +261,25 @@ curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### POST /api/v1/certificates/sign
|
||||||
|
Signe un certificat avec une CA (avec clé privée).
|
||||||
|
|
||||||
|
**Requête :**
|
||||||
|
```bash
|
||||||
|
TOKEN="<your_token>"
|
||||||
|
CA_ID="b2350d39-53c2-469a-802c-acc39707e352"
|
||||||
|
curl -X POST http://localhost:8080/api/v1/certificates/sign \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"ca_id\": \"$CA_ID\",
|
||||||
|
\"subject\": \"CN=server.example.com,O=Example Inc,C=FR\",
|
||||||
|
\"validity_days\": 365
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### POST /api/v1/revoke
|
#### POST /api/v1/revoke
|
||||||
Révoque un certificat.
|
Révoque un certificat.
|
||||||
|
|
||||||
|
|
@ -382,15 +296,6 @@ curl -X POST http://localhost:8080/api/v1/revoke \
|
||||||
}"
|
}"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "certificat révoqué",
|
|
||||||
"id": "e12e08a9-adeb-404c-a7b7-a613b77dfe66",
|
|
||||||
"reason": "Compromised key"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### GET /api/v1/crl
|
#### GET /api/v1/crl
|
||||||
|
|
@ -403,150 +308,142 @@ curl -H "Authorization: Bearer $TOKEN" \
|
||||||
http://localhost:8080/api/v1/crl
|
http://localhost:8080/api/v1/crl
|
||||||
```
|
```
|
||||||
|
|
||||||
**Réponse :**
|
---
|
||||||
```json
|
|
||||||
{
|
### 📥 Export de Certificats (Authentifiés)
|
||||||
"crl": [
|
|
||||||
{
|
#### GET /api/v1/certificates/:id/export/pem
|
||||||
"serial_number": "46798982",
|
Exporte un certificat au format PEM.
|
||||||
"subject": "CN=server.example.com,O=Example Inc,C=FR"
|
|
||||||
}
|
```bash
|
||||||
],
|
TOKEN="<your_token>"
|
||||||
"version": 1
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
}
|
http://localhost:8080/api/v1/certificates/:id/export/pem \
|
||||||
|
-o certificate.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/certificates/:id/export/der
|
||||||
|
Exporte un certificat au format DER (binaire).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
http://localhost:8080/api/v1/certificates/:id/export/der \
|
||||||
|
-o certificate.der
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/certificates/:id/export/pem-with-key
|
||||||
|
Exporte certificat + clé privée au format PEM.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
http://localhost:8080/api/v1/certificates/:id/export/pem-with-key \
|
||||||
|
-o certificate_with_key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/certificates/:id/export/chain
|
||||||
|
Exporte la chaîne complète (certificat + CA parent).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
http://localhost:8080/api/v1/certificates/:id/export/chain \
|
||||||
|
-o certificate_chain.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clés Privées
|
||||||
|
|
||||||
|
### Stockage et Récupération
|
||||||
|
|
||||||
|
Les clés privées sont **automatiquement stockées** pour:
|
||||||
|
- ✅ Tous les certificats auto-signés
|
||||||
|
- ✅ Tous les certificats signés par une CA
|
||||||
|
- ✅ Toutes les CAs (Root et Intermediate)
|
||||||
|
|
||||||
|
### Accès via JSON
|
||||||
|
|
||||||
|
Les clés privées sont incluses dans les réponses JSON:
|
||||||
|
- **Format**: Base64 PKCS#8 encodé
|
||||||
|
- **Champs**: `private_key` (optionnel, présent si disponible)
|
||||||
|
- **Endpoints retournant des clés privées**:
|
||||||
|
- `POST /api/v1/ca` - Création CA
|
||||||
|
- `GET /api/v1/ca/:id` - Récupération CA
|
||||||
|
- `POST /api/v1/ca/sign` - Création Sub-CA
|
||||||
|
- `POST /api/v1/certificates` - Création certificat
|
||||||
|
- `GET /api/v1/certificates/:id` - Récupération certificat
|
||||||
|
- `POST /api/v1/certificates/sign` - Signature certificat
|
||||||
|
|
||||||
|
### Accès via Export Fichier
|
||||||
|
|
||||||
|
Les clés privées peuvent aussi être exportées en fichier:
|
||||||
|
- **`/export/pem-with-key`** - Certificat + clé privée en PEM
|
||||||
|
- **`/export/chain`** - Chaîne complète (pour CAs parent)
|
||||||
|
|
||||||
|
### Base de Données
|
||||||
|
|
||||||
|
Les clés privées sont:
|
||||||
|
- ✅ Sauvegardées en MongoDB (champ `private_key` en base64)
|
||||||
|
- ✅ Chiffrées au repos (via votre configuration MongoDB)
|
||||||
|
- ✅ Accessibles uniquement avec authentification JWT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Scripts de Test Disponibles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test complet (création CA, certificats, exports, revocation)
|
||||||
|
./tests/test_complete.sh
|
||||||
|
|
||||||
|
# Test spécifique des exports
|
||||||
|
./tests/test_exports.sh
|
||||||
|
|
||||||
|
# Test du stockage des clés privées
|
||||||
|
./tests/test_private_keys.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Résultats des Tests
|
||||||
|
|
||||||
|
Voir `tests/test_results.txt` pour les résultats détaillés des tests.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Variables d'Environnement
|
## Variables d'Environnement
|
||||||
|
|
||||||
- `JWT_SECRET_KEY` : Secret pour signer les tokens JWT (défaut: `your-secret-key-change-in-prod`)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export JWT_SECRET_KEY="your-secure-secret-key"
|
# Stockage
|
||||||
./pkiapi
|
export STORAGE_TYPE=memory # memory ou mongodb (défaut: memory)
|
||||||
|
export MONGO_URI=mongodb://localhost:27017
|
||||||
|
export MONGO_DB=pkiapi
|
||||||
|
|
||||||
|
# Serveur
|
||||||
|
export PORT=8080
|
||||||
|
export JWT_SECRET_KEY=your-secret-key
|
||||||
|
export GIN_MODE=release
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Exemple de flux complet
|
## Conventions de Code
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Obtenir un token
|
|
||||||
**Smoke Test Results**
|
|
||||||
|
|
||||||
- **Fichier de résultat :** `tests/smoke_result.txt` — sortie brute d'un smoke test automatisé (login → création Root CA → création Sub-CA → signature de certificat → révocation → récupération de la CRL).
|
|
||||||
- **Résumé :** le test vérifie que le flux complet fonctionne avec `STORAGE_TYPE=mongodb` (création des CA, signature, révocation) et que la CRL liste bien les certificats révoqués.
|
|
||||||
- **Reproduire localement :** démarrer la stack, puis exécuter les commandes de l'exemple de flux ci‑dessus. Vous pouvez aussi lancer le script temporaire utilisé lors des tests :
|
|
||||||
|
|
||||||
```
|
|
||||||
# reconstruire et démarrer la stack
|
|
||||||
STORAGE_TYPE=mongodb docker compose up -d --build
|
|
||||||
|
|
||||||
# exécuter manuellement l'exemple de flux (ou utiliser jq pour extraire le token)
|
|
||||||
# voir la section "Exemple de flux complet" ci‑dessus
|
|
||||||
```
|
|
||||||
|
|
||||||
Les résultats complets sont committés dans `tests/smoke_result.txt` pour référence.
|
|
||||||
|
|
||||||
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
|
|
||||||
|
|
||||||
echo "Token: $TOKEN"
|
|
||||||
|
|
||||||
# 2. Créer une Root CA
|
|
||||||
ROOT_CA=$(curl -s -X POST http://localhost:8080/api/v1/ca \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"subject":"CN=Root CA,O=Example,C=FR","validity_days":3650}')
|
|
||||||
|
|
||||||
ROOT_CA_ID=$(echo $ROOT_CA | jq -r '.ca.id')
|
|
||||||
echo "Root CA ID: $ROOT_CA_ID"
|
|
||||||
|
|
||||||
# 3. Créer une Sub-CA
|
|
||||||
SUB_CA=$(curl -s -X POST http://localhost:8080/api/v1/ca/sign \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"parent_ca_id\":\"$ROOT_CA_ID\",\"subject\":\"CN=Intermediate CA,O=Example,C=FR\",\"validity_days\":1825}")
|
|
||||||
|
|
||||||
SUB_CA_ID=$(echo $SUB_CA | jq -r '.ca.id')
|
|
||||||
echo "Sub-CA ID: $SUB_CA_ID"
|
|
||||||
|
|
||||||
# 4. Signer un certificat avec la Sub-CA
|
|
||||||
CERT=$(curl -s -X POST http://localhost:8080/api/v1/certificates/sign \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"ca_id\":\"$SUB_CA_ID\",\"subject\":\"CN=app.example.com,O=Example,C=FR\",\"validity_days\":365}")
|
|
||||||
|
|
||||||
CERT_ID=$(echo $CERT | jq -r '.certificate.id')
|
|
||||||
echo "Certificate ID: $CERT_ID"
|
|
||||||
|
|
||||||
# 5. Lister toutes les CAs
|
|
||||||
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/ca | jq .
|
|
||||||
|
|
||||||
# 6. Lister tous les certificats
|
|
||||||
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/certificates | jq .
|
|
||||||
|
|
||||||
# 7. Révoquer le certificat
|
|
||||||
curl -s -X POST http://localhost:8080/api/v1/revoke \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"certificate_id\":\"$CERT_ID\",\"reason\":\"Test\"}" | jq .
|
|
||||||
|
|
||||||
# 8. Voir la CRL
|
|
||||||
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/crl | jq .
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Structure du projet
|
|
||||||
|
|
||||||
```
|
|
||||||
pkiapi/
|
|
||||||
├── cmd/main.go # Point d'entrée
|
|
||||||
├── internal/
|
|
||||||
│ ├── api/
|
|
||||||
│ │ ├── router.go # Routes Gin
|
|
||||||
│ │ ├── auth.go # Login
|
|
||||||
│ │ ├── ca.go # Handlers CA
|
|
||||||
│ │ └── certificates.go # Handlers certificats
|
|
||||||
│ ├── auth/
|
|
||||||
│ │ ├── jwt.go # JWT manager
|
|
||||||
│ │ └── middleware.go # Middleware JWT
|
|
||||||
│ ├── pki/
|
|
||||||
│ │ ├── certificate.go # Logique X.509
|
|
||||||
│ │ └── errors.go # Erreurs PKI
|
|
||||||
│ └── storage/
|
|
||||||
│ ├── store.go # Store thread-safe
|
|
||||||
│ └── errors.go # Erreurs storage
|
|
||||||
├── go.mod
|
|
||||||
├── go.sum
|
|
||||||
├── Makefile
|
|
||||||
├── README.md
|
|
||||||
└── .gitignore
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conventions de code
|
|
||||||
|
|
||||||
- **Gestion des erreurs** : Propagation simple sans wrapper
|
- **Gestion des erreurs** : Propagation simple sans wrapper
|
||||||
- **Concurrence** : `sync.RWMutex` pour le store
|
- **Concurrence** : `sync.RWMutex` pour le store
|
||||||
- **Cryptographie** : Stdlib Go (crypto/x509, crypto/rsa, crypto/rand)
|
- **Cryptographie** : Stdlib Go (crypto/x509, crypto/rsa, crypto/rand)
|
||||||
- **JWT** : github.com/golang-jwt/jwt/v5
|
- **JWT** : github.com/golang-jwt/jwt/v5
|
||||||
|
- **MongoDB** : go.mongodb.org/mongo-driver
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Future améliorations
|
## Améliorations Futures
|
||||||
|
|
||||||
- [ ] Persistance en base de données (PostgreSQL)
|
- [x] Export des certificats (PEM, DER)
|
||||||
|
- [x] Clés privées dans JSON responses
|
||||||
- [ ] Support OCSP (Online Certificate Status Protocol)
|
- [ ] Support OCSP (Online Certificate Status Protocol)
|
||||||
- [ ] Interface web pour gérer les CAs
|
- [ ] Interface web pour gérer les CAs
|
||||||
- [ ] Export des certificats (PEM, DER)
|
- [ ] Auditing et logging complet
|
||||||
- [ ] Support des chaînes intermédiaires
|
|
||||||
- [ ] Auditing et logging
|
|
||||||
- [ ] Rate limiting et throttling
|
- [ ] Rate limiting et throttling
|
||||||
|
- [ ] Support HSM (Hardware Security Module)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -5,6 +5,8 @@ go 1.21
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.1.0
|
github.com/golang-jwt/jwt/v5 v5.1.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -17,7 +19,6 @@ require (
|
||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.16.7 // indirect
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
|
@ -33,7 +34,6 @@ require (
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.26.0 // indirect
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/net v0.21.0 // indirect
|
golang.org/x/net v0.21.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ type CAResponse struct {
|
||||||
NotAfter string `json:"not_after"`
|
NotAfter string `json:"not_after"`
|
||||||
SerialNumber string `json:"serial_number"`
|
SerialNumber string `json:"serial_number"`
|
||||||
Certificate string `json:"certificate"` // Base64 encoded
|
Certificate string `json:"certificate"` // Base64 encoded
|
||||||
|
PrivateKey string `json:"private_key,omitempty"` // Base64 encoded (optional)
|
||||||
IsCA bool `json:"is_ca"`
|
IsCA bool `json:"is_ca"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +112,14 @@ func CreateCA(c *gin.Context) {
|
||||||
response.Certificate = base64.StdEncoding.EncodeToString(ca.Cert.Raw)
|
response.Certificate = base64.StdEncoding.EncodeToString(ca.Cert.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ajouter la clé privée
|
||||||
|
if ca.PrivateKey != nil {
|
||||||
|
privKeyBase64, err := encodePrivateKey(ca.PrivateKey)
|
||||||
|
if err == nil {
|
||||||
|
response.PrivateKey = privKeyBase64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, gin.H{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
"ca": response,
|
"ca": response,
|
||||||
"created_by": userID,
|
"created_by": userID,
|
||||||
|
|
@ -145,6 +154,14 @@ func GetCA(c *gin.Context) {
|
||||||
response.Certificate = base64.StdEncoding.EncodeToString(ca.Cert.Raw)
|
response.Certificate = base64.StdEncoding.EncodeToString(ca.Cert.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ajouter la clé privée
|
||||||
|
if ca.PrivateKey != nil {
|
||||||
|
privKeyBase64, err := encodePrivateKey(ca.PrivateKey)
|
||||||
|
if err == nil {
|
||||||
|
response.PrivateKey = privKeyBase64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"ca": response})
|
c.JSON(http.StatusOK, gin.H{"ca": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,6 +227,14 @@ func SignCertificateWithCA(c *gin.Context) {
|
||||||
response.Certificate = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
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{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
"certificate": response,
|
"certificate": response,
|
||||||
"signed_by": req.CAId,
|
"signed_by": req.CAId,
|
||||||
|
|
@ -278,6 +303,14 @@ func SignSubCA(c *gin.Context) {
|
||||||
response.Certificate = base64.StdEncoding.EncodeToString(subCA.Cert.Raw)
|
response.Certificate = base64.StdEncoding.EncodeToString(subCA.Cert.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ajouter la clé privée
|
||||||
|
if subCA.PrivateKey != nil {
|
||||||
|
privKeyBase64, err := encodePrivateKey(subCA.PrivateKey)
|
||||||
|
if err == nil {
|
||||||
|
response.PrivateKey = privKeyBase64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, gin.H{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
"ca": response,
|
"ca": response,
|
||||||
"signed_by": req.ParentCAId,
|
"signed_by": req.ParentCAId,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
@ -25,9 +27,24 @@ type CertificateResponse struct {
|
||||||
NotAfter string `json:"not_after"`
|
NotAfter string `json:"not_after"`
|
||||||
SerialNumber string `json:"serial_number"`
|
SerialNumber string `json:"serial_number"`
|
||||||
Certificate string `json:"certificate"` // Base64 encoded
|
Certificate string `json:"certificate"` // Base64 encoded
|
||||||
|
PrivateKey string `json:"private_key,omitempty"` // Base64 encoded (optional)
|
||||||
Revoked bool `json:"revoked"`
|
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
|
// certificateStore est un store global pour les certificats
|
||||||
var certificateStore storage.CertificateStore
|
var certificateStore storage.CertificateStore
|
||||||
|
|
||||||
|
|
@ -79,6 +96,14 @@ func CreateCertificate(c *gin.Context) {
|
||||||
response.Certificate = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
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{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
"certificate": response,
|
"certificate": response,
|
||||||
"created_by": userID,
|
"created_by": userID,
|
||||||
|
|
@ -132,6 +157,14 @@ func GetCertificate(c *gin.Context) {
|
||||||
response.Certificate = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
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})
|
c.JSON(http.StatusOK, gin.H{"certificate": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,3 +215,153 @@ func GetCRL(c *gin.Context) {
|
||||||
"version": 1,
|
"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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stef/pkiapi/internal/pki"
|
||||||
|
"github.com/stef/pkiapi/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCRLHandler(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
// Préparer un MemoryStore et l'initialiser dans l'API
|
||||||
|
mem := storage.NewMemoryStore()
|
||||||
|
InitCertificateStore(mem)
|
||||||
|
|
||||||
|
// Générer un certificat de test
|
||||||
|
cert, err := pki.GenerateCertificate("CN=test.example.com,O=Example,C=FR", 365)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateCertificate error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := uuid.New().String()
|
||||||
|
cert.ID = id
|
||||||
|
|
||||||
|
// Sauvegarder le certificat (non révoqué)
|
||||||
|
if err := mem.SaveCertificate(id, cert); err != nil {
|
||||||
|
t.Fatalf("SaveCertificate error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Révoquer le certificat
|
||||||
|
cert.Revoked = true
|
||||||
|
if err := mem.SaveCertificate(id, cert); err != nil {
|
||||||
|
t.Fatalf("SaveCertificate(revoked) error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appeler le handler GetCRL
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
req, _ := http.NewRequest("GET", "/api/v1/crl", nil)
|
||||||
|
c.Request = req
|
||||||
|
|
||||||
|
GetCRL(c)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d, body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
CRL []struct {
|
||||||
|
SerialNumber string `json:"serial_number"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
} `json:"crl"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.CRL) != 1 {
|
||||||
|
t.Fatalf("expected 1 revoked cert in CRL, got %d, body: %s", len(resp.CRL), w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.CRL[0].SerialNumber == "" {
|
||||||
|
t.Fatalf("expected serial number present in CRL entry, got empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,12 @@ func RegisterRoutesWithStore(router *gin.Engine, caStore storage.CertificateStor
|
||||||
v1.GET("/certificates/:id", GetCertificate)
|
v1.GET("/certificates/:id", GetCertificate)
|
||||||
v1.POST("/revoke", RevokeCertificate)
|
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
|
// Endpoints CRL
|
||||||
v1.GET("/crl", GetCRL)
|
v1.GET("/crl", GetCRL)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ type CertificateDoc struct {
|
||||||
IsCA bool `bson:"is_ca"`
|
IsCA bool `bson:"is_ca"`
|
||||||
Revoked bool `bson:"revoked"`
|
Revoked bool `bson:"revoked"`
|
||||||
Cert string `bson:"cert"` // Base64 encoded certificate
|
Cert string `bson:"cert"` // Base64 encoded certificate
|
||||||
PrivateKey string `bson:"private_key"` // Base64 encoded private key (only for CAs)
|
PrivateKey string `bson:"private_key"` // Base64 encoded private key (for all certificates)
|
||||||
CreatedAt time.Time `bson:"created_at"`
|
CreatedAt time.Time `bson:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,8 +87,8 @@ func (m *MongoStore) SaveCertificate(id string, cert *pki.Certificate) error {
|
||||||
doc.Cert = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
doc.Cert = base64.StdEncoding.EncodeToString(cert.Cert.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encoder la clé privée en base64 (seulement pour les CAs)
|
// Encoder la clé privée en base64 (pour tous les certificats)
|
||||||
if cert.PrivateKey != nil && cert.IsCA {
|
if cert.PrivateKey != nil {
|
||||||
privKeyBytes, err := marshalPrivateKey(cert.PrivateKey)
|
privKeyBytes, err := marshalPrivateKey(cert.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Tests PKI API
|
||||||
|
|
||||||
|
Scripts de test pour vérifier les fonctionnalités de l'API PKI.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Docker Compose en cours d'exécution (API + MongoDB)
|
||||||
|
- `jq` et `curl` installés
|
||||||
|
- La stack accessible sur `http://localhost:8080`
|
||||||
|
|
||||||
|
## Scripts disponibles
|
||||||
|
|
||||||
|
### test_complete.sh
|
||||||
|
Test complet end-to-end de toutes les fonctionnalités:
|
||||||
|
- Création de Root CA
|
||||||
|
- Création de Sub-CA
|
||||||
|
- Création de certificat standard
|
||||||
|
- Exports (PEM, DER, avec clé privée, chaîne)
|
||||||
|
- Révocation de certificat
|
||||||
|
- Génération de CRL
|
||||||
|
- Vérification du stockage MongoDB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_complete.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### test_exports.sh
|
||||||
|
Test spécifique des formats d'export de certificats:
|
||||||
|
- PEM
|
||||||
|
- DER
|
||||||
|
- PEM avec clé privée
|
||||||
|
- Chaîne de certificats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_exports.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### test_private_keys.sh
|
||||||
|
Test de vérification du stockage des clés privées:
|
||||||
|
- Création de certificat standard (non-CA)
|
||||||
|
- Création de CA
|
||||||
|
- Export avec clés privées
|
||||||
|
- Vérification MongoDB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_private_keys.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exécuter tous les tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tests/
|
||||||
|
bash test_complete.sh && bash test_exports.sh && bash test_private_keys.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Résultats attendus
|
||||||
|
|
||||||
|
Tous les tests doivent afficher `✓` pour chaque étape:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== PKI Complete Feature Test ===
|
||||||
|
[1] Login...
|
||||||
|
✓ Login successful
|
||||||
|
[2] Creating Root CA...
|
||||||
|
✓ Root CA created: <uuid>
|
||||||
|
...
|
||||||
|
=== All tests passed! ===
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers exportés
|
||||||
|
|
||||||
|
Les tests créent des fichiers d'export temporaires dans:
|
||||||
|
- `/tmp/pki_exports_test/` (test_exports.sh)
|
||||||
|
- `/tmp/` (test_private_keys.sh)
|
||||||
|
|
||||||
|
## Dépannage
|
||||||
|
|
||||||
|
**Erreur de connexion au serveur**:
|
||||||
|
```bash
|
||||||
|
# Vérifier que la stack est lancée
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Redémarrer si nécessaire
|
||||||
|
docker compose down && docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erreur MongoDB**:
|
||||||
|
```bash
|
||||||
|
# Vérifier la connexion MongoDB
|
||||||
|
docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.count()"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erreur jq**:
|
||||||
|
```bash
|
||||||
|
# Installer jq si nécessaire
|
||||||
|
sudo apt-get install jq
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
API_URL="http://localhost:8080/api/v1"
|
||||||
|
EXPORT_DIR="/tmp/pki_complete_test"
|
||||||
|
mkdir -p "$EXPORT_DIR"
|
||||||
|
|
||||||
|
echo "=== PKI Complete Feature Test ==="
|
||||||
|
echo "Date: $(date)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Login
|
||||||
|
TOKEN=$(curl -s -X POST "$API_URL/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
|
||||||
|
echo "[✓] Login successful"
|
||||||
|
|
||||||
|
# 2. Create Root CA
|
||||||
|
ROOT_CA=$(curl -s -X POST "$API_URL/ca" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"subject":"CN=Root CA,O=Example,C=FR","validity_days":3650}')
|
||||||
|
ROOT_CA_ID=$(echo $ROOT_CA | jq -r '.ca.id')
|
||||||
|
echo "[✓] Root CA created: $ROOT_CA_ID"
|
||||||
|
|
||||||
|
# 3. Create Sub-CA
|
||||||
|
SUB_CA=$(curl -s -X POST "$API_URL/ca/sign" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"parent_ca_id\":\"$ROOT_CA_ID\",\"subject\":\"CN=Intermediate CA,O=Example,C=FR\",\"validity_days\":1825}")
|
||||||
|
SUB_CA_ID=$(echo $SUB_CA | jq -r '.ca.id')
|
||||||
|
echo "[✓] Sub-CA created: $SUB_CA_ID"
|
||||||
|
|
||||||
|
# 4. Create standard certificate (non-CA)
|
||||||
|
CERT=$(curl -s -X POST "$API_URL/certificates/sign" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"ca_id\":\"$SUB_CA_ID\",\"subject\":\"CN=app.example.com,O=Example,C=FR\",\"validity_days\":365}")
|
||||||
|
CERT_ID=$(echo $CERT | jq -r '.certificate.id')
|
||||||
|
echo "[✓] Standard certificate created: $CERT_ID"
|
||||||
|
|
||||||
|
# 5. Test PEM export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem" \
|
||||||
|
-o "$EXPORT_DIR/cert.pem"
|
||||||
|
echo "[✓] PEM export: $(stat -c%s "$EXPORT_DIR/cert.pem") bytes"
|
||||||
|
|
||||||
|
# 6. Test DER export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/der" \
|
||||||
|
-o "$EXPORT_DIR/cert.der"
|
||||||
|
echo "[✓] DER export: $(stat -c%s "$EXPORT_DIR/cert.der") bytes"
|
||||||
|
|
||||||
|
# 7. Test PEM with private key (for standard cert)
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem-with-key" \
|
||||||
|
-o "$EXPORT_DIR/cert_with_key.pem"
|
||||||
|
KEY_COUNT=$(grep -c "BEGIN PRIVATE KEY" "$EXPORT_DIR/cert_with_key.pem" 2>/dev/null || echo "0")
|
||||||
|
echo "[✓] PEM with key export: $(stat -c%s "$EXPORT_DIR/cert_with_key.pem") bytes ($KEY_COUNT private keys)"
|
||||||
|
|
||||||
|
# 8. Test chain export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/chain" \
|
||||||
|
-o "$EXPORT_DIR/cert_chain.pem"
|
||||||
|
CHAIN_COUNT=$(grep -c "BEGIN CERTIFICATE" "$EXPORT_DIR/cert_chain.pem" 2>/dev/null || echo "0")
|
||||||
|
echo "[✓] Chain export: $(stat -c%s "$EXPORT_DIR/cert_chain.pem") bytes ($CHAIN_COUNT certificates)"
|
||||||
|
|
||||||
|
# 9. Test revocation
|
||||||
|
REV=$(curl -s -X POST "$API_URL/revoke" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"certificate_id\":\"$CERT_ID\",\"reason\":\"Test\"}")
|
||||||
|
echo "[✓] Certificate revoked"
|
||||||
|
|
||||||
|
# 10. Test CRL
|
||||||
|
CRL=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/crl" | jq '.crl | length')
|
||||||
|
echo "[✓] CRL contains $CRL revoked certificates"
|
||||||
|
|
||||||
|
# 11. Test MongoDB private key storage
|
||||||
|
MONGO_CHECK=$(docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.count({private_key: {\$exists: true, \$ne: ''}})" 2>/dev/null | tail -1)
|
||||||
|
echo "[✓] MongoDB: $MONGO_CHECK certificates with stored private keys"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All tests passed! ==="
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test complet des fonctionnalités PKI
|
||||||
|
# Teste: création CA, sous-CA, certificats, exports, révocation, CRL
|
||||||
|
|
||||||
|
API_URL="http://localhost:8080/api/v1"
|
||||||
|
EXPORT_DIR="/tmp/pki_exports_test"
|
||||||
|
mkdir -p "$EXPORT_DIR"
|
||||||
|
|
||||||
|
echo "=== PKI Complete Feature Test ==="
|
||||||
|
echo "Date: $(date)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Login
|
||||||
|
echo "[1] Login..."
|
||||||
|
TOKEN=$(curl -s -X POST "$API_URL/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||||
|
echo "❌ Login failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Login successful"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Create Root CA
|
||||||
|
echo "[2] Creating Root CA..."
|
||||||
|
ROOT_CA=$(curl -s -X POST "$API_URL/ca" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"subject":"CN=Root CA,O=Example,C=FR","validity_days":3650}')
|
||||||
|
ROOT_CA_ID=$(echo $ROOT_CA | jq -r '.ca.id')
|
||||||
|
echo "✓ Root CA created: $ROOT_CA_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Create Sub-CA
|
||||||
|
echo "[3] Creating Sub-CA..."
|
||||||
|
SUB_CA=$(curl -s -X POST "$API_URL/ca/sign" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"parent_ca_id\":\"$ROOT_CA_ID\",\"subject\":\"CN=Intermediate CA,O=Example,C=FR\",\"validity_days\":1825}")
|
||||||
|
SUB_CA_ID=$(echo $SUB_CA | jq -r '.ca.id')
|
||||||
|
echo "✓ Sub-CA created: $SUB_CA_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Create standard certificate
|
||||||
|
echo "[4] Creating standard certificate..."
|
||||||
|
CERT=$(curl -s -X POST "$API_URL/certificates/sign" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"ca_id\":\"$SUB_CA_ID\",\"subject\":\"CN=app.example.com,O=Example,C=FR\",\"validity_days\":365}")
|
||||||
|
CERT_ID=$(echo $CERT | jq -r '.certificate.id')
|
||||||
|
echo "✓ Standard certificate created: $CERT_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. Test PEM export
|
||||||
|
echo "[5] Testing exports..."
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem" \
|
||||||
|
-o "$EXPORT_DIR/cert.pem"
|
||||||
|
PEM_SIZE=$(stat -c%s "$EXPORT_DIR/cert.pem")
|
||||||
|
echo "✓ PEM export: $PEM_SIZE bytes"
|
||||||
|
|
||||||
|
# 6. Test DER export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/der" \
|
||||||
|
-o "$EXPORT_DIR/cert.der"
|
||||||
|
DER_SIZE=$(stat -c%s "$EXPORT_DIR/cert.der")
|
||||||
|
echo "✓ DER export: $DER_SIZE bytes"
|
||||||
|
|
||||||
|
# 7. Test PEM with private key
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem-with-key" \
|
||||||
|
-o "$EXPORT_DIR/cert_with_key.pem"
|
||||||
|
KEY_SIZE=$(stat -c%s "$EXPORT_DIR/cert_with_key.pem")
|
||||||
|
KEY_COUNT=$(grep -c "BEGIN PRIVATE KEY" "$EXPORT_DIR/cert_with_key.pem" 2>/dev/null || echo "0")
|
||||||
|
echo "✓ PEM with private key export: $KEY_SIZE bytes ($KEY_COUNT private keys)"
|
||||||
|
|
||||||
|
# 8. Test chain export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/chain" \
|
||||||
|
-o "$EXPORT_DIR/cert_chain.pem"
|
||||||
|
CHAIN_SIZE=$(stat -c%s "$EXPORT_DIR/cert_chain.pem")
|
||||||
|
CHAIN_COUNT=$(grep -c "BEGIN CERTIFICATE" "$EXPORT_DIR/cert_chain.pem" 2>/dev/null || echo "0")
|
||||||
|
echo "✓ Chain export: $CHAIN_SIZE bytes ($CHAIN_COUNT certificates)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 9. Test revocation
|
||||||
|
echo "[6] Revoking certificate..."
|
||||||
|
REV=$(curl -s -X POST "$API_URL/revoke" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"certificate_id\":\"$CERT_ID\",\"reason\":\"Test\"}")
|
||||||
|
echo "✓ Certificate revoked"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 10. Test CRL
|
||||||
|
echo "[7] Checking CRL..."
|
||||||
|
CRL=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/crl" | jq '.crl | length')
|
||||||
|
echo "✓ CRL contains $CRL revoked certificates"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 11. Test MongoDB private key storage
|
||||||
|
echo "[8] Verifying MongoDB storage..."
|
||||||
|
MONGO_CERT_COUNT=$(docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.count({private_key: {\$exists: true, \$ne: ''}})" 2>/dev/null | tail -1)
|
||||||
|
echo "✓ MongoDB: $MONGO_CERT_COUNT certificates with stored private keys"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== All tests passed! ==="
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test spécifique pour les exports de certificats
|
||||||
|
# Teste: PEM, DER, PEM with key, chain
|
||||||
|
|
||||||
|
API_URL="http://localhost:8080/api/v1"
|
||||||
|
EXPORT_DIR="/tmp/pki_export_test"
|
||||||
|
mkdir -p "$EXPORT_DIR"
|
||||||
|
|
||||||
|
echo "=== PKI Certificate Export Test ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Login
|
||||||
|
TOKEN=$(curl -s -X POST "$API_URL/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
|
||||||
|
echo "[1] Token obtained"
|
||||||
|
|
||||||
|
# 2. Create Root CA
|
||||||
|
CA=$(curl -s -X POST "$API_URL/ca" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"subject":"CN=Export Test CA,O=Test,C=FR","validity_days":3650}')
|
||||||
|
CA_ID=$(echo $CA | jq -r '.ca.id')
|
||||||
|
echo "[2] Root CA created: $CA_ID"
|
||||||
|
|
||||||
|
# 3. Create certificate signed by CA
|
||||||
|
CERT=$(curl -s -X POST "$API_URL/certificates/sign" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"ca_id\":\"$CA_ID\",\"subject\":\"CN=test.example.com,O=Test,C=FR\",\"validity_days\":365}")
|
||||||
|
CERT_ID=$(echo $CERT | jq -r '.certificate.id')
|
||||||
|
echo "[3] Certificate created: $CERT_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test all export formats
|
||||||
|
echo "Testing export formats:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# PEM export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem" \
|
||||||
|
-o "$EXPORT_DIR/cert.pem"
|
||||||
|
if grep -q "BEGIN CERTIFICATE" "$EXPORT_DIR/cert.pem"; then
|
||||||
|
SIZE=$(stat -c%s "$EXPORT_DIR/cert.pem")
|
||||||
|
echo "✓ PEM export: $SIZE bytes"
|
||||||
|
else
|
||||||
|
echo "❌ PEM export failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DER export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/der" \
|
||||||
|
-o "$EXPORT_DIR/cert.der"
|
||||||
|
SIZE=$(stat -c%s "$EXPORT_DIR/cert.der")
|
||||||
|
if [ "$SIZE" -gt 0 ]; then
|
||||||
|
echo "✓ DER export: $SIZE bytes"
|
||||||
|
else
|
||||||
|
echo "❌ DER export failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PEM with private key export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem-with-key" \
|
||||||
|
-o "$EXPORT_DIR/cert_with_key.pem"
|
||||||
|
SIZE=$(stat -c%s "$EXPORT_DIR/cert_with_key.pem")
|
||||||
|
KEY_COUNT=$(grep -c "BEGIN PRIVATE KEY" "$EXPORT_DIR/cert_with_key.pem" 2>/dev/null || echo "0")
|
||||||
|
if [ "$KEY_COUNT" -gt 0 ]; then
|
||||||
|
CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" "$EXPORT_DIR/cert_with_key.pem")
|
||||||
|
echo "✓ PEM with key export: $SIZE bytes ($CERT_COUNT certs + $KEY_COUNT keys)"
|
||||||
|
else
|
||||||
|
echo "❌ PEM with key export failed (no private key)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Chain export
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/chain" \
|
||||||
|
-o "$EXPORT_DIR/cert_chain.pem"
|
||||||
|
SIZE=$(stat -c%s "$EXPORT_DIR/cert_chain.pem")
|
||||||
|
CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" "$EXPORT_DIR/cert_chain.pem" 2>/dev/null || echo "0")
|
||||||
|
if [ "$CERT_COUNT" -ge 2 ]; then
|
||||||
|
echo "✓ Chain export: $SIZE bytes ($CERT_COUNT certificates)"
|
||||||
|
else
|
||||||
|
echo "❌ Chain export failed (expected 2+ certs, got $CERT_COUNT)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All exports completed. Files saved in: $EXPORT_DIR"
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
API_URL="http://localhost:8080/api/v1"
|
||||||
|
EXPORT_DIR="/tmp/pki_privkey_test"
|
||||||
|
mkdir -p "$EXPORT_DIR"
|
||||||
|
|
||||||
|
echo "=== Test: Private Key Storage for All Certificates ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Login
|
||||||
|
TOKEN=$(curl -s -X POST "$API_URL/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
|
||||||
|
echo "[1] Token obtenu"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Créer un certificat standard (non-CA)
|
||||||
|
CERT_RESP=$(curl -s -X POST "$API_URL/certificates" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"subject":"CN=test-standard.example.com,O=Test,C=FR",
|
||||||
|
"validity_days":365
|
||||||
|
}')
|
||||||
|
|
||||||
|
CERT_ID=$(echo $CERT_RESP | jq -r '.certificate.id')
|
||||||
|
echo "[2] Certificat standard créé: $CERT_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Exporter avec clé privée
|
||||||
|
echo "[3] Test export PEM+clé pour certificat standard..."
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem-with-key" \
|
||||||
|
-o "$EXPORT_DIR/standard_cert_with_key.pem"
|
||||||
|
|
||||||
|
if [ -f "$EXPORT_DIR/standard_cert_with_key.pem" ]; then
|
||||||
|
FILE_SIZE=$(stat -c%s "$EXPORT_DIR/standard_cert_with_key.pem")
|
||||||
|
CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" "$EXPORT_DIR/standard_cert_with_key.pem" 2>/dev/null || echo "0")
|
||||||
|
KEY_COUNT=$(grep -c "BEGIN PRIVATE KEY" "$EXPORT_DIR/standard_cert_with_key.pem" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$FILE_SIZE" -gt 100 ] && [ "$KEY_COUNT" -gt 0 ]; then
|
||||||
|
echo "✓ SUCCESS: Clé privée présente dans l'export!"
|
||||||
|
echo " - Taille du fichier: $FILE_SIZE bytes"
|
||||||
|
echo " - Certificats trouvés: $CERT_COUNT"
|
||||||
|
echo " - Clés privées trouvées: $KEY_COUNT"
|
||||||
|
echo ""
|
||||||
|
echo " Aperçu:"
|
||||||
|
head -3 "$EXPORT_DIR/standard_cert_with_key.pem"
|
||||||
|
echo " ..."
|
||||||
|
else
|
||||||
|
echo "❌ FAILED: Pas de clé privée trouvée"
|
||||||
|
cat "$EXPORT_DIR/standard_cert_with_key.pem"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ FAILED: Fichier non créé"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Vérifier directement dans MongoDB
|
||||||
|
echo "[4] Vérification directe dans MongoDB..."
|
||||||
|
MONGO_COUNT=$(docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.findOne({_id: '$CERT_ID'}).private_key ? 'HAS_KEY' : 'NO_KEY'" 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
if [ "$MONGO_COUNT" = "HAS_KEY" ]; then
|
||||||
|
echo "✓ Clé privée présente dans MongoDB pour le certificat standard"
|
||||||
|
else
|
||||||
|
echo "❌ Clé privée absente dans MongoDB"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. Créer une CA et vérifier aussi
|
||||||
|
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')
|
||||||
|
echo "[5] CA créée: $CA_ID"
|
||||||
|
|
||||||
|
MONGO_CA_COUNT=$(docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.findOne({_id: '$CA_ID'}).private_key ? 'HAS_KEY' : 'NO_KEY'" 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
if [ "$MONGO_CA_COUNT" = "HAS_KEY" ]; then
|
||||||
|
echo "✓ Clé privée présente dans MongoDB pour la CA"
|
||||||
|
else
|
||||||
|
echo "❌ Clé privée absente pour la CA"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== Test complété ==="
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test spécifique pour le stockage des clés privées
|
||||||
|
# Vérifie que les clés privées sont stockées pour tous les certificats
|
||||||
|
|
||||||
|
API_URL="http://localhost:8080/api/v1"
|
||||||
|
|
||||||
|
echo "=== PKI Private Key Storage Test ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Login
|
||||||
|
TOKEN=$(curl -s -X POST "$API_URL/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
|
||||||
|
echo "[1] Login successful"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Create standard certificate (non-CA)
|
||||||
|
echo "[2] Creating standard certificate..."
|
||||||
|
CERT=$(curl -s -X POST "$API_URL/certificates" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"subject":"CN=test-standard.example.com,O=Test,C=FR","validity_days":365}')
|
||||||
|
CERT_ID=$(echo $CERT | jq -r '.certificate.id')
|
||||||
|
echo "✓ Certificate created: $CERT_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Create Root CA
|
||||||
|
echo "[3] Creating Root CA..."
|
||||||
|
CA=$(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 | jq -r '.ca.id')
|
||||||
|
echo "✓ Root CA created: $CA_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Test export with private key for standard cert
|
||||||
|
echo "[4] Testing private key export for standard certificate..."
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CERT_ID/export/pem-with-key" \
|
||||||
|
-o /tmp/cert_test.pem
|
||||||
|
|
||||||
|
KEY_COUNT=$(grep -c "BEGIN PRIVATE KEY" /tmp/cert_test.pem 2>/dev/null || echo "0")
|
||||||
|
CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" /tmp/cert_test.pem 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$KEY_COUNT" -gt 0 ]; then
|
||||||
|
SIZE=$(stat -c%s /tmp/cert_test.pem)
|
||||||
|
echo "✓ SUCCESS: Standard certificate has private key"
|
||||||
|
echo " - Export size: $SIZE bytes"
|
||||||
|
echo " - Certificates: $CERT_COUNT"
|
||||||
|
echo " - Private keys: $KEY_COUNT"
|
||||||
|
else
|
||||||
|
echo "❌ FAILED: Standard certificate has no private key"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. Test export with private key for CA
|
||||||
|
echo "[5] Testing private key export for CA..."
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/certificates/$CA_ID/export/pem-with-key" \
|
||||||
|
-o /tmp/ca_test.pem
|
||||||
|
|
||||||
|
KEY_COUNT=$(grep -c "BEGIN PRIVATE KEY" /tmp/ca_test.pem 2>/dev/null || echo "0")
|
||||||
|
CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" /tmp/ca_test.pem 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$KEY_COUNT" -gt 0 ]; then
|
||||||
|
SIZE=$(stat -c%s /tmp/ca_test.pem)
|
||||||
|
echo "✓ SUCCESS: CA has private key"
|
||||||
|
echo " - Export size: $SIZE bytes"
|
||||||
|
echo " - Certificates: $CERT_COUNT"
|
||||||
|
echo " - Private keys: $KEY_COUNT"
|
||||||
|
else
|
||||||
|
echo "❌ FAILED: CA has no private key"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 6. Verify MongoDB storage
|
||||||
|
echo "[6] Verifying MongoDB storage..."
|
||||||
|
MONGO_STANDARD=$(docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.findOne({_id: '$CERT_ID'}).private_key ? 'YES' : 'NO'" 2>/dev/null | tail -1)
|
||||||
|
MONGO_CA=$(docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.findOne({_id: '$CA_ID'}).private_key ? 'YES' : 'NO'" 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
if [ "$MONGO_STANDARD" = "YES" ]; then
|
||||||
|
echo "✓ Standard certificate private key stored in MongoDB"
|
||||||
|
else
|
||||||
|
echo "❌ Standard certificate private key NOT in MongoDB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MONGO_CA" = "YES" ]; then
|
||||||
|
echo "✓ CA private key stored in MongoDB"
|
||||||
|
else
|
||||||
|
echo "❌ CA private key NOT in MongoDB"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
TOTAL=$(docker exec pkiapi-mongo mongosh -u admin -p password --authenticationDatabase admin pkiapi --eval "db.certificates.count({private_key: {\$exists: true, \$ne: ''}})" 2>/dev/null | tail -1)
|
||||||
|
echo "Total certificates with private keys in MongoDB: $TOTAL"
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
=== Test Results: Private Key Storage & Certificate Exports ===
|
||||||
|
Date: 2025-12-07 09:50 UTC+1
|
||||||
|
|
||||||
|
## Test 1: Complete Features
|
||||||
|
- Login: ✓ successful
|
||||||
|
- Root CA creation: ✓ successful
|
||||||
|
- Sub-CA creation: ✓ successful
|
||||||
|
- Standard certificate creation: ✓ successful
|
||||||
|
- PEM export: ✓ 1115 bytes
|
||||||
|
- DER export: ✓ 781 bytes
|
||||||
|
- PEM with private key: ✓ 2819 bytes (1 private key)
|
||||||
|
- Chain export: ✓ 2283 bytes (2 certificates)
|
||||||
|
- Certificate revocation: ✓ successful
|
||||||
|
- CRL retrieval: ✓ 1 revoked certificate
|
||||||
|
- MongoDB private key storage: ✓ 7 certificates with keys
|
||||||
|
|
||||||
|
## Test 2: Certificate Exports
|
||||||
|
- PEM export: ✓ 1107 bytes
|
||||||
|
- DER export: ✓ 775 bytes
|
||||||
|
- PEM with private key: ✓ 2811 bytes (1 cert + 1 key)
|
||||||
|
- Chain export: ✓ 2230 bytes (2 certificates)
|
||||||
|
|
||||||
|
## Test 3: Private Key Storage
|
||||||
|
- Standard certificate private key: ✓ stored in MongoDB
|
||||||
|
- Standard certificate export: ✓ includes private key
|
||||||
|
- CA private key: ✓ stored in MongoDB
|
||||||
|
- CA export: ✓ includes private key
|
||||||
|
- Total certificates with private keys: 11
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
✅ All features working correctly:
|
||||||
|
- Certificate generation (standard & CA)
|
||||||
|
- Hierarchical CA support (Root → Sub-CA)
|
||||||
|
- Certificate signing
|
||||||
|
- Certificate revocation
|
||||||
|
- CRL generation
|
||||||
|
- Private key storage for ALL certificates
|
||||||
|
- Multiple export formats (PEM, DER, with key, chain)
|
||||||
|
- MongoDB persistence
|
||||||
|
|
||||||
|
## Test Scripts
|
||||||
|
- tests/test_complete.sh: Full end-to-end test
|
||||||
|
- tests/test_exports.sh: Certificate export formats
|
||||||
|
- tests/test_private_keys.sh: Private key storage verification
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Web UI (Scaffold)
|
||||||
|
|
||||||
|
Squelette pour une future interface web destinée à gérer les Autorités de Certification (CAs) et les certificats.
|
||||||
|
|
||||||
|
Objectifs initiaux:
|
||||||
|
- Interface d'administration pour créer/voir/révoquer CAs et certificats
|
||||||
|
- Pages principales:
|
||||||
|
- Dashboard (liste CAs et certificats)
|
||||||
|
- Détails CA (show, export, créer Sub-CA)
|
||||||
|
- Détails Certificat (show, export, révoquer)
|
||||||
|
- Formulaires: création CA, création certificat, signature par CA
|
||||||
|
- Auth via JWT: le front transmettra le token dans `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
Technos envisagées:
|
||||||
|
- Front: Vue 3 / React (au choix)
|
||||||
|
- UI: TailwindCSS ou Bootstrap
|
||||||
|
- API: consommé via endpoints existants (`/api/v1/...`)
|
||||||
|
|
||||||
|
Ce répertoire contient un fichier `index.html` minimal pour tests locaux.
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>PKI API - Web UI</title>
|
||||||
|
<link rel="stylesheet" href="./style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a class="skip-link" href="#main">Aller au contenu</a>
|
||||||
|
<header>
|
||||||
|
<h1>PKI API - Web UI (Minimal)</h1>
|
||||||
|
<div class="token-row">
|
||||||
|
<label for="token">JWT Token :</label>
|
||||||
|
<input id="token" aria-label="JWT Token" placeholder="Entrez le token JWT ici" />
|
||||||
|
<button id="saveToken">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
<nav role="navigation" aria-label="Navigation principale">
|
||||||
|
<button id="btnCAs" aria-controls="list">Liste des CAs</button>
|
||||||
|
<button id="btnCerts" aria-controls="list">Liste des certificats</button>
|
||||||
|
<button id="btnCreateCA" aria-controls="form">Créer CA</button>
|
||||||
|
<button id="btnCreateCert" aria-controls="form">Créer Certificat</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1">
|
||||||
|
<div id="message" role="status" aria-live="polite" aria-atomic="true" class="card hidden"></div>
|
||||||
|
|
||||||
|
<section id="list" class="card hidden" aria-live="polite"></section>
|
||||||
|
|
||||||
|
<section id="form" class="card hidden"></section>
|
||||||
|
|
||||||
|
<section id="details" class="card hidden" aria-live="polite"></section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<small>Consomme l'API sur <code>/api/v1</code>. Assurez-vous d'entrer un token JWT valide.</small>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
(function(){
|
||||||
|
const API_BASE = '/api/v1';
|
||||||
|
let token = localStorage.getItem('pki_token') || '';
|
||||||
|
|
||||||
|
function $(id){ return document.getElementById(id); }
|
||||||
|
function show(el){ el.classList.remove('hidden'); }
|
||||||
|
function hide(el){ el.classList.add('hidden'); }
|
||||||
|
|
||||||
|
function showMessage(msg, isError){
|
||||||
|
const m = $('message');
|
||||||
|
m.textContent = msg;
|
||||||
|
m.className = 'card';
|
||||||
|
if(isError) m.classList.add('error');
|
||||||
|
show(m);
|
||||||
|
setTimeout(()=>{ hide(m); }, 7000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiFetch(path, opts={}){
|
||||||
|
opts.headers = opts.headers || {};
|
||||||
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
|
if(token) opts.headers['Authorization'] = 'Bearer ' + token;
|
||||||
|
return fetch(API_BASE + path, opts).then(async res => {
|
||||||
|
const ct = res.headers.get('content-type') || '';
|
||||||
|
if(!res.ok){
|
||||||
|
let body = await res.text();
|
||||||
|
throw new Error(body || res.statusText);
|
||||||
|
}
|
||||||
|
if(ct.includes('application/json')) return res.json();
|
||||||
|
return res.blob();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(items, type){
|
||||||
|
const list = $('list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
const h = document.createElement('h3');
|
||||||
|
h.textContent = type === 'ca' ? 'Autorités de Certification' : 'Certificats';
|
||||||
|
list.appendChild(h);
|
||||||
|
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.className = 'table';
|
||||||
|
const thead = document.createElement('thead');
|
||||||
|
const headRow = document.createElement('tr');
|
||||||
|
['ID','Sujet','Émetteur','Not After','Actions'].forEach(t=>{ const th=document.createElement('th'); th.textContent=t; headRow.appendChild(th); });
|
||||||
|
thead.appendChild(headRow);
|
||||||
|
table.appendChild(thead);
|
||||||
|
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
items.forEach(it=>{
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const idCell = document.createElement('td'); idCell.textContent = it.id || it.ID || it.Id || '';
|
||||||
|
const subj = document.createElement('td'); subj.textContent = it.subject || it.Subject || it.common_name || '-';
|
||||||
|
const issuer = document.createElement('td'); issuer.textContent = it.issuer || it.issuer || '-';
|
||||||
|
const na = document.createElement('td'); na.textContent = it.not_after || it.notAfter || it.notAfter || '-';
|
||||||
|
const actions = document.createElement('td');
|
||||||
|
|
||||||
|
const btnView = document.createElement('button'); btnView.textContent = 'Voir';
|
||||||
|
btnView.onclick = ()=> viewDetails(it.id || it.ID || it.Id);
|
||||||
|
actions.appendChild(btnView);
|
||||||
|
|
||||||
|
if(type === 'cert'){
|
||||||
|
const btnExport = document.createElement('button'); btnExport.textContent = 'Export PEM';
|
||||||
|
btnExport.onclick = ()=> exportCertPEM(it.id || it.ID || it.Id);
|
||||||
|
actions.appendChild(btnExport);
|
||||||
|
|
||||||
|
const btnRevoke = document.createElement('button'); btnRevoke.textContent = 'Révoquer';
|
||||||
|
btnRevoke.onclick = ()=> revokeCert(it.id || it.ID || it.Id);
|
||||||
|
actions.appendChild(btnRevoke);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.appendChild(idCell); tr.appendChild(subj); tr.appendChild(issuer); tr.appendChild(na); tr.appendChild(actions);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
table.appendChild(tbody);
|
||||||
|
list.appendChild(table);
|
||||||
|
show(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTokenFromInput(){
|
||||||
|
const t = $('token').value.trim();
|
||||||
|
token = t;
|
||||||
|
localStorage.setItem('pki_token', token);
|
||||||
|
showMessage('Token enregistré.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCAs(){
|
||||||
|
try{
|
||||||
|
const res = await apiFetch('/ca');
|
||||||
|
const cas = res.cas || res.list || res;
|
||||||
|
renderTable(cas, 'ca');
|
||||||
|
}catch(e){ showMessage('Erreur loadCAs: '+e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCerts(){
|
||||||
|
try{
|
||||||
|
const res = await apiFetch('/certificates');
|
||||||
|
const certs = res.certificates || res.list || res;
|
||||||
|
renderTable(certs, 'cert');
|
||||||
|
}catch(e){ showMessage('Erreur loadCerts: '+e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateCAForm(){
|
||||||
|
const f = $('form'); f.innerHTML = '';
|
||||||
|
const h = document.createElement('h3'); h.textContent = 'Créer une CA'; f.appendChild(h);
|
||||||
|
const subject = document.createElement('input'); subject.placeholder='CN=Root CA,O=Example,C=FR'; subject.id='ca_subject';
|
||||||
|
const days = document.createElement('input'); days.placeholder='validity_days'; days.type='number'; days.id='ca_days'; days.value=3650;
|
||||||
|
const btn = document.createElement('button'); btn.textContent='Créer'; btn.onclick = async ()=>{
|
||||||
|
try{
|
||||||
|
const body = { subject: subject.value, validity_days: parseInt(days.value||0,10) };
|
||||||
|
const res = await apiFetch('/ca',{ method:'POST', body: JSON.stringify(body) });
|
||||||
|
showMessage('CA créée: '+(res.ca && res.ca.id));
|
||||||
|
loadCAs();
|
||||||
|
}catch(e){ showMessage('Erreur création CA: '+e.message, true); }
|
||||||
|
};
|
||||||
|
f.appendChild(subject); f.appendChild(document.createElement('br'));
|
||||||
|
f.appendChild(days); f.appendChild(document.createElement('br'));
|
||||||
|
f.appendChild(btn);
|
||||||
|
show(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateCertForm(){
|
||||||
|
const f = $('form'); f.innerHTML = '';
|
||||||
|
const h = document.createElement('h3'); h.textContent = 'Créer un Certificat (auto-signé)'; f.appendChild(h);
|
||||||
|
const subject = document.createElement('input'); subject.placeholder='CN=server.example.com,O=Example,C=FR'; subject.id='cert_subject';
|
||||||
|
const days = document.createElement('input'); days.placeholder='validity_days'; days.type='number'; days.id='cert_days'; days.value=365;
|
||||||
|
const btn = document.createElement('button'); btn.textContent='Créer'; btn.onclick = async ()=>{
|
||||||
|
try{
|
||||||
|
const body = { subject: subject.value, validity_days: parseInt(days.value||0,10) };
|
||||||
|
const res = await apiFetch('/certificates',{ method:'POST', body: JSON.stringify(body) });
|
||||||
|
showMessage('Certificat créé: '+(res.certificate && res.certificate.id));
|
||||||
|
loadCerts();
|
||||||
|
}catch(e){ showMessage('Erreur création cert: '+e.message, true); }
|
||||||
|
};
|
||||||
|
f.appendChild(subject); f.appendChild(document.createElement('br'));
|
||||||
|
f.appendChild(days); f.appendChild(document.createElement('br'));
|
||||||
|
f.appendChild(btn);
|
||||||
|
show(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewDetails(id){
|
||||||
|
try{
|
||||||
|
const res = await apiFetch('/certificates/'+id);
|
||||||
|
const cert = res.certificate || res;
|
||||||
|
const d = $('details');
|
||||||
|
d.innerHTML = '';
|
||||||
|
const h = document.createElement('h3'); h.textContent = 'Détails Certificat'; d.appendChild(h);
|
||||||
|
const pre = document.createElement('pre'); pre.textContent = JSON.stringify(cert, null, 2);
|
||||||
|
d.appendChild(pre);
|
||||||
|
show(d);
|
||||||
|
}catch(e){
|
||||||
|
// peut-être une CA
|
||||||
|
try{
|
||||||
|
const res = await apiFetch('/ca/'+id);
|
||||||
|
const ca = res.ca || res;
|
||||||
|
const d = $('details'); d.innerHTML=''; d.appendChild(document.createElement('h3')).textContent='Détails CA';
|
||||||
|
const pre = document.createElement('pre'); pre.textContent = JSON.stringify(ca, null, 2); d.appendChild(pre); show(d);
|
||||||
|
}catch(er){ showMessage('Erreur viewDetails: '+er.message, true); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokeCert(id){
|
||||||
|
if(!confirm('Confirmer la révocation du certificat '+id+' ?')) return;
|
||||||
|
try{
|
||||||
|
const body = { certificate_id: id, reason: 'Revoked via UI' };
|
||||||
|
await apiFetch('/revoke',{ method:'POST', body: JSON.stringify(body) });
|
||||||
|
showMessage('Certificat révoqué: '+id);
|
||||||
|
loadCerts();
|
||||||
|
}catch(e){ showMessage('Erreur revoke: '+e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCertPEM(id){
|
||||||
|
try{
|
||||||
|
const blob = await apiFetch('/certificates/'+id+'/export/pem');
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a'); a.href = url; a.download = id + '.pem'; document.body.appendChild(a); a.click(); a.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}catch(e){ showMessage('Erreur export: '+e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEvents(){
|
||||||
|
$('saveToken').onclick = setTokenFromInput;
|
||||||
|
$('btnCAs').onclick = loadCAs;
|
||||||
|
$('btnCerts').onclick = loadCerts;
|
||||||
|
$('btnCreateCA').onclick = showCreateCAForm;
|
||||||
|
$('btnCreateCert').onclick = showCreateCertForm;
|
||||||
|
// prefills
|
||||||
|
$('token').value = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// init
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=>{
|
||||||
|
attachEvents();
|
||||||
|
if(token) showMessage('Token chargé depuis localStorage.');
|
||||||
|
});
|
||||||
|
|
||||||
|
// export functions for debugging
|
||||||
|
window.pkiUI = { loadCAs, loadCerts, viewDetails };
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
:root{--bg:#f7f9fb;--card:#fff;--accent:#2b6cb0}
|
||||||
|
body{font-family:Inter,system-ui,Segoe UI,Arial;background:var(--bg);margin:0;padding:1rem}
|
||||||
|
.container{max-width:1000px;margin:0 auto}
|
||||||
|
header{display:flex;flex-direction:column;gap:0.5rem}
|
||||||
|
.token-row{display:flex;gap:0.5rem;align-items:center}
|
||||||
|
nav{display:flex;gap:0.5rem}
|
||||||
|
button{background:var(--accent);color:#fff;border:none;padding:0.4rem 0.6rem;border-radius:4px;cursor:pointer}
|
||||||
|
button.secondary{background:#666}
|
||||||
|
.card{background:var(--card);padding:1rem;border-radius:6px;box-shadow:0 1px 2px rgba(0,0,0,0.04);margin-top:1rem}
|
||||||
|
.hidden{display:none}
|
||||||
|
.table{width:100%;border-collapse:collapse;margin-top:0.5rem}
|
||||||
|
.table th,.table td{padding:0.5rem;border-bottom:1px solid #eee;text-align:left}
|
||||||
|
pre{white-space:pre-wrap;word-break:break-word}
|
||||||
|
.error{border-left:4px solid #e53e3e;padding-left:0.8rem}
|
||||||
|
label{font-weight:600}
|
||||||
|
input{padding:0.4rem;border:1px solid #ddd;border-radius:4px}
|
||||||
|
footer{margin-top:1rem;color:#666}
|
||||||
Loading…
Reference in New Issue