diff --git a/certs/cert.go b/certs/cert.go index da651a2..1ab8cb3 100644 --- a/certs/cert.go +++ b/certs/cert.go @@ -7,8 +7,9 @@ import ( "encoding/hex" "encoding/pem" "fmt" - "github.com/sirupsen/logrus" "strings" + + "github.com/sirupsen/logrus" ) // Certificate is a X509 certifcate / private key pair diff --git a/certs/fixtures/file.yml b/certs/fixtures/file.yml new file mode 100644 index 0000000..fc739a5 --- /dev/null +++ b/certs/fixtures/file.yml @@ -0,0 +1,4 @@ +ca: + cert: fixtures/k8s-ca-crt.pem + privateKey: fixtures/k8s-ca-key.pem + password: foobar \ No newline at end of file diff --git a/certs/fixtures/k8s-ca-crt.pem b/certs/fixtures/k8s-ca-crt.pem new file mode 100644 index 0000000..526bebe --- /dev/null +++ b/certs/fixtures/k8s-ca-crt.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICxDCCAaygAwIBAgIIQ5RaS8gSnKIwDQYJKoZIhvcNAQELBQAwDjEMMAoGA1UE +AxMDazhzMB4XDTIwMDMwOTEzNTkwMVoXDTIxMDMwODE0MTQwMVowDjEMMAoGA1UE +AxMDazhzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAturUlexOo5VR +FpFliNGByHjM6zSZcegqkZNDiXlm7U38PesAvOLHBK/D1rLOxtgSV35OeINHue4N +DaFpx2GKrnxZqzSXIyc0G/R0ROFMGru/JjR6Rq5kZLsoTI1LmdX/BfqCZYX3icl8 +qBQSAcmsr9NyWaR3UCuo8fwYJHI2CGpDqGaqWEirTy16JrOeBwnJTbnRISymECvY +JugwEneJmw9CK1EXibT6kdyBfUu1IG0xdqkk2wsSl+yJGHfHCXnD3NM78X6IuU3a +Ge/gEGBAgbZrUhoGhB9eHIvI+Yq+gzpoL0gL0oIPl0edOFqGa84nPV2NRklDjASy +JuSWMUrpywIDAQABoyYwJDAOBgNVHQ8BAf8EBAMCAqQwEgYDVR0TAQH/BAgwBgEB +/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAlNI7ccMv/iudSkUo/Qd/rW3CD0ZSNw/t +RiaoOAhv+UBrwihWxl+/tFydIFthyIK1bF2B4s8MManbRaIjEPmM5iKgz4SE/qjy +5otlHUqXxVGFesaT5CD37GAIiSZvf9hJSC8nRVJ7r9Iq/l8T8IhZWwjv6vivqQlU +YgR6Gbul+MnJkkK38V6vnCMaBRo29a8mSbFQErXk6niaVTxUPSNCggBXXbHmf9W0 +OpSzi7xqcmE+PtRekFfoNCoKqLxesjNXJZP9OCtBK0T0w4rdzw83T/TB58Sv2fAo +CsD7Hign1e9TjjzjCcJjtAmyCKmQlL/HUOFSVq/RNvRjdd17Dk/DsQ== +-----END CERTIFICATE----- diff --git a/certs/fixtures/k8s-ca-key.pem b/certs/fixtures/k8s-ca-key.pem new file mode 100644 index 0000000..b80d031 --- /dev/null +++ b/certs/fixtures/k8s-ca-key.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,06ba1b9fbd57bfac9d71d0520fc61eff + +PHju67RTiquKLN1dc9kT/OCt0gTcO1QC/VDODfJczkFvjFqGs/gjLPD5Tr95FXga +Cs/1y2qi9lzQoWEkdqtlEIeEWscS/nJVCTopgsO4IpuNYeKpgL72O1GFv431JP9M +qO0SqMqawdMi+GCGlizmkTfwfAuxdX9r+dLSiXlaiTYfkulGiFICkSkyNN0KlV2S +OSZ1EOnAp3s8ovz/stRa8qPNz0dkkX2YyVSFIwjeImz8AbM+VKdYatYPtPw5Yn/x +P3eQXC+hC9K4ruyGAaQbdc3tPG9RxFWY6BMykteD5lJm066St0VxjMqBXl5mwuao +de7u057zsikESl5MuSW3jLeT+jKtNYpEz9C5qzE+cUOEODqDVygQhs5z9Iqo/gwU +MajRgZOQnmh3WXPgV0xhFfPGL5Q+7dYx3tw5IG0NHJAUGxAK0N79D06wVm8UjTfg +z2HpzXDGTJc2wM+oUIH3ES1N5H7L2ICzJvVlRJ/V5sW9AlEeha6BgfDcDsZrA6Xy +4qEng+kex0MwYfK2xsjaImWmQLhAGzPd6LHr+mjP8nH9z2o7bbZhJePjD0jpkvn+ +NS2Zqa75DwhZ6jOAT8o4BWfixdSZsJcN2ezoV2A9cSM7O3DucpZX+Fw0Og6fo241 +5Gm8CoDK9UCYqyq9AJUfMgQxZe2M3HlYG9WZBdmF59vMgGMwxsdtr1XW/lZ6+kMS +DCoNrwhcgzmV3x8r2VuoNUGdTYoZQbNkK1p/v+Jydt39WhGaahXfoVlquq6odWHI +whuCKjzjBBBQRw2eqC6pnlTuQsC/NRJVesN7cJfUnaTz6R6qUV+rBvfcnp/mQLIO +BvrhbVy6SF8t9ICoMZVAFRDiXREY7QYMohZGX+3nlLC5WeaCMSNh/oLR4di3NFmG +noAoDAfvhaZVDXxbpPmfo5u5JfXTUen1AnevB+NW3R3OMrVJw7gQB8STfsmSSw6g +s+vF/9Ri4emZHENQAMjz5mgMbFnOftJW89s+S0hhQX3qe4Bx0ICTo7j+qdZ0vrbW +VgGPcCBQGTLluTMzrlslvNAbh68tBge1OqjFTzQsgF3W86SBRZTn9FpIWVcNTA9D +ZLXpjrjH3rez8ZQgCzXrL/Le3MWy1JGWzLBPsxI5xN6WYZTciWXCdBebjnyjwDv1 +XAXMl5vt9i5C3ftJksfhxf+irnqJrOpEYKApSWv4j12WzVqXK7/uU8IJF2qmtA0w +d9phS4JPZ0TsSUfDA7C3Lss5OE7X1oBs3+7J9ExYnhfINB196/ni/Vn79oHovDR6 +BzlHUagxv3xcib1VBbKDDCJAuBYD476v+oU0IjY5ieoWNd/EDnWJWVWYEAwOxo5I +gshd77YQwFCJE4OC68s9ZoqmK76sz6pa3fiv0uUl8cEpe/jNdGOaVXr/rbZfQI41 +AgduwW79QzRWnibmsjJg5w8/E4ZN+rvolZrbjZ4qWjBc3Ab7Ki8dpihcFiHo3Pdd +4Ojh5X+XOF4l2ZJQGUlsRzrMyqt3J4ZCogoZsMgwLOas4H0mnzbCk60ZewSk4sQr +Pu/Ud7rQRkJlwPoYMcwwk4yKagmjvxEC7/Lra6tvlCw7t6AyWiPIfGmKDQAvhsPe +-----END RSA PRIVATE KEY----- diff --git a/certs/fixtures/literal.yml b/certs/fixtures/literal.yml new file mode 100644 index 0000000..0c78521 --- /dev/null +++ b/certs/fixtures/literal.yml @@ -0,0 +1,53 @@ +ca: + cert: | + -----BEGIN CERTIFICATE----- + MIIC/jCCAeagAwIBAgIIF/nAdnF01cAwDQYJKoZIhvcNAQELBQAwKzEpMCcGA1UE + AxMgd2lsZGNhcmQubGl0ZXJhbC5mbGFua3NvdXJjZS5jb20wHhcNMjAwMzA5MTU1 + OTQ5WhcNMjEwMzA4MTYxNDQ5WjArMSkwJwYDVQQDEyB3aWxkY2FyZC5saXRlcmFs + LmZsYW5rc291cmNlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB + AMAZUJYyRCA+V2EVZPNrj/1iuc1mSgCTase3Zjd4rvJST5i4MZ9/+gRZMsvM4zUj + //XCajzIThSSbJ6SbWenB5P6GbDVs6pmiljszbHzt/XAnMJhNNWh9s5GsotHQD03 + 0XPjqeNiAoJ4dhG3kXa7VgJGo3CgKOyxXQa6qcy1RN+x4FGPQJ8AZCz7mIxeHwh1 + +aZoYSoVpfgYJg/XfrxkpkMqD6w6G0Kas6YMomwYj1m4llIABuKnqtwBKgjWNYZa + 8QHN9L/fxXRwA1UpSqFWPegr7hfRygkDh+NgUwy2Vh6OmTIR/thsQRjPyMqcJZZl + f5KkrlphUhqB9VRdpc71HwECAwEAAaMmMCQwDgYDVR0PAQH/BAQDAgKkMBIGA1Ud + EwEB/wQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAG3wWxaywIX/EYSkKbK0 + ASIGCKg+egycs2cq3IgG9cQkrNhaAIP5qwzMLrMEq4yvNjiTE3qf9FTdXSkWU43m + nWvURNTpu22h4YBqE+rt2CPampLLfTNccpUjdIYPeBkc6AMBh+chiB58EHHra0Rx + u3N9EbYlK6GcBOq64c6OSG5igWqOueX58HItzlsPPEDmkvLP/MzQVBls9kPBcj/f + HIzZqq7Y0Dcyf1pATHxigLHMplee1jnd8y6YbMkquLiqvLZxPz1o9SqTcueJjMOC + wg6Cd1aQp0kCGuGlX3orJerQFYLQRMk6OtADoc+A8XB6rK6D/DN63OOXB4zLPa2d + t6o= + -----END CERTIFICATE----- + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: AES-256-CBC,f99f5acaba229399088cc6d8ee0ad9d8 + + hTTCuDOZbKY8IMRh+FMQChBrGpkuZKEkItdqGSq2363GRnVoeNu4wWNRrod6beEC + B3X0EjOncUfhRd8kj20N6rczSMB+4nRwoVKSjUummoOH+duFFDiPL5RXpMQyaa/l + 1okuFPB0oOc60Gxj5v1Y3dQ5R4sV3d4pMrVWHkHN7BMJOhIfH1wOM51yRSIIEEK2 + iRaIx7x9Lw7xyjDuGZb2UKvildgEem2mUN/8bu7hsGOJpB964R9tkccVQxLrjxJ8 + bJPiusnkBXA1ihT0UgfeiSQfmdPFmL0ZYLvUpsgPaPuW8vyMcINgIAORGLgsvLYk + GFlg7q4hE9PAGjPahfUASHlqlaXPMozo+RTCyIkqIreQpFSLPsVOwWpoBXsitLZg + sr9274BgFM5uQ/AwqnYe0ndHHhovqqPyU+x4DioWTiVQBYdTVJCm7EybWTqR+/e6 + vOXvasdEbn/BZQVp93zubWrfRChFyOy9wu2xkIT9hginv3DCkeUeJCB1WDMl8+gR + PqJ4bAn7d7U+m9XdcerhN5hI/0Qfff3rwSbearCYE1JrgzYCrnl6PZX2hLYMz/1i + 59Q1SF3wiQOkVmegZjOuctQnxHgPSR1Et5gmoGGOTbfeLB0iGWMGAM6CclaKFpJu + hhB4SPT96k8Ok1EterrqfTnRHBwYYeaX/dyA+AkTNY4aVNWThSEScaWgmfyEzrgG + u9/+7M5QnuvWlAk7P662+j7ohYQZEzgU7ZROTEy6qUBq/AdDOqjU8Cpg/vsXUpMp + APdHunMbMyAFjTIzDBhGZeIFbouWWq9QBKN2QYuYXlXnJvw4JBijRvsJZv9ZAia0 + ZJy8t65jVzicWIf0wNWbpY7HenvadEXbrtWOAh+WLEt15VBf3BLHQF6MX6gLN6Uy + ztQeGjq/EA1ue+BGJrOwFjdSz6vMdXj/IC0MZTOQADcBT0y/XrxImrkLeaup2RiO + V88N2dB4UQr19mzmENgshili4qxe8NJ1KjQRllDLaKNq2aqtYYrKXOuWaYSC/P+g + qSvrSz2QtcI7a0w6kYggGbe2vb6NOyShTGYrmhuaEzRUEXqsGLDMVcXW+sqtk3Zx + zzKzbLCBMspCbicLGwh5KIvNrmKBY0vCm+elDJWHt7wso3YIBv1xTjnX586yKZwL + JDw0ga4aVTGD6eguOT9LTXgtheyl0SsE6LBQRRBvlXmNMFWvyyDoCpoXZD8KmTML + SrilY88iCiDufyFOG+NIklJrGibt7y/uworiSToFrlQHi7HGqSUWhUX05GgwIunf + 1gWZaUNa98Y4xzCY3xOHgdYeP6ek/QqkS/7T/4mlWP0X7oxOfvdhZ2XYbIkhSrPq + A8LxVkbNwfYgnZjqOU6veYLVqGh4eCA0Q8zDG1ENsPMkPpDR8zy3SmqoA7CjaBA4 + bSCoq6avg+xXFWXusWNv/hc7NyB3YdXK8aL4RojzQWuo0cIST2usyKY+lGgyee9J + 8PFgluTQcMIKg2m7Ft13o1A4T/tq/8XhCdSN8XCVI+PwX8wSuSqvV21VHnfYKyF3 + dx7YcFGd5pMb///PY/0hAJKsVz6AFUgSjRbEgAgrPhp6x1Hpg5npHUlHGcPbvhoZ + -----END RSA PRIVATE KEY----- + password: foobar \ No newline at end of file diff --git a/certs/fixtures/remote.yml b/certs/fixtures/remote.yml new file mode 100644 index 0000000..3f2461a --- /dev/null +++ b/certs/fixtures/remote.yml @@ -0,0 +1,3 @@ +ca: + cert: "https://raw.githubusercontent.com/grpc/grpc-java/v1.28.x/testing/src/main/resources/certs/server0.pem" + privateKey: "https://raw.githubusercontent.com/grpc/grpc-java/v1.28.x/testing/src/main/resources/certs/server0.key" \ No newline at end of file diff --git a/certs/yaml.go b/certs/yaml.go new file mode 100644 index 0000000..4df22ae --- /dev/null +++ b/certs/yaml.go @@ -0,0 +1,104 @@ +package certs + +import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +const ( + // CertificateHeader is the first line of a certificate file + CertificateHeader = "-----BEGIN CERTIFICATE-----" + // PrivateKeyHeader is the first line of a private key file + PrivateKeyHeader = "-----BEGIN RSA PRIVATE KEY-----" +) + +type CertificateMarshaller struct { + CertFile string `yaml:"cert,omitempty"` + PrivateKeyFile string `yaml:"privateKey,omitempty"` + Password string `yaml:"password,omitempty"` +} + +func (c *Certificate) UnmarshalYAML(unmarshal func(interface{}) error) error { + var cm CertificateMarshaller + if err := unmarshal(&cm); err != nil { + return err + } + + cert, err := LoadCertificate(cm.CertFile) + if err != nil { + return errors.Wrap(err, "failed to load certificate") + } + privateKey, err := LoadPrivateKey(cm.PrivateKeyFile) + if err != nil { + return errors.Wrap(err, "failed to load private key") + } + + password := loadPassword(cm.Password) + + certificate, err := DecryptCertificate(cert, privateKey, []byte(password)) + if err != nil { + return errors.Wrap(err, "failed to decrypt certificate") + } + + *c = *certificate + return nil +} + +func LoadCertificate(certificate string) ([]byte, error) { + if strings.HasPrefix(certificate, CertificateHeader) { + return []byte(certificate), nil + } + + return loadCertificateBytes(certificate) +} + +func LoadPrivateKey(privateKey string) ([]byte, error) { + if strings.HasPrefix(privateKey, PrivateKeyHeader) { + return []byte(privateKey), nil + } + + return loadCertificateBytes(privateKey) +} + +func loadCertificateBytes(certificate string) ([]byte, error) { + if strings.HasPrefix(certificate, "http") || strings.HasPrefix(certificate, "https") { + resp, err := http.Get(certificate) + if err != nil { + return nil, errors.Wrapf(err, "failed to download certificate from url %s", certificate) + } + defer resp.Body.Close() + certBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "failed to read response body from url %s", certificate) + } + + return certBytes, nil + } + + fullPath, err := filepath.Abs(certificate) + if err != nil { + return nil, errors.Wrap(err, "failed to expand path") + } + + body, err := ioutil.ReadFile(fullPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to read certificate %s from disk", certificate) + } + + return body, nil +} + +func loadPassword(password string) string { + if strings.HasPrefix(password, "$") { + env := os.Getenv(password[1:]) + if env != "" { + return env + } + } + return password +} diff --git a/certs/yaml_test.go b/certs/yaml_test.go new file mode 100644 index 0000000..9bb3334 --- /dev/null +++ b/certs/yaml_test.go @@ -0,0 +1,55 @@ +package certs + +import ( + "io/ioutil" + "testing" + + "github.com/pkg/errors" + + . "github.com/onsi/gomega" + "gopkg.in/yaml.v2" +) + +type exampleConfig struct { + CA Certificate `yaml:"ca"` +} + +func TestLoadCertificateFromFiles(t *testing.T) { + g := NewWithT(t) + cfg, err := loadConfig("fixtures/file.yml") + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cfg.CA.X509.Subject.CommonName).To(Equal("k8s")) +} + +func TestLoadCertificateFromURL(t *testing.T) { + g := NewWithT(t) + cfg, err := loadConfig("fixtures/remote.yml") + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cfg.CA.X509.Subject.CommonName).To(Equal("*.test.google.com.au")) +} + +func TestLoadCertificateFromLiteral(t *testing.T) { + g := NewWithT(t) + cfg, err := loadConfig("fixtures/literal.yml") + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cfg.CA.X509.Subject.CommonName).To(Equal("wildcard.literal.flanksource.com")) +} + +func loadConfig(path string) (*exampleConfig, error) { + cfgBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, errors.Wrapf(err, "failed to read file %s", path) + } + + cfg := exampleConfig{} + err = yaml.Unmarshal(cfgBytes, &cfg) + + if err != nil { + return nil, errors.Wrapf(err, "failed to parse yml for file %s", path) + } + + return &cfg, nil +}