package metacryptclient import ( "bytes" "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "fmt" "io" "net/http" "os" "time" ) // loginMCIAS authenticates against the MCIAS server and returns a bearer token. func loginMCIAS(ctx context.Context, client *http.Client, mciasURL, username, password, totpCode string) (token string, exp time.Time, err error) { body, _ := json.Marshal(map[string]string{ "username": username, "password": password, "totp_code": totpCode, }) req, err := http.NewRequestWithContext(ctx, http.MethodPost, mciasURL+"/v1/auth/login", bytes.NewReader(body)) if err != nil { return "", time.Time{}, err } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return "", time.Time{}, fmt.Errorf("MCIAS login request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", time.Time{}, fmt.Errorf("MCIAS login: unexpected status %d", resp.StatusCode) } var result struct { Token string `json:"token"` ExpiresAt string `json:"expires_at"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", time.Time{}, fmt.Errorf("MCIAS login: decode response: %w", err) } if result.Token == "" { return "", time.Time{}, fmt.Errorf("MCIAS login: empty token in response") } var expTime time.Time if result.ExpiresAt != "" { expTime, _ = time.Parse(time.RFC3339, result.ExpiresAt) } return result.Token, expTime, nil } // fetchEAB calls POST /v1/acme/{mount}/eab on Metacrypt and returns the EAB kid and HMAC key. func fetchEAB(ctx context.Context, client *http.Client, metacryptURL, mount, token string) (kid string, hmacKey []byte, err error) { url := metacryptURL + "/v1/acme/" + mount + "/eab" req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody) if err != nil { return "", nil, err } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return "", nil, fmt.Errorf("fetch EAB: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) return "", nil, fmt.Errorf("fetch EAB: unexpected status %d: %s", resp.StatusCode, body) } var result struct { KID string `json:"kid"` HMACKey []byte `json:"hmac_key"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", nil, fmt.Errorf("fetch EAB: decode response: %w", err) } return result.KID, result.HMACKey, nil } // buildHTTPClient creates an HTTP client that optionally trusts a custom CA. func buildHTTPClient(caCertPath string) *http.Client { tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13} if caCertPath != "" { pool := x509.NewCertPool() if data, err := os.ReadFile(caCertPath); err == nil { pool.AppendCertsFromPEM(data) tlsCfg.RootCAs = pool } } return &http.Client{ Transport: &http.Transport{TLSClientConfig: tlsCfg}, Timeout: 30 * time.Second, } } // generateKey generates a certificate key of the requested type. // Supported types: "EC256" (default), "EC384", "RSA2048", "RSA4096". func generateKey(keyType string) (crypto.Signer, error) { switch keyType { case "", "EC256": return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) case "EC384": return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) case "RSA2048": return rsa.GenerateKey(rand.Reader, 2048) case "RSA4096": return rsa.GenerateKey(rand.Reader, 4096) default: return nil, fmt.Errorf("unsupported key type: %s", keyType) } } // buildCSR creates a DER-encoded certificate signing request. func buildCSR(key crypto.Signer, domains []string) ([]byte, error) { if len(domains) == 0 { return nil, fmt.Errorf("at least one domain required") } template := &x509.CertificateRequest{ Subject: pkix.Name{CommonName: domains[0]}, DNSNames: domains, } return x509.CreateCertificateRequest(rand.Reader, template, key) } // encodeCertChainPEM encodes a chain of DER-encoded certificates to PEM. func encodeCertChainPEM(chain [][]byte) []byte { var buf bytes.Buffer for _, der := range chain { pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: der}) } return buf.Bytes() } // encodeKeyPEM encodes a private key to PEM (PKCS8 format). func encodeKeyPEM(key crypto.Signer) ([]byte, error) { der, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { return nil, err } return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil } // parseCertExpiry parses the expiry time from a DER-encoded certificate. func parseCertExpiry(der []byte) (time.Time, error) { cert, err := x509.ParseCertificate(der) if err != nil { return time.Time{}, err } return cert.NotAfter, nil }