diff --git a/README.md b/README.md index 43ba0e7..762a99b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Octoplex is a live video restreamer for the terminal. -* Restream RTMP to unlimited destinations +* Restream RTMP/RTMPS to unlimited destinations * Broadcast using OBS and other standard tools * Add and remove destinations while streaming * Automatic reconnections @@ -100,6 +100,9 @@ sources: mediaServer: streamKey: live # defaults to "live" host: rtmp.example.com # defaults to "localhost" + tls: # optional. If RTMPS is enabled, defaults to a + cert: /etc/mycert.pem # self-signed keypair corresponding to the host + key: /etc/mykey.pem # key. rtmp: enabled: true # defaults to false ip: 127.0.0.1 # defaults to 127.0.0.1 diff --git a/internal/app/app.go b/internal/app/app.go index f3df7b9..3df3d0d 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -88,10 +88,18 @@ func Run(ctx context.Context, params RunParams) error { updateUI := func() { ui.SetState(*state) } updateUI() + var tlsCertPath, tlsKeyPath string + if cfg.Sources.MediaServer.TLS != nil { + tlsCertPath = cfg.Sources.MediaServer.TLS.CertPath + tlsKeyPath = cfg.Sources.MediaServer.TLS.KeyPath + } + srv, err := mediaserver.NewActor(ctx, mediaserver.NewActorParams{ RTMPAddr: buildNetAddr(cfg.Sources.MediaServer.RTMP), RTMPSAddr: buildNetAddr(cfg.Sources.MediaServer.RTMPS), Host: cfg.Sources.MediaServer.Host, + TLSCertPath: tlsCertPath, + TLSKeyPath: tlsKeyPath, StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey), ContainerClient: containerClient, Logger: logger.With("component", "mediaserver"), diff --git a/internal/app/integration_test.go b/internal/app/integration_test.go index ed2d1e4..a6eaf05 100644 --- a/internal/app/integration_test.go +++ b/internal/app/integration_test.go @@ -5,9 +5,13 @@ package app_test import ( "cmp" "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" "errors" "fmt" "net" + "os" "testing" "time" @@ -292,6 +296,75 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) { <-done } +func TestIntegrationCustomTLSCerts(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute) + defer cancel() + + logger := testhelpers.NewTestLogger(t).With("component", "integration") + dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) + require.NoError(t, err) + + configService := setupConfigService(t, config.Config{ + Sources: config.Sources{ + MediaServer: config.MediaServerSource{ + TLS: &config.TLS{ + CertPath: "testdata/server.crt", + KeyPath: "testdata/server.key", + }, + RTMPS: config.RTMPSource{Enabled: true}, + }, + }, + }) + screen, screenCaptureC, getContents := setupSimulationScreen(t) + + done := make(chan struct{}) + go func() { + defer func() { + done <- struct{}{} + }() + + require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) + }() + + require.EventuallyWithT( + t, + func(c *assert.CollectT) { + certPEM, err := os.ReadFile("testdata/server.crt") + require.NoError(c, err) + + block, _ := pem.Decode(certPEM) + require.NotNil(c, block, "failed to decode PEM block containing certificate") + require.True(c, block.Type == "CERTIFICATE", "expected PEM block to be a certificate") + certDERBytes := block.Bytes + + rootCAs := x509.NewCertPool() + require.True(c, rootCAs.AppendCertsFromPEM(certPEM), "failed to append cert to root CA pool") + + conn, err := tls.Dial("tcp", "localhost:1936", &tls.Config{ + RootCAs: rootCAs, + ServerName: "localhost", + InsecureSkipVerify: false, + }) + require.NoError(c, err) + + state := conn.ConnectionState() + peerCert := state.PeerCertificates[0] + + expectedCert, err := x509.ParseCertificate(certDERBytes) + require.NoError(c, err) + require.True(c, peerCert.Equal(expectedCert), "expected peer certificate to match the expected certificate") + }, + waitTime, + time.Second, + ) + + printScreen(t, getContents, "After starting the app with custom TLS certs") + + cancel() + + <-done +} + func TestIntegrationCustomRTMPURL(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute) defer cancel() diff --git a/internal/app/testdata/openssl.cnf b/internal/app/testdata/openssl.cnf new file mode 100644 index 0000000..c6bf20f --- /dev/null +++ b/internal/app/testdata/openssl.cnf @@ -0,0 +1,17 @@ +# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout server.key -out server.crt -config openssl.cnf + +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +x509_extensions = v3_req + +[dn] +CN = localhost + +[v3_req] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost diff --git a/internal/app/testdata/server.crt b/internal/app/testdata/server.crt new file mode 100644 index 0000000..6ee4eb2 --- /dev/null +++ b/internal/app/testdata/server.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7TCCAdWgAwIBAgIUTeqv46R19q+BS2e4DBkbIHuWyIIwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDQyMDA4NTMwN1oXDTM1MDQx +ODA4NTMwN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA0v/KndfKfG8XItStHeMQ/3z1r8vhkH9KGpfSwDMp8MdH +Mox6vcAsIIr1RFKmalQQg+T+TK9v3XM6F4sJ+WPyb5/31xLUqG6zivitrMy1AZ8w +XLgAz/CTufXL3OBntDwg29QXWt9lOUJyjRa66AQqreTlItuLG65bswfPA4g35f+U +hyr49paukqnVHRr44GtyiNxlfYCEdQWdOR0EQmZ7y6WNQQhnR8odQyftR2lykf17 +MSJ8us4JAgZ2fr1QR+DfX5bCSS/WJ2aO7xxeES40NizBx08qYFami1zXrGMMo35I +SfedCohcok8ZZ1oWL+MfSJ2OLVclDnznDPTx39pZPQIDAQABozcwNTAUBgNVHREE +DTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFCgZah+m2NXkI9biS2vnhNUrd3FiMA0G +CSqGSIb3DQEBCwUAA4IBAQAPbofZIKCm3DnudFnK+LRkdlpMNOyH2zn3g8h8vrfL +Tfi0oBgHb7EYxcHYDanZbcIKracWCfQVze2FRLgNFBWiyhDO4IXe/LpwSnbyLWCh +psbGuyVmEz9CuiyVdIi+CWQs5dBBRUCFg6NE2/r6Diw9LD0fVCVUwkvqopetfp1B +tvA74O0RduLWs+iXNs5XW4sODVkrOmhBbRrP9GRCVqiqVWJka6CzrNdBm0Y9zZMQ +GD/6fEgDaW8YlShoO+e4FwmD2IgIx+m4xamr/cQkWpbOHMxAwv7vP0stfkpyUacW +dh9eJmsDAmgGgdtMJvbIfyR9ilG8D6zwOmSlkF6fDJ3E +-----END CERTIFICATE----- diff --git a/internal/app/testdata/server.key b/internal/app/testdata/server.key new file mode 100644 index 0000000..220c74b --- /dev/null +++ b/internal/app/testdata/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDS/8qd18p8bxci +1K0d4xD/fPWvy+GQf0oal9LAMynwx0cyjHq9wCwgivVEUqZqVBCD5P5Mr2/dczoX +iwn5Y/Jvn/fXEtSobrOK+K2szLUBnzBcuADP8JO59cvc4Ge0PCDb1Bda32U5QnKN +FrroBCqt5OUi24sbrluzB88DiDfl/5SHKvj2lq6SqdUdGvjga3KI3GV9gIR1BZ05 +HQRCZnvLpY1BCGdHyh1DJ+1HaXKR/XsxIny6zgkCBnZ+vVBH4N9flsJJL9YnZo7v +HF4RLjQ2LMHHTypgVqaLXNesYwyjfkhJ950KiFyiTxlnWhYv4x9InY4tVyUOfOcM +9PHf2lk9AgMBAAECggEAC3E3qaukHW9gz9C8upwvtcsu/6OMzes5N4v4L9gWdCo6 +YDFiDpw3SGSAvH3G7Ik2hBCNAdeZt2aiRdiSZ+XVpdwE8rLguWmXbvfhYzeOsVHS +q5SG5r/jIviDX60DsrB4D7PGuHTY5mwGDkSnSiG/tsJs8qD5QD0KWAEaZtSiQ2Sp +kcRbdq13/2tjHyx7nBxEYUFC4EJQjK3cNNV4G7nG2xcfT46uPvFV0+1CQtMpFYhi +IsGaSBhW9gOAheycYxCi+LRdUh1IAnLUyYUenu0o8PoXsHp6KD8eS5RXtfA6THd/ +Jr614gdAB2Sffw+bFf6FIBNWa5Jwsg9UtbGtjNdo+QKBgQDrOJ2nj7El6MIqeDHs +1cCeGDKmjB1CYWALLHrwwiwmrvEoeBMiJuMN4epZdQw9hwExa7fNpERI7Ay8s5HD +cdppxgcW7CWChNncbVZ39P+YI9URWC2Q2Y8FBhc9FA0sKpDak0rf5UE63SGjU8/I +FGgwjd1Ln5wws00OsYXBZw1lzwKBgQDlo2kRy6xvrUNAbeggT9OQeg2SdkWqvS3v +NUhBzZkVhJNf1oApNRoAvRMQt+Xt+Euw1pQ+TvdOZQhhqxs/pD/wGdM7rhq9r0+G +itsQ5LvNCxCePbSkbFMLgC8JgNuM3aRqhtsU+Illk9xvCj2nKsd+UUN3NxYgjCqa +evTKSzUfMwKBgFapy1w7EteWxEMFec96ibc1zyORqA4W9l3ni3w87itqdSul4dbJ +YQpyW/eNqm7Y2NWujE/V39rGLYMw3dmWjxQ9g8ssQj2uWN5f4mXb/He/a/cx98fQ +gGMndVRpmNjW7fu6HPIU802Ov5//dySOcDzDZ+8+5TsENLXfLhqtrz/9AoGBALc+ +/BQoTFTdlSHv0mEecjwDOZtbZ+KEjggpo5xm/TbPkW7T03eOmU5nkrQvm3qXPYdC +5A8Ioo5bTyHpEZhqcF8frJEeMNaW88XwPjmv3TEVGFC9+s2OZ4Jw6pgRzKEPKSmc +rWyBm9qD8E5nhKVGHOVu4YBbY/va/hBB998Jvr1DAoGBAK5nnswLyQZi0lgpkl1P +ITkmvnQlZBfuqvoD7wcQ3nx/K/mdacsxepRne+U/4+iNzRtd3gU0iccCWUTJl4aB +cFRW1eXWuff+4vmM4JToDevGPXrS0CHE20mATJRZPH+YjZFl0pFSc4/tnjxBnx4y +vgM382WU9N9jIHCCnM6DYsbK +-----END PRIVATE KEY----- diff --git a/internal/config/config.go b/internal/config/config.go index 76c1113..c6caae3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,12 +35,19 @@ type RTMPSource struct { NetAddr `yaml:",inline"` } +// TLS holds the TLS configuration. +type TLS struct { + CertPath string `yaml:"cert,omitempty"` + KeyPath string `yaml:"key,omitempty"` +} + // MediaServerSource holds the configuration for the media server source. type MediaServerSource struct { StreamKey string `yaml:"streamKey,omitempty"` Host string `yaml:"host,omitempty"` - RTMP RTMPSource `yaml:"rtmp,omitempty"` - RTMPS RTMPSource `yaml:"rtmps,omitempty"` + TLS *TLS `yaml:"tls,omitempty"` + RTMP RTMPSource `yaml:"rtmp"` + RTMPS RTMPSource `yaml:"rtmps"` } // Sources holds the configuration for the sources. diff --git a/internal/config/service_test.go b/internal/config/service_test.go index f124d11..ef96e45 100644 --- a/internal/config/service_test.go +++ b/internal/config/service_test.go @@ -100,6 +100,10 @@ func TestConfigServiceReadConfig(t *testing.T) { MediaServer: config.MediaServerSource{ StreamKey: "s3cr3t", Host: "rtmp.example.com", + TLS: &config.TLS{ + CertPath: "/etc/cert.pem", + KeyPath: "/etc/key.pem", + }, RTMP: config.RTMPSource{ Enabled: true, NetAddr: config.NetAddr{ diff --git a/internal/config/testdata/complete.yml b/internal/config/testdata/complete.yml index 3a98d50..5883e4c 100644 --- a/internal/config/testdata/complete.yml +++ b/internal/config/testdata/complete.yml @@ -6,6 +6,9 @@ sources: mediaServer: streamKey: s3cr3t host: rtmp.example.com + tls: + cert: /etc/cert.pem + key: /etc/key.pem rtmp: enabled: true ip: 0.0.0.0 diff --git a/internal/domain/types.go b/internal/domain/types.go index 0bdc66a..0488d59 100644 --- a/internal/domain/types.go +++ b/internal/domain/types.go @@ -66,6 +66,16 @@ func (n NetAddr) IsZero() bool { return n.IP == "" && n.Port == 0 } +// KeyPair holds a TLS key pair. +type KeyPair struct { + Cert, Key []byte +} + +// IsZero returns true if the KeyPair is zero value. +func (k KeyPair) IsZero() bool { + return k.Cert == nil && k.Key == nil +} + // Container status strings. // // TODO: refactor to strictly reflect Docker status strings. diff --git a/internal/domain/types_test.go b/internal/domain/types_test.go index 2e2323c..08998f2 100644 --- a/internal/domain/types_test.go +++ b/internal/domain/types_test.go @@ -40,3 +40,12 @@ func TestNetAddr(t *testing.T) { addr.Port = 3000 assert.False(t, addr.IsZero()) } + +func TestKeyPair(t *testing.T) { + var keyPair domain.KeyPair + assert.True(t, keyPair.IsZero()) + + keyPair.Cert = []byte("cert") + keyPair.Key = []byte("key") + assert.False(t, keyPair.IsZero()) +} diff --git a/internal/mediaserver/actor.go b/internal/mediaserver/actor.go index 810bcdd..3e2f3f5 100644 --- a/internal/mediaserver/actor.go +++ b/internal/mediaserver/actor.go @@ -8,6 +8,7 @@ import ( "fmt" "log/slog" "net/http" + "os" "time" typescontainer "github.com/docker/docker/api/types/container" @@ -37,6 +38,11 @@ const ( defaultStreamKey StreamKey = "live" // Default stream key. See [StreamKey]. componentName = "mediaserver" // component name, mostly used for Docker labels httpClientTimeout = time.Second // timeout for outgoing HTTP client requests + configPath = "/mediamtx.yml" // path to the media server config file + tlsInternalCertPath = "/etc/tls-internal.crt" // path to the internal TLS cert + tlsInternalKeyPath = "/etc/tls-internal.key" // path to the internal TLS key + tlsCertPath = "/etc/tls.crt" // path to the custom TLS cert + tlsKeyPath = "/etc/tls.key" // path to the custom TLS key ) // action is an action to be performed by the actor. @@ -54,8 +60,9 @@ type Actor struct { host string streamKey StreamKey updateStateInterval time.Duration - pass string // password for the media server - tlsCert, tlsKey []byte // TLS cert and key for the media server + pass string // password for the media server + keyPairInternal domain.KeyPair // TLS key pair for the media server + keyPairCustom domain.KeyPair // TLS key pair for the media server logger *slog.Logger apiClient *http.Client @@ -70,6 +77,8 @@ type NewActorParams struct { RTMPSAddr OptionalNetAddr // defaults to disabled, or 127.0.0.1:1936 APIPort int // defaults to 9997 Host string // defaults to "localhost" + TLSCertPath string // defaults to empty + TLSKeyPath string // defaults to empty StreamKey StreamKey // defaults to "live" ChanSize int // defaults to 64 UpdateStateInterval time.Duration // defaults to 5 seconds @@ -89,11 +98,25 @@ type OptionalNetAddr struct { // // Callers must consume the state channel exposed via [C]. func NewActor(ctx context.Context, params NewActorParams) (_ *Actor, err error) { - tlsCert, tlsKey, err := generateTLSCert() + keyPairInternal, err := generateTLSCert() if err != nil { return nil, fmt.Errorf("generate TLS cert: %w", err) } - apiClient, err := buildAPIClient(tlsCert) + + var keyPairCustom domain.KeyPair + if params.TLSCertPath != "" { + keyPairCustom.Cert, err = os.ReadFile(params.TLSCertPath) + if err != nil { + return nil, fmt.Errorf("read TLS cert: %w", err) + } + keyPairCustom.Key, err = os.ReadFile(params.TLSKeyPath) + if err != nil { + return nil, fmt.Errorf("read TLS key: %w", err) + } + } + + // TODO: custom cert for API? + apiClient, err := buildAPIClient(keyPairInternal.Cert) if err != nil { return nil, fmt.Errorf("build API client: %w", err) } @@ -106,8 +129,8 @@ func NewActor(ctx context.Context, params NewActorParams) (_ *Actor, err error) host: cmp.Or(params.Host, defaultHost), streamKey: cmp.Or(params.StreamKey, defaultStreamKey), updateStateInterval: cmp.Or(params.UpdateStateInterval, defaultUpdateStateInterval), - tlsCert: tlsCert, - tlsKey: tlsKey, + keyPairInternal: keyPairInternal, + keyPairCustom: keyPairCustom, pass: generatePassword(), actorC: make(chan action, chanSize), state: new(domain.Source), @@ -138,6 +161,45 @@ func (a *Actor) Start(ctx context.Context) error { return fmt.Errorf("build server config: %w", err) } + copyFiles := []container.CopyFileConfig{ + { + Path: configPath, + Payload: bytes.NewReader(cfg), + Mode: 0600, + }, + { + Path: tlsInternalCertPath, + Payload: bytes.NewReader(a.keyPairInternal.Cert), + Mode: 0600, + }, + { + Path: tlsInternalKeyPath, + Payload: bytes.NewReader(a.keyPairInternal.Key), + Mode: 0600, + }, + { + Path: "/etc/healthcheckopts.txt", + Payload: bytes.NewReader([]byte(fmt.Sprintf("--user api:%s", a.pass))), + Mode: 0600, + }, + } + + if !a.keyPairCustom.IsZero() { + copyFiles = append( + copyFiles, + container.CopyFileConfig{ + Path: tlsCertPath, + Payload: bytes.NewReader(a.keyPairCustom.Cert), + Mode: 0600, + }, + container.CopyFileConfig{ + Path: tlsKeyPath, + Payload: bytes.NewReader(a.keyPairCustom.Key), + Mode: 0600, + }, + ) + } + args := []any{"host", a.host} if a.rtmpAddr.IsZero() { args = append(args, "rtmp.enabled", false) @@ -166,7 +228,7 @@ func (a *Actor) Start(ctx context.Context) error { "curl", "--fail", "--silent", - "--cacert", "/etc/tls.crt", + "--cacert", "/etc/tls-internal.crt", "--config", "/etc/healthcheckopts.txt", a.healthCheckURL(), }, @@ -183,28 +245,7 @@ func (a *Actor) Start(ctx context.Context) error { }, NetworkCountConfig: container.NetworkCountConfig{Rx: "eth0", Tx: "eth1"}, Logs: container.LogConfig{Stdout: true}, - CopyFiles: []container.CopyFileConfig{ - { - Path: "/mediamtx.yml", - Payload: bytes.NewReader(cfg), - Mode: 0600, - }, - { - Path: "/etc/tls.crt", - Payload: bytes.NewReader(a.tlsCert), - Mode: 0600, - }, - { - Path: "/etc/tls.key", - Payload: bytes.NewReader(a.tlsKey), - Mode: 0600, - }, - { - Path: "/etc/healthcheckopts.txt", - Payload: bytes.NewReader([]byte(fmt.Sprintf("--user api:%s", a.pass))), - Mode: 0600, - }, - }, + CopyFiles: copyFiles, }, ) @@ -224,6 +265,15 @@ func (a *Actor) buildServerConfig() ([]byte, error) { encryptionString = "optional" } + var certPath, keyPath string + if a.keyPairCustom.IsZero() { + certPath = tlsInternalCertPath + keyPath = tlsInternalKeyPath + } else { + certPath = tlsCertPath + keyPath = tlsKeyPath + } + return yaml.Marshal( Config{ LogLevel: "debug", @@ -256,12 +306,12 @@ func (a *Actor) buildServerConfig() ([]byte, error) { RTMPEncryption: encryptionString, RTMPAddress: ":1935", RTMPSAddress: ":1936", - RTMPServerCert: "/etc/tls.crt", // TODO: custom certs - RTMPServerKey: "/etc/tls.key", // TODO: custom certs + RTMPServerCert: certPath, + RTMPServerKey: keyPath, API: true, APIEncryption: true, - APIServerCert: "/etc/tls.crt", - APIServerKey: "/etc/tls.key", + APIServerCert: tlsInternalCertPath, + APIServerKey: tlsInternalKeyPath, Paths: map[string]Path{ string(a.streamKey): {Source: "publisher"}, }, diff --git a/internal/mediaserver/tls.go b/internal/mediaserver/tls.go index bff6139..194ce91 100644 --- a/internal/mediaserver/tls.go +++ b/internal/mediaserver/tls.go @@ -10,23 +10,20 @@ import ( "encoding/pem" "math/big" "time" -) -type ( - tlsCert []byte - tlsKey []byte + "git.netflux.io/rob/octoplex/internal/domain" ) // generateTLSCert generates a self-signed TLS certificate and private key. -func generateTLSCert() (tlsCert, tlsKey, error) { +func generateTLSCert() (domain.KeyPair, error) { privKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { - return nil, nil, err + return domain.KeyPair{}, err } serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { - return nil, nil, err + return domain.KeyPair{}, err } now := time.Now() @@ -45,23 +42,26 @@ func generateTLSCert() (tlsCert, tlsKey, error) { certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) if err != nil { - return nil, nil, err + return domain.KeyPair{}, err } var certPEM, keyPEM bytes.Buffer if err = pem.Encode(&certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { - return nil, nil, err + return domain.KeyPair{}, err } privKeyDER, err := x509.MarshalECPrivateKey(privKey) if err != nil { - return nil, nil, err + return domain.KeyPair{}, err } if err := pem.Encode(&keyPEM, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privKeyDER}); err != nil { - return nil, nil, err + return domain.KeyPair{}, err } - return certPEM.Bytes(), keyPEM.Bytes(), nil + return domain.KeyPair{ + Cert: certPEM.Bytes(), + Key: keyPEM.Bytes(), + }, nil } diff --git a/internal/mediaserver/tls_test.go b/internal/mediaserver/tls_test.go index 94c2113..3291800 100644 --- a/internal/mediaserver/tls_test.go +++ b/internal/mediaserver/tls_test.go @@ -12,12 +12,12 @@ import ( ) func TestGenerateTLSCert(t *testing.T) { - certPEM, keyPEM, err := generateTLSCert() + keyPair, err := generateTLSCert() require.NoError(t, err) - require.NotEmpty(t, certPEM) - require.NotEmpty(t, keyPEM) + require.NotEmpty(t, keyPair.Cert) + require.NotEmpty(t, keyPair.Key) - block, _ := pem.Decode(certPEM) + block, _ := pem.Decode(keyPair.Cert) require.NotNil(t, block, "failed to decode certificate PEM") cert, err := x509.ParseCertificate(block.Bytes) @@ -34,7 +34,7 @@ func TestGenerateTLSCert(t *testing.T) { assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth) - block, _ = pem.Decode(keyPEM) + block, _ = pem.Decode(keyPair.Key) require.NotNil(t, block, "failed to decode private key PEM") privKey, err := x509.ParseECPrivateKey(block.Bytes)