manager.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. package certs
  2. import (
  3. "context"
  4. "crypto"
  5. "crypto/ecdsa"
  6. "crypto/elliptic"
  7. "crypto/rand"
  8. "crypto/rsa"
  9. "crypto/sha256"
  10. "crypto/tls"
  11. "crypto/x509"
  12. "crypto/x509/pkix"
  13. "encoding/hex"
  14. "encoding/json"
  15. "encoding/pem"
  16. "errors"
  17. "fmt"
  18. "math/big"
  19. "net"
  20. "os"
  21. "path/filepath"
  22. "strings"
  23. "sync"
  24. "time"
  25. "golang.org/x/crypto/acme"
  26. "proxmox-router/internal/logging"
  27. )
  28. const (
  29. DefaultCertsDir = "/etc/proxmox-router/certs"
  30. DefaultMetadataFile = "certs.json"
  31. TypeCA = "ca"
  32. TypeServer = "server"
  33. TypeClient = "client"
  34. TypeACME = "acme"
  35. KeyRSA2048 = "RSA-2048"
  36. KeyRSA4096 = "RSA-4096"
  37. KeyECDSAP256 = "ECDSA-P256"
  38. )
  39. type CertificateInfo struct {
  40. ID string `json:"id"`
  41. Name string `json:"name"`
  42. CommonName string `json:"common_name"`
  43. SANs []string `json:"sans"`
  44. IsCA bool `json:"is_ca"`
  45. Issuer string `json:"issuer"`
  46. KeyType string `json:"key_type"`
  47. ValidFrom time.Time `json:"valid_from"`
  48. ValidTo time.Time `json:"valid_to"`
  49. Fingerprint string `json:"fingerprint"`
  50. CertPath string `json:"cert_path"`
  51. KeyPath string `json:"key_path"`
  52. Type string `json:"type"`
  53. Comment string `json:"comment,omitempty"`
  54. }
  55. type CreateCARequest struct {
  56. Name string `json:"name"`
  57. CommonName string `json:"common_name"`
  58. Organization string `json:"organization,omitempty"`
  59. Country string `json:"country,omitempty"`
  60. Days int `json:"days,omitempty"`
  61. KeyType string `json:"key_type,omitempty"`
  62. Comment string `json:"comment,omitempty"`
  63. }
  64. type CreateCertRequest struct {
  65. Name string `json:"name"`
  66. CommonName string `json:"common_name"`
  67. CAName string `json:"ca_name,omitempty"`
  68. SANs []string `json:"sans,omitempty"`
  69. Days int `json:"days,omitempty"`
  70. KeyType string `json:"key_type,omitempty"`
  71. IsServer bool `json:"is_server"`
  72. Organization string `json:"organization,omitempty"`
  73. Country string `json:"country,omitempty"`
  74. Comment string `json:"comment,omitempty"`
  75. }
  76. type ImportCertRequest struct {
  77. Name string `json:"name"`
  78. CertPEM string `json:"cert_pem"`
  79. KeyPEM string `json:"key_pem"`
  80. Comment string `json:"comment,omitempty"`
  81. }
  82. type LetsEncryptRequest struct {
  83. Name string `json:"name"`
  84. Domain string `json:"domain"`
  85. Email string `json:"email"`
  86. Staging bool `json:"staging"`
  87. Comment string `json:"comment,omitempty"`
  88. }
  89. type CertManager struct {
  90. dir string
  91. metaPath string
  92. mu sync.RWMutex
  93. certs map[string]*CertificateInfo
  94. challenges map[string]string // token -> keyAuth
  95. challengeMu sync.RWMutex
  96. }
  97. func NewCertManager(customDir string) (*CertManager, error) {
  98. d := customDir
  99. if d == "" {
  100. d = DefaultCertsDir
  101. }
  102. if err := os.MkdirAll(d, 0700); err != nil {
  103. return nil, fmt.Errorf("failed to create certs directory %s: %w", d, err)
  104. }
  105. cm := &CertManager{
  106. dir: d,
  107. metaPath: filepath.Join(d, DefaultMetadataFile),
  108. certs: make(map[string]*CertificateInfo),
  109. challenges: make(map[string]string),
  110. }
  111. if err := cm.load(); err != nil {
  112. logging.Log("certs,warning", "Warning loading cert metadata: %v", err)
  113. }
  114. return cm, nil
  115. }
  116. func (cm *CertManager) load() error {
  117. cm.mu.Lock()
  118. defer cm.mu.Unlock()
  119. data, err := os.ReadFile(cm.metaPath)
  120. if err != nil {
  121. if os.IsNotExist(err) {
  122. return nil
  123. }
  124. return err
  125. }
  126. var list []*CertificateInfo
  127. if err := json.Unmarshal(data, &list); err != nil {
  128. return err
  129. }
  130. for _, c := range list {
  131. if c.Name != "" {
  132. cm.certs[strings.ToLower(c.Name)] = c
  133. }
  134. }
  135. return nil
  136. }
  137. func (cm *CertManager) saveLocked() error {
  138. var list []*CertificateInfo
  139. for _, c := range cm.certs {
  140. list = append(list, c)
  141. }
  142. data, err := json.MarshalIndent(list, "", " ")
  143. if err != nil {
  144. return err
  145. }
  146. tmp := fmt.Sprintf("%s.tmp.%d", cm.metaPath, time.Now().UnixNano())
  147. if err := os.WriteFile(tmp, data, 0600); err != nil {
  148. return err
  149. }
  150. return os.Rename(tmp, cm.metaPath)
  151. }
  152. // RegisterHTTP01Challenge stores an ACME challenge token and its response for HTTP-01 verification.
  153. func (cm *CertManager) RegisterHTTP01Challenge(token, keyAuth string) {
  154. cm.challengeMu.Lock()
  155. defer cm.challengeMu.Unlock()
  156. cm.challenges[token] = keyAuth
  157. }
  158. // UnregisterHTTP01Challenge removes an ACME challenge token.
  159. func (cm *CertManager) UnregisterHTTP01Challenge(token string) {
  160. cm.challengeMu.Lock()
  161. defer cm.challengeMu.Unlock()
  162. delete(cm.challenges, token)
  163. }
  164. // GetHTTP01Challenge looks up an ACME challenge token for HTTP-01 verification.
  165. func (cm *CertManager) GetHTTP01Challenge(token string) (string, bool) {
  166. cm.challengeMu.RLock()
  167. defer cm.challengeMu.RUnlock()
  168. auth, ok := cm.challenges[token]
  169. return auth, ok
  170. }
  171. // ListCertificates returns all certificates known to the manager.
  172. func (cm *CertManager) ListCertificates() []*CertificateInfo {
  173. cm.mu.RLock()
  174. defer cm.mu.RUnlock()
  175. var result []*CertificateInfo
  176. for _, c := range cm.certs {
  177. copyItem := *c
  178. result = append(result, &copyItem)
  179. }
  180. return result
  181. }
  182. // GetCertificate returns metadata for a certificate by name.
  183. func (cm *CertManager) GetCertificate(name string) (*CertificateInfo, error) {
  184. cm.mu.RLock()
  185. defer cm.mu.RUnlock()
  186. c, ok := cm.certs[strings.ToLower(name)]
  187. if !ok {
  188. return nil, fmt.Errorf("certificate %q not found", name)
  189. }
  190. copyItem := *c
  191. return &copyItem, nil
  192. }
  193. // GetTLSCertificate loads a parsed tls.Certificate ready for TLS listeners.
  194. func (cm *CertManager) GetTLSCertificate(name string) (tls.Certificate, error) {
  195. cm.mu.RLock()
  196. c, ok := cm.certs[strings.ToLower(name)]
  197. cm.mu.RUnlock()
  198. if !ok {
  199. return tls.Certificate{}, fmt.Errorf("certificate %q not found", name)
  200. }
  201. if _, err := os.Stat(c.CertPath); err != nil {
  202. return tls.Certificate{}, fmt.Errorf("certificate file missing: %w", err)
  203. }
  204. if _, err := os.Stat(c.KeyPath); err != nil {
  205. return tls.Certificate{}, fmt.Errorf("private key file missing: %w", err)
  206. }
  207. return tls.LoadX509KeyPair(c.CertPath, c.KeyPath)
  208. }
  209. // ExportPEM returns the PEM encoded certificate and private key content.
  210. func (cm *CertManager) ExportPEM(name string) (certPEM string, keyPEM string, err error) {
  211. cm.mu.RLock()
  212. c, ok := cm.certs[strings.ToLower(name)]
  213. cm.mu.RUnlock()
  214. if !ok {
  215. return "", "", fmt.Errorf("certificate %q not found", name)
  216. }
  217. certBytes, err := os.ReadFile(c.CertPath)
  218. if err != nil {
  219. return "", "", fmt.Errorf("failed to read cert file: %w", err)
  220. }
  221. keyBytes, err := os.ReadFile(c.KeyPath)
  222. if err != nil {
  223. return "", "", fmt.Errorf("failed to read key file: %w", err)
  224. }
  225. return string(certBytes), string(keyBytes), nil
  226. }
  227. // DeleteCertificate removes a certificate, its files, and metadata.
  228. func (cm *CertManager) DeleteCertificate(name string) error {
  229. cm.mu.Lock()
  230. defer cm.mu.Unlock()
  231. key := strings.ToLower(name)
  232. c, ok := cm.certs[key]
  233. if !ok {
  234. return fmt.Errorf("certificate %q not found", name)
  235. }
  236. _ = os.Remove(c.CertPath)
  237. _ = os.Remove(c.KeyPath)
  238. delete(cm.certs, key)
  239. if err := cm.saveLocked(); err != nil {
  240. return err
  241. }
  242. logging.Log("certs,info", "Certificate %s deleted", name)
  243. return nil
  244. }
  245. // CreateCA creates a new Self-Signed Certificate Authority (CA).
  246. func (cm *CertManager) CreateCA(req CreateCARequest) (*CertificateInfo, error) {
  247. name := strings.TrimSpace(req.Name)
  248. if name == "" {
  249. return nil, errors.New("certificate name is required")
  250. }
  251. cn := strings.TrimSpace(req.CommonName)
  252. if cn == "" {
  253. cn = name
  254. }
  255. cm.mu.Lock()
  256. defer cm.mu.Unlock()
  257. if _, exists := cm.certs[strings.ToLower(name)]; exists {
  258. return nil, fmt.Errorf("certificate %q already exists", name)
  259. }
  260. days := req.Days
  261. if days <= 0 {
  262. days = 3650 // 10 years default for CA
  263. }
  264. keyType := req.KeyType
  265. if keyType == "" {
  266. keyType = KeyRSA2048
  267. }
  268. privKey, pubKey, err := generateKeyPair(keyType)
  269. if err != nil {
  270. return nil, fmt.Errorf("failed generating key: %w", err)
  271. }
  272. serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
  273. if err != nil {
  274. return nil, fmt.Errorf("failed generating serial: %w", err)
  275. }
  276. org := req.Organization
  277. if org == "" {
  278. org = "DebTik Network Appliance"
  279. }
  280. country := req.Country
  281. if country == "" {
  282. country = "GR"
  283. }
  284. now := time.Now().UTC()
  285. template := x509.Certificate{
  286. SerialNumber: serialNumber,
  287. Subject: pkix.Name{
  288. CommonName: cn,
  289. Organization: []string{org},
  290. Country: []string{country},
  291. },
  292. NotBefore: now.Add(-1 * time.Hour),
  293. NotAfter: now.AddDate(0, 0, days),
  294. IsCA: true,
  295. KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
  296. BasicConstraintsValid: true,
  297. MaxPathLenZero: false,
  298. }
  299. certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pubKey, privKey)
  300. if err != nil {
  301. return nil, fmt.Errorf("failed creating CA certificate: %w", err)
  302. }
  303. certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name))
  304. keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name))
  305. if err := savePEM(certPath, "CERTIFICATE", certBytes); err != nil {
  306. return nil, err
  307. }
  308. if err := savePrivateKeyPEM(keyPath, privKey); err != nil {
  309. _ = os.Remove(certPath)
  310. return nil, err
  311. }
  312. fp := sha256.Sum256(certBytes)
  313. info := &CertificateInfo{
  314. ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()),
  315. Name: name,
  316. CommonName: cn,
  317. IsCA: true,
  318. Issuer: "Self-Signed CA",
  319. KeyType: keyType,
  320. ValidFrom: template.NotBefore,
  321. ValidTo: template.NotAfter,
  322. Fingerprint: formatFingerprint(fp[:]),
  323. CertPath: certPath,
  324. KeyPath: keyPath,
  325. Type: TypeCA,
  326. Comment: req.Comment,
  327. }
  328. cm.certs[strings.ToLower(name)] = info
  329. if err := cm.saveLocked(); err != nil {
  330. return nil, err
  331. }
  332. logging.Log("certs,info", "Created new Certificate Authority: %s (CN: %s)", name, cn)
  333. return info, nil
  334. }
  335. // CreateCertificate issues a server or client certificate signed by a CA or self-signed.
  336. func (cm *CertManager) CreateCertificate(req CreateCertRequest) (*CertificateInfo, error) {
  337. name := strings.TrimSpace(req.Name)
  338. if name == "" {
  339. return nil, errors.New("certificate name is required")
  340. }
  341. cn := strings.TrimSpace(req.CommonName)
  342. if cn == "" {
  343. return nil, errors.New("common name is required")
  344. }
  345. cm.mu.Lock()
  346. defer cm.mu.Unlock()
  347. if _, exists := cm.certs[strings.ToLower(name)]; exists {
  348. return nil, fmt.Errorf("certificate %q already exists", name)
  349. }
  350. days := req.Days
  351. if days <= 0 {
  352. days = 365 // 1 year default
  353. }
  354. keyType := req.KeyType
  355. if keyType == "" {
  356. keyType = KeyRSA2048
  357. }
  358. privKey, pubKey, err := generateKeyPair(keyType)
  359. if err != nil {
  360. return nil, fmt.Errorf("failed generating key: %w", err)
  361. }
  362. serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
  363. if err != nil {
  364. return nil, fmt.Errorf("failed generating serial: %w", err)
  365. }
  366. org := req.Organization
  367. if org == "" {
  368. org = "DebTik Network Appliance"
  369. }
  370. country := req.Country
  371. if country == "" {
  372. country = "GR"
  373. }
  374. now := time.Now().UTC()
  375. template := x509.Certificate{
  376. SerialNumber: serialNumber,
  377. Subject: pkix.Name{
  378. CommonName: cn,
  379. Organization: []string{org},
  380. Country: []string{country},
  381. },
  382. NotBefore: now.Add(-1 * time.Hour),
  383. NotAfter: now.AddDate(0, 0, days),
  384. KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
  385. BasicConstraintsValid: true,
  386. IsCA: false,
  387. }
  388. if req.IsServer {
  389. template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
  390. } else {
  391. template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
  392. }
  393. // Process SANs (DNS names & IP addresses)
  394. sansMap := make(map[string]bool)
  395. if ip := net.ParseIP(cn); ip != nil {
  396. template.IPAddresses = append(template.IPAddresses, ip)
  397. sansMap[cn] = true
  398. } else if strings.Contains(cn, ".") || cn != "" {
  399. template.DNSNames = append(template.DNSNames, cn)
  400. sansMap[cn] = true
  401. }
  402. for _, s := range req.SANs {
  403. s = strings.TrimSpace(s)
  404. if s == "" || sansMap[s] {
  405. continue
  406. }
  407. sansMap[s] = true
  408. if ip := net.ParseIP(s); ip != nil {
  409. template.IPAddresses = append(template.IPAddresses, ip)
  410. } else {
  411. template.DNSNames = append(template.DNSNames, s)
  412. }
  413. }
  414. var sansList []string
  415. for s := range sansMap {
  416. sansList = append(sansList, s)
  417. }
  418. var signerCert *x509.Certificate
  419. var signerKey crypto.PrivateKey
  420. issuerName := "Self-Signed"
  421. var caPEMBytes []byte
  422. if req.CAName != "" {
  423. caInfo, ok := cm.certs[strings.ToLower(req.CAName)]
  424. if !ok || !caInfo.IsCA {
  425. return nil, fmt.Errorf("signing CA %q not found or is not a CA", req.CAName)
  426. }
  427. caCert, caKey, err := loadCertAndKey(caInfo.CertPath, caInfo.KeyPath)
  428. if err != nil {
  429. return nil, fmt.Errorf("failed loading CA cert/key: %w", err)
  430. }
  431. signerCert = caCert
  432. signerKey = caKey
  433. issuerName = caInfo.Name
  434. caPEMBytes, _ = os.ReadFile(caInfo.CertPath)
  435. } else {
  436. signerCert = &template
  437. signerKey = privKey
  438. }
  439. certBytes, err := x509.CreateCertificate(rand.Reader, &template, signerCert, pubKey, signerKey)
  440. if err != nil {
  441. return nil, fmt.Errorf("failed creating certificate: %w", err)
  442. }
  443. certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name))
  444. keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name))
  445. // If signed by CA, build chain: [Cert] + [CA Cert]
  446. certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
  447. if len(caPEMBytes) > 0 {
  448. certPEM = append(certPEM, caPEMBytes...)
  449. }
  450. if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
  451. return nil, err
  452. }
  453. if err := savePrivateKeyPEM(keyPath, privKey); err != nil {
  454. _ = os.Remove(certPath)
  455. return nil, err
  456. }
  457. fp := sha256.Sum256(certBytes)
  458. info := &CertificateInfo{
  459. ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()),
  460. Name: name,
  461. CommonName: cn,
  462. SANs: sansList,
  463. IsCA: false,
  464. Issuer: issuerName,
  465. KeyType: keyType,
  466. ValidFrom: template.NotBefore,
  467. ValidTo: template.NotAfter,
  468. Fingerprint: formatFingerprint(fp[:]),
  469. CertPath: certPath,
  470. KeyPath: keyPath,
  471. Type: TypeServer,
  472. Comment: req.Comment,
  473. }
  474. if !req.IsServer {
  475. info.Type = TypeClient
  476. }
  477. cm.certs[strings.ToLower(name)] = info
  478. if err := cm.saveLocked(); err != nil {
  479. return nil, err
  480. }
  481. logging.Log("certs,info", "Created new certificate %s (Issuer: %s, CN: %s)", name, issuerName, cn)
  482. return info, nil
  483. }
  484. // ImportCertificate imports user-provided PEM certificate and private key.
  485. func (cm *CertManager) ImportCertificate(req ImportCertRequest) (*CertificateInfo, error) {
  486. name := strings.TrimSpace(req.Name)
  487. if name == "" {
  488. return nil, errors.New("certificate name is required")
  489. }
  490. certBlock, _ := pem.Decode([]byte(req.CertPEM))
  491. if certBlock == nil || certBlock.Type != "CERTIFICATE" {
  492. return nil, errors.New("invalid or empty PEM certificate")
  493. }
  494. parsedCert, err := x509.ParseCertificate(certBlock.Bytes)
  495. if err != nil {
  496. return nil, fmt.Errorf("failed to parse certificate: %w", err)
  497. }
  498. // Verify key matches certificate
  499. _, err = tls.X509KeyPair([]byte(req.CertPEM), []byte(req.KeyPEM))
  500. if err != nil {
  501. return nil, fmt.Errorf("private key does not match certificate: %w", err)
  502. }
  503. cm.mu.Lock()
  504. defer cm.mu.Unlock()
  505. if _, exists := cm.certs[strings.ToLower(name)]; exists {
  506. return nil, fmt.Errorf("certificate %q already exists", name)
  507. }
  508. certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name))
  509. keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name))
  510. if err := os.WriteFile(certPath, []byte(strings.TrimSpace(req.CertPEM)+"\n"), 0644); err != nil {
  511. return nil, err
  512. }
  513. if err := os.WriteFile(keyPath, []byte(strings.TrimSpace(req.KeyPEM)+"\n"), 0600); err != nil {
  514. _ = os.Remove(certPath)
  515. return nil, err
  516. }
  517. var sans []string
  518. for _, dns := range parsedCert.DNSNames {
  519. sans = append(sans, dns)
  520. }
  521. for _, ip := range parsedCert.IPAddresses {
  522. sans = append(sans, ip.String())
  523. }
  524. fp := sha256.Sum256(parsedCert.Raw)
  525. cType := TypeServer
  526. if parsedCert.IsCA {
  527. cType = TypeCA
  528. }
  529. keyType := "Unknown"
  530. switch parsedCert.PublicKeyAlgorithm {
  531. case x509.RSA:
  532. keyType = "RSA"
  533. case x509.ECDSA:
  534. keyType = "ECDSA"
  535. }
  536. info := &CertificateInfo{
  537. ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()),
  538. Name: name,
  539. CommonName: parsedCert.Subject.CommonName,
  540. SANs: sans,
  541. IsCA: parsedCert.IsCA,
  542. Issuer: parsedCert.Issuer.CommonName,
  543. KeyType: keyType,
  544. ValidFrom: parsedCert.NotBefore,
  545. ValidTo: parsedCert.NotAfter,
  546. Fingerprint: formatFingerprint(fp[:]),
  547. CertPath: certPath,
  548. KeyPath: keyPath,
  549. Type: cType,
  550. Comment: req.Comment,
  551. }
  552. cm.certs[strings.ToLower(name)] = info
  553. if err := cm.saveLocked(); err != nil {
  554. return nil, err
  555. }
  556. logging.Log("certs,info", "Imported certificate: %s (CN: %s)", name, info.CommonName)
  557. return info, nil
  558. }
  559. // IssueLetsEncrypt requests and issues a certificate from Let's Encrypt via ACME HTTP-01 challenge.
  560. func (cm *CertManager) IssueLetsEncrypt(ctx context.Context, req LetsEncryptRequest) (*CertificateInfo, error) {
  561. name := strings.TrimSpace(req.Name)
  562. domain := strings.TrimSpace(req.Domain)
  563. email := strings.TrimSpace(req.Email)
  564. if domain == "" {
  565. return nil, errors.New("domain is required for Let's Encrypt")
  566. }
  567. if name == "" {
  568. name = domain
  569. }
  570. cm.mu.Lock()
  571. if _, exists := cm.certs[strings.ToLower(name)]; exists {
  572. cm.mu.Unlock()
  573. return nil, fmt.Errorf("certificate %q already exists", name)
  574. }
  575. cm.mu.Unlock()
  576. logging.Log("certs,info", "Starting Let's Encrypt order for domain %s (Staging=%t)", domain, req.Staging)
  577. // Generate ACME account key
  578. accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  579. if err != nil {
  580. return nil, fmt.Errorf("failed generating ACME account key: %w", err)
  581. }
  582. client := &acme.Client{
  583. Key: accountKey,
  584. }
  585. if req.Staging {
  586. client.DirectoryURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
  587. } else {
  588. client.DirectoryURL = acme.LetsEncryptURL
  589. }
  590. // Register account
  591. account := &acme.Account{}
  592. if email != "" {
  593. account.Contact = []string{"mailto:" + email}
  594. }
  595. _, err = client.Register(ctx, account, acme.AcceptTOS)
  596. if err != nil && !strings.Contains(err.Error(), "already exists") {
  597. return nil, fmt.Errorf("failed registering ACME account: %w", err)
  598. }
  599. // Create order for domain
  600. order, err := client.AuthorizeOrder(ctx, acme.DomainIDs(domain))
  601. if err != nil {
  602. return nil, fmt.Errorf("failed requesting order for %s: %w", domain, err)
  603. }
  604. // Complete authorization for each authorization URL
  605. for _, authzURL := range order.AuthzURLs {
  606. authz, err := client.GetAuthorization(ctx, authzURL)
  607. if err != nil {
  608. return nil, fmt.Errorf("failed getting authorization %s: %w", authzURL, err)
  609. }
  610. if authz.Status == acme.StatusValid {
  611. continue
  612. }
  613. var challenge *acme.Challenge
  614. for _, ch := range authz.Challenges {
  615. if ch.Type == "http-01" {
  616. challenge = ch
  617. break
  618. }
  619. }
  620. if challenge == nil {
  621. return nil, fmt.Errorf("no http-01 challenge available for domain %s", domain)
  622. }
  623. keyAuth, err := client.HTTP01ChallengeResponse(challenge.Token)
  624. if err != nil {
  625. return nil, fmt.Errorf("failed generating challenge response: %w", err)
  626. }
  627. // Register in our web server challenge map
  628. cm.RegisterHTTP01Challenge(challenge.Token, keyAuth)
  629. defer cm.UnregisterHTTP01Challenge(challenge.Token)
  630. // Inform ACME server challenge is ready
  631. if _, err := client.Accept(ctx, challenge); err != nil {
  632. return nil, fmt.Errorf("failed accepting challenge: %w", err)
  633. }
  634. // Wait for authorization to succeed
  635. if _, err := client.WaitAuthorization(ctx, challenge.URI); err != nil {
  636. return nil, fmt.Errorf("ACME authorization failed for %s: %w", domain, err)
  637. }
  638. }
  639. // Generate certificate private key
  640. certPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
  641. if err != nil {
  642. return nil, fmt.Errorf("failed generating certificate key: %w", err)
  643. }
  644. csrTemplate := x509.CertificateRequest{
  645. Subject: pkix.Name{CommonName: domain},
  646. DNSNames: []string{domain},
  647. }
  648. csr, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, certPrivKey)
  649. if err != nil {
  650. return nil, fmt.Errorf("failed creating CSR: %w", err)
  651. }
  652. // Finalize order
  653. certDerList, certURL, err := client.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
  654. if err != nil {
  655. return nil, fmt.Errorf("failed finalizing ACME order: %w", err)
  656. }
  657. if len(certDerList) == 0 && certURL != "" {
  658. certDerList, err = client.FetchCert(ctx, certURL, true)
  659. if err != nil {
  660. return nil, fmt.Errorf("failed fetching issued cert: %w", err)
  661. }
  662. }
  663. // Build certificate chain in PEM format
  664. var certPEM []byte
  665. var firstCert *x509.Certificate
  666. for idx, der := range certDerList {
  667. certPEM = append(certPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...)
  668. if idx == 0 {
  669. firstCert, _ = x509.ParseCertificate(der)
  670. }
  671. }
  672. certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name))
  673. keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name))
  674. if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
  675. return nil, err
  676. }
  677. if err := savePrivateKeyPEM(keyPath, certPrivKey); err != nil {
  678. _ = os.Remove(certPath)
  679. return nil, err
  680. }
  681. cm.mu.Lock()
  682. defer cm.mu.Unlock()
  683. var fp [32]byte
  684. validFrom := time.Now()
  685. validTo := time.Now().AddDate(0, 0, 90)
  686. if firstCert != nil {
  687. fp = sha256.Sum256(firstCert.Raw)
  688. validFrom = firstCert.NotBefore
  689. validTo = firstCert.NotAfter
  690. }
  691. issuer := "Let's Encrypt"
  692. if req.Staging {
  693. issuer = "Let's Encrypt (Staging)"
  694. }
  695. info := &CertificateInfo{
  696. ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()),
  697. Name: name,
  698. CommonName: domain,
  699. SANs: []string{domain},
  700. IsCA: false,
  701. Issuer: issuer,
  702. KeyType: KeyRSA2048,
  703. ValidFrom: validFrom,
  704. ValidTo: validTo,
  705. Fingerprint: formatFingerprint(fp[:]),
  706. CertPath: certPath,
  707. KeyPath: keyPath,
  708. Type: TypeACME,
  709. Comment: req.Comment,
  710. }
  711. cm.certs[strings.ToLower(name)] = info
  712. if err := cm.saveLocked(); err != nil {
  713. return nil, err
  714. }
  715. logging.Log("certs,info", "Successfully issued Let's Encrypt certificate for %s", domain)
  716. return info, nil
  717. }
  718. // Helpers
  719. func generateKeyPair(keyType string) (crypto.PrivateKey, crypto.PublicKey, error) {
  720. switch strings.ToUpper(keyType) {
  721. case KeyRSA4096:
  722. priv, err := rsa.GenerateKey(rand.Reader, 4096)
  723. if err != nil {
  724. return nil, nil, err
  725. }
  726. return priv, &priv.PublicKey, nil
  727. case KeyECDSAP256:
  728. priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  729. if err != nil {
  730. return nil, nil, err
  731. }
  732. return priv, &priv.PublicKey, nil
  733. default: // KeyRSA2048
  734. priv, err := rsa.GenerateKey(rand.Reader, 2048)
  735. if err != nil {
  736. return nil, nil, err
  737. }
  738. return priv, &priv.PublicKey, nil
  739. }
  740. }
  741. func savePEM(path, blockType string, data []byte) error {
  742. block := &pem.Block{Type: blockType, Bytes: data}
  743. return os.WriteFile(path, pem.EncodeToMemory(block), 0644)
  744. }
  745. func savePrivateKeyPEM(path string, key crypto.PrivateKey) error {
  746. var block *pem.Block
  747. switch k := key.(type) {
  748. case *rsa.PrivateKey:
  749. block = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
  750. case *ecdsa.PrivateKey:
  751. b, err := x509.MarshalECPrivateKey(k)
  752. if err != nil {
  753. return err
  754. }
  755. block = &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
  756. default:
  757. b, err := x509.MarshalPKCS8PrivateKey(key)
  758. if err != nil {
  759. return err
  760. }
  761. block = &pem.Block{Type: "PRIVATE KEY", Bytes: b}
  762. }
  763. return os.WriteFile(path, pem.EncodeToMemory(block), 0600)
  764. }
  765. func loadCertAndKey(certPath, keyPath string) (*x509.Certificate, crypto.PrivateKey, error) {
  766. certPEM, err := os.ReadFile(certPath)
  767. if err != nil {
  768. return nil, nil, err
  769. }
  770. block, _ := pem.Decode(certPEM)
  771. if block == nil {
  772. return nil, nil, errors.New("failed decoding certificate PEM")
  773. }
  774. cert, err := x509.ParseCertificate(block.Bytes)
  775. if err != nil {
  776. return nil, nil, err
  777. }
  778. keyPEM, err := os.ReadFile(keyPath)
  779. if err != nil {
  780. return nil, nil, err
  781. }
  782. keyBlock, _ := pem.Decode(keyPEM)
  783. if keyBlock == nil {
  784. return nil, nil, errors.New("failed decoding private key PEM")
  785. }
  786. if k, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes); err == nil {
  787. return cert, k, nil
  788. }
  789. if k, err := x509.ParseECPrivateKey(keyBlock.Bytes); err == nil {
  790. return cert, k, nil
  791. }
  792. if k, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes); err == nil {
  793. return cert, k, nil
  794. }
  795. return nil, nil, errors.New("unrecognized private key format")
  796. }
  797. func formatFingerprint(b []byte) string {
  798. hexStr := hex.EncodeToString(b)
  799. var parts []string
  800. for i := 0; i < len(hexStr); i += 2 {
  801. parts = append(parts, strings.ToUpper(hexStr[i:i+2]))
  802. }
  803. return strings.Join(parts, ":")
  804. }