diff --git a/generate_cert.go b/generate_cert.go deleted file mode 100644 index c8bf5eb..0000000 --- a/generate_cert.go +++ /dev/null @@ -1,213 +0,0 @@ -//go:build ignore - -// Generate a X.509 certificate for a TLS server. - -package main - -import ( - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "flag" - "log" - "math/big" - "net" - "os" - "strings" - "time" -) - -var ( - host = flag.String("host", "", "Comma-separated hostnames and IPs to generate a certificate for") - validFrom = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011") - validFor = flag.Duration("duration", 365*24*time.Hour, "Duration that certificate is valid for") - parentCA = flag.String("ca", "self", "File name for the Certificate Authority that should sign this certificate. A value of \"self\" means this should be a self-signed CA certificate.") - rsaBits = flag.Int("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set") - ecdsaCurve = flag.String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521") - ed25519Key = flag.Bool("ed25519", false, "Generate an Ed25519 key") - outFile = flag.String("out", "cert.pem", "File name where the generated certificate and its key should be written") -) - -func publicKey(priv any) any { - switch k := priv.(type) { - case *rsa.PrivateKey: - return &k.PublicKey - case *ecdsa.PrivateKey: - return &k.PublicKey - case ed25519.PrivateKey: - return k.Public().(ed25519.PublicKey) - default: - return nil - } -} - -func main() { - flag.Parse() - - if len(*host) == 0 { - log.Fatalf("Missing required --host parameter") - } - - var priv crypto.Signer - var err error - switch *ecdsaCurve { - case "": - if *ed25519Key { - _, priv, err = ed25519.GenerateKey(rand.Reader) - } else { - priv, err = rsa.GenerateKey(rand.Reader, *rsaBits) - } - case "P224": - priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) - case "P256": - priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case "P384": - priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case "P521": - priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - default: - log.Fatalf("Unrecognized elliptic curve: %q", *ecdsaCurve) - } - if err != nil { - log.Fatalf("Failed to generate private key: %v", err) - } - - // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature - // KeyUsage bits set in the x509.Certificate template - keyUsage := x509.KeyUsageDigitalSignature - // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In - // the context of TLS this KeyUsage is particular to RSA key exchange and - // authentication. - if _, isRSA := priv.(*rsa.PrivateKey); isRSA { - keyUsage |= x509.KeyUsageKeyEncipherment - } - - var notBefore time.Time - if len(*validFrom) == 0 { - notBefore = time.Now() - } else { - notBefore, err = time.Parse("Jan 2 15:04:05 2006", *validFrom) - if err != nil { - log.Fatalf("Failed to parse creation date: %v", err) - } - } - - notAfter := notBefore.Add(*validFor) - - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - log.Fatalf("Failed to generate serial number: %v", err) - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{Organization: []string{"Acme Co"}}, - NotBefore: notBefore, - NotAfter: notAfter, - - KeyUsage: keyUsage, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - hosts := strings.Split(*host, ",") - for _, h := range hosts { - if ip := net.ParseIP(h); ip != nil { - template.IPAddresses = append(template.IPAddresses, ip) - } else { - template.DNSNames = append(template.DNSNames, h) - } - } - - var parent *x509.Certificate - var signer crypto.Signer - if *parentCA == "self" { - template.IsCA = true - template.KeyUsage |= x509.KeyUsageCertSign - parent = &template - signer = priv - } else { - bytes, err := os.ReadFile(*parentCA) - if err != nil { - log.Fatal("Failed to read parent certificate: %v", err) - } - var block *pem.Block - for { - block, bytes = pem.Decode(bytes) - if block == nil { - break - } - switch block.Type { - case "CERTIFICATE": - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - log.Fatal("Failed to parse certificate: %v", err) - } - parent = cert - - case "RSA PRIVATE KEY": - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - log.Fatal("Failed to parse private key: %v", err) - } - signer = key - - case "PRIVATE KEY": - key, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - log.Fatal("Failed to parse private key: %v", err) - } - switch key := key.(type) { - case *rsa.PrivateKey: - signer = key - case *ecdsa.PrivateKey: - signer = key - case ed25519.PrivateKey: - signer = key - default: - log.Fatal("Unsupported signing key type %T", key) - } - } - } - - if parent == nil { - log.Fatal("could not find a parent CA certificate") - } - - if signer == nil { - log.Fatal("could not find a parent CA signing key") - } - } - - derBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, publicKey(priv), signer) - if err != nil { - log.Fatalf("Failed to create certificate: %v", err) - } - - f, err := os.Create(*outFile) - if err != nil { - log.Fatalf("Failed to open %s for writing: %v", *outFile, err) - } - if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { - log.Fatalf("Failed to write data to %s: %v", *outFile, err) - } - - privBytes, err := x509.MarshalPKCS8PrivateKey(priv) - if err != nil { - log.Fatalf("Unable to marshal private key: %v", err) - } - if err := pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { - log.Fatalf("Failed to write data to %s: %v", *outFile, err) - } - if err := f.Close(); err != nil { - log.Fatalf("Error closing %s: %v", *outFile, err) - } - log.Printf("wrote %s\n", *outFile) -} diff --git a/main.go b/main.go index a0ad6da..c8a5572 100644 --- a/main.go +++ b/main.go @@ -2,63 +2,81 @@ package gumroad import ( + "context" "crypto/tls" "crypto/x509" "encoding/json" + "errors" "fmt" "io" "net/http" "net/url" + "strings" "time" ) -// Check a key against a product permalink in gumroad. -func Check(product, key string) error { - return doCheck("https://api.gumroad.com/v2/licenses/verify", product, key) +// GumroadProduct represents a product in Gumroad on which license keys can be verified. +type GumroadProduct struct { + API string + Product string + Client *http.Client } -// Capture the root certificate pool at build time. `x509.SystemCertPool` is guaranteed not to return -// an error when GOOS is darwin or windows. When GOOS is unix or plan9, `x509.SystemCertPool` will -// only return an error if it was unable to find or parse any system certificates. -var certPool, _ = x509.SystemCertPool() - -// construct a package-level http.RoundTripper to use instead of http.DefaultTransport -var transport = &http.Transport{ - // don't use the runtime system's cert pool, since it may include a certificate - // that this package does not want to trust - TLSClientConfig: &tls.Config{RootCAs: certPool}, - - // since TLSClientConfig above is not nil, HTTP/2 needs to be explicitly enabled - ForceAttemptHTTP2: true, - - // copy the other non-zero-value attributes from http.DefaultTransport - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - Proxy: http.ProxyFromEnvironment, -} +// NewGumroadProduct returns a new GumroadProduct with reasonable defaults. +func NewGumroadProduct(product string) (GumroadProduct, error) { + // early return if product permalink is empty + if product == "" { + return GumroadProduct{}, errors.New("license: product permalink cannot be empty") + } -// construct a package-local client to use instead of http.DefaultClient -var client = &http.Client{ - // 5 seconds should be plenty for GumRoad to respond - Timeout: 5 * time.Second, - Transport: transport, + // Capture the root certificate pool at build time. `x509.SystemCertPool` is guaranteed not to return + // an error when GOOS is darwin or windows. When GOOS is unix or plan9, `x509.SystemCertPool` will + // only return an error if it was unable to find or parse any system certificates. + certPool, _ := x509.SystemCertPool() + + // construct a package-level http.RoundTripper to use instead of http.DefaultTransport + transport := &http.Transport{ + // don't use the runtime system's cert pool, since it may include a certificate + // that this package does not want to trust + TLSClientConfig: &tls.Config{RootCAs: certPool}, + + // since TLSClientConfig above is not nil, HTTP/2 needs to be explicitly enabled + ForceAttemptHTTP2: true, + + // copy the other non-zero-value attributes from http.DefaultTransport + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + Proxy: http.ProxyFromEnvironment, + } + + return GumroadProduct{ + API: "https://api.gumroad.com/v2/licenses/verify", + Product: product, + Client: &http.Client{ + // 5 seconds should be plenty for GumRoad to respond + Timeout: 5 * time.Second, + Transport: transport, + }, + }, nil } -func doCheck(api, product, key string) error { - switch "" { - case product: - return fmt.Errorf("license: failed check license: product is blank") - case key: - return fmt.Errorf("license: failed check license: license key is blank") +// CheckWithContext verifies a license key against a product in Gumroad. +func (gp GumroadProduct) VerifyWithContext(ctx context.Context, key string) error { + // early return if license key is empty + if key == "" { + return errors.New("license: license key cannot be empty") } - - resp, err := client.PostForm(api, - url.Values{ - "product_permalink": {product}, - "license_key": {key}, - }) + req, err := http.NewRequestWithContext(ctx, "POST", gp.API, strings.NewReader(url.Values{ + "product_permalink": {gp.Product}, + "license_key": {key}, + }.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := gp.Client.Do(req) if err != nil { return fmt.Errorf("license: failed check license: %w", err) } @@ -93,6 +111,11 @@ func doCheck(api, product, key string) error { return nil } +// Verify returns the result of VerifyWithContext with the background context. +func (gp GumroadProduct) Verify(key string) error { + return gp.VerifyWithContext(context.Background(), key) +} + // GumroadResponse is an API response. type GumroadResponse struct { Success bool `json:"success"` diff --git a/main_test.go b/main_test.go index cb569d9..5d10141 100644 --- a/main_test.go +++ b/main_test.go @@ -1,7 +1,6 @@ package gumroad import ( - "crypto/tls" "encoding/json" "fmt" "io/ioutil" @@ -10,7 +9,6 @@ import ( "net/http/httptest" "net/http/httputil" "net/url" - "path/filepath" "strings" "testing" "time" @@ -19,12 +17,25 @@ import ( func TestIntegrationInvalidLicense(t *testing.T) { t.Parallel() expected := "license: invalid license: That license does not exist for the provided product." - err := Check("this-does-not-exist-probably", "nope") + p, err := NewGumroadProduct("this-does-not-exist-probably") + if err != nil { + t.Errorf("unexpected error %v", err) + } + err = p.Verify("nope") if err == nil || err.Error() != expected { t.Errorf("expected an error %q, got %v", expected, err) } } +func TestEmptyProduct(t *testing.T) { + t.Parallel() + expected := "license: product permalink cannot be empty" + _, err := NewGumroadProduct("") + if err == nil || err.Error() != expected { + t.Fatalf("expected %q, got %v", expected, err) + } +} + func TestErrors(t *testing.T) { t.Parallel() for name, val := range testCases { @@ -40,7 +51,12 @@ func TestErrors(t *testing.T) { })) t.Cleanup(ts.Close) - err := doCheck(ts.URL, tt.product, tt.key) + p, err := NewGumroadProduct(tt.product) + if err != nil { + t.Errorf("unexpected error %v", err) + } + p.API = ts.URL + err = p.Verify(tt.key) if tt.eeer == "" { if err != nil { @@ -55,9 +71,6 @@ func TestErrors(t *testing.T) { } } -//go:generate go run generate_cert.go -host 127.0.0.1,::1 -ca self -duration 87600h -rsa-bits 4096 -out testdata/ca.pem -//go:generate go run generate_cert.go -host 127.0.0.1,::1 -ca testdata/ca.pem -ecdsa-curve P256 -out testdata/mitm.pem - func TestMITM(t *testing.T) { t.Parallel() license := "DEADBEEF-CAFE1234-5678DEAD-BEEFCAFE" @@ -96,73 +109,64 @@ func TestMITM(t *testing.T) { })) t.Cleanup(server.Close) - // add server's certificate to the trusted pool, as if it was signed by one the build system's - // trusted CAs (which is the case for GumRoad). - certPool.AddCert(server.Certificate()) - - err := doCheck(server.URL, "product", "fake-key") + p, err := NewGumroadProduct("product") + if err != nil { + t.Errorf("unexpected error %v", err) + } + p.API = server.URL + // Save the default client, which does not trust the test TLS certificate + defaultClient := p.Client + // The server.Client() is configured to trust the test TLS certificate + p.Client = server.Client() + err = p.Verify("fake-key") if !strings.Contains(err.Error(), "invalid license") { t.Fatalf("expected error to indicate that the license is invalid, but got: %s", err) } - - if err := doCheck(server.URL, "product", license); err != nil { + if err := p.Verify(license); err != nil { t.Fatalf("unexpected error when checking valid key: %s", err) } // mitm will act as a man-in-the-middle proxy, and will respond as if any license is valid - mitm := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + mitm := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) w.Header().Add("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(GumroadResponse{Success: true}); err != nil { t.Errorf("error encoding response: %s", err) } })) - - mitm.TLS = &tls.Config{Certificates: []tls.Certificate{parseCert(t, "mitm.pem")}} - mitm.Config.ErrorLog = log.New(ioutil.Discard, "", 0) - mitm.StartTLS() t.Cleanup(mitm.Close) + // Throw away the log message from http.Server.go complaining about the invalid TLS cert + mitm.Config.ErrorLog = log.New(ioutil.Discard, "", 0) - err = doCheck(mitm.URL, "product", license) - if !strings.Contains(err.Error(), "failed check license") { + p.API = mitm.URL + // Set the client back to the default, which doesn't trust the test certificate used by mitm + p.Client = defaultClient + err = p.Verify(license) + if err == nil { + t.Fatalf("MITM was successful") + } else if !strings.Contains(err.Error(), "failed check license") { t.Fatalf("expected error to indicate that the license check failed, but got: %s", err) } // proxyServer will act as a legitimate reverse proxy serverURL, _ := url.Parse(server.URL) proxy := httputil.NewSingleHostReverseProxy(serverURL) - proxy.Transport = transport + // The proxy needs to trust the test TLS certificate + proxy.Transport = server.Client().Transport proxyServer := httptest.NewTLSServer(proxy) t.Cleanup(proxyServer.Close) - err = doCheck(proxyServer.URL, "product", "fake-key") + p.API = proxyServer.URL + p.Client = proxyServer.Client() + err = p.Verify("fake-key") if !strings.Contains(err.Error(), "invalid license") { t.Fatalf("expected error to indicate that the license is invalid, but got: %s", err) } - - if err := doCheck(proxyServer.URL, "product", license); err != nil { + if err := p.Verify(license); err != nil { t.Fatalf("unexpected error when checking valid key: %s", err) } } -func parseCert(t *testing.T, file string) tls.Certificate { - t.Helper() - - fp := filepath.Join("testdata", file) - - bytes, err := ioutil.ReadFile(fp) - if err != nil { - t.Fatalf("cannot read %s: %s", fp, err) - } - - cert, err := tls.X509KeyPair(bytes, bytes) - if err != nil { - t.Fatalf("error creating tls.Certificate: %s", err) - } - - return cert -} - func BenchmarkErrors(b *testing.B) { for name, tt := range testCases { b.Run(name, func(b *testing.B) { @@ -176,7 +180,9 @@ func BenchmarkErrors(b *testing.B) { b.Cleanup(ts.Close) for n := 0; n < b.N; n++ { - _ = doCheck(ts.URL, "product", "key") + p, _ := NewGumroadProduct("product") + p.API = ts.URL + _ = p.Verify("key") } }) } @@ -236,12 +242,8 @@ var testCases = map[string]struct { }, }, }, - "blank product": { - product: "", key: "key", - eeer: "license: failed check license: product is blank", - }, "blank key": { product: "product", key: "", - eeer: "license: failed check license: license key is blank", + eeer: "license: license key cannot be empty", }, } diff --git a/testdata/ca.pem b/testdata/ca.pem deleted file mode 100644 index c009d83..0000000 --- a/testdata/ca.pem +++ /dev/null @@ -1,82 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFKTCCAxGgAwIBAgIRAONnAmtReKxS6DBxTbH7nI0wDQYJKoZIhvcNAQELBQAw -EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yMjA0MDQyMDQyMTBaFw0zMjA0MDEyMDQy -MTBaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDHp2lcqSb/VmeFjdG9yuzemLBhP7JH7OgEq9hCMlq9uL+YerjdrzFH -/O/0xJDe+Repeu74KNXS9Z9WWR9PYL6EvULOyq7Zbc6oSq7xfZ2a4vad/jVS4hF0 -ENx8VX/1dqzANRj49U94iwikJN9wTdYzCiWW8ZNXyKRYccMxUlmX58nGsDKh4kwM -+Q2LQP6CQTDIKH97bOvEQQOFjkpgJtv0phmhEOllRwfBOL8iQFTsI3cQElk7JgLD -NfiedNEB8ro6YwbuKAl2S/1uSm5WbiBJ4H5urtnn3O7/9/6YBSp+7sZ0d5M6WyEN -fKzHnzgJwwXJjWCyLGv19UJO18Wq3jV7TviXTqRJiGZNDYcZW/xM+yQPcKWKID9e -I1Zt2YZRXPT9Iy5qwc4OHu7eM82TpQUxXkxID+O0GEY4RiAUWHQbAyj5aP9Wbu6s -rOxpUHaEJPOzFOuHdTqvM/b8b9Rn7vl333seyIgt3GhX7OjLPz8nW4wl2BlVxvRh -t0+qBlyjCAQ01eO4R9u+C7jzmVEGDpBeNBNn8nxHhjfIeaAn1cP7EpYWjqUXaPsH -4qrYAnXrSFehJPV4c0MpHEI/nAKXtCCiClya/pH4/olLNF1iv5XKFzPgQbkkm8y7 -7JnDTVO3gmAAfb1kM0VdqFismxDEUNMGHONZYb+hiYuov+lrhejLAwIDAQABo3ow -eDAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUVYfZS3rSmrEij9yA09ZP4eHYam8wIQYDVR0RBBow -GIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAgEASRxb -W8syrMjvn+qZ2KdVSllS/D7to89HMBxRdT6o92Fee19ILT6kgiqo6Zo9s/RriOEU -zDA4X4+BZq5Fk9TjyXqSJkWj5fVxgWV7wmOAHpEm/z4Sq76xWDE2hMafgpkPwoV+ -XAZ8l9Zak/00DWjSVHMyIsN+R28DLiGiim3eTVOuUG9lhiZLxoNHEs9XC0+x7Yil -XN2/33ONU3kdx6eQkOrDgLbQtknuHTdBkIcXf3zighexL9KXG2bVhriEbMLNPi3S -KZ849GjiGHnwTC5o8kuGwPU7MdCZa5n+xwOdVeHRo5L+RyhiYxL+U/v6L2x3vTjp -z3KZIBv5i/QdniskLnfjefk43YDObmxqGQyjvmHrqCJb9zXyn46StlecHb3URL17 -mIrmH5QQXainAHQnaI8f0/L/qKjCO5oorFdNIuxl/JLKGfGpZMZejSJFHDX5wfPL -9XrPDS2YHz80P1W/hOh6ABdAW4+41HXWOX9bH4/A9l9fD9jH7hmrWrjuudOQM9es -pTAEDjtfoYhnQdhI94t2ntl0f3cAMAe2Jl0FSyDyf1i/5UI826vVHIGl/6SOuJyx -NQh6bJT5YltVhfl7EM/xWNNggoxd6W8MM3bHUX3rIExpVNTayczKibeBvoKwm4AZ -KxZsVz6tFvNGnUxWsZKL4fGAEYLtxyaLQtLDUHk= ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDHp2lcqSb/VmeF -jdG9yuzemLBhP7JH7OgEq9hCMlq9uL+YerjdrzFH/O/0xJDe+Repeu74KNXS9Z9W -WR9PYL6EvULOyq7Zbc6oSq7xfZ2a4vad/jVS4hF0ENx8VX/1dqzANRj49U94iwik -JN9wTdYzCiWW8ZNXyKRYccMxUlmX58nGsDKh4kwM+Q2LQP6CQTDIKH97bOvEQQOF -jkpgJtv0phmhEOllRwfBOL8iQFTsI3cQElk7JgLDNfiedNEB8ro6YwbuKAl2S/1u -Sm5WbiBJ4H5urtnn3O7/9/6YBSp+7sZ0d5M6WyENfKzHnzgJwwXJjWCyLGv19UJO -18Wq3jV7TviXTqRJiGZNDYcZW/xM+yQPcKWKID9eI1Zt2YZRXPT9Iy5qwc4OHu7e -M82TpQUxXkxID+O0GEY4RiAUWHQbAyj5aP9Wbu6srOxpUHaEJPOzFOuHdTqvM/b8 -b9Rn7vl333seyIgt3GhX7OjLPz8nW4wl2BlVxvRht0+qBlyjCAQ01eO4R9u+C7jz -mVEGDpBeNBNn8nxHhjfIeaAn1cP7EpYWjqUXaPsH4qrYAnXrSFehJPV4c0MpHEI/ -nAKXtCCiClya/pH4/olLNF1iv5XKFzPgQbkkm8y77JnDTVO3gmAAfb1kM0VdqFis -mxDEUNMGHONZYb+hiYuov+lrhejLAwIDAQABAoICAQCE6BoIeAYg1fkF+/mSuYJj -UAuiQU/B96Agc/D7aB/lyhWpQJDN6jEJNgTa0PuLoxAwX0izZCOrUp0mUx+3rLRH -EuJlXKF21lVKzKSW18NR9yKq3C3NJfbuZ8cO8a/DzKOFNBOSr2Ke4Rb4hh9D2Fzu -DyQkzl4Cgut8gQaqT6Kar88vd93MzCNKQ4TTnYmPVixOIj9y/3gXZhwdDTLPvS6R -Nv06rlJgfB9abhc2itK9e4Jn7X7/E/ek0cnWWfdEr7tu4LsdUQOzfzSO9fdri9Xs -uAgSUE14vydOuFz///hhFTyBDZSxtTZUjxlJreEVBoFUwcuL8GxSCFmCuLq6W0AH -zgVhBzwHOXv3f0OdaqzL3mtDdV8/ZhtPDdoYuvrA0RO+f5XIdiUapbCuBvtO4zg9 -MumrJHVOSS8uiXcFmzcD+vFQO0xV2LD0V1/JFf36DjvAeD+MAZCcq2IfEY107ybL -0iWuUTO9oWBtD0XUDUoaQ43B3Ef4BL9cwiIZl57cvYbJZQB0ldXSyQpC4iIsfehf -VmY9cfgAHfPdEmQgoR63e5ifXPjTgDOqnMsNjg7u4+8usLTyQVKxjhIBa1/V29zx -0XJ5znJcH6sc8rxlgpI+f5RvrP+R8PbhKarcC8fMRNM6T3Hh39llq3m28qF7wgSC -S1fmmcwBpZbrKxGFh3APKQKCAQEA/S047Gc5J3dQi2B/sxhoxMuIfilBg2kYo2C5 -cgXxHXsjctOZ92CvVXNvlTXUx3SKJb3LsEbg3AuhjOZvDIsp7We2KOhv6IXo2nOM -VOXwC+4qZKNZ14m0IfdW94c62EOGv0xQIdj5KJtaXlwWs/J/O4FFxbHZ0389unAd -KoGSosYQ6ry6RlVYbDGJs+ZVcT32COm6xZ/J/IC2+w0ulHxTiHCEndWi/+RPV89W -6NgwzlfBm0HOlY7wl0EmimUhxZB4dfJqliJ7tYYxJ68rn3PEiyiGEMPrWO3n8IlO -PNM10SVQdtRldQTrSHPDBe4ZI17GMpIKLylKjpuQJZnXNVrGVwKCAQEAyeFkBmw6 -SMwyDsfkzv1I3pKVyhGlrQW/eDgXS8dPA80/b9FRbYXdPHZVpEhdcu6vK8tshgJ6 -Cjg7QyLBJnIIbwcgDQqBr0BMrYhklfwXIha0iiPstc+NH31+PSfS7KmpFeS3UKeG -lqwKHC81rcvLOSa11ZcNv0WZ/qbtJbHtgiUhtSUR6IN0GCHVngJ3S9E0mkTCAtIj -jDcHsYkvkqv/kYQpSeDltU/npz4YW2AHCUI02LEs/rec99SXu0vgpdNSnKJQdwKt -VmU9PAhc4sR/jdKdJEggRz+68WbyQj7MGw3rn6It2LnqaN0ukFm8ozdHs/G7oH1t -hXPrpI7eBx09NQKCAQEAyGUdI3Ho+WYlqMyvB3GQrAR2e3XoVz2+tMMAxlIRscYg -HumCex5aaga/EZnUXDPQjWMXcOYJYeMK8l4LFCCrBTEMLIEe4yvrTc4cC68dPDQ9 -9f6eRE7I+AGxIqg1WazJYW3kF4aIYS3mb73wcmrFbrI6KetGgXO7xnCrhVOeX5zr -EwJjZvxyyuDDIFR22PW0YwWuQnLiDC1NqJWcgweNjveQ4OWS6X0rmFOTdSZ6Kzmn -E18ZR6Qmp3J4MR8vhhN+Zc2FtGSDCwG2srSikMmvjn5675ZEGNXmuqvT860yceNZ -N1WTIPbNzUFj4rO9zhNcSfbyfLXwP8S+aiTFBYRebQKCAQAJwwVULi2PUZhmEJJm -0+aPJKVrlPje6fU9OfEeb+/JLkaQAJ4gXqW5A7vWas1h/lqoEnkX7cy668cp0udm -FYOE4uJQ3x4wPDq49WxvuDXiJxjpyRzsGbIoQKg3CtLvMLEFZv7aPo+Kf50pibrd -wYn9JHsMbfZzNklg/+9dsEHc1jDQEo/QH3Mgq5opvtLH7sDNaN+8xS6E3DBaDFkp -jXmZLrvUX7Sb1NZNxvnkiTst3m6xv7d+RaCodzYzD1M4kYRgIJUlfBKnp9OTMJBV -Qs0sLW/9ziMJbvJ6M9EpFOJHuJ+ZI312Cl9kXLWgs0Yk2kp0UA8Tjp0aI+XUGjhw -PaC1AoIBAFN9SjO/TvDQD6eTWvvrcvZk9M2sFHuaX6WocN6jxsicoizRkIN9Nvhy -BCpu/0TLaNPGqJwDyDLl3zTllfWB56hRawjx1fmEGHTeIY1AGWAizYl5Gqgvr2RQ -HLRGoWav2UoTz4lc9SEtAdsr1HX+WwAkwvlsZlvXmDg2nLTYLrZWC5JOtzkU5g+x -9gGZD1VO+pnGxeGNT0gw9nNOtCDdNimxZKLdKJkt2PN2WijrKmJ3qe8vl7F+lv0j -6eQTxrY3es+UMpSFoMLMxa3JwVEYp1jbTPM6eH9sjTasRui2LHHye7kUlJneL4ot -k4h9MN1LGXNHtXw6Yn6Gzhh+RB3yzaM= ------END PRIVATE KEY----- diff --git a/testdata/mitm.pem b/testdata/mitm.pem deleted file mode 100644 index 2b5a617..0000000 --- a/testdata/mitm.pem +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPDCCASSgAwIBAgIRALtrTkm2opz73YURG+G5fAEwDQYJKoZIhvcNAQELBQAw -EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yMjA0MDQyMDQyMTBaFw0yMzA0MDQyMDQy -MTBaMBIxEDAOBgNVBAoTB0FjbWUgQ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC -AAQpErXk5lH19mK5OJBpC+yJvI8yGQ2IzrrxuPU8mSISFkqnJ6tBaIjY1Seh8BIU -qke4qFjiHI1ws0LNWVJzfXavo1gwVjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAww -CgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAhBgNVHREEGjAYhwR/AAABhxAAAAAA -AAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4ICAQAbnmJWARruICG0srHKh3T9 -bxkGof+NucwN7BtIeA5zoyKfUVIoWI73pdYdJNzzDJRnAHVzOczMgAgEzVL8O+oH -ehwSenc1j9s5S3UpdIYeoKACjgWVffYcayH990o1GMHzvq3qUjO0oZTu3M1TRY/M -NZZJHXk0E+IwqM9U+hezIa7OMkrul3eWJeRjn2YNQ1LgqsK/rr/LrBV17/AsJjmI -WHpusLnnbqef4KhWake2gPZlxCb5bYLybZ3zEGlSaDxxF/DRPh2umE5ACnbBqg13 -zRKzbwzP7nmobY7StWNcEkmxKXxW6PMd1R1u3qlTG0ymOfyqSeWTGdPXjNzZHjki -2u3OAPP4nzobbQ7LATjfBPad+/KCPEvpDF6QIntXBOGQZeU2VItqWfW5AxD/GIn1 -zj7n1eji9zD7c5AY2zMzadDDxGWO6Ou7qKUeoVyQjIawAuYVSEoigGGpOraPWfyy -3RLCHLLlZ0qvRwTnKxdaTRLcbeS3WhZZTSWZjEdXwvWR3DeuThCAoZJT5jrKoDoR -5z8rJJYRB/cojzDW2gRIt54pX9K8FAPM1BTUMSr24xu23RaGS9rkaPD9u4lR56Ld -c9fgLCwE5Dw7HNpjzgpaUG+6GewNQY7qF3YtDUugYE3Lj9sEVv25IsFXwYX8h+yD -RsL+mDacQg3jHcvlUrwn6A== ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsYFZGuRzz9pExTfO -sPAyqnwVWqaLZhc8n+IhvxyI7t+hRANCAAQpErXk5lH19mK5OJBpC+yJvI8yGQ2I -zrrxuPU8mSISFkqnJ6tBaIjY1Seh8BIUqke4qFjiHI1ws0LNWVJzfXav ------END PRIVATE KEY-----