refactor: accept certificates as base64, remove external dependencies
- Replace file path with base64 content in register endpoint - Use native Go pkcs12 for certificate validation and loading - Remove PowerShell script dependency (validator.go) - Remove Python script dependency (client.go) - Remove hardcoded Windows paths - Cross-platform: works on Linux, macOS, Windows without external tools - Update documentation (api.md, seguridad.md, prerequisites.md)
This commit is contained in:
parent
b99d09789b
commit
9c4f11d7c7
|
|
@ -17,7 +17,7 @@ import (
|
|||
|
||||
type RegisterInput struct {
|
||||
CertName string `json:"cert_name"`
|
||||
CertPath string `json:"cert_path"`
|
||||
CertFileBase64 string `json:"cert_file"`
|
||||
PasswordEncrypted string `json:"password_encrypted"`
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ func (h *Handler) RegisterCert(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if input.CertName == "" || input.CertPath == "" || input.PasswordEncrypted == "" {
|
||||
if input.CertName == "" || input.CertFileBase64 == "" || input.PasswordEncrypted == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"success":false,"error":"missing_fields"}`))
|
||||
return
|
||||
|
|
@ -94,7 +94,7 @@ func (h *Handler) RegisterCert(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
plainPass := string(plainPassBytes)
|
||||
|
||||
validation := cert.ValidateP12(input.CertPath, plainPass)
|
||||
validation := cert.ValidateP12(input.CertFileBase64, plainPass)
|
||||
if !validation.Valid {
|
||||
resp, _ := json.Marshal(map[string]interface{}{
|
||||
"success": false,
|
||||
|
|
@ -106,7 +106,7 @@ func (h *Handler) RegisterCert(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
tempPath, err := h.cert.StoreTemp(input.CertName, input.CertPath, plainPass)
|
||||
tempPath, err := h.cert.StoreFromBase64(input.CertName, input.CertFileBase64)
|
||||
if err != nil {
|
||||
h.cert.DeleteTemp(tempPath)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
|||
|
|
@ -11,29 +11,9 @@
|
|||
go version
|
||||
```
|
||||
|
||||
### Python 3
|
||||
|
||||
Required for test scripts and certificate conversion utilities.
|
||||
|
||||
- **Version:** 3.8 or higher
|
||||
- **Verify installation:**
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
|
||||
### OpenSSL (optional but recommended)
|
||||
|
||||
Used as an alternative to the Python conversion script for `.p12` to `.pem` certificates.
|
||||
|
||||
- **Linux:** `sudo apt install openssl` / `sudo pacman -S openssl`
|
||||
- **macOS:** `brew install openssl`
|
||||
- **Windows:** https://slproweb.com/products/Win32OpenSSL.html
|
||||
- **Verify installation:**
|
||||
```bash
|
||||
openssl version
|
||||
```
|
||||
|
||||
No se requieren Python, OpenSSL ni scripts externos. Todo el procesamiento de certificados (.p12/.pfx) es nativo en Go.
|
||||
|
||||
---
|
||||
|
||||
## Project Setup
|
||||
|
||||
|
|
@ -161,7 +141,3 @@ The API falls back to defaults but logs a warning. Create the file as described
|
|||
### `403 Forbidden` from AEAT
|
||||
|
||||
The certificate is not authorized in the AEAT testing environment. Contact AEAT or verify your FNMT certificate is enabled for VeriFactu.
|
||||
|
||||
### Hardcoded Windows paths
|
||||
|
||||
`verifactu/client.go` and `internal/cert/validator.go` contain hardcoded Windows paths (`C:\Users\jmest\...`). Update them to your environment or use the `cert_file` config option instead.
|
||||
|
|
|
|||
|
|
@ -32,13 +32,13 @@ Obtiene la clave pública RSA para cifrar contraseñas.
|
|||
```
|
||||
POST /api/v1/auth/register
|
||||
```
|
||||
Registra y valida un certificado digital.
|
||||
Registra y valida un certificado digital. El certificado se envía como base64 en el body (no como ruta de fichero).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"cert_name": "mi_certificado",
|
||||
"cert_path": "C:/ruta/al/certificado.p12",
|
||||
"cert_name": "mi-certificado",
|
||||
"cert_file": "BASE64_CONTENT_OF_P12_FILE",
|
||||
"password_encrypted": "base64_encoded_password"
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,27 +10,28 @@
|
|||
|
||||
### Validación
|
||||
|
||||
La API valida:
|
||||
1. **Existencia del archivo**
|
||||
La API valida nativamente (sin scripts externos):
|
||||
1. **Formato PKCS#12 válido**
|
||||
2. **Contraseña correcta**
|
||||
3. **Fechas de validez** (no expirado, no futuror)
|
||||
3. **Fechas de validez** (no expirado, no futuro)
|
||||
4. **Días hasta expiración**
|
||||
|
||||
### Almacenamiento
|
||||
|
||||
```
|
||||
Flujo temporal:
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Original │───▶│ /tmp/ │───▶│ /certs/ │
|
||||
│ (user) │ │ (validado)│ │ (permanente)
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
El certificado se envía como base64 en el JSON de registro:
|
||||
|
||||
```json
|
||||
{
|
||||
"cert_name": "mi-cert",
|
||||
"cert_file": "BASE64_P12_CONTENT",
|
||||
"password_encrypted": "..."
|
||||
}
|
||||
```
|
||||
|
||||
1. El usuario envía el certificado original
|
||||
2. Se guarda temporalmente en `data/certs/tmp/`
|
||||
3. Se valida
|
||||
4. Si es válido, se mueve a `data/certs/`
|
||||
5. Si falla, se borra el temporal
|
||||
1. El cliente envía el certificado como base64
|
||||
2. Se valida el PKCS#12 nativamente en Go
|
||||
3. Se guarda en `data/certs/<cert_name>.p12`
|
||||
4. Se genera un token de sesión
|
||||
|
||||
## Cifrado RSA
|
||||
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -3,3 +3,5 @@ module VerifactuMidAPI
|
|||
go 1.26
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
require golang.org/x/crypto v0.51.0 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -1,3 +1,5 @@
|
|||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cert
|
|||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -19,7 +20,6 @@ type Storage struct {
|
|||
|
||||
type Certificate struct {
|
||||
ID string `json:"id"`
|
||||
OriginalPath string `json:"original_path"`
|
||||
StoredPath string `json:"stored_path"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
|
|
@ -46,8 +46,6 @@ func (s *Storage) Init() error {
|
|||
if err := os.MkdirAll(s.basePath, 0700); err != nil {
|
||||
return fmt.Errorf("creating cert storage directory: %w", err)
|
||||
}
|
||||
|
||||
// Load existing certificates from disk
|
||||
return s.loadFromDisk()
|
||||
}
|
||||
|
||||
|
|
@ -80,23 +78,19 @@ func (s *Storage) loadFromDisk() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) StoreTemp(id, origPath, password string) (string, error) {
|
||||
tmpPath := filepath.Join(s.basePath, "tmp")
|
||||
if err := os.MkdirAll(tmpPath, 0700); err != nil {
|
||||
return "", fmt.Errorf("creating tmp directory: %w", err)
|
||||
func (s *Storage) StoreFromBase64(id, base64Content string) (string, error) {
|
||||
if err := os.MkdirAll(s.basePath, 0700); err != nil {
|
||||
return "", fmt.Errorf("creating cert directory: %w", err)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(origPath)
|
||||
storedFilename := fmt.Sprintf("%s%s", id, ext)
|
||||
storedPath := filepath.Join(tmpPath, storedFilename)
|
||||
|
||||
data, err := os.ReadFile(origPath)
|
||||
der, err := base64.StdEncoding.DecodeString(base64Content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading certificate file: %w", err)
|
||||
return "", fmt.Errorf("invalid base64: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(storedPath, data, 0600); err != nil {
|
||||
return "", fmt.Errorf("storing certificate: %w", err)
|
||||
storedPath := filepath.Join(s.basePath, id+".p12")
|
||||
if err := os.WriteFile(storedPath, der, 0600); err != nil {
|
||||
return "", fmt.Errorf("writing certificate: %w", err)
|
||||
}
|
||||
|
||||
return storedPath, nil
|
||||
|
|
@ -106,9 +100,7 @@ func (s *Storage) MoveToPerm(id, tempPath, password string) (string, error) {
|
|||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
ext := filepath.Ext(tempPath)
|
||||
storedFilename := fmt.Sprintf("%s%s", id, ext)
|
||||
storedPath := filepath.Join(s.basePath, storedFilename)
|
||||
storedPath := filepath.Join(s.basePath, id+".p12")
|
||||
|
||||
if _, err := os.Stat(storedPath); err == nil {
|
||||
if err := os.Remove(storedPath); err != nil {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package cert
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
type ValidationResult struct {
|
||||
|
|
@ -26,70 +26,57 @@ type CertInfo struct {
|
|||
|
||||
const WarningDaysThreshold = 30
|
||||
|
||||
func ValidateP12(filePath, password string) *ValidationResult {
|
||||
func ValidateP12(base64Content, password string) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
der, err := base64.StdEncoding.DecodeString(base64Content)
|
||||
if err != nil {
|
||||
result.Valid = false
|
||||
result.Error = "file_not_found"
|
||||
result.Error = "invalid_base64"
|
||||
return result
|
||||
}
|
||||
|
||||
scriptPath := "C:\\Users\\jmest\\GolandProjects\\VerifactuMidAPI\\validate_cert.ps1"
|
||||
cmd := exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-File", scriptPath, "-p12Path", filePath, "-pwd", password)
|
||||
out, err := cmd.CombinedOutput()
|
||||
output := strings.TrimSpace(string(out))
|
||||
|
||||
if err != nil || output == "" {
|
||||
_, cert, err := pkcs12.Decode(der, password)
|
||||
if err != nil {
|
||||
result.Valid = false
|
||||
result.Error = "invalid_password_or_format"
|
||||
return result
|
||||
}
|
||||
|
||||
if strings.HasPrefix(output, "NOT_FOUND") {
|
||||
if cert == nil {
|
||||
result.Valid = false
|
||||
result.Error = "file_not_found"
|
||||
result.Error = "no_certificate_found"
|
||||
return result
|
||||
}
|
||||
|
||||
if strings.HasPrefix(output, "INVALID") {
|
||||
result.Valid = false
|
||||
result.Error = "invalid_password_or_format"
|
||||
return result
|
||||
}
|
||||
|
||||
if strings.HasPrefix(output, "NOT_YET_VALID") {
|
||||
now := time.Now()
|
||||
if now.Before(cert.NotBefore) {
|
||||
result.Valid = false
|
||||
result.Error = "certificate_not_yet_valid"
|
||||
return result
|
||||
}
|
||||
|
||||
if strings.HasPrefix(output, "EXPIRED") {
|
||||
if now.After(cert.NotAfter) {
|
||||
result.Valid = false
|
||||
result.Error = "certificate_expired"
|
||||
result.CertInfo = &CertInfo{Expired: true}
|
||||
return result
|
||||
}
|
||||
|
||||
if strings.HasPrefix(output, "OK:") {
|
||||
daysStr := strings.TrimPrefix(output, "OK:")
|
||||
days, _ := strconv.Atoi(daysStr)
|
||||
daysUntilExpiry := int(cert.NotAfter.Sub(now).Hours() / 24)
|
||||
|
||||
result.CertInfo = &CertInfo{
|
||||
Subject: "Certificate",
|
||||
Issuer: "Certificate",
|
||||
DaysUntilExpiry: days,
|
||||
}
|
||||
|
||||
if days <= WarningDaysThreshold {
|
||||
result.Warnings = append(result.Warnings, "certificate_expiring_soon")
|
||||
result.CertInfo.ExpiringSoon = true
|
||||
}
|
||||
|
||||
return result
|
||||
result.CertInfo = &CertInfo{
|
||||
Subject: cert.Subject.String(),
|
||||
Issuer: cert.Issuer.String(),
|
||||
NotBefore: cert.NotBefore.Format("2006-01-02"),
|
||||
NotAfter: cert.NotAfter.Format("2006-01-02"),
|
||||
DaysUntilExpiry: daysUntilExpiry,
|
||||
}
|
||||
|
||||
if daysUntilExpiry <= WarningDaysThreshold {
|
||||
result.Warnings = append(result.Warnings, "certificate_expiring_soon")
|
||||
result.CertInfo.ExpiringSoon = true
|
||||
}
|
||||
|
||||
result.Valid = false
|
||||
result.Error = "invalid_password_or_format"
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
|
@ -54,35 +54,37 @@ func NewClient(cfg ClientConfig) (*Client, error) {
|
|||
}
|
||||
|
||||
func LoadCertificate(certPath, password string) (*tls.Certificate, error) {
|
||||
dir := filepath.Dir(certPath)
|
||||
keyPath := filepath.Join(dir, "cert_key.pem")
|
||||
certPath2 := filepath.Join(dir, "cert_cert.pem")
|
||||
|
||||
pyScript := "C:\\Users\\jmest\\GolandProjects\\VerifactuMidAPI\\convert_cert.py"
|
||||
|
||||
cmd := exec.Command("python", pyScript, certPath, password, keyPath, certPath2)
|
||||
out, err := cmd.CombinedOutput()
|
||||
log.Printf("cert convert: out=%s err=%v", string(out), err)
|
||||
der, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting: %w - %s", err, string(out))
|
||||
return nil, fmt.Errorf("reading cert file: %w", err)
|
||||
}
|
||||
|
||||
certData, err := os.ReadFile(certPath2)
|
||||
return parseP12(der, password)
|
||||
}
|
||||
|
||||
func LoadCertificateFromBytes(der []byte, password string) (*tls.Certificate, error) {
|
||||
return parseP12(der, password)
|
||||
}
|
||||
|
||||
func parseP12(der []byte, password string) (*tls.Certificate, error) {
|
||||
key, cert, err := pkcs12.Decode(der, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading cert: %w", err)
|
||||
}
|
||||
keyData, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading key: %w", err)
|
||||
return nil, fmt.Errorf("decoding PKCS#12: %w", err)
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(certData, keyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing: %w", err)
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("no certificate found in PKCS#12")
|
||||
}
|
||||
log.Printf("cert loaded: has private key=%v", cert.PrivateKey != nil)
|
||||
|
||||
return &cert, nil
|
||||
tlsCert := &tls.Certificate{
|
||||
Certificate: [][]byte{cert.Raw},
|
||||
PrivateKey: key,
|
||||
Leaf: cert,
|
||||
}
|
||||
|
||||
log.Printf("cert loaded: subject=%s has private key=%v", cert.Subject, key != nil)
|
||||
|
||||
return tlsCert, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetCertificate(certPath, password string) error {
|
||||
|
|
|
|||
Loading…
Reference in New Issue