package verifactu import ( "bytes" "crypto/tls" "crypto/x509" "fmt" "io" "log" "net/http" "os" "strings" "time" "VerifactuMidAPI/internal/cert" ) type Client struct { BaseURL string HTTPClient *http.Client Certificate *tls.Certificate } type ClientConfig struct { BaseURL string Timeout time.Duration CertificatePath string CertificatePassword string } func NewClient(cfg ClientConfig) (*Client, error) { httpClient := &http.Client{ Timeout: cfg.Timeout, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: false, }, }, } var tlsCert *tls.Certificate if cfg.CertificatePath != "" { c, err := LoadCertificate(cfg.CertificatePath, cfg.CertificatePassword) if err != nil { return nil, fmt.Errorf("loading certificate: %w", err) } tlsCert = c } return &Client{ BaseURL: cfg.BaseURL, HTTPClient: httpClient, Certificate: tlsCert, }, nil } func LoadCertificate(certPath, password string) (*tls.Certificate, error) { der, err := os.ReadFile(certPath) if err != nil { return nil, fmt.Errorf("reading cert file: %w", err) } return LoadCertificateFromBytes(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, x509Cert, chain, err := cert.DecodeLeafAndKey(der, password) if err != nil { return nil, fmt.Errorf("decoding PKCS#12: %w", err) } if x509Cert == nil { return nil, fmt.Errorf("no certificate found in PKCS#12") } tlsCert := &tls.Certificate{ Certificate: [][]byte{x509Cert.Raw}, PrivateKey: key, Leaf: x509Cert, } for _, c := range chain { tlsCert.Certificate = append(tlsCert.Certificate, c.Raw) } log.Printf("cert loaded: subject=%s, expires=%s, chain=%d", x509Cert.Subject.CommonName, x509Cert.NotAfter.Format("2006-01-02"), len(chain)) return tlsCert, nil } func (c *Client) SetCertificate(certPath, password string) error { tlsCert, err := LoadCertificate(certPath, password) if err != nil { return err } c.Certificate = tlsCert return nil } func (c *Client) SendAlta(data AltaData) (*Response, error) { env, err := BuildAltaSOAPRequest(data) if err != nil { return nil, fmt.Errorf("building Alta request: %w", err) } if xmlBytes, err2 := env.ToBytes(); err2 == nil { log.Printf("[SOAP-Alta] XML generado (%d bytes):\n%s", len(xmlBytes), string(xmlBytes)) } return c.SendRequest(env) } func (c *Client) SendAnulacion(data AltaData) (*Response, error) { env, err := BuildAnulacionSOAPRequest(data) if err != nil { return nil, fmt.Errorf("building Anulacion request: %w", err) } return c.SendRequest(env) } func (c *Client) SendRequest(env *SOAPEnvelope) (*Response, error) { body, err := env.ToBytes() if err != nil { return nil, fmt.Errorf("marshaling request: %w", err) } req, err := http.NewRequest("POST", c.BaseURL, bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } req.Header.Set("Content-Type", "text/xml; charset=utf-8") req.Header.Set("SOAPAction", "") if c.Certificate != nil { log.Printf("Using client certificate with TLS") tlsConfig := &tls.Config{ Certificates: []tls.Certificate{*c.Certificate}, InsecureSkipVerify: true, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS12, } c.HTTPClient.Transport = &http.Transport{ TLSClientConfig: tlsConfig, } } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("sending request: %w", err) } defer resp.Body.Close() log.Printf("AEAT response status: %d", resp.StatusCode) respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("reading response: %w", err) } if resp.StatusCode != 200 { return nil, fmt.Errorf("HTTP error: %d - %s", resp.StatusCode, string(respBody)) } response, err := ParseResponse(respBody) if err != nil { log.Printf("[AEAT] Respuesta no es SOAP:\n%s", string(respBody)) return nil, fmt.Errorf("parsing response: %w", err) } return response, nil } // CertSubject extrae el NIF y nombre del emisor del certificado cargado. func (c *Client) CertSubject() (nif, nombre string) { if c.Certificate == nil || c.Certificate.Leaf == nil { return "", "" } subj := c.Certificate.Leaf.Subject nif = normalizeNIF(subj.SerialNumber) // Fallback: buscar NIF: en el CommonName (ej. ACCV: "NOMBRE - NIF:12345678A") if nif == "" { nif = extractNIFFromCN(subj.CommonName) } if len(subj.Organization) > 0 { nombre = subj.Organization[0] } else { // Usar la parte del CN antes del separador, sin el NIF nombre = cleanNombre(subj.CommonName) } return } func normalizeNIF(serial string) string { s := strings.ToUpper(strings.TrimSpace(serial)) for _, prefix := range []string{"IDCES-", "VATES-", "ES"} { if strings.HasPrefix(s, prefix) { return s[len(prefix):] } } return s } // extractNIFFromCN busca patrones como "NIF:12345678A" o "- 12345678A" en el CN. func extractNIFFromCN(cn string) string { upper := strings.ToUpper(cn) // PatrĂ³n "NIF:XXXXXXXXX" if idx := strings.Index(upper, "NIF:"); idx != -1 { rest := strings.TrimSpace(cn[idx+4:]) end := strings.IndexAny(rest, " ,;)") if end == -1 { end = len(rest) } return strings.ToUpper(rest[:end]) } return "" } // cleanNombre devuelve el CN sin el fragmento del NIF. func cleanNombre(cn string) string { if idx := strings.Index(strings.ToUpper(cn), " - NIF:"); idx != -1 { return strings.TrimSpace(cn[:idx]) } return cn } func (c *Client) SetRootCA(caPath string) error { caData, err := os.ReadFile(caPath) if err != nil { return fmt.Errorf("reading CA file: %w", err) } pool := x509.NewCertPool() pool.AppendCertsFromPEM(caData) if transport, ok := c.HTTPClient.Transport.(*http.Transport); ok { if transport.TLSClientConfig == nil { transport.TLSClientConfig = &tls.Config{} } transport.TLSClientConfig.RootCAs = pool } return nil } func GetTestURL() string { return "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP" } func GetProdURL() string { return "https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP" }