From 289c9d234382b65174a97ec6c2c25ab4203e6d91 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 14 Nov 2025 21:59:03 -0800 Subject: [PATCH] cmd: add cert-bundler Builds certificate bundles. --- cmd/cert-bundler/README.txt | 148 ++++++ cmd/cert-bundler/main.go | 489 ++++++++++++++++++ cmd/cert-bundler/testdata/bundle.yaml | 43 ++ cmd/cert-bundler/testdata/pems/goog-wr2.pem | 29 ++ cmd/cert-bundler/testdata/pems/gts-r1.pem | 31 ++ .../testdata/pems/isrg-root-x1.pem | 31 ++ cmd/cert-bundler/testdata/pems/le-e7.pem | 26 + cmd/cert-bundler/testdata/pkg/bundle.sha256 | 4 + cmd/cert-bundler/testdata/pkg/core_certs.tgz | Bin 0 -> 5407 bytes cmd/cert-bundler/testdata/pkg/core_certs.zip | Bin 0 -> 11106 bytes .../testdata/pkg/google_certs.tgz | Bin 0 -> 2562 bytes .../testdata/pkg/lets_encrypt.zip | Bin 0 -> 5526 bytes 12 files changed, 801 insertions(+) create mode 100644 cmd/cert-bundler/README.txt create mode 100644 cmd/cert-bundler/main.go create mode 100644 cmd/cert-bundler/testdata/bundle.yaml create mode 100644 cmd/cert-bundler/testdata/pems/goog-wr2.pem create mode 100644 cmd/cert-bundler/testdata/pems/gts-r1.pem create mode 100644 cmd/cert-bundler/testdata/pems/isrg-root-x1.pem create mode 100644 cmd/cert-bundler/testdata/pems/le-e7.pem create mode 100644 cmd/cert-bundler/testdata/pkg/bundle.sha256 create mode 100644 cmd/cert-bundler/testdata/pkg/core_certs.tgz create mode 100644 cmd/cert-bundler/testdata/pkg/core_certs.zip create mode 100644 cmd/cert-bundler/testdata/pkg/google_certs.tgz create mode 100644 cmd/cert-bundler/testdata/pkg/lets_encrypt.zip diff --git a/cmd/cert-bundler/README.txt b/cmd/cert-bundler/README.txt new file mode 100644 index 0000000..8984b86 --- /dev/null +++ b/cmd/cert-bundler/README.txt @@ -0,0 +1,148 @@ +cert-bundler: create certificate chain archives +------------------------------------------------ + +Description + cert-bundler creates archives of certificate chains from a YAML configuration + file. It validates certificates, checks expiration dates, and generates + archives in multiple formats (zip, tar.gz) with optional manifest files + containing SHA256 checksums. + +Usage + cert-bundler [options] + + Options: + -c Path to YAML configuration file (default: bundle.yaml) + -o Output directory for archives (default: pkg) + +YAML Configuration Format + + The configuration file uses the following structure: + + config: + hashes: + expiry: + chains: + : + certs: + - root: + intermediates: + - + - + - root: + intermediates: + - + outputs: + include_single: + include_individual: + manifest: + encoding: + formats: + - + - + +Configuration Fields + + config: + hashes: (optional) Name of the file to write SHA256 checksums of all + generated archives. If omitted, no hash file is created. + expiry: (optional) Expiration warning threshold. Certificates expiring + within this period will trigger a warning. Supports formats like + "1y" (year), "6m" (month), "30d" (day). Default: 1y + + chains: + Each key under "chains" defines a named certificate group. All certificates + in a group are bundled together into archives with the group name. + + certs: + List of certificate chains. Each chain has: + root: Path to root CA certificate (PEM or DER format) + intermediates: List of paths to intermediate certificates + + All intermediates are validated against their root CA. An error is + reported if signature verification fails. + + outputs: + Defines output formats and content for the group's archives: + + include_single: (bool) If true, all certificates in the group are + concatenated into a single file named "bundle.pem" + (or "bundle.crt" for DER encoding). + + include_individual: (bool) If true, each certificate is included as + a separate file in the archive, named after the + original file (e.g., "int/cca2.pem" becomes + "cca2.pem"). + + manifest: (bool) If true, a MANIFEST file is included containing + SHA256 checksums of all files in the archive. + + encoding: Specifies certificate encoding in the archive: + - "pem": PEM format with .pem extension (default) + - "der": DER format with .crt extension + - "both": Both PEM and DER versions are included + + formats: List of archive formats to generate: + - "zip": Creates a .zip archive + - "tgz": Creates a .tar.gz archive + +Output Files + + For each group and format combination, an archive is created: + .zip or .tar.gz + + If config.hashes is specified, a hash file is created in the output directory + containing SHA256 checksums of all generated archives. + +Example Configuration + + config: + hashes: bundle.sha256 + expiry: 1y + chains: + core_certs: + certs: + - root: roots/core-ca.pem + intermediates: + - int/cca1.pem + - int/cca2.pem + - int/cca3.pem + - root: roots/ssh-ca.pem + intermediates: + - ssh/ssh_dmz1.pem + - ssh/ssh_internal.pem + outputs: + include_single: true + include_individual: true + manifest: true + encoding: pem + formats: + - zip + - tgz + + This configuration: + - Creates core_certs.zip and core_certs.tar.gz in the output directory + - Each archive contains bundle.pem (all certificates concatenated) + - Each archive contains individual certificates (core-ca.pem, cca1.pem, etc.) + - Each archive includes a MANIFEST file with SHA256 checksums + - Creates bundle.sha256 with checksums of the two archives + - Warns if any certificate expires within 1 year + +Examples + + # Create bundles using default configuration (bundle.yaml -> pkg/) + cert-bundler + + # Use custom configuration and output directory + cert-bundler -c myconfig.yaml -o output + + # Create bundles from testdata configuration + cert-bundler -c testdata/bundle.yaml -o testdata/pkg + +Notes + - Certificate paths in the YAML are relative to the current working directory + - All intermediates must be properly signed by their specified root CA + - Certificates are checked for expiration; warnings are printed to stderr + - Expired certificates do not prevent archive creation but generate warnings + - Both PEM and DER certificate formats are supported as input + - Archive filenames use the group name, not individual chain names + - If both include_single and include_individual are true, archives contain both diff --git a/cmd/cert-bundler/main.go b/cmd/cert-bundler/main.go new file mode 100644 index 0000000..c9e0e1f --- /dev/null +++ b/cmd/cert-bundler/main.go @@ -0,0 +1,489 @@ +package main + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "crypto/sha256" + "crypto/x509" + _ "embed" + "encoding/pem" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "git.wntrmute.dev/kyle/goutils/certlib" + "gopkg.in/yaml.v2" +) + +// Config represents the top-level YAML configuration +type Config struct { + Config struct { + Hashes string `yaml:"hashes"` + Expiry string `yaml:"expiry"` + } `yaml:"config"` + Chains map[string]ChainGroup `yaml:"chains"` +} + +// ChainGroup represents a named group of certificate chains +type ChainGroup struct { + Certs []CertChain `yaml:"certs"` + Outputs Outputs `yaml:"outputs"` +} + +// CertChain represents a root certificate and its intermediates +type CertChain struct { + Root string `yaml:"root"` + Intermediates []string `yaml:"intermediates"` +} + +// Outputs defines output format options +type Outputs struct { + IncludeSingle bool `yaml:"include_single"` + IncludeIndividual bool `yaml:"include_individual"` + Manifest bool `yaml:"manifest"` + Formats []string `yaml:"formats"` + Encoding string `yaml:"encoding"` +} + +var ( + configFile string + outputDir string +) + +var formatExtensions = map[string]string{ + "zip": ".zip", + "tgz": ".tar.gz", +} + +//go:embed README.txt +var readmeContent string + +func usage() { + fmt.Fprint(os.Stderr, readmeContent) +} + +func main() { + flag.Usage = usage + flag.StringVar(&configFile, "c", "bundle.yaml", "path to YAML configuration file") + flag.StringVar(&outputDir, "o", "pkg", "output directory for archives") + flag.Parse() + + if configFile == "" { + fmt.Fprintf(os.Stderr, "Error: configuration file required (-c flag)\n") + os.Exit(1) + } + + // Load and parse configuration + cfg, err := loadConfig(configFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + + // Parse expiry duration (default 1 year) + expiryDuration := 365 * 24 * time.Hour + if cfg.Config.Expiry != "" { + expiryDuration, err = parseDuration(cfg.Config.Expiry) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing expiry: %v\n", err) + os.Exit(1) + } + } + + // Create output directory if it doesn't exist + if err := os.MkdirAll(outputDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err) + os.Exit(1) + } + + // Process each chain group + createdFiles := []string{} + for groupName, group := range cfg.Chains { + files, err := processChainGroup(groupName, group, expiryDuration) + if err != nil { + fmt.Fprintf(os.Stderr, "Error processing chain group %s: %v\n", groupName, err) + os.Exit(1) + } + createdFiles = append(createdFiles, files...) + } + + // Generate hash file for all created archives + if cfg.Config.Hashes != "" { + hashFile := filepath.Join(outputDir, cfg.Config.Hashes) + if err := generateHashFile(hashFile, createdFiles); err != nil { + fmt.Fprintf(os.Stderr, "Error generating hash file: %v\n", err) + os.Exit(1) + } + } + + fmt.Println("Certificate bundling completed successfully") +} + +func loadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + +func parseDuration(s string) (time.Duration, error) { + // Support simple formats like "1y", "6m", "30d" + if len(s) < 2 { + return 0, fmt.Errorf("invalid duration format: %s", s) + } + + unit := s[len(s)-1] + value := s[:len(s)-1] + + var multiplier time.Duration + switch unit { + case 'y', 'Y': + multiplier = 365 * 24 * time.Hour + case 'm', 'M': + multiplier = 30 * 24 * time.Hour + case 'd', 'D': + multiplier = 24 * time.Hour + default: + return time.ParseDuration(s) + } + + var num int + _, err := fmt.Sscanf(value, "%d", &num) + if err != nil { + return 0, fmt.Errorf("invalid duration value: %s", s) + } + + return time.Duration(num) * multiplier, nil +} + +func processChainGroup(groupName string, group ChainGroup, expiryDuration time.Duration) ([]string, error) { + var createdFiles []string + + // Default encoding to "pem" if not specified + encoding := group.Outputs.Encoding + if encoding == "" { + encoding = "pem" + } + + // Collect data from all chains in the group + var singleFileCerts []*x509.Certificate + var individualCerts []certWithPath + + for _, chain := range group.Certs { + // Step 1: Load all certificates for this chain + allCerts := make(map[string]*x509.Certificate) + + // Load root certificate + rootCert, err := certlib.LoadCertificate(chain.Root) + if err != nil { + return nil, fmt.Errorf("failed to load root certificate %s: %v", chain.Root, err) + } + allCerts[chain.Root] = rootCert + + // Check expiry for root + checkExpiry(chain.Root, rootCert, expiryDuration) + + // Add root to single file if needed + if group.Outputs.IncludeSingle { + singleFileCerts = append(singleFileCerts, rootCert) + } + + // Add root to individual files if needed + if group.Outputs.IncludeIndividual { + individualCerts = append(individualCerts, certWithPath{ + cert: rootCert, + path: chain.Root, + }) + } + + // Step 2: Load and validate intermediates + for _, intPath := range chain.Intermediates { + intCert, err := certlib.LoadCertificate(intPath) + if err != nil { + return nil, fmt.Errorf("failed to load intermediate certificate %s: %v", intPath, err) + } + allCerts[intPath] = intCert + + // Validate that intermediate is signed by root + if err := intCert.CheckSignatureFrom(rootCert); err != nil { + return nil, fmt.Errorf("intermediate %s is not properly signed by root %s: %v", intPath, chain.Root, err) + } + + // Check expiry for intermediate + checkExpiry(intPath, intCert, expiryDuration) + + // Add intermediate to a single file if needed + if group.Outputs.IncludeSingle { + singleFileCerts = append(singleFileCerts, intCert) + } + + // Add intermediate to individual files if needed + if group.Outputs.IncludeIndividual { + individualCerts = append(individualCerts, certWithPath{ + cert: intCert, + path: intPath, + }) + } + } + } + + // Prepare files for inclusion in archives for the entire group. + var archiveFiles []fileEntry + + // Handle a single bundle file. + if group.Outputs.IncludeSingle && len(singleFileCerts) > 0 { + files, err := encodeCertsToFiles(singleFileCerts, "bundle", encoding, true) + if err != nil { + return nil, fmt.Errorf("failed to encode single bundle: %v", err) + } + archiveFiles = append(archiveFiles, files...) + } + + // Handle individual files + if group.Outputs.IncludeIndividual { + for _, cp := range individualCerts { + baseName := strings.TrimSuffix(filepath.Base(cp.path), filepath.Ext(cp.path)) + files, err := encodeCertsToFiles([]*x509.Certificate{cp.cert}, baseName, encoding, false) + if err != nil { + return nil, fmt.Errorf("failed to encode individual cert %s: %v", cp.path, err) + } + archiveFiles = append(archiveFiles, files...) + } + } + + // Generate manifest if requested + if group.Outputs.Manifest { + manifestContent := generateManifest(archiveFiles) + archiveFiles = append(archiveFiles, fileEntry{ + name: "MANIFEST", + content: manifestContent, + }) + } + + // Create archives for the entire group + for _, format := range group.Outputs.Formats { + ext, ok := formatExtensions[format] + if !ok { + return nil, fmt.Errorf("unsupported format: %s", format) + } + archivePath := filepath.Join(outputDir, groupName+ext) + switch format { + case "zip": + if err := createZipArchive(archivePath, archiveFiles); err != nil { + return nil, fmt.Errorf("failed to create zip archive: %v", err) + } + case "tgz": + if err := createTarGzArchive(archivePath, archiveFiles); err != nil { + return nil, fmt.Errorf("failed to create tar.gz archive: %v", err) + } + default: + return nil, fmt.Errorf("unsupported format: %s", format) + } + createdFiles = append(createdFiles, archivePath) + } + + return createdFiles, nil +} + +func checkExpiry(path string, cert *x509.Certificate, expiryDuration time.Duration) { + now := time.Now() + expiryThreshold := now.Add(expiryDuration) + + if cert.NotAfter.Before(expiryThreshold) { + daysUntilExpiry := int(cert.NotAfter.Sub(now).Hours() / 24) + if daysUntilExpiry < 0 { + fmt.Fprintf(os.Stderr, "WARNING: Certificate %s has EXPIRED (expired %d days ago)\n", path, -daysUntilExpiry) + } else { + fmt.Fprintf(os.Stderr, "WARNING: Certificate %s will expire in %d days (on %s)\n", path, daysUntilExpiry, cert.NotAfter.Format("2006-01-02")) + } + } +} + +type fileEntry struct { + name string + content []byte +} + +type certWithPath struct { + cert *x509.Certificate + path string +} + +// encodeCertsToFiles converts certificates to file entries based on encoding type +// If isSingle is true, certs are concatenated into a single file; otherwise one cert per file +func encodeCertsToFiles(certs []*x509.Certificate, baseName string, encoding string, isSingle bool) ([]fileEntry, error) { + var files []fileEntry + + switch encoding { + case "pem": + pemContent, err := encodeCertsToPEM(certs, isSingle) + if err != nil { + return nil, err + } + files = append(files, fileEntry{ + name: baseName + ".pem", + content: pemContent, + }) + case "der": + if isSingle { + // For single file in DER, concatenate all cert DER bytes + var derContent []byte + for _, cert := range certs { + derContent = append(derContent, cert.Raw...) + } + files = append(files, fileEntry{ + name: baseName + ".crt", + content: derContent, + }) + } else { + // Individual DER file (should only have one cert) + if len(certs) > 0 { + files = append(files, fileEntry{ + name: baseName + ".crt", + content: certs[0].Raw, + }) + } + } + case "both": + // Add PEM version + pemContent, err := encodeCertsToPEM(certs, isSingle) + if err != nil { + return nil, err + } + files = append(files, fileEntry{ + name: baseName + ".pem", + content: pemContent, + }) + // Add DER version + if isSingle { + var derContent []byte + for _, cert := range certs { + derContent = append(derContent, cert.Raw...) + } + files = append(files, fileEntry{ + name: baseName + ".crt", + content: derContent, + }) + } else { + if len(certs) > 0 { + files = append(files, fileEntry{ + name: baseName + ".crt", + content: certs[0].Raw, + }) + } + } + default: + return nil, fmt.Errorf("unsupported encoding: %s (must be 'pem', 'der', or 'both')", encoding) + } + + return files, nil +} + +// encodeCertsToPEM encodes certificates to PEM format +func encodeCertsToPEM(certs []*x509.Certificate, concatenate bool) ([]byte, error) { + var pemContent []byte + for _, cert := range certs { + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + pemContent = append(pemContent, pem.EncodeToMemory(pemBlock)...) + } + return pemContent, nil +} + +func generateManifest(files []fileEntry) []byte { + var manifest strings.Builder + for _, file := range files { + if file.name == "MANIFEST" { + continue + } + hash := sha256.Sum256(file.content) + manifest.WriteString(fmt.Sprintf("%x %s\n", hash, file.name)) + } + return []byte(manifest.String()) +} + +func createZipArchive(path string, files []fileEntry) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + w := zip.NewWriter(f) + defer w.Close() + + for _, file := range files { + fw, err := w.Create(file.name) + if err != nil { + return err + } + if _, err := fw.Write(file.content); err != nil { + return err + } + } + + return nil +} + +func createTarGzArchive(path string, files []fileEntry) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + gw := gzip.NewWriter(f) + defer gw.Close() + + tw := tar.NewWriter(gw) + defer tw.Close() + + for _, file := range files { + hdr := &tar.Header{ + Name: file.name, + Mode: 0644, + Size: int64(len(file.content)), + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err := tw.Write(file.content); err != nil { + return err + } + } + + return nil +} + +func generateHashFile(path string, files []string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + for _, file := range files { + data, err := os.ReadFile(file) + if err != nil { + return err + } + + hash := sha256.Sum256(data) + fmt.Fprintf(f, "%x %s\n", hash, filepath.Base(file)) + } + + return nil +} diff --git a/cmd/cert-bundler/testdata/bundle.yaml b/cmd/cert-bundler/testdata/bundle.yaml new file mode 100644 index 0000000..91e369c --- /dev/null +++ b/cmd/cert-bundler/testdata/bundle.yaml @@ -0,0 +1,43 @@ +config: + hashes: bundle.sha256 + expiry: 1y +chains: + core_certs: + certs: + - root: pems/gts-r1.pem + intermediates: + - pems/goog-wr2.pem + - root: pems/isrg-root-x1.pem + intermediates: + - pems/le-e7.pem + outputs: + include_single: true + include_individual: true + manifest: true + formats: + - zip + - tgz + google_certs: + certs: + - root: pems/gts-r1.pem + intermediates: + - pems/goog-wr2.pem + outputs: + include_single: true + include_individual: false + manifest: true + encoding: der + formats: + - tgz + lets_encrypt: + certs: + - root: pems/isrg-root-x1.pem + intermediates: + - pems/le-e7.pem + outputs: + include_single: false + include_individual: true + manifest: false + encoding: both + formats: + - zip \ No newline at end of file diff --git a/cmd/cert-bundler/testdata/pems/goog-wr2.pem b/cmd/cert-bundler/testdata/pems/goog-wr2.pem new file mode 100644 index 0000000..e374984 --- /dev/null +++ b/cmd/cert-bundler/testdata/pems/goog-wr2.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw +MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl +cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc ++MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji +aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc +LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX +xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX +FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/ +AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk +rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG +GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw +Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq +hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS +TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe +SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT +DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu +ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB +vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl +Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG +iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr +Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw +qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU +/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0= +-----END CERTIFICATE----- diff --git a/cmd/cert-bundler/testdata/pems/gts-r1.pem b/cmd/cert-bundler/testdata/pems/gts-r1.pem new file mode 100644 index 0000000..a13aa05 --- /dev/null +++ b/cmd/cert-bundler/testdata/pems/gts-r1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- diff --git a/cmd/cert-bundler/testdata/pems/isrg-root-x1.pem b/cmd/cert-bundler/testdata/pems/isrg-root-x1.pem new file mode 100644 index 0000000..b85c803 --- /dev/null +++ b/cmd/cert-bundler/testdata/pems/isrg-root-x1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/cmd/cert-bundler/testdata/pems/le-e7.pem b/cmd/cert-bundler/testdata/pems/le-e7.pem new file mode 100644 index 0000000..d30d176 --- /dev/null +++ b/cmd/cert-bundler/testdata/pems/le-e7.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw +WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST +CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef +QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw +gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD +ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4 +wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB +AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g +BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu +Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD +aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF +h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG +yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr +OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o +yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S +M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ +UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq +Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I +tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ +YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty ++VUwFj9tmWxyR/M= +-----END CERTIFICATE----- diff --git a/cmd/cert-bundler/testdata/pkg/bundle.sha256 b/cmd/cert-bundler/testdata/pkg/bundle.sha256 new file mode 100644 index 0000000..c8bed0c --- /dev/null +++ b/cmd/cert-bundler/testdata/pkg/bundle.sha256 @@ -0,0 +1,4 @@ +5ed8bf9ed693045faa8a5cb0edc4a870052e56aef6291ce8b1604565affbc2a4 core_certs.zip +e59eddc590d2f7b790a87c5b56e81697088ab54be382c0e2c51b82034006d308 core_certs.tgz +51b9b63b1335118079e90700a3a5b847c363808e9116e576ca84f301bc433289 google_certs.tgz +3d1910ca8835c3ded1755a8c7d6c48083c2f3ff68b2bfbf932aaf27e29d0a232 lets_encrypt.zip diff --git a/cmd/cert-bundler/testdata/pkg/core_certs.tgz b/cmd/cert-bundler/testdata/pkg/core_certs.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7efc31d5754543877a7e400cb9e3bac746883982 GIT binary patch literal 5407 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®~t?<#ZeAsCE5KsrJ0bx51^ev3iU@t@FdaI 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 Yp$%*|8&OSEXK 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 literal 0 HcmV?d00001