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 0000000..7efc31d Binary files /dev/null and b/cmd/cert-bundler/testdata/pkg/core_certs.tgz differ diff --git a/cmd/cert-bundler/testdata/pkg/core_certs.zip b/cmd/cert-bundler/testdata/pkg/core_certs.zip new file mode 100644 index 0000000..f26676f Binary files /dev/null and b/cmd/cert-bundler/testdata/pkg/core_certs.zip differ diff --git a/cmd/cert-bundler/testdata/pkg/google_certs.tgz b/cmd/cert-bundler/testdata/pkg/google_certs.tgz new file mode 100644 index 0000000..13a953f Binary files /dev/null and b/cmd/cert-bundler/testdata/pkg/google_certs.tgz differ diff --git a/cmd/cert-bundler/testdata/pkg/lets_encrypt.zip b/cmd/cert-bundler/testdata/pkg/lets_encrypt.zip new file mode 100644 index 0000000..86d4884 Binary files /dev/null and b/cmd/cert-bundler/testdata/pkg/lets_encrypt.zip differ