214 lines
5.3 KiB
Go
214 lines
5.3 KiB
Go
package cert
|
|
|
|
import (
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"math"
|
|
"os"
|
|
"time"
|
|
|
|
goPkcs12 "software.sslmate.com/src/go-pkcs12"
|
|
stdPkcs12 "golang.org/x/crypto/pkcs12"
|
|
)
|
|
|
|
type ValidationResult struct {
|
|
Valid bool `json:"valid"`
|
|
CertInfo *CertInfo `json:"cert_info,omitempty"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type CertInfo struct {
|
|
Subject string `json:"subject"`
|
|
Issuer string `json:"issuer"`
|
|
NotBefore string `json:"not_before"`
|
|
NotAfter string `json:"not_after"`
|
|
Expired bool `json:"expired"`
|
|
ExpiringSoon bool `json:"expiring_soon"`
|
|
DaysUntilExpiry int `json:"days_until_expiry"`
|
|
}
|
|
|
|
const WarningDaysThreshold = 30
|
|
|
|
func ValidateP12(filePath, password string) *ValidationResult {
|
|
p12Data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
result := &ValidationResult{Valid: true}
|
|
if os.IsNotExist(err) {
|
|
result.Valid = false
|
|
result.Error = "file_not_found"
|
|
return result
|
|
}
|
|
result.Valid = false
|
|
result.Error = "invalid_password_or_format"
|
|
return result
|
|
}
|
|
|
|
return ValidateP12Bytes(p12Data, password)
|
|
}
|
|
|
|
// ValidateP12Bytes valida un P12 a partir de sus bytes (util para tests sin fichero).
|
|
// Soporta tanto P12 simples (go-pkcs12) como P12 con cadena de certificados (x/crypto/pkcs12).
|
|
func ValidateP12Bytes(p12Data []byte, password string) *ValidationResult {
|
|
result := &ValidationResult{Valid: true}
|
|
|
|
x509Cert, err := decodeLeafCert(p12Data, password)
|
|
if err != nil {
|
|
result.Valid = false
|
|
result.Error = "invalid_password_or_format"
|
|
return result
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
if now.Before(x509Cert.NotBefore) {
|
|
result.Valid = false
|
|
result.Error = "certificate_not_yet_valid"
|
|
return result
|
|
}
|
|
|
|
if now.After(x509Cert.NotAfter) {
|
|
result.Valid = false
|
|
result.Error = "certificate_expired"
|
|
result.CertInfo = &CertInfo{
|
|
Subject: x509Cert.Subject.CommonName,
|
|
Issuer: x509Cert.Issuer.CommonName,
|
|
NotBefore: x509Cert.NotBefore.Format("2006-01-02"),
|
|
NotAfter: x509Cert.NotAfter.Format("2006-01-02"),
|
|
Expired: true,
|
|
}
|
|
return result
|
|
}
|
|
|
|
daysUntilExpiry := int(math.Ceil(x509Cert.NotAfter.Sub(now).Hours() / 24))
|
|
|
|
result.CertInfo = &CertInfo{
|
|
Subject: x509Cert.Subject.CommonName,
|
|
Issuer: x509Cert.Issuer.CommonName,
|
|
NotBefore: x509Cert.NotBefore.Format("2006-01-02"),
|
|
NotAfter: x509Cert.NotAfter.Format("2006-01-02"),
|
|
DaysUntilExpiry: daysUntilExpiry,
|
|
}
|
|
|
|
if daysUntilExpiry <= WarningDaysThreshold {
|
|
result.Warnings = append(result.Warnings, "certificate_expiring_soon")
|
|
result.CertInfo.ExpiringSoon = true
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// decodeLeafCert extrae el certificado hoja del P12.
|
|
// Intenta primero go-pkcs12 (P12 simples) y luego x/crypto/pkcs12 (P12 con cadena FNMT/ACCV).
|
|
func decodeLeafCert(p12Data []byte, password string) (*x509.Certificate, error) {
|
|
// Intento 1: go-pkcs12 — soporta P12 simples (1 clave + 1 cert)
|
|
_, cert, err := goPkcs12.Decode(p12Data, password)
|
|
if err == nil {
|
|
return cert, nil
|
|
}
|
|
|
|
// Intento 2: x/crypto/pkcs12 — soporta P12 con cadena de certificados (FNMT, ACCV, AEAT)
|
|
blocks, err := stdPkcs12.ToPEM(p12Data, password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var privateKey *rsa.PrivateKey
|
|
var certs []*x509.Certificate
|
|
|
|
for _, b := range blocks {
|
|
switch b.Type {
|
|
case "PRIVATE KEY":
|
|
key, err := x509.ParsePKCS8PrivateKey(b.Bytes)
|
|
if err != nil {
|
|
key2, err2 := x509.ParsePKCS1PrivateKey(b.Bytes)
|
|
if err2 == nil {
|
|
privateKey = key2
|
|
}
|
|
} else if rsaKey, ok := key.(*rsa.PrivateKey); ok {
|
|
privateKey = rsaKey
|
|
}
|
|
case "CERTIFICATE":
|
|
cert, err := x509.ParseCertificate(b.Bytes)
|
|
if err == nil {
|
|
certs = append(certs, cert)
|
|
}
|
|
}
|
|
}
|
|
|
|
if privateKey == nil || len(certs) == 0 {
|
|
return nil, stdPkcs12.ErrIncorrectPassword
|
|
}
|
|
|
|
for _, c := range certs {
|
|
if rsaPub, ok := c.PublicKey.(*rsa.PublicKey); ok {
|
|
if rsaPub.N.Cmp(privateKey.PublicKey.N) == 0 {
|
|
return c, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return certs[0], nil
|
|
}
|
|
|
|
// DecodeLeafAndKey extrae la clave privada y el certificado hoja de un P12.
|
|
// Usado por el cliente TLS para establecer conexiones mTLS.
|
|
func DecodeLeafAndKey(p12Data []byte, password string) (interface{}, *x509.Certificate, []*x509.Certificate, error) {
|
|
key, cert, err := goPkcs12.Decode(p12Data, password)
|
|
if err == nil {
|
|
return key, cert, nil, nil
|
|
}
|
|
|
|
blocks, err := stdPkcs12.ToPEM(p12Data, password)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
var privateKey interface{}
|
|
var leafCert *x509.Certificate
|
|
var chain []*x509.Certificate
|
|
|
|
for _, b := range blocks {
|
|
switch b.Type {
|
|
case "PRIVATE KEY":
|
|
k, err := x509.ParsePKCS8PrivateKey(b.Bytes)
|
|
if err != nil {
|
|
k2, err2 := x509.ParsePKCS1PrivateKey(b.Bytes)
|
|
if err2 == nil {
|
|
privateKey = k2
|
|
}
|
|
} else {
|
|
privateKey = k
|
|
}
|
|
case "CERTIFICATE":
|
|
c, err := x509.ParseCertificate(b.Bytes)
|
|
if err == nil {
|
|
chain = append(chain, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
if privateKey == nil || len(chain) == 0 {
|
|
return nil, nil, nil, stdPkcs12.ErrIncorrectPassword
|
|
}
|
|
|
|
if rsaKey, ok := privateKey.(*rsa.PrivateKey); ok {
|
|
for i, c := range chain {
|
|
if rsaPub, ok := c.PublicKey.(*rsa.PublicKey); ok {
|
|
if rsaPub.N.Cmp(rsaKey.PublicKey.N) == 0 {
|
|
leafCert = c
|
|
chain = append(chain[:i], chain[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if leafCert == nil {
|
|
leafCert = chain[0]
|
|
chain = chain[1:]
|
|
}
|
|
|
|
return privateKey, leafCert, chain, nil
|
|
}
|