diff --git a/main.go b/main.go index 87de3e8..101b667 100644 --- a/main.go +++ b/main.go @@ -324,6 +324,15 @@ func writePEM(path, blockType string, der []byte, mode os.FileMode) error { if err := pem.Encode(f, &pem.Block{Type: blockType, Bytes: der}); err != nil { return fmt.Errorf("encode %s: %w", path, err) } + // fsync the data file so a crash after writePEM returns doesn't lose the cert. + if err := f.Sync(); err != nil { + return fmt.Errorf("sync %s: %w", path, err) + } + // fsync the parent directory so the directory entry is committed. + if dir, err := os.Open(filepath.Dir(path)); err == nil { + dir.Sync() // best-effort; Close will report any delayed error + dir.Close() + } return nil } diff --git a/zz_pilot_ca_test.go b/zz_pilot_ca_test.go index c1af99d..c7ff396 100644 --- a/zz_pilot_ca_test.go +++ b/zz_pilot_ca_test.go @@ -264,6 +264,37 @@ func TestRandomSerial_Unique(t *testing.T) { } } +// TestWritePEM_FsyncBeforeClose pins that writePEM produces valid +// PEM files and that the file is readable immediately after the call +// returns (data durability via fsync). +func TestWritePEM_FsyncBeforeClose(t *testing.T) { + t.Parallel() + dir := t.TempDir() + path := filepath.Join(dir, "test.crt") + der := make([]byte, 32) + for i := range der { + der[i] = byte(i) + } + if err := writePEM(path, "CERTIFICATE", der, 0o644); err != nil { + t.Fatalf("writePEM: %v", err) + } + // Immediately read back — data must be on disk (fsync'd). + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read back: %v", err) + } + block, _ := pem.Decode(raw) + if block == nil { + t.Fatal("no PEM block found") + } + if block.Type != "CERTIFICATE" { + t.Fatalf("expected CERTIFICATE, got %q", block.Type) + } + if len(block.Bytes) != 32 { + t.Fatalf("expected 32 DER bytes, got %d", len(block.Bytes)) + } +} + // mustReadCert reads a PEM-encoded cert and parses it, failing the // test on any error. func mustReadCert(t *testing.T, path string) *x509.Certificate {