2025-11-20 16:57:34 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-21 16:55:38 +01:00
|
|
|
"crypto/x509"
|
|
|
|
|
"encoding/pem"
|
2025-11-25 16:47:06 +01:00
|
|
|
"image/color"
|
2025-11-21 16:55:38 +01:00
|
|
|
"log"
|
|
|
|
|
"os"
|
2025-11-24 19:55:46 +01:00
|
|
|
certpair "pkcs-generator/modules/certpairs"
|
2025-11-21 16:55:38 +01:00
|
|
|
"slices"
|
|
|
|
|
|
|
|
|
|
"fyne.io/fyne/v2"
|
|
|
|
|
"fyne.io/fyne/v2/app"
|
2025-11-25 16:47:06 +01:00
|
|
|
"fyne.io/fyne/v2/canvas"
|
2025-11-21 16:55:38 +01:00
|
|
|
"fyne.io/fyne/v2/container"
|
|
|
|
|
"fyne.io/fyne/v2/dialog"
|
|
|
|
|
"fyne.io/fyne/v2/layout"
|
2025-11-24 10:32:56 +01:00
|
|
|
"fyne.io/fyne/v2/storage"
|
2025-11-25 16:47:06 +01:00
|
|
|
"fyne.io/fyne/v2/theme"
|
2025-11-21 16:55:38 +01:00
|
|
|
"fyne.io/fyne/v2/widget"
|
2025-11-20 16:57:34 +01:00
|
|
|
)
|
|
|
|
|
|
2025-11-21 22:04:52 +01:00
|
|
|
var (
|
2025-11-24 10:32:56 +01:00
|
|
|
windowSize fyne.Size = fyne.NewSize(700, 500)
|
|
|
|
|
|
|
|
|
|
keyTextFilter []string = []string{".key", ".txt", ".pem"}
|
|
|
|
|
crtTextFilter []string = []string{".crt", ".cer", ".txt", ".pem"}
|
2025-11-21 22:04:52 +01:00
|
|
|
)
|
|
|
|
|
|
2025-11-20 16:57:34 +01:00
|
|
|
func main() {
|
2025-11-24 19:55:46 +01:00
|
|
|
a := app.NewWithID("nl.systemec.pkcs-generator")
|
2025-11-25 16:47:06 +01:00
|
|
|
a.Settings().SetTheme(theme.DefaultTheme())
|
|
|
|
|
|
2025-11-24 19:55:46 +01:00
|
|
|
w := a.NewWindow("Systemec PKCS-Generator")
|
2025-11-21 22:04:52 +01:00
|
|
|
w.Resize(windowSize)
|
2025-11-21 16:55:38 +01:00
|
|
|
|
|
|
|
|
var keyPath, certPath string
|
|
|
|
|
|
|
|
|
|
// Labels to show selected filenames
|
|
|
|
|
fileLabel1 := widget.NewLabel("No file selected")
|
|
|
|
|
fileLabel2 := widget.NewLabel("No file selected")
|
|
|
|
|
|
|
|
|
|
radioLabel1 := widget.NewLabel("Select which Sectigo Intermediate")
|
|
|
|
|
|
|
|
|
|
// Certificate Keyfile
|
2025-11-21 22:04:52 +01:00
|
|
|
keyBtn := widget.NewButton("Upload Private Key File", func() {
|
|
|
|
|
keyDiag := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
|
2025-11-21 16:55:38 +01:00
|
|
|
if r != nil {
|
|
|
|
|
fileLabel1.SetText(r.URI().Name())
|
|
|
|
|
keyPath = r.URI().Path()
|
2025-11-21 22:04:52 +01:00
|
|
|
r.Close()
|
2025-11-21 16:55:38 +01:00
|
|
|
}
|
|
|
|
|
}, w)
|
2025-11-24 10:32:56 +01:00
|
|
|
keyFilter := storage.NewExtensionFileFilter(keyTextFilter)
|
|
|
|
|
keyDiag.SetFilter(keyFilter)
|
|
|
|
|
|
2025-11-21 22:04:52 +01:00
|
|
|
keyDiag.Resize(windowSize)
|
|
|
|
|
keyDiag.Show()
|
2025-11-21 16:55:38 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Certificate file
|
2025-11-21 22:04:52 +01:00
|
|
|
certBtn := widget.NewButton("Upload Certificate File", func() {
|
|
|
|
|
// Use NewFileOpen to get the dialog object
|
|
|
|
|
certDiag := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
|
2025-11-21 16:55:38 +01:00
|
|
|
if r != nil {
|
|
|
|
|
fileLabel2.SetText(r.URI().Name())
|
|
|
|
|
certPath = r.URI().Path()
|
2025-11-21 22:04:52 +01:00
|
|
|
r.Close()
|
2025-11-21 16:55:38 +01:00
|
|
|
}
|
|
|
|
|
}, w)
|
2025-11-24 10:32:56 +01:00
|
|
|
certFilter := storage.NewExtensionFileFilter(crtTextFilter)
|
|
|
|
|
certDiag.SetFilter(certFilter)
|
|
|
|
|
|
2025-11-21 22:04:52 +01:00
|
|
|
// Resize the dialog
|
|
|
|
|
certDiag.Resize(windowSize)
|
|
|
|
|
certDiag.Show()
|
2025-11-21 16:55:38 +01:00
|
|
|
})
|
|
|
|
|
|
2025-11-24 19:51:04 +01:00
|
|
|
// Certificate Intermediate selector
|
|
|
|
|
// Basic in form but can easily be expanded.
|
|
|
|
|
var sectigo2025 bool
|
2025-11-24 16:55:16 +01:00
|
|
|
caRadio := widget.NewRadioGroup([]string{"New Sectigo (2025-03-22+)", "Old Sectigo (2025-03-22-)"}, func(selected string) {
|
2025-11-21 16:55:38 +01:00
|
|
|
switch selected {
|
2025-11-25 09:14:30 +01:00
|
|
|
case "New Sectigo (2025-03-22+)":
|
2025-11-24 19:51:04 +01:00
|
|
|
sectigo2025 = true
|
2025-11-25 09:14:30 +01:00
|
|
|
case "Old Sectigo (2025-03-22-)":
|
2025-11-24 19:51:04 +01:00
|
|
|
sectigo2025 = false
|
2025-11-21 16:55:38 +01:00
|
|
|
default: //Fallback
|
2025-11-24 19:51:04 +01:00
|
|
|
sectigo2025 = true
|
2025-11-21 16:55:38 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
caRadio.SetSelected("New Sectigo (2025+)") // default
|
|
|
|
|
|
2025-11-25 16:47:06 +01:00
|
|
|
// Multiline entry
|
|
|
|
|
textLabl := widget.NewLabel("Status will appear here...")
|
|
|
|
|
textLabl.Selectable = true
|
|
|
|
|
|
|
|
|
|
border := canvas.NewRectangle(color.Transparent)
|
|
|
|
|
border.StrokeColor = color.RGBA{255, 255, 255, 255}
|
|
|
|
|
border.StrokeWidth = 2
|
|
|
|
|
border.CornerRadius = 5
|
|
|
|
|
|
|
|
|
|
statusBox := container.NewStack(border, textLabl)
|
2025-11-21 16:55:38 +01:00
|
|
|
|
2025-11-24 10:32:56 +01:00
|
|
|
actionBtn := widget.NewButton("Generate", func() {
|
2025-11-21 16:55:38 +01:00
|
|
|
files := []string{keyPath, certPath}
|
|
|
|
|
|
2025-11-24 19:51:04 +01:00
|
|
|
// The following structure is basic but can easily be expanded to fit more pairs
|
|
|
|
|
// 0 = ROOT
|
|
|
|
|
// 1 = CA
|
|
|
|
|
// As per spec in the certpair module.
|
|
|
|
|
var certPair []string
|
|
|
|
|
if sectigo2025 {
|
|
|
|
|
certPair = certpair.SectigoNewChain
|
2025-11-21 16:55:38 +01:00
|
|
|
} else {
|
2025-11-24 19:51:04 +01:00
|
|
|
certPair = certpair.SectigoOldChain
|
2025-11-21 16:55:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allPresent := true
|
|
|
|
|
if slices.Contains(files, "") {
|
|
|
|
|
allPresent = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !allPresent {
|
2025-11-25 16:47:06 +01:00
|
|
|
textLabl.SetText("One or more files missing!")
|
2025-11-24 19:59:52 +01:00
|
|
|
log.Println("One or more files missing!")
|
2025-11-21 16:55:38 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:51:04 +01:00
|
|
|
respText, pfxData := integrityCheckAndGo(keyPath, certPath, certPair[1], certPair[0])
|
2025-11-24 10:32:56 +01:00
|
|
|
|
2025-11-24 16:55:16 +01:00
|
|
|
if pfxData == nil {
|
|
|
|
|
log.Println(respText)
|
2025-11-25 16:47:06 +01:00
|
|
|
textLabl.SetText(respText)
|
2025-11-24 16:55:16 +01:00
|
|
|
return
|
|
|
|
|
}
|
2025-11-24 10:32:56 +01:00
|
|
|
|
|
|
|
|
// Show Save File dialog immediately
|
|
|
|
|
svDialog := dialog.NewFileSave(
|
|
|
|
|
func(writer fyne.URIWriteCloser, err error) {
|
|
|
|
|
if err != nil {
|
|
|
|
|
dialog.ShowError(err, w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if writer == nil {
|
|
|
|
|
// User cancelled
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, writeErr := writer.Write(pfxData)
|
|
|
|
|
if writeErr != nil {
|
|
|
|
|
dialog.ShowError(writeErr, w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.Close()
|
|
|
|
|
|
2025-11-25 16:47:06 +01:00
|
|
|
var dnText string = "PKCS file saved to: " + writer.URI().Path() + "\n" + respText
|
2025-11-24 10:32:56 +01:00
|
|
|
|
2025-11-25 16:47:06 +01:00
|
|
|
textLabl.SetText(dnText)
|
2025-11-24 10:32:56 +01:00
|
|
|
}, w)
|
|
|
|
|
svDialog.Resize(windowSize)
|
|
|
|
|
|
|
|
|
|
svDialog.SetFileName("certificate_store.pfx")
|
|
|
|
|
svDialog.SetFilter(storage.NewExtensionFileFilter([]string{".pfx"}))
|
|
|
|
|
|
|
|
|
|
svDialog.Show()
|
2025-11-21 16:55:38 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
cancelBtn := widget.NewButton("Exit", func() {
|
2025-11-24 19:59:52 +01:00
|
|
|
log.Println("Quitting...")
|
2025-11-21 16:55:38 +01:00
|
|
|
os.Exit(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// CREATE LAYOUTS
|
|
|
|
|
|
|
|
|
|
bottom := container.NewHBox(
|
|
|
|
|
actionBtn, // left
|
|
|
|
|
layout.NewSpacer(), // flexible space
|
|
|
|
|
cancelBtn, // right
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
centerContent := container.NewVBox(
|
|
|
|
|
widget.NewLabel("Select relevant files."),
|
2025-11-21 22:04:52 +01:00
|
|
|
container.New(layout.NewGridLayout(2), keyBtn, fileLabel1),
|
|
|
|
|
container.New(layout.NewGridLayout(2), certBtn, fileLabel2),
|
2025-11-24 10:32:56 +01:00
|
|
|
widget.NewLabel("\n"),
|
2025-11-21 16:55:38 +01:00
|
|
|
container.New(layout.NewGridLayout(2), radioLabel1, caRadio),
|
2025-11-24 10:32:56 +01:00
|
|
|
layout.NewSpacer(), // optional flexible space
|
2025-11-25 16:47:06 +01:00
|
|
|
statusBox, // Add the referenced text container
|
2025-11-24 10:32:56 +01:00
|
|
|
widget.NewLabel(""), // Add empty line for space
|
2025-11-21 16:55:38 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
content := container.NewBorder(
|
|
|
|
|
nil, // top
|
|
|
|
|
bottom, // bottom
|
|
|
|
|
nil, nil, // left, right
|
|
|
|
|
centerContent,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
w.SetContent(content)
|
|
|
|
|
w.ShowAndRun()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readFile(path string) []byte {
|
|
|
|
|
rawData, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
2025-11-24 19:59:52 +01:00
|
|
|
log.Printf("Error reading contents of %s, %e", path, err)
|
2025-11-21 16:55:38 +01:00
|
|
|
return nil
|
|
|
|
|
} else {
|
|
|
|
|
return rawData
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parsePrivateKey(keyData []byte) any {
|
|
|
|
|
block, _ := pem.Decode(keyData)
|
|
|
|
|
if block == nil {
|
|
|
|
|
log.Fatal("failed to decode key PEM")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return privateKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseX509(certData []byte) *x509.Certificate {
|
|
|
|
|
block, _ := pem.Decode(certData)
|
|
|
|
|
if block == nil {
|
|
|
|
|
log.Fatal("failed to parse PEM block")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return cert
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:55:46 +01:00
|
|
|
func integrityCheckAndGo(keyPath, certPath, caRootString, caCertString string) (string, []byte) {
|
2025-11-21 16:55:38 +01:00
|
|
|
if _, err := os.Stat(keyPath); err != nil {
|
2025-11-24 19:59:52 +01:00
|
|
|
log.Printf("Error checking Private Keyfile, %e", err)
|
2025-11-24 10:32:56 +01:00
|
|
|
return "Error...", nil
|
2025-11-21 16:55:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(certPath); err != nil {
|
2025-11-24 19:59:52 +01:00
|
|
|
log.Printf("Error checking Certificate file, %e", err)
|
2025-11-24 10:32:56 +01:00
|
|
|
return "Error...", nil
|
2025-11-21 16:55:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
keyData := readFile(keyPath) // Read the private key file (PEM/DER)
|
|
|
|
|
certData := readFile(certPath) // Read the certificate file (PEM/DER)
|
|
|
|
|
|
|
|
|
|
key := parsePrivateKey(keyData) // Convert bytes to Go private key object
|
|
|
|
|
cert := parseX509(certData) // Convert bytes to Go x509.Certificate
|
|
|
|
|
caCert := parseX509([]byte(caCertString))
|
2025-11-24 10:32:56 +01:00
|
|
|
rootCert := parseX509([]byte(caRootString))
|
2025-11-21 16:55:38 +01:00
|
|
|
|
|
|
|
|
checkPublicKey(cert) // Print the information about the key
|
|
|
|
|
|
|
|
|
|
if checkCertKeyPair(cert.PublicKey, key) {
|
2025-11-24 16:55:16 +01:00
|
|
|
log.Println("Private key matches certificate")
|
2025-11-21 16:55:38 +01:00
|
|
|
} else {
|
2025-11-24 16:55:16 +01:00
|
|
|
return "Private key does NOT match certificate", nil
|
2025-11-21 16:55:38 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 10:32:56 +01:00
|
|
|
caCertList := []*x509.Certificate{caCert, rootCert}
|
2025-11-21 16:55:38 +01:00
|
|
|
|
2025-11-25 16:47:06 +01:00
|
|
|
msg, pfxData, err := generatePKCS12(key, cert, caCertList)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return msg, nil
|
|
|
|
|
}
|
|
|
|
|
return msg, pfxData
|
2025-11-20 16:57:34 +01:00
|
|
|
}
|