Repository: ovrclk/akash
Pull request #1519 (2022-02-22 20:36:12)

Rework how certificate management happens
This refactors the certificate management commands to be non-interactive to the extent that is possible. This should eliminate the chance for user confusion around the workflow. It is now split into 3 steps

1. generate - create the cert locally
2. publish - put the cert on the blockchain
3. revoke - update the blockchain to indicate the cert is no longer valid

None of these should be interactive, except for the prompt to send a transaction. Of note is that the `generate` command is under the `akash tx` command but there is not really anything going on there that is sending a transaction. I can  move this to somewhere else if someone suggests it.

Most of the logic to handle the keys is now wrapped into a single interface.

I haven't changed the location of the files in the home directory or the format of them at this time.

I added a number of unit tests, which try and cover the obvious cases.


Patch:
diff --git a/_run/common.mk b/_run/common.mk
index 48c3b495..c08a9746 100644
--- a/_run/common.mk
+++ b/_run/common.mk
@@ -78,11 +78,13 @@ node-init-genesis-certs: $(patsubst %,node-init-genesis-client-cert-%,$(CLIENT_C
 
 .PHONY: node-init-genesis-client-cert-%
 node-init-genesis-client-cert-%:
-	$(AKASH) tx cert create client --to-genesis=true --from=$(@:node-init-genesis-client-cert-%=%)
+	$(AKASH) tx cert generate client --from=$*
+	$(AKASH) tx cert publish client --to-genesis=true --from=$*
 
 .PHONY: node-init-genesis-server-cert-%
 node-init-genesis-server-cert-%:
-	$(AKASH) tx cert create server localhost akash-provider.localhost --to-genesis=true --from=$(@:node-init-genesis-server-cert-%=%)
+	$(AKASH) tx cert generate server localhost akash-provider.localhost --from=$*
+	$(AKASH) tx cert publish server --to-genesis=true --from=$*
 
 .PHONY: node-init-genesis-accounts
 node-init-genesis-accounts: $(patsubst %,node-init-genesis-account-%,$(GENESIS_ACCOUNTS))
diff --git a/provider/cmd/auth_server.go b/provider/cmd/auth_server.go
index e93790f6..1c8c53f4 100644
--- a/provider/cmd/auth_server.go
+++ b/provider/cmd/auth_server.go
@@ -3,9 +3,6 @@ package cmd
 import (
 	"bytes"
 	"context"
-	"crypto/tls"
-	"crypto/x509"
-	"encoding/pem"
 	"errors"
 	"io"
 	"net/http"
@@ -15,7 +12,6 @@ import (
 
 	sdkclient "github.com/cosmos/cosmos-sdk/client"
 	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/client/tx"
 	"github.com/ovrclk/akash/cmd/common"
 	gwrest "github.com/ovrclk/akash/provider/gateway/rest"
 	cmodule "github.com/ovrclk/akash/x/cert"
@@ -66,25 +62,17 @@ func doAuthServerCmd(ctx context.Context, cmd *cobra.Command, _ []string) error
 		return err
 	}
 
-	txFactory := tx.NewFactoryCLI(cctx, cmd.Flags()).WithTxConfig(cctx.TxConfig).WithAccountRetriever(cctx.AccountRetriever)
-
 	var certFromFlag io.Reader
 	if val := cmd.Flag(FlagAuthPem).Value.String(); val != "" {
 		certFromFlag = bytes.NewBufferString(val)
 	}
 
-	cpem, err := cutils.LoadPEMForAccount(cctx, txFactory.Keybase(), cutils.PEMFromReader(certFromFlag))
-	if err != nil {
-		return err
-	}
-
-	blk, _ := pem.Decode(cpem.Cert)
-	x509cert, err := x509.ParseCertificate(blk.Bytes)
+	kpm, err := cutils.NewKeyPairManager(cctx, cctx.FromAddress)
 	if err != nil {
 		return err
 	}
 
-	cert, err := tls.X509KeyPair(cpem.Cert, cpem.Priv)
+	x509cert, tlsCert, err := kpm.ReadX509KeyPair(certFromFlag)
 	if err != nil {
 		return err
 	}
@@ -97,7 +85,7 @@ func doAuthServerCmd(ctx context.Context, cmd *cobra.Command, _ []string) error
 		cquery,
 		jwtGwAddr,
 		cctx.FromAddress,
-		cert,
+		tlsCert,
 		x509cert.SerialNumber.String(),
 		expiresAfter,
 	)
diff --git a/provider/cmd/authenticate.go b/provider/cmd/authenticate.go
index aa028c3b..5e7ab56c 100644
--- a/provider/cmd/authenticate.go
+++ b/provider/cmd/authenticate.go
@@ -8,7 +8,6 @@ import (
 
 	sdkclient "github.com/cosmos/cosmos-sdk/client"
 	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/client/tx"
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	akashclient "github.com/ovrclk/akash/client"
 	"github.com/ovrclk/akash/cmd/common"
@@ -46,7 +45,6 @@ func AuthenticateCmd() *cobra.Command {
 
 func doAuthenticateCmd(ctx context.Context, cmd *cobra.Command, _ []string) error {
 	cctx, err := sdkclient.GetClientTxContext(cmd)
-	txFactory := tx.NewFactoryCLI(cctx, cmd.Flags()).WithTxConfig(cctx.TxConfig).WithAccountRetriever(cctx.AccountRetriever)
 	if err != nil {
 		return err
 	}
@@ -61,17 +59,17 @@ func doAuthenticateCmd(ctx context.Context, cmd *cobra.Command, _ []string) erro
 		certFromFlag = bytes.NewBufferString(val)
 	}
 
-	cpem, err := cutils.LoadPEMForAccount(cctx, txFactory.Keybase(), cutils.PEMFromReader(certFromFlag))
+	kpm, err := cutils.NewKeyPairManager(cctx, cctx.FromAddress)
 	if err != nil {
 		return err
 	}
 
-	cert, err := tls.X509KeyPair(cpem.Cert, cpem.Priv)
+	_, tlsCert, err := kpm.ReadX509KeyPair(certFromFlag)
 	if err != nil {
 		return err
 	}
 
-	gclient, err := gwrest.NewJwtClient(ctx, akashclient.NewQueryClientFromCtx(cctx), prov, []tls.Certificate{cert})
+	gclient, err := gwrest.NewJwtClient(ctx, akashclient.NewQueryClientFromCtx(cctx), prov, []tls.Certificate{tlsCert})
 	if err != nil {
 		return err
 	}
diff --git a/provider/cmd/leaseEvents.go b/provider/cmd/leaseEvents.go
index 44478281..0afc35b1 100644
--- a/provider/cmd/leaseEvents.go
+++ b/provider/cmd/leaseEvents.go
@@ -40,7 +40,7 @@ func doLeaseEvents(cmd *cobra.Command) error {
 		return err
 	}
 
-	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, nil)
 	if err != nil {
 		return markRPCServerError(err)
 	}
diff --git a/provider/cmd/leaseLogs.go b/provider/cmd/leaseLogs.go
index e38e1807..635ffcb5 100644
--- a/provider/cmd/leaseLogs.go
+++ b/provider/cmd/leaseLogs.go
@@ -43,7 +43,7 @@ func doLeaseLogs(cmd *cobra.Command) error {
 		return err
 	}
 
-	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, nil)
 	if err != nil {
 		return markRPCServerError(err)
 	}
diff --git a/provider/cmd/leaseStatus.go b/provider/cmd/leaseStatus.go
index 14500a91..ce6892e9 100644
--- a/provider/cmd/leaseStatus.go
+++ b/provider/cmd/leaseStatus.go
@@ -45,7 +45,7 @@ func doLeaseStatus(cmd *cobra.Command) error {
 		return err
 	}
 
-	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, nil)
 	if err != nil {
 		return markRPCServerError(err)
 	}
diff --git a/provider/cmd/manifest.go b/provider/cmd/manifest.go
index 95cb91a5..86855942 100644
--- a/provider/cmd/manifest.go
+++ b/provider/cmd/manifest.go
@@ -62,7 +62,7 @@ func doSendManifest(cmd *cobra.Command, sdlpath string) error {
 	}
 
 	// TODO - dump mani
-	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, nil)
 	if err != nil {
 		return markRPCServerError(err)
 	}
diff --git a/provider/cmd/migrate_hostnames.go b/provider/cmd/migrate_hostnames.go
index 6ea425cf..0454d750 100644
--- a/provider/cmd/migrate_hostnames.go
+++ b/provider/cmd/migrate_hostnames.go
@@ -28,7 +28,7 @@ func migrateHostnames(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, nil)
 	if err != nil {
 		return markRPCServerError(err)
 	}
diff --git a/provider/cmd/run.go b/provider/cmd/run.go
index 5d237fcb..38f7699d 100644
--- a/provider/cmd/run.go
+++ b/provider/cmd/run.go
@@ -4,8 +4,6 @@ import (
 	"bytes"
 	"context"
 	"crypto/tls"
-	"crypto/x509"
-	"encoding/pem"
 	"fmt"
 	"io"
 	"net/http"
@@ -458,17 +456,17 @@ func doRunCmd(ctx context.Context, cmd *cobra.Command, _ []string) error {
 		certFromFlag = bytes.NewBufferString(val)
 	}
 
-	cpem, err := cutils.LoadPEMForAccount(cctx, txFactory.Keybase(), cutils.PEMFromReader(certFromFlag))
+	kpm, err := cutils.NewKeyPairManager(cctx, cctx.FromAddress)
 	if err != nil {
 		return err
 	}
 
-	blk, _ := pem.Decode(cpem.Cert)
-	x509cert, err := x509.ParseCertificate(blk.Bytes)
+	x509cert, tlsCert, err := kpm.ReadX509KeyPair(certFromFlag)
 	if err != nil {
 		return err
 	}
 
+	// Check that the certificate exists on chain and is not revoked
 	cquery := cmodule.AppModuleBasic{}.GetQueryClient(cctx)
 	cresp, err := cquery.Certificates(cmd.Context(), &ctypes.QueryCertificatesRequest{
 		Filter: ctypes.CertificateFilter{
@@ -485,27 +483,20 @@ func doRunCmd(ctx context.Context, cmd *cobra.Command, _ []string) error {
 		return errors.Errorf("no valid found on chain certificate for account %s", cctx.FromAddress)
 	}
 
-	cert, err := tls.X509KeyPair(cpem.Cert, cpem.Priv)
-	if err != nil {
-		return err
-	}
-
-	log := openLogger()
+	logger := openLogger()
 
-	broadcaster, err := broadcaster.NewSerialClient(log, cctx, txFactory, info)
+	broadcasterClient, err := broadcaster.NewSerialClient(logger, cctx, txFactory, info)
 	if err != nil {
 		return err
 	}
 
-	// TODO: actually get the passphrase?
-	// passphrase, err := keys.GetPassphrase(fromName)
 	aclient := client.NewClientWithBroadcaster(
-		log,
+		logger,
 		cctx,
 		txFactory,
 		info,
 		client.NewQueryClientFromCtx(cctx),
-		broadcaster,
+		broadcasterClient,
 	)
 
 	res, err := aclient.Query().Provider(
@@ -539,7 +530,7 @@ func doRunCmd(ctx context.Context, cmd *cobra.Command, _ []string) error {
 		builder.SettingsKey: kubeSettings,
 	}
 
-	cclient, err := createClusterClient(log, cmd, kubeConfig)
+	cclient, err := createClusterClient(logger, cmd, kubeConfig)
 	if err != nil {
 		return err
 	}
@@ -549,7 +540,7 @@ func doRunCmd(ctx context.Context, cmd *cobra.Command, _ []string) error {
 		return err
 	}
 	currentBlockHeight := statusResult.SyncInfo.LatestBlockHeight
-	session := session.New(log, aclient, pinfo, currentBlockHeight)
+	session := session.New(logger, aclient, pinfo, currentBlockHeight)
 
 	if err := cctx.Client.Start(); err != nil {
 		return err
@@ -611,12 +602,12 @@ func doRunCmd(ctx context.Context, cmd *cobra.Command, _ []string) error {
 
 	gateway, err := gwrest.NewServer(
 		ctx,
-		log,
+		logger,
 		service,
 		cquery,
 		gwaddr,
 		cctx.FromAddress,
-		[]tls.Certificate{cert},
+		[]tls.Certificate{tlsCert},
 		clusterSettings,
 	)
 	if err != nil {
@@ -658,7 +649,7 @@ func doRunCmd(ctx context.Context, cmd *cobra.Command, _ []string) error {
 	}
 
 	err = group.Wait()
-	broadcaster.Close()
+	broadcasterClient.Close()
 	if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, http.ErrServerClosed) {
 		return err
 	}
diff --git a/provider/cmd/run_resource_server.go b/provider/cmd/run_resource_server.go
index 8ad34892..1a3bd655 100644
--- a/provider/cmd/run_resource_server.go
+++ b/provider/cmd/run_resource_server.go
@@ -4,8 +4,6 @@ import (
 	"bytes"
 	"context"
 	"crypto/ecdsa"
-	"crypto/x509"
-	"encoding/pem"
 	"errors"
 	"io"
 	"net/http"
@@ -14,7 +12,6 @@ import (
 
 	sdkclient "github.com/cosmos/cosmos-sdk/client"
 	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/client/tx"
 	"github.com/ovrclk/akash/cmd/common"
 	gwrest "github.com/ovrclk/akash/provider/gateway/rest"
 	cutils "github.com/ovrclk/akash/x/cert/utils"
@@ -71,20 +68,17 @@ func doRunResourceServer(ctx context.Context, cmd *cobra.Command, _ []string) er
 		return err
 	}
 
-	txFactory := tx.NewFactoryCLI(cctx, cmd.Flags()).WithTxConfig(cctx.TxConfig).WithAccountRetriever(cctx.AccountRetriever)
-
 	var certFromFlag io.Reader
 	if val := cmd.Flag(FlagAuthPem).Value.String(); val != "" {
 		certFromFlag = bytes.NewBufferString(val)
 	}
 
-	cpem, err := cutils.LoadPEMForAccount(cctx, txFactory.Keybase(), cutils.PEMFromReader(certFromFlag))
+	kpm, err := cutils.NewKeyPairManager(cctx, cctx.GetFromAddress())
 	if err != nil {
 		return err
 	}
 
-	blk, _ := pem.Decode(cpem.Cert)
-	x509cert, err := x509.ParseCertificate(blk.Bytes)
+	x509cert, _, err := kpm.ReadX509KeyPair(certFromFlag)
 	if err != nil {
 		return err
 	}
diff --git a/provider/cmd/serviceStatus.go b/provider/cmd/serviceStatus.go
index c785f501..854ccba6 100644
--- a/provider/cmd/serviceStatus.go
+++ b/provider/cmd/serviceStatus.go
@@ -53,7 +53,7 @@ func doServiceStatus(cmd *cobra.Command) error {
 		return err
 	}
 
-	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, nil)
 	if err != nil {
 		return markRPCServerError(err)
 	}
diff --git a/provider/cmd/shell.go b/provider/cmd/shell.go
index fc49c715..0f81c9d1 100644
--- a/provider/cmd/shell.go
+++ b/provider/cmd/shell.go
@@ -115,7 +115,7 @@ func doLeaseShell(cmd *cobra.Command, args []string) error {
 	}
 	lID := bidID.LeaseID()
 
-	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(cmd.Context(), cctx, nil)
 	if err != nil {
 		return markRPCServerError(err)
 	}
diff --git a/provider/gateway/rest/client.go b/provider/gateway/rest/client.go
index 4f399003..45de2e39 100644
--- a/provider/gateway/rest/client.go
+++ b/provider/gateway/rest/client.go
@@ -185,7 +185,7 @@ func (cd *ClientDirectory) GetClient(providerAddr sdk.Address) (Client, error) {
 }
 
 func NewClientDirectory(ctx context.Context, cctx cosmosclient.Context) (*ClientDirectory, error) {
-	cert, err := cutils.LoadAndQueryCertificateForAccount(ctx, cctx, cctx.Keyring)
+	cert, err := cutils.LoadAndQueryCertificateForAccount(ctx, cctx, nil)
 	if err != nil {
 		return nil, err
 	}
diff --git a/x/cert/client/cli/query.go b/x/cert/client/cli/query.go
index 171fb613..ab5a69a5 100644
--- a/x/cert/client/cli/query.go
+++ b/x/cert/client/cli/query.go
@@ -1,7 +1,6 @@
 package cli
 
 import (
-	"context"
 	"math/big"
 
 	"github.com/cosmos/cosmos-sdk/client"
@@ -13,6 +12,11 @@ import (
 	types "github.com/ovrclk/akash/x/cert/types/v1beta2"
 )
 
+const (
+	stateValid   = "valid"
+	stateRevoked = "revoked"
+)
+
 func GetQueryCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:                        types.ModuleName,
@@ -72,14 +76,14 @@ func cmdGetCertificates() *cobra.Command {
 			}
 
 			if value := cmd.Flag("state").Value.String(); value != "" {
-				if value != "valid" && value != "revoked" {
+				if value != stateValid && value != stateRevoked {
 					return errors.Errorf("invalid value of --state flag. expected valid|revoked")
 				}
 
 				params.Filter.State = value
 			}
 
-			res, err := queryClient.Certificates(context.Background(), params)
+			res, err := queryClient.Certificates(cmd.Context(), params)
 			if err != nil {
 				return err
 			}
diff --git a/x/cert/client/cli/tx.go b/x/cert/client/cli/tx.go
index 6373c88a..89e0b32c 100644
--- a/x/cert/client/cli/tx.go
+++ b/x/cert/client/cli/tx.go
@@ -1,44 +1,32 @@
 package cli
 
 import (
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
 	"crypto/x509"
-	"crypto/x509/pkix"
-	"encoding/asn1"
-	"encoding/json"
 	"encoding/pem"
 	"fmt"
-	"math/big"
-	"net"
-	"os"
-	"strconv"
-	"strings"
-	"time"
-
 	sdkclient "github.com/cosmos/cosmos-sdk/client"
-	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/server"
-	"github.com/cosmos/cosmos-sdk/x/genutil"
-	genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
-	"github.com/pkg/errors"
-	"github.com/spf13/cobra"
-
 	"github.com/ovrclk/akash/sdkutil"
-	"github.com/ovrclk/akash/util/cli"
+	certerrors "github.com/ovrclk/akash/x/cert/errors"
 	types "github.com/ovrclk/akash/x/cert/types/v1beta2"
-	cutils "github.com/ovrclk/akash/x/cert/utils"
+	"github.com/ovrclk/akash/x/cert/utils"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+	"math/big"
+	"time"
 )
 
 const (
-	flagNbf    = "nbf"
-	flagNaf    = "naf"
-	flagRie    = "rie"
-	flagSerial = "serial"
+	flagSerial    = "serial"
+	flagOverwrite = "overwrite"
+	flagValidTime = "valid-duration"
+	flagStart     = "start-time"
+	flagToGenesis = "to-genesis"
 )
 
-var AuthVersionOID = asn1.ObjectIdentifier{2, 23, 133, 2, 6}
+var (
+	errCertificateDoesNotExist    = fmt.Errorf("%w: does not exist", certerrors.ErrCertificate)
+	errCannotOverwriteCertificate = fmt.Errorf("%w: cannot overwrite certificate", certerrors.ErrCertificate)
+)
 
 func GetTxCmd() *cobra.Command {
 	cmd := &cobra.Command{
@@ -48,465 +36,169 @@ func GetTxCmd() *cobra.Command {
 		RunE:                       sdkclient.ValidateCmd,
 	}
 
-	cmd.AddCommand(
-		cmdCreate(),
-		cmdRevoke(),
-	)
-
-	return cmd
-}
+	/**
+	Commands
+	1. Generate - create public / private key pair
+	2. Publish - publish a key pair to the blockchain
+	3. Revoke - revoke a key pair on the blockchain
 
-func cmdCreate() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:                        "create",
-		Short:                      "create/update api certificates",
-		SuggestionsMinimumDistance: 2,
-		RunE:                       sdkclient.ValidateCmd,
-	}
+	*/
 
 	cmd.AddCommand(
-		cmdCreateClient(),
-		cmdCreateServer(),
+		cmdGenerate(),
+		cmdPublish(),
+		cmdRevoke(),
 	)
 
 	return cmd
 }
 
-func cmdRevoke() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:          "revoke",
-		Short:        "revoke api certificate",
-		SilenceUsage: true,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			cctx, err := sdkclient.GetClientTxContext(cmd)
-			if err != nil {
-				return err
-			}
-
-			serial := cmd.Flag(flagSerial).Value.String()
-
-			if serial != "" {
-				if _, valid := new(big.Int).SetString(serial, 10); !valid {
-					return errInvalidSerialFlag
-				}
-			} else {
-				cpem, err := cutils.LoadPEMForAccount(cctx, cctx.Keyring)
-				if err != nil {
-					return err
-				}
-
-				blk, _ := pem.Decode(cpem.Cert)
-				cert, err := x509.ParseCertificate(blk.Bytes)
-				if err != nil {
-					return err
-				}
-
-				serial = cert.SerialNumber.String()
-			}
-
-			return doRevoke(cmd, cctx, serial)
-		},
-	}
-
-	cmd.Flags().String(flagSerial, "", "revoke certificate by serial number")
-	flags.AddTxFlagsToCmd(cmd)
-	if err := cmd.MarkFlagRequired(flags.FlagFrom); err != nil {
-		panic(err.Error())
-	}
-
-	return cmd
-}
-
-func cmdCreateClient() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:          "client",
-		Short:        "create client api certificate",
-		SilenceUsage: true,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return doCreateCmd(cmd, nil)
-		},
-	}
-
-	setCreateFlags(cmd)
-
-	return cmd
-}
-
-func cmdCreateServer() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:          "server [space separated dns names and/or IP addresses]",
-		Short:        "create server api certificate",
-		SilenceUsage: true,
-		Args:         cobra.MinimumNArgs(1),
-		RunE:         doCreateCmd,
-	}
-
-	setCreateFlags(cmd)
-
-	return cmd
-}
-
-func handleCreate(cctx sdkclient.Context, cmd *cobra.Command, pemFile string, domains []string) error {
-	toGenesis, _ := cmd.Flags().GetBool("to-genesis")
-
-	msg, err := createAuthPem(cmd, pemFile, domains)
-	if err != nil {
-		return err
-	}
-
-	if !toGenesis {
-		return sdkutil.BroadcastTX(cmd.Context(), cctx, cmd.Flags(), msg)
-	}
-
-	return addCertToGenesis(cmd, types.GenesisCertificate{
-		Owner: msg.Owner,
-		Certificate: types.Certificate{
-			State:  types.CertificateValid,
-			Cert:   msg.Cert,
-			Pubkey: msg.Pubkey,
-		},
-	})
-}
-
-func doCreateCmd(cmd *cobra.Command, domains []string) error {
-	revokeIfExists, err := cmd.Flags().GetBool(flagRie)
-	if err != nil {
-		return err
-	}
+func doGenerateCmd(cmd *cobra.Command, domains []string) error {
+	allowOverwrite := viper.GetBool(flagOverwrite)
 
 	cctx, err := sdkclient.GetClientTxContext(cmd)
 	if err != nil {
 		return err
 	}
-
 	fromAddress := cctx.GetFromAddress()
 
-	pemFile := cctx.HomeDir + "/" + fromAddress.String() + ".pem"
-
-	if _, err = os.Stat(pemFile); os.IsNotExist(err) {
-		_ = cctx.PrintString(fmt.Sprintf("no certificate found for address %s. generating new...\n", fromAddress))
-
-		return handleCreate(cctx, cmd, pemFile, domains)
-	}
-
-	cpem, err := cutils.LoadPEMForAccount(cctx, cctx.Keyring)
+	kpm, err := utils.NewKeyPairManager(cctx, fromAddress)
 	if err != nil {
 		return err
 	}
 
-	blk, _ := pem.Decode(cpem.Cert)
-	x509cert, err := x509.ParseCertificate(blk.Bytes)
+	exists, err := kpm.KeyExists()
 	if err != nil {
 		return err
 	}
-
-	// if revoke-if-exists flag is true query is performed automatically
-	// then certificate is being revoked (if valid) and file is removed without any prompts
-	yes := revokeIfExists
-	if !yes {
-		yes, err = cli.GetConfirmation(cmd, fmt.Sprintf("certificate file for address %q already exist. check it on-chain status?", fromAddress))
-		if err != nil {
-			return err
-		}
+	if !allowOverwrite && exists {
+		return errCannotOverwriteCertificate
 	}
 
-	if yes {
-		params := &types.QueryCertificatesRequest{
-			Filter: types.CertificateFilter{
-				Owner:  cctx.FromAddress.String(),
-				Serial: x509cert.SerialNumber.String(),
-			},
-		}
-
-		res, err := types.NewQueryClient(cctx).Certificates(cmd.Context(), params)
+	var startTime time.Time
+	startTimeStr := viper.GetString(flagStart)
+	if len(startTimeStr) == 0 {
+		startTime = time.Now().Truncate(time.Second)
+	} else {
+		startTime, err = time.Parse(time.RFC3339, startTimeStr)
 		if err != nil {
 			return err
 		}
-
-		removeFile := revokeIfExists
-
-		if len(res.Certificates) == 0 {
-			if !revokeIfExists {
-				yes, err = cli.GetConfirmation(cmd, "this certificate has not been found on chain. would you like to commit it?")
-				if err != nil {
-					return err
-				}
-
-				if !yes {
-					yes, err = cli.GetConfirmation(cmd, "would you like to remove the file?")
-					if err != nil {
-						return err
-					}
-
-					removeFile = yes
-				} else {
-					cpem, err := cutils.LoadPEMForAccount(cctx, cctx.Keyring)
-					if err != nil {
-						return err
-					}
-
-					msg := &types.MsgCreateCertificate{
-						Owner:  fromAddress.String(),
-						Cert:   cpem.Cert,
-						Pubkey: cpem.Pub,
-					}
-
-					if err = msg.ValidateBasic(); err != nil {
-						return err
-					}
-
-					return sdkutil.BroadcastTX(cmd.Context(), cctx, cmd.Flags(), msg)
-				}
-			}
-		} else {
-			if res.Certificates[0].Certificate.IsState(types.CertificateValid) {
-				err = doRevoke(cmd, cctx, x509cert.SerialNumber.String())
-				if err == nil {
-					removeFile = true
-				}
-			}
-		}
-
-		if removeFile {
-			if err = os.Remove(pemFile); err != nil {
-				return err
-			}
-
-			_ = cctx.PrintString("generating new...\n")
-			return handleCreate(cctx, cmd, pemFile, domains)
-		}
 	}
+	validDuration := viper.GetDuration(flagValidTime)
 
-	return nil
+	return kpm.Generate(startTime, startTime.Add(validDuration), domains)
 }
 
-func setCreateFlags(cmd *cobra.Command) {
-	flags.AddTxFlagsToCmd(cmd)
-	if err := cmd.MarkFlagRequired(flags.FlagFrom); err != nil {
-		panic(err.Error())
-	}
+func doPublishCmd(cmd *cobra.Command) error {
+	toGenesis := viper.GetBool(flagToGenesis)
 
-	cmd.Flags().String(flagNbf, "", "certificate is not valid before this date. default current timestamp. RFC3339")
-	cmd.Flags().String(flagNaf, "", "certificate is not valid after this date. default 365d. days or RFC3339")
-	cmd.Flags().Bool(flagRie, false, "revoke current certificate if it already present on chain")
-
-	// fixme shall we use gentx instead? 🤔
-	cmd.Flags().Bool("to-genesis", false, "export certificate to genesis")
-}
-
-func addCertToGenesis(cmd *cobra.Command, cert types.GenesisCertificate) error {
 	cctx, err := sdkclient.GetClientTxContext(cmd)
 	if err != nil {
 		return err
 	}
+	fromAddress := cctx.GetFromAddress()
 
-	cdc := cctx.Codec
-
-	serverCtx := server.GetServerContextFromCmd(cmd)
-	config := serverCtx.Config
-
-	config.SetRoot(cctx.HomeDir)
-
-	if err := cert.Validate(); err != nil {
-		return errors.Errorf("failed to validate new genesis certificate: %v", err)
+	kpm, err := utils.NewKeyPairManager(cctx, fromAddress)
+	if err != nil {
+		return err
 	}
 
-	genFile := config.GenesisFile()
-	appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile)
+	exists, err := kpm.KeyExists()
 	if err != nil {
-		return errors.Errorf("failed to unmarshal genesis state: %v", err)
+		return err
+	}
+	if !exists {
+		return errCertificateDoesNotExist
 	}
 
-	certsGenState := types.GetGenesisStateFromAppState(cdc, appState)
+	cert, _, pubKey, err := kpm.Read()
+	if err != nil {
+		return err
+	}
 
-	if certsGenState.Certificates.Contains(cert) {
-		return errors.Errorf("cannot add already existing certificate")
+	msg := &types.MsgCreateCertificate{
+		Owner: fromAddress.String(),
+		Cert: pem.EncodeToMemory(&pem.Block{
+			Type:  types.PemBlkTypeCertificate,
+			Bytes: cert,
+		}),
+		Pubkey: pem.EncodeToMemory(&pem.Block{
+			Type:  types.PemBlkTypeECPublicKey,
+			Bytes: pubKey,
+		}),
 	}
-	certsGenState.Certificates = append(certsGenState.Certificates, cert)
 
-	certsGenStateBz, err := cdc.MarshalJSON(certsGenState)
-	if err != nil {
-		return errors.Errorf("failed to marshal auth genesis state: %v", err)
+	if err = msg.ValidateBasic(); err != nil {
+		return err
 	}
 
-	appState[types.ModuleName] = certsGenStateBz
+	if toGenesis {
+		return addCertToGenesis(cmd, types.GenesisCertificate{
+			Owner: msg.Owner,
+			Certificate: types.Certificate{
+				State:  types.CertificateValid,
+				Cert:   msg.Cert,
+				Pubkey: msg.Pubkey,
+			},
+		})
 
-	appStateJSON, err := json.Marshal(appState)
-	if err != nil {
-		return errors.Errorf("failed to marshal application genesis state: %v", err)
 	}
 
-	genDoc.AppState = appStateJSON
-	return genutil.ExportGenesisFile(genDoc, genFile)
+	return sdkutil.BroadcastTX(cmd.Context(), cctx, cmd.Flags(), msg)
 }
 
-func createAuthPem(cmd *cobra.Command, pemFile string, domains []string) (*types.MsgCreateCertificate, error) {
+func doRevokeCmd(cmd *cobra.Command) error {
+	serial := viper.GetString(flagSerial)
 	cctx, err := sdkclient.GetClientTxContext(cmd)
 	if err != nil {
-		return nil, err
+		return err
 	}
-
 	fromAddress := cctx.GetFromAddress()
-	// note operation below needs more digging to ensure security. current implementation is more like example
-	//      private key we generate has to be password protected
-	//      from user prospective remembering/handling yet another password
-	//      would be a subject of obliviousness. instead we utilize account's key
-	//      to generate signature of it's address and use it as a password to encrypt
-	//      private key.
-	//      from security prospective this signature must never be exposed to prevent certificate leak.
-	//      from other hand user must never obtain signature of it's own addresses in shell
-	//      so yet again - to be discussed
-	sig, _, err := cctx.Keyring.SignByAddress(fromAddress, fromAddress.Bytes())
-	if err != nil {
-		return nil, err
-	}
-
-	var priv *ecdsa.PrivateKey
-
-	if priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil {
-		return nil, err
-	}
-
-	nbf := time.Now()
-	naf := nbf.Add(time.Hour * 24 * 365)
 
-	if val := cmd.Flag(flagNbf).Value.String(); val != "" {
-		nbf, err = time.Parse(time.RFC3339, val)
+	if len(serial) != 0 {
+		if _, valid := new(big.Int).SetString(serial, 10); !valid {
+			return errInvalidSerialFlag
+		}
+	} else {
+		kpm, err := utils.NewKeyPairManager(cctx, fromAddress)
 		if err != nil {
-			return nil, err
+			return err
 		}
-	}
 
-	if val := cmd.Flag(flagNaf).Value.String(); val != "" {
-		if strings.HasSuffix(val, "d") {
-			days, err := strconv.ParseUint(strings.TrimSuffix(val, "d"), 10, 32)
-			if err != nil {
-				return nil, err
-			}
-
-			naf = nbf.Add(time.Hour * 24 * time.Duration(days))
-		} else {
-			naf, err = time.Parse(time.RFC3339, val)
-			if err != nil {
-				return nil, err
-			}
+		cert, _, _, err := kpm.Read()
+		if err != nil {
+			return err
 		}
-	}
 
-	serialNumber := new(big.Int).SetInt64(time.Now().UTC().UnixNano())
-
-	extKeyUsage := []x509.ExtKeyUsage{
-		x509.ExtKeyUsageClientAuth,
-	}
-
-	if len(domains) > 0 {
-		extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageServerAuth)
-	}
-
-	template := x509.Certificate{
-		SerialNumber: serialNumber,
-		Subject: pkix.Name{
-			CommonName: fromAddress.String(),
-			ExtraNames: []pkix.AttributeTypeAndValue{
-				{
-					Type:  AuthVersionOID,
-					Value: "v0.0.1",
-				},
-			},
-		},
-		Issuer: pkix.Name{
-			CommonName: fromAddress.String(),
-		},
-		NotBefore:             nbf,
-		NotAfter:              naf,
-		KeyUsage:              x509.KeyUsageDataEncipherment | x509.KeyUsageKeyEncipherment,
-		ExtKeyUsage:           extKeyUsage,
-		BasicConstraintsValid: true,
-	}
-
-	var ips []net.IP
-
-	for i := len(domains) - 1; i >= 0; i-- {
-		if ip := net.ParseIP(domains[i]); ip != nil {
-			ips = append(ips, ip)
-			domains = append(domains[:i], domains[i+1:]...)
+		parsedCert, err := x509.ParseCertificate(cert)
+		if err != nil {
+			return err
 		}
-	}
 
-	if len(domains) != 0 || len(ips) != 0 {
-		template.PermittedDNSDomainsCritical = true
-		template.PermittedDNSDomains = domains
-		template.DNSNames = domains
-		template.IPAddresses = ips
+		serial = parsedCert.SerialNumber.String()
 	}
 
-	var certDer []byte
-	if certDer, err = x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv); err != nil {
-		_ = cctx.PrintString(fmt.Sprintf("Failed to create certificate: %v\n", err))
-		return nil, err
-	}
-
-	var keyDer []byte
-	if keyDer, err = x509.MarshalPKCS8PrivateKey(priv); err != nil {
-		return nil, err
+	params := &types.QueryCertificatesRequest{
+		Filter: types.CertificateFilter{
+			Owner:  fromAddress.String(),
+			Serial: serial,
+			State:  stateValid,
+		},
 	}
 
-	var pubKeyDer []byte
-	if pubKeyDer, err = x509.MarshalPKIXPublicKey(priv.Public()); err != nil {
-		return nil, err
-	}
+	queryClient := types.NewQueryClient(cctx)
 
-	var blk *pem.Block
-	// fixme #1182
-	blk, err = x509.EncryptPEMBlock(rand.Reader, types.PemBlkTypeECPrivateKey, keyDer, sig, x509.PEMCipherAES256) // nolint: staticcheck
+	res, err := queryClient.Certificates(cmd.Context(), params)
 	if err != nil {
-		_ = cctx.PrintString(fmt.Sprintf("failed to encrypt key file: %v\n", err))
-		return nil, err
-	}
-
-	var pemOut *os.File
-	if pemOut, err = os.OpenFile(pemFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
-		return nil, err
-	}
-
-	defer func() {
-		if err = pemOut.Close(); err != nil {
-			_ = cctx.PrintString(fmt.Sprintf("failed to close key file: %v\n", err))
-		} else {
-			_ = os.Chmod(pemFile, 0400)
-		}
-	}()
-
-	if err = pem.Encode(pemOut, &pem.Block{Type: types.PemBlkTypeCertificate, Bytes: certDer}); err != nil {
-		_ = cctx.PrintString(fmt.Sprintf("failed to write certificate to pem file: %v\n", err))
-		return nil, err
-	}
-
-	if err = pem.Encode(pemOut, blk); err != nil {
-		_ = cctx.PrintString(fmt.Sprintf("failed to write key to pem file: %v\n", err))
-		return nil, err
-	}
-
-	msg := &types.MsgCreateCertificate{
-		Owner: fromAddress.String(),
-		Cert: pem.EncodeToMemory(&pem.Block{
-			Type:  types.PemBlkTypeCertificate,
-			Bytes: certDer,
-		}),
-		Pubkey: pem.EncodeToMemory(&pem.Block{
-			Type:  types.PemBlkTypeECPublicKey,
-			Bytes: pubKeyDer,
-		}),
+		return err
 	}
 
-	if err = msg.ValidateBasic(); err != nil {
-		return nil, err
+	exists := len(res.Certificates) != 0
+	if !exists {
+		return fmt.Errorf("%w: certificate with serial %v does not exist on chain and cannot be revoked", certerrors.ErrCertificate, serial)
 	}
 
-	return msg, nil
-}
-
-func doRevoke(cmd *cobra.Command, cctx sdkclient.Context, serial string) error {
 	msg := &types.MsgRevokeCertificate{
 		ID: types.CertificateID{
 			Owner:  cctx.FromAddress.String(),
diff --git a/x/cert/client/cli/tx_cmd.go b/x/cert/client/cli/tx_cmd.go
new file mode 100644
index 00000000..74d8a7ac
--- /dev/null
+++ b/x/cert/client/cli/tx_cmd.go
@@ -0,0 +1,253 @@
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	sdkclient "github.com/cosmos/cosmos-sdk/client"
+	"github.com/cosmos/cosmos-sdk/client/flags"
+	"github.com/cosmos/cosmos-sdk/server"
+	"github.com/cosmos/cosmos-sdk/x/genutil"
+	genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
+	types "github.com/ovrclk/akash/x/cert/types/v1beta2"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+	"time"
+)
+
+func cmdGenerate() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "generate",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE:                       sdkclient.ValidateCmd,
+	}
+
+	cmd.AddCommand(cmdGenerateClient(),
+		cmdGenerateServer(),
+	)
+
+	return cmd
+}
+
+func addGenerateflags(cmd *cobra.Command) error {
+	cmd.Flags().String(flagStart, "", "certificate is not valid before this date. default current timestamp. RFC3339")
+	if err := viper.BindPFlag(flagStart, cmd.Flags().Lookup(flagStart)); err != nil {
+		return err
+	}
+
+	cmd.Flags().Duration(flagValidTime, time.Hour*24*365, "certificate is not valid after this date. RFC3339")
+	if err := viper.BindPFlag(flagValidTime, cmd.Flags().Lookup(flagValidTime)); err != nil {
+		return err
+	}
+	cmd.Flags().Bool(flagOverwrite, false, "overwrite existing certificate if present")
+	if err := viper.BindPFlag(flagOverwrite, cmd.Flags().Lookup(flagOverwrite)); err != nil {
+		return err
+	}
+
+	flags.AddTxFlagsToCmd(cmd) // TODO - add just the keyring flags? not all the TX ones
+	return nil
+}
+
+func cmdGenerateClient() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "client",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE:                       doGenerateCmd,
+		SilenceUsage:               true,
+		Args:                       cobra.ExactArgs(0),
+	}
+	err := addGenerateflags(cmd)
+	if err != nil {
+		panic(err)
+	}
+
+	return cmd
+}
+
+func cmdGenerateServer() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "server",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE:                       doGenerateCmd,
+		SilenceUsage:               true,
+		Args:                       cobra.MinimumNArgs(1),
+	}
+	err := addGenerateflags(cmd)
+	if err != nil {
+		panic(err)
+	}
+
+	return cmd
+}
+
+func cmdPublish() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "publish",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE:                       sdkclient.ValidateCmd,
+	}
+
+	cmd.AddCommand(cmdPublishClient(),
+		cmdPublishServer())
+
+	return cmd
+}
+
+func cmdPublishClient() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "client",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return doPublishCmd(cmd)
+		},
+		SilenceUsage: true,
+		Args:         cobra.ExactArgs(0),
+	}
+	err := addPublishFlags(cmd)
+	if err != nil {
+		panic(err)
+	}
+
+	return cmd
+}
+
+func cmdPublishServer() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "server",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return doPublishCmd(cmd)
+		},
+		SilenceUsage: true,
+		Args:         cobra.ExactArgs(0),
+	}
+	err := addPublishFlags(cmd)
+	if err != nil {
+		panic(err)
+	}
+
+	return cmd
+}
+
+func addPublishFlags(cmd *cobra.Command) error {
+	cmd.Flags().Bool(flagToGenesis, false, "add to genesis")
+	if err := viper.BindPFlag(flagToGenesis, cmd.Flags().Lookup(flagToGenesis)); err != nil {
+		return err
+	}
+
+	flags.AddTxFlagsToCmd(cmd)
+
+	return nil
+}
+
+func addCertToGenesis(cmd *cobra.Command, cert types.GenesisCertificate) error {
+	cctx, err := sdkclient.GetClientTxContext(cmd)
+	if err != nil {
+		return err
+	}
+
+	cdc := cctx.Codec
+
+	serverCtx := server.GetServerContextFromCmd(cmd)
+	config := serverCtx.Config
+
+	config.SetRoot(cctx.HomeDir)
+
+	if err := cert.Validate(); err != nil {
+		return fmt.Errorf("%w: failed to validate new genesis certificate", err)
+	}
+
+	genFile := config.GenesisFile()
+	appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile)
+	if err != nil {
+		return fmt.Errorf("%w: failed to unmarshal genesis state", err)
+	}
+
+	certsGenState := types.GetGenesisStateFromAppState(cdc, appState)
+
+	if certsGenState.Certificates.Contains(cert) {
+		return fmt.Errorf("%w: cannot add already existing certificate", err)
+	}
+	certsGenState.Certificates = append(certsGenState.Certificates, cert)
+
+	certsGenStateBz, err := cdc.MarshalJSON(certsGenState)
+	if err != nil {
+		return fmt.Errorf("%w: failed to marshal auth genesis state", err)
+	}
+
+	appState[types.ModuleName] = certsGenStateBz
+
+	appStateJSON, err := json.Marshal(appState)
+	if err != nil {
+		return fmt.Errorf("%w: failed to marshal application genesis state", err)
+	}
+
+	genDoc.AppState = appStateJSON
+	return genutil.ExportGenesisFile(genDoc, genFile)
+}
+
+func cmdRevoke() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "revoke",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE:                       sdkclient.ValidateCmd,
+	}
+	cmd.AddCommand(cmdRevokeClient(),
+		cmdRevokeServer())
+
+	return cmd
+}
+
+func cmdRevokeClient() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "server",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return doRevokeCmd(cmd)
+		},
+		SilenceUsage: true,
+		Args:         cobra.ExactArgs(0),
+	}
+	err := addRevokeCmdFlags(cmd)
+	if err != nil {
+		panic(err)
+	}
+
+	return cmd
+}
+
+func cmdRevokeServer() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:                        "server",
+		Short:                      "",
+		SuggestionsMinimumDistance: 2,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return doRevokeCmd(cmd)
+		},
+		SilenceUsage: true,
+		Args:         cobra.ExactArgs(0),
+	}
+	err := addRevokeCmdFlags(cmd)
+	if err != nil {
+		panic(err)
+	}
+
+	return cmd
+}
+
+func addRevokeCmdFlags(cmd *cobra.Command) error {
+	cmd.Flags().String(flagSerial, "", "revoke certificate by serial number")
+	if err := viper.BindPFlag(flagSerial, cmd.Flags().Lookup(flagSerial)); err != nil {
+		return err
+	}
+
+	flags.AddTxFlagsToCmd(cmd)
+	return nil
+}
diff --git a/x/cert/errors/certificate_errors.go b/x/cert/errors/certificate_errors.go
new file mode 100644
index 00000000..3bf4512b
--- /dev/null
+++ b/x/cert/errors/certificate_errors.go
@@ -0,0 +1,7 @@
+package errors
+
+import "errors"
+
+var (
+	ErrCertificate = errors.New("certificate error")
+)
diff --git a/x/cert/utils/constants.go b/x/cert/utils/constants.go
new file mode 100644
index 00000000..1e9eaaa7
--- /dev/null
+++ b/x/cert/utils/constants.go
@@ -0,0 +1,5 @@
+package utils
+
+import "encoding/asn1"
+
+var AuthVersionOID = asn1.ObjectIdentifier{2, 23, 133, 2, 6}
diff --git a/x/cert/utils/key_pair_manager.go b/x/cert/utils/key_pair_manager.go
new file mode 100644
index 00000000..1d3838c0
--- /dev/null
+++ b/x/cert/utils/key_pair_manager.go
@@ -0,0 +1,278 @@
+package utils
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"fmt"
+	sdkclient "github.com/cosmos/cosmos-sdk/client"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	certerrors "github.com/ovrclk/akash/x/cert/errors"
+	types "github.com/ovrclk/akash/x/cert/types/v1beta2"
+	"io"
+	"math/big"
+	"net"
+	"os"
+	"time"
+)
+
+var (
+	errCertificateNotFoundInPEM = fmt.Errorf("%w: certificate not found in PEM", certerrors.ErrCertificate)
+	errPrivateKeyNotFoundInPEM  = fmt.Errorf("%w: private key not found in PEM", certerrors.ErrCertificate)
+	errPublicKeyNotFoundInPEM   = fmt.Errorf("%w: public key not found in PEM", certerrors.ErrCertificate)
+)
+
+type KeyPairManager interface {
+	KeyExists() (bool, error)
+	Generate(notBefore, notAfter time.Time, domains []string) error
+
+	// Read the PEM blocks, containing the cert, private key, & public key
+	Read(fin ...io.Reader) ([]byte, []byte, []byte, error)
+
+	ReadX509KeyPair(fin ...io.Reader) (*x509.Certificate, tls.Certificate, error)
+}
+
+type keyPairManager struct {
+	addr          sdk.AccAddress
+	passwordBytes []byte
+	homeDir       string
+}
+
+func NewKeyPairManager(cctx sdkclient.Context, fromAddress sdk.AccAddress) (KeyPairManager, error) {
+	sig, _, err := cctx.Keyring.SignByAddress(fromAddress, fromAddress.Bytes())
+	if err != nil {
+		return nil, err
+	}
+
+	return &keyPairManager{
+		addr:          fromAddress,
+		passwordBytes: sig,
+		homeDir:       cctx.HomeDir,
+	}, nil
+}
+
+func (kpm *keyPairManager) getKeyPath() string {
+	return kpm.homeDir + "/" + kpm.addr.String() + ".pem"
+}
+
+func (kpm *keyPairManager) ReadX509KeyPair(fin ...io.Reader) (*x509.Certificate, tls.Certificate, error) {
+	certData, privKeyData, _, err := kpm.Read(fin...)
+	if err != nil {
+		return nil, tls.Certificate{}, err
+	}
+
+	x509cert, err := x509.ParseCertificate(certData)
+	if err != nil {
+		return nil, tls.Certificate{}, fmt.Errorf("could not parse x509 cert: %w", err)
+	}
+
+	result := tls.Certificate{
+		Certificate: [][]byte{certData},
+	}
+
+	result.PrivateKey, err = x509.ParsePKCS8PrivateKey(privKeyData)
+	if err != nil {
+		return nil, tls.Certificate{}, fmt.Errorf("%w: failed parsing private key data", err)
+	}
+
+	return x509cert, result, err
+}
+
+func (kpm *keyPairManager) KeyExists() (bool, error) {
+	_, err := os.Stat(kpm.getKeyPath())
+	if err == nil {
+		return true, nil
+	}
+
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+
+	return false, err
+}
+
+func (kpm *keyPairManager) Generate(notBefore, notAfter time.Time, domains []string) error {
+	var err error
+	var pemOut *os.File
+	if pemOut, err = os.OpenFile(kpm.getKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
+		return err
+	}
+
+	err = kpm.generateImpl(notBefore, notAfter, domains, pemOut)
+
+	closeErr := pemOut.Close()
+	if closeErr != nil {
+		return closeErr
+	}
+
+	return err
+}
+
+func (kpm *keyPairManager) generateImpl(notBefore, notAfter time.Time, domains []string, fout io.Writer) error {
+	var err error
+	// Generate the private key
+	var priv *ecdsa.PrivateKey
+	if priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil {
+		return fmt.Errorf("could not generate key: %w", err)
+	}
+
+	serialNumber := new(big.Int).SetInt64(time.Now().UTC().UnixNano())
+
+	extKeyUsage := []x509.ExtKeyUsage{
+		x509.ExtKeyUsageClientAuth,
+	}
+
+	if len(domains) != 0 {
+		extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageServerAuth)
+	}
+
+	template := x509.Certificate{
+		SerialNumber: serialNumber,
+		Subject: pkix.Name{
+			CommonName: kpm.addr.String(),
+			ExtraNames: []pkix.AttributeTypeAndValue{
+				{
+					Type:  AuthVersionOID,
+					Value: "v0.0.1",
+				},
+			},
+		},
+		Issuer: pkix.Name{
+			CommonName: kpm.addr.String(),
+		},
+		NotBefore:             notBefore,
+		NotAfter:              notAfter,
+		KeyUsage:              x509.KeyUsageDataEncipherment | x509.KeyUsageKeyEncipherment,
+		ExtKeyUsage:           extKeyUsage,
+		BasicConstraintsValid: true,
+	}
+
+	var ips []net.IP
+
+	for i := len(domains) - 1; i >= 0; i-- {
+		if ip := net.ParseIP(domains[i]); ip != nil {
+			ips = append(ips, ip)
+			domains = append(domains[:i], domains[i+1:]...)
+		}
+	}
+
+	if len(domains) != 0 || len(ips) != 0 {
+		template.PermittedDNSDomainsCritical = true
+		template.PermittedDNSDomains = domains
+		template.DNSNames = domains
+		template.IPAddresses = ips
+	}
+
+	var certDer []byte
+	if certDer, err = x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv); err != nil {
+		return fmt.Errorf("could not create certificate: %w", err)
+	}
+
+	var keyDer []byte
+	if keyDer, err = x509.MarshalPKCS8PrivateKey(priv); err != nil {
+		return fmt.Errorf("could not create private key: %w", err)
+	}
+
+	var blk *pem.Block
+	blk, err = x509.EncryptPEMBlock(rand.Reader, types.PemBlkTypeECPrivateKey, keyDer, kpm.passwordBytes, x509.PEMCipherAES256) // nolint: staticcheck
+	if err != nil {
+		return fmt.Errorf("could not encrypt private key as PEM: %w", err)
+	}
+
+	// Write the certificate
+	if err = pem.Encode(fout, &pem.Block{Type: types.PemBlkTypeCertificate, Bytes: certDer}); err != nil {
+		return fmt.Errorf("could not encode certificate as PEM: %w", err)
+	}
+
+	// Write the encrypted private key
+	if err = pem.Encode(fout, blk); err != nil {
+		return fmt.Errorf("could not encode private key as PEM: %w", err)
+	}
+
+	return nil
+}
+
+func (kpm *keyPairManager) Read(fin ...io.Reader) ([]byte, []byte, []byte, error) {
+	var pemIn io.Reader
+	var closeMe io.ReadCloser
+
+	if len(fin) != 0 {
+		if len(fin) != 1 {
+			return nil, nil, nil, fmt.Errorf("%w: Read() takes exactly 1 or 0 arguments, not %d", certerrors.ErrCertificate, len(fin))
+		}
+		pemIn = fin[0]
+	}
+
+	if pemIn == nil {
+		fopen, err := os.OpenFile(kpm.getKeyPath(), os.O_RDONLY, 0x0)
+		if err != nil {
+			return nil, nil, nil, fmt.Errorf("could not open certificate PEM file: %w", err)
+		}
+		closeMe = fopen
+		pemIn = fopen
+	}
+
+	cert, privKey, pubKey, err := kpm.readImpl(pemIn)
+
+	if closeMe != nil {
+		closeErr := closeMe.Close()
+		if closeErr != nil {
+			return nil, nil, nil, fmt.Errorf("could not close PEM file: %w", closeErr)
+		}
+	}
+
+	return cert, privKey, pubKey, err
+}
+
+func (kpm *keyPairManager) readImpl(fin io.Reader) ([]byte, []byte, []byte, error) {
+	buf := &bytes.Buffer{}
+	_, err := io.Copy(buf, fin)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("failed reading certificate PEM file: %w", err)
+	}
+	data := buf.Bytes()
+
+	// Read certificate
+	block, remaining := pem.Decode(data)
+	if block == nil {
+		return nil, nil, nil, errCertificateNotFoundInPEM
+	}
+	cert := block.Bytes
+
+	// Read private key
+	block, _ = pem.Decode(remaining)
+	if block == nil {
+		return nil, nil, nil, errPrivateKeyNotFoundInPEM
+	}
+
+	var privKeyPlaintext []byte
+	// fixme #1182
+	if privKeyPlaintext, err = x509.DecryptPEMBlock(block, kpm.passwordBytes); err != nil { // nolint: staticcheck
+		return nil, nil, nil, fmt.Errorf("%w: failed decrypting x509 block with private key", err)
+	}
+
+	var privKeyI interface{}
+	if privKeyI, err = x509.ParsePKCS8PrivateKey(privKeyPlaintext); err != nil {
+		return nil, nil, nil, fmt.Errorf("%w: failed parsing private key data", err)
+	}
+
+	eckey, valid := privKeyI.(*ecdsa.PrivateKey)
+	if !valid {
+		return nil, nil, nil, fmt.Errorf("%w: unexpected private key type, expected %T but got %T",
+			errPublicKeyNotFoundInPEM,
+			&ecdsa.PrivateKey{},
+			privKeyI)
+	}
+
+	var pubKey []byte
+	if pubKey, err = x509.MarshalPKIXPublicKey(eckey.Public()); err != nil {
+		return nil, nil, nil, fmt.Errorf("%w: failed extracting public key", err)
+	}
+
+	return cert, privKeyPlaintext, pubKey, nil
+}
diff --git a/x/cert/utils/utils.go b/x/cert/utils/utils.go
index 8daf04ae..d2dcc132 100644
--- a/x/cert/utils/utils.go
+++ b/x/cert/utils/utils.go
@@ -2,171 +2,57 @@ package utils
 
 import (
 	"context"
-	"crypto/ecdsa"
 	"crypto/tls"
-	"crypto/x509"
-	"encoding/pem"
+	"fmt"
+	certerrors "github.com/ovrclk/akash/x/cert/errors"
 	"io"
-	"io/ioutil"
-	"os"
-	"reflect"
 	"time"
 
 	"github.com/cosmos/cosmos-sdk/client"
-	"github.com/cosmos/cosmos-sdk/crypto/keyring"
-	"github.com/pkg/errors"
-
 	ctypes "github.com/ovrclk/akash/x/cert/types/v1beta2"
 )
 
-type PEMBlocks struct {
-	Cert []byte
-	Priv []byte
-	Pub  []byte
-}
-
-type loaderOption struct {
-	rd io.Reader
-}
-
-type LoaderOption func(*loaderOption)
-
-func PEMFromReader(rd io.Reader) LoaderOption {
-	return func(opt *loaderOption) {
-		opt.rd = rd
-	}
-}
-
-// LoadPEMForAccount load certificate/private key from file named as
-// account name supplied in FlagFrom
-// file must contain two PEM blocks, certificate followed by a private key
-func LoadPEMForAccount(cctx client.Context, keyring keyring.Keyring, opts ...LoaderOption) (PEMBlocks, error) {
-	sig, _, err := keyring.SignByAddress(cctx.FromAddress, cctx.FromAddress.Bytes())
+// LoadAndQueryCertificateForAccount wraps LoadAndQueryPEMForAccount and tls.X509KeyPair
+func LoadAndQueryCertificateForAccount(ctx context.Context, cctx client.Context, fin io.Reader) (tls.Certificate, error) {
+	kpm, err := NewKeyPairManager(cctx, cctx.FromAddress)
 	if err != nil {
-		return PEMBlocks{}, err
-	}
-
-	opt := &loaderOption{}
-
-	for _, o := range opts {
-		o(opt)
-	}
-
-	var pdata []byte
-
-	if opt.rd == nil {
-		pdata, err = ioutil.ReadFile(cctx.HomeDir + "/" + cctx.FromAddress.String() + ".pem")
-		if os.IsNotExist(err) {
-			pdata, err = ioutil.ReadFile(cctx.HomeDir + "/" + cctx.FromName + ".pem")
-		}
-	} else {
-		pdata, err = ioutil.ReadAll(opt.rd)
-	}
-
-	if err != nil && !errors.Is(err, io.EOF) {
-		return PEMBlocks{}, err
-	}
-
-	var bcrt *pem.Block
-	var bkey *pem.Block
-
-	var kdata []byte
-	bcrt, kdata = pem.Decode(pdata)
-	bkey, _ = pem.Decode(kdata)
-
-	if bcrt == nil {
-		return PEMBlocks{}, errors.Errorf("no certificate found")
-	}
-
-	if bkey == nil {
-		return PEMBlocks{}, errors.Errorf("no private key found")
+		return tls.Certificate{}, nil
 	}
 
-	pdata = pdata[:len(pdata)-len(kdata)-1]
-
-	var pkey []byte
-	// fixme #1182
-	if pkey, err = x509.DecryptPEMBlock(bkey, sig); err != nil { // nolint: staticcheck
-		return PEMBlocks{}, err
-	}
-
-	var priv interface{}
-	if priv, err = x509.ParsePKCS8PrivateKey(pkey); err != nil {
-		return PEMBlocks{}, errors.Wrapf(err, "coudn't parse private key")
-	}
-
-	eckey, valid := priv.(*ecdsa.PrivateKey)
-	if !valid {
-		return PEMBlocks{}, errors.Errorf("unknown key type. expected %s, desired %s",
-			reflect.TypeOf(&ecdsa.PrivateKey{}), reflect.TypeOf(eckey))
-	}
-
-	var pubKey []byte
-	if pubKey, err = x509.MarshalPKIXPublicKey(eckey.Public()); err != nil {
-		return PEMBlocks{}, err
+	x509cert, tlsCert, err := kpm.ReadX509KeyPair(fin)
+	if err != nil {
+		return tls.Certificate{}, nil
 	}
 
-	return PEMBlocks{
-		Cert: pdata,
-		Priv: pem.EncodeToMemory(&pem.Block{Type: ctypes.PemBlkTypeECPrivateKey, Bytes: pkey}),
-		Pub: pem.EncodeToMemory(&pem.Block{
-			Type:  ctypes.PemBlkTypeECPublicKey,
-			Bytes: pubKey,
-		}),
-	}, nil
-}
-
-func LoadAndQueryPEMForAccount(ctx context.Context, cctx client.Context, keyring keyring.Keyring, opts ...LoaderOption) (PEMBlocks, error) {
-	pblk, err := LoadPEMForAccount(cctx, keyring, opts...)
-	if err != nil {
-		return PEMBlocks{}, err
+	// Check if valid according to time
+	if x509cert.NotBefore.After(time.Now().UTC()) {
+		return tls.Certificate{}, fmt.Errorf("%w: certificate is not yet active, start ts %s", certerrors.ErrCertificate, x509cert.NotBefore)
 	}
 
-	blk, _ := pem.Decode(pblk.Cert)
-	cert, err := x509.ParseCertificate(blk.Bytes)
-	if err != nil {
-		return PEMBlocks{}, err
+	if time.Now().UTC().After(x509cert.NotAfter) {
+		return tls.Certificate{}, fmt.Errorf("%w: certificate has been expired since %s", certerrors.ErrCertificate, x509cert.NotAfter)
 	}
 
 	params := &ctypes.QueryCertificatesRequest{
 		Filter: ctypes.CertificateFilter{
-			Owner:  cert.Subject.CommonName,
-			Serial: cert.SerialNumber.String(),
-			State:  "valid",
+			Owner:  x509cert.Subject.CommonName,
+			Serial: x509cert.SerialNumber.String(),
 		},
 	}
 
 	certs, err := ctypes.NewQueryClient(cctx).Certificates(ctx, params)
 	if err != nil {
-		return PEMBlocks{}, err
+		return tls.Certificate{}, nil
 	}
 
 	if len(certs.Certificates) == 0 {
-		return PEMBlocks{}, errors.Errorf("certificate has not been committed to blockchain")
+		return tls.Certificate{}, fmt.Errorf("%w: certificate has not been committed to blockchain", certerrors.ErrCertificate)
 	}
 
-	if cert.NotBefore.After(time.Now().UTC()) {
-		return PEMBlocks{}, errors.Errorf("certificate is not yet active, start ts %s", cert.NotBefore)
-	}
-
-	if time.Now().UTC().After(cert.NotAfter) {
-		return PEMBlocks{}, errors.Errorf("certificate has been expired")
-	}
-
-	return pblk, nil
-}
-
-// LoadAndQueryCertificateForAccount wraps LoadAndQueryPEMForAccount and tls.X509KeyPair
-func LoadAndQueryCertificateForAccount(ctx context.Context, cctx client.Context, keyring keyring.Keyring, opts ...LoaderOption) (tls.Certificate, error) {
-	pblk, err := LoadAndQueryPEMForAccount(ctx, cctx, keyring, opts...)
-	if err != nil {
-		return tls.Certificate{}, err
-	}
-
-	cert, err := tls.X509KeyPair(pblk.Cert, pblk.Priv)
-	if err != nil {
-		return tls.Certificate{}, err
+	foundCert := certs.Certificates[0]
+	if foundCert.GetCertificate().State != ctypes.CertificateValid {
+		return tls.Certificate{}, fmt.Errorf("%w: certificate is not valid", certerrors.ErrCertificate)
 	}
 
-	return cert, nil
+	return tlsCert, nil
 }
diff --git a/x/deployment/client/cli/tx.go b/x/deployment/client/cli/tx.go
index 66021a17..925065d1 100644
--- a/x/deployment/client/cli/tx.go
+++ b/x/deployment/client/cli/tx.go
@@ -53,7 +53,7 @@ func cmdCreate(key string) *cobra.Command {
 			}
 
 			// first lets validate certificate exists for given account
-			if _, err = cutils.LoadAndQueryCertificateForAccount(cmd.Context(), clientCtx, clientCtx.Keyring); err != nil {
+			if _, err = cutils.LoadAndQueryCertificateForAccount(cmd.Context(), clientCtx, nil); err != nil {
 				if os.IsNotExist(err) {
 					err = errors.Errorf("no certificate file found for account %q.\n"+
 						"consider creating it as certificate required to submit manifest", clientCtx.FromAddress.String())


Test patch:
diff --git a/integration/e2e_test.go b/integration/e2e_test.go
index 6a7c8fbe..c59969d9 100644
--- a/integration/e2e_test.go
+++ b/integration/e2e_test.go
@@ -204,11 +204,20 @@ func (s *IntegrationTestSuite) SetupSuite() {
 	s.Require().NoError(s.network.WaitForNextBlock())
 	clitestutil.ValidateTxSuccessful(s.T(), s.validator.ClientCtx, res.Bytes())
 
-	// Create provider's certificate
-	_, err = ccli.TxCreateServerExec(
+	// Generate provider's certificate
+	_, err = ccli.TxGenerateServerExec(
+		context.Background(),
 		s.validator.ClientCtx,
-		s.keyTenant.GetAddress(),
+		s.keyProvider.GetAddress(),
 		"localhost",
+	)
+	s.Require().NoError(err)
+
+	// Publish provider's certificate
+	_, err = ccli.TxPublishServerExec(
+		context.Background(),
+		s.validator.ClientCtx,
+		s.keyProvider.GetAddress(),
 		fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
 		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
 		fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
@@ -218,11 +227,23 @@ func (s *IntegrationTestSuite) SetupSuite() {
 	s.Require().NoError(s.network.WaitForNextBlock())
 	clitestutil.ValidateTxSuccessful(s.T(), s.validator.ClientCtx, res.Bytes())
 
-	// Create tenant's certificate
-	_, err = ccli.TxCreateServerExec(
+	// Generate tenant's certificate
+	_, err = ccli.TxGenerateClientExec(
+		context.Background(),
 		s.validator.ClientCtx,
-		s.keyProvider.GetAddress(),
-		"localhost",
+		s.keyTenant.GetAddress(),
+		fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
+		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
+		fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
+		fmt.Sprintf("--gas=%d", flags.DefaultGasLimit),
+	)
+	s.Require().NoError(err)
+
+	// Publish tenant's certificate
+	_, err = ccli.TxPublishClientExec(
+		context.Background(),
+		s.validator.ClientCtx,
+		s.keyTenant.GetAddress(),
 		fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
 		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
 		fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
diff --git a/provider/testutil/provider.go b/provider/testutil/provider.go
index 43611ce7..cced9b4b 100644
--- a/provider/testutil/provider.go
+++ b/provider/testutil/provider.go
@@ -75,7 +75,6 @@ func TestLeaseShell(ctx context.Context, clientCtx client.Context, extraArgs []s
 	args = append(args, extraArgs...)
 	args = append(args, serviceName)
 	args = append(args, cmd...)
-	fmt.Printf("%v\n", args)
 
 	takeCmdLock()
 	cobraCmd := pcmd.LeaseShellCmd()
diff --git a/testutil/cert.go b/testutil/cert.go
index b54e97f3..c87aa39f 100644
--- a/testutil/cert.go
+++ b/testutil/cert.go
@@ -8,7 +8,6 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	"encoding/asn1"
 	"encoding/pem"
 	"math/big"
 	"net"
@@ -20,9 +19,9 @@ import (
 
 	"github.com/ovrclk/akash/client/mocks"
 	types "github.com/ovrclk/akash/x/cert/types/v1beta2"
-)
 
-var AuthVersionOID = asn1.ObjectIdentifier{2, 23, 133, 2, 6}
+	certutils "github.com/ovrclk/akash/x/cert/utils"
+)
 
 type TestCertificate struct {
 	Cert   []tls.Certificate
@@ -103,7 +102,7 @@ func Certificate(t testing.TB, addr sdk.Address, opts ...CertificateOption) Test
 			CommonName: addr.String(),
 			ExtraNames: []pkix.AttributeTypeAndValue{
 				{
-					Type:  AuthVersionOID,
+					Type:  certutils.AuthVersionOID,
 					Value: "v0.0.1",
 				},
 			},
diff --git a/testutil/ids.go b/testutil/ids.go
index 01134afc..2246e26d 100644
--- a/testutil/ids.go
+++ b/testutil/ids.go
@@ -15,6 +15,7 @@ import (
 )
 
 func Keyring(t testing.TB) keyring.Keyring {
+	t.Helper()
 	obj := keyring.NewInMemory()
 	return obj
 }
@@ -27,6 +28,11 @@ func AccAddress(t testing.TB) sdk.AccAddress {
 	return sdk.AccAddress(privKey.PubKey().Address())
 }
 
+func Key(t testing.TB) ed25519.PrivKey {
+	t.Helper()
+	return ed25519.GenPrivKey()
+}
+
 func DeploymentID(t testing.TB) dtypes.DeploymentID {
 	t.Helper()
 	return dtypes.DeploymentID{
diff --git a/testutil/network_suite.go b/testutil/network_suite.go
new file mode 100644
index 00000000..b1bded25
--- /dev/null
+++ b/testutil/network_suite.go
@@ -0,0 +1,241 @@
+package testutil
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	sdkclient "github.com/cosmos/cosmos-sdk/client"
+	"github.com/cosmos/cosmos-sdk/client/tx"
+	"github.com/cosmos/cosmos-sdk/crypto/hd"
+	"github.com/cosmos/cosmos-sdk/crypto/keyring"
+	sdknetworktest "github.com/cosmos/cosmos-sdk/testutil/network"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	"github.com/cosmos/cosmos-sdk/types/tx/signing"
+	authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
+	cosmosauthtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
+	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
+	"github.com/gogo/protobuf/jsonpb"
+	"github.com/spf13/pflag"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	"reflect"
+	"strings"
+	"time"
+)
+
+type NetworkTestSuite struct {
+	suite.Suite
+	cfg     sdknetworktest.Config
+	network *sdknetworktest.Network
+	testIdx int
+
+	kr        keyring.Keyring
+	container interface{}
+
+	testCtx       context.Context
+	cancelTestCtx context.CancelFunc
+}
+
+func NewNetworkTestSuite(cfg *sdknetworktest.Config, container interface{}) NetworkTestSuite {
+	nts := NetworkTestSuite{
+		container: container,
+		testIdx:   -1,
+	}
+	if cfg == nil {
+		nts.cfg = DefaultConfig()
+		nts.cfg.NumValidators = 1
+	} else {
+		nts.cfg = *cfg
+	}
+
+	return nts
+}
+
+func (nts *NetworkTestSuite) countTests() int {
+	vof := reflect.TypeOf(nts.container)
+
+	cnt := 0
+	for i := 0; i != vof.NumMethod(); i++ {
+		method := vof.Method(i)
+		methodName := method.Name
+		if strings.HasPrefix(methodName, "Test") {
+			cnt++
+		}
+	}
+
+	return cnt
+}
+
+func (nts *NetworkTestSuite) TearDownSuite() {
+	nts.network.Cleanup()
+}
+
+func (nts *NetworkTestSuite) SetupSuite() {
+	nts.kr = Keyring(nts.T())
+	nts.network = sdknetworktest.New(nts.T(), nts.cfg)
+
+	_, err := nts.network.WaitForHeightWithTimeout(1, time.Second*30)
+	require.NoError(nts.T(), err)
+
+	walletCount := nts.countTests()
+	nts.T().Logf("setting up %d wallets for test", walletCount)
+	var msgs []sdk.Msg
+
+	for i := 0; i != walletCount; i++ {
+		name := fmt.Sprintf("wallet%d", i)
+		kinfo, str, err := nts.kr.NewMnemonic(name, keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
+		require.NoError(nts.T(), err)
+		require.NotEmpty(nts.T(), str)
+
+		toAddr := kinfo.GetAddress()
+
+		coins := sdk.NewCoins(sdk.NewCoin(nts.Config().BondDenom, sdk.NewInt(1000000)))
+		msg := banktypes.NewMsgSend(nts.Validator().Address, toAddr, coins)
+		msgs = append(msgs, msg)
+	}
+
+	txf := tx.NewFactoryCLI(nts.Context(), &pflag.FlagSet{})
+	txf = txf.WithSignMode(signing.SignMode_SIGN_MODE_DIRECT)
+	txf = txf.WithSimulateAndExecute(false)
+
+	require.Equal(nts.T(), "node0", nts.Context().GetFromName())
+	keyInfo, err := txf.Keybase().Key(nts.Context().GetFromName())
+	require.NoError(nts.T(), err)
+	require.NotNil(nts.T(), keyInfo)
+
+	num, seq, err := txf.AccountRetriever().GetAccountNumberSequence(nts.Context(), nts.Validator().Address)
+	require.NoError(nts.T(), err)
+	txf = txf.WithAccountNumber(num)
+	txf = txf.WithSequence(seq)
+	txf = txf.WithGas(uint64(150000 * nts.countTests()))                 // Just made this up
+	txf = txf.WithFees(fmt.Sprintf("%d%s", 100, nts.Config().BondDenom)) // Just made this up
+
+	txb, err := tx.BuildUnsignedTx(txf, msgs...)
+	require.NoError(nts.T(), err)
+
+	txb.SetFeeGranter(nts.Context().GetFeeGranterAddress())
+
+	require.NoError(nts.T(), tx.Sign(txf, nts.Context().GetFromName(), txb, true))
+	txBytes, err := nts.Context().TxConfig.TxEncoder()(txb.GetTx())
+	require.NoError(nts.T(), err)
+
+	txr, err := nts.Context().BroadcastTxSync(txBytes)
+	require.NoError(nts.T(), err)
+	require.Equal(nts.T(), uint32(0), txr.Code)
+
+	lctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	for lctx.Err() == nil {
+		// check the TX
+		txStatus, err := authtx.QueryTx(nts.Context(), txr.TxHash)
+		if err != nil {
+			if strings.Contains(err.Error(), ") not found") {
+				continue
+			}
+		}
+		require.NoError(nts.T(), err)
+		require.NotNil(nts.T(), txStatus)
+		require.Equalf(nts.T(), uint32(0), txStatus.Code, "tx status is %v", txStatus)
+		break
+	}
+	require.NoError(nts.T(), lctx.Err())
+
+}
+
+func (nts *NetworkTestSuite) Validator(idxT ...int) *sdknetworktest.Validator {
+	idx := 0
+	if len(idxT) != 0 {
+		if len(idxT) > 1 {
+			nts.T().Fatal("pass zero or one arguments to Validator()")
+		}
+		idx = idxT[0]
+
+		if idx > len(nts.network.Validators) {
+			nts.T().Fatal("not enough validators for each test")
+		}
+	}
+	return nts.network.Validators[idx]
+}
+
+func (nts *NetworkTestSuite) WalletNameForTest() string {
+	return fmt.Sprintf("wallet%d", nts.testIdx)
+}
+
+func (nts *NetworkTestSuite) WalletForTest() sdk.AccAddress {
+	k, err := nts.kr.Key(nts.WalletNameForTest())
+	require.NoError(nts.T(), err)
+	return k.GetAddress()
+}
+
+func (nts *NetworkTestSuite) ContextForTest() sdkclient.Context {
+	result := nts.Context()
+	k, err := nts.kr.Key(nts.WalletNameForTest())
+	require.NoError(nts.T(), err)
+	return result.WithKeyring(nts.kr).WithFromAddress(k.GetAddress()).WithFromName(nts.WalletNameForTest())
+}
+
+func (nts *NetworkTestSuite) GoContextForTest() context.Context {
+	return nts.testCtx
+}
+
+func (nts *NetworkTestSuite) Network() *sdknetworktest.Network {
+	return nts.network
+}
+
+func (nts *NetworkTestSuite) Context(idxT ...int) sdkclient.Context {
+	validator := nts.Validator()
+	idx := 0
+	if len(idxT) != 0 {
+		idx = idxT[0]
+	}
+	result := validator.ClientCtx
+
+	return result.WithFromAddress(validator.Address).WithFromName(fmt.Sprintf("node%d", idx))
+}
+
+func (nts *NetworkTestSuite) Config() sdknetworktest.Config {
+	return nts.cfg
+}
+
+func (nts *NetworkTestSuite) SetupTest() {
+	nts.testIdx++
+	nts.testCtx, nts.cancelTestCtx = context.WithTimeout(context.Background(), 30*time.Second)
+}
+
+func (nts *NetworkTestSuite) TearDownTest() {
+	nts.cancelTestCtx()
+}
+
+func (nts *NetworkTestSuite) ValidateTx(resultData []byte) string {
+	nts.T().Helper()
+
+	var resp sdk.TxResponse
+
+	err := jsonpb.Unmarshal(bytes.NewBuffer(resultData), &resp)
+	require.NoError(nts.T(), err, "failed trying to unmarshal JSON transaction result")
+
+	for {
+		res, err := cosmosauthtx.QueryTx(nts.ContextForTest(), resp.TxHash)
+		if err != nil {
+			ctxDone := nts.GoContextForTest().Err() != nil
+			if ctxDone {
+				require.NoErrorf(nts.T(), err, "failed querying for transaction %q", resp.TxHash)
+			} else {
+				nts.T().Logf("waiting before checking for TX %s", resp.TxHash)
+				select {
+				case <-nts.GoContextForTest().Done():
+					require.NoError(nts.T(), nts.GoContextForTest().Err())
+				case <-time.After(500 * time.Millisecond):
+
+				}
+			}
+			continue
+		}
+
+		require.Zero(nts.T(), res.Code, res, "expected response code in transaction to be zero")
+		break
+	}
+
+	return resp.TxHash
+}
diff --git a/x/cert/client/cli/test_helpers.go b/x/cert/client/cli/test_helpers.go
index edee06b3..c1892330 100644
--- a/x/cert/client/cli/test_helpers.go
+++ b/x/cert/client/cli/test_helpers.go
@@ -1,32 +1,71 @@
 package cli
 
 import (
+	"context"
 	"fmt"
-
 	"github.com/cosmos/cosmos-sdk/client"
 	sdktest "github.com/cosmos/cosmos-sdk/testutil"
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
+	testutilcli "github.com/ovrclk/akash/testutil/cli"
 )
 
 // TxCreateServerExec is used for testing create server certificate tx
-func TxCreateServerExec(clientCtx client.Context, from fmt.Stringer, host string, extraArgs ...string) (sdktest.BufferWriter, error) {
+func TxGenerateServerExec(ctx context.Context, clientCtx client.Context, from fmt.Stringer, host string, extraArgs ...string) (sdktest.BufferWriter, error) {
+	var args []string
+
+	if len(host) != 0 { // for testing purposes, of passing no arguments
+		args = []string{host}
+	}
+	args = append(args, fmt.Sprintf("--from=%s", from.String()))
+	args = append(args, extraArgs...)
+	return testutilcli.ExecTestCLICmd(ctx, clientCtx, cmdGenerateServer(), args...)
+}
+
+// TxCreateClientExec is used for testing create client certificate tx
+func TxGenerateClientExec(ctx context.Context, clientCtx client.Context, from fmt.Stringer, extraArgs ...string) (sdktest.BufferWriter, error) {
 	args := []string{
-		host,
 		fmt.Sprintf("--from=%s", from.String()),
 	}
 
 	args = append(args, extraArgs...)
+	return testutilcli.ExecTestCLICmd(ctx, clientCtx, cmdGenerateClient(), args...)
+}
 
-	return clitestutil.ExecTestCLICmd(clientCtx, cmdCreateServer(), args)
+// TxCreateServerExec is used for testing create server certificate tx
+func TxPublishServerExec(ctx context.Context, clientCtx client.Context, from fmt.Stringer, extraArgs ...string) (sdktest.BufferWriter, error) {
+	args := []string{
+		fmt.Sprintf("--from=%s", from.String()),
+	}
+
+	args = append(args, extraArgs...)
+	return testutilcli.ExecTestCLICmd(ctx, clientCtx, cmdPublishServer(), args...)
 }
 
 // TxCreateClientExec is used for testing create client certificate tx
-func TxCreateClientExec(clientCtx client.Context, from fmt.Stringer, extraArgs ...string) (sdktest.BufferWriter, error) {
+func TxPublishClientExec(ctx context.Context, clientCtx client.Context, from fmt.Stringer, extraArgs ...string) (sdktest.BufferWriter, error) {
 	args := []string{
 		fmt.Sprintf("--from=%s", from.String()),
 	}
 
 	args = append(args, extraArgs...)
+	return testutilcli.ExecTestCLICmd(ctx, clientCtx, cmdPublishClient(), args...)
+}
 
-	return clitestutil.ExecTestCLICmd(clientCtx, cmdCreateClient(), args)
+// TxCreateServerExec is used for testing create server certificate tx
+func TxRevokeServerExec(ctx context.Context, clientCtx client.Context, from fmt.Stringer, extraArgs ...string) (sdktest.BufferWriter, error) {
+	args := []string{
+		fmt.Sprintf("--from=%s", from.String()),
+	}
+
+	args = append(args, extraArgs...)
+	return testutilcli.ExecTestCLICmd(ctx, clientCtx, cmdRevokeServer(), args...)
+}
+
+// TxCreateClientExec is used for testing create client certificate tx
+func TxRevokeClientExec(ctx context.Context, clientCtx client.Context, from fmt.Stringer, extraArgs ...string) (sdktest.BufferWriter, error) {
+	args := []string{
+		fmt.Sprintf("--from=%s", from.String()),
+	}
+
+	args = append(args, extraArgs...)
+	return testutilcli.ExecTestCLICmd(ctx, clientCtx, cmdRevokeClient(), args...)
 }
diff --git a/x/cert/client/cli/tx_test.go b/x/cert/client/cli/tx_test.go
new file mode 100644
index 00000000..a20073f8
--- /dev/null
+++ b/x/cert/client/cli/tx_test.go
@@ -0,0 +1,131 @@
+package cli_test
+
+import (
+	"fmt"
+	"github.com/ovrclk/akash/testutil"
+	"github.com/ovrclk/akash/x/cert/client/cli"
+
+	certerrors "github.com/ovrclk/akash/x/cert/errors"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	"testing"
+)
+
+const testHost = "foobar.dev"
+
+type certificateCLISuite struct {
+	testutil.NetworkTestSuite
+}
+
+func (s *certificateCLISuite) TestGeneratePublishAndRevokeServer() {
+	result, err := cli.TxGenerateServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(), testHost)
+	require.NoError(s.T(), err)
+	require.NotNil(s.T(), result)
+
+	result, err = cli.TxPublishServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(),
+		fmt.Sprintf("--fees=%d%s", 1000, s.Config().BondDenom),
+		"--yes")
+	require.NoError(s.T(), err)
+	require.NoError(s.T(), s.Network().WaitForNextBlock())
+	_ = s.ValidateTx(result.Bytes())
+
+	result, err = cli.TxRevokeServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(),
+		fmt.Sprintf("--fees=%d%s", 1000, s.Config().BondDenom),
+		"--yes")
+
+	require.NoError(s.T(), err)
+	require.NoError(s.T(), s.Network().WaitForNextBlock())
+	_ = s.ValidateTx(result.Bytes())
+}
+
+func (s *certificateCLISuite) TestGenerateServerRequiresArguments() {
+	_, err := cli.TxGenerateServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(), "")
+	require.Error(s.T(), err)
+	require.Contains(s.T(), err.Error(), "requires at least 1 arg(s), only received 0")
+}
+
+func (s *certificateCLISuite) TestGenerateServerAllowsManyArguments() {
+	_, err := cli.TxGenerateServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(), "a.dev", "b.dev")
+	require.NoError(s.T(), err)
+}
+
+func (s *certificateCLISuite) TestGenerateClientRejectsArguments() {
+	_, err := cli.TxGenerateClientExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(), testHost)
+	require.Error(s.T(), err)
+	require.Contains(s.T(), err.Error(), "accepts 0 arg(s), received 1")
+}
+
+func (s *certificateCLISuite) TestGeneratePublishAndRevokeClient() {
+	result, err := cli.TxGenerateClientExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest())
+	require.NoError(s.T(), err)
+	require.NotNil(s.T(), result)
+
+	result, err = cli.TxPublishClientExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(),
+		fmt.Sprintf("--fees=%d%s", 1000, s.Config().BondDenom),
+		"--yes")
+	require.NoError(s.T(), err)
+	require.NoError(s.T(), s.Network().WaitForNextBlock())
+	_ = s.ValidateTx(result.Bytes())
+
+	result, err = cli.TxRevokeClientExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(),
+		fmt.Sprintf("--fees=%d%s", 1000, s.Config().BondDenom),
+		"--yes")
+
+	require.NoError(s.T(), err)
+	require.NoError(s.T(), s.Network().WaitForNextBlock())
+	_ = s.ValidateTx(result.Bytes())
+}
+
+func (s *certificateCLISuite) TestGenerateAndRevokeFailsServer() {
+	result, err := cli.TxGenerateServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(), testHost)
+	require.NoError(s.T(), err)
+	require.NotNil(s.T(), result)
+
+	_, err = cli.TxRevokeServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(),
+		fmt.Sprintf("--fees=%d%s", 1000, s.Config().BondDenom),
+		"--yes")
+	require.ErrorIs(s.T(), err, certerrors.ErrCertificate)
+	require.Contains(s.T(), err.Error(), "does not exist on chain")
+}
+
+func (s *certificateCLISuite) TestRevokeFailsServer() {
+	_, err := cli.TxRevokeServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(),
+		fmt.Sprintf("--fees=%d%s", 1000, s.Config().BondDenom),
+		"--yes",
+		"--serial=1")
+	require.ErrorIs(s.T(), err, certerrors.ErrCertificate)
+	require.Contains(s.T(), err.Error(), "serial 1 does not exist on chain")
+}
+
+func (s *certificateCLISuite) TestRevokeFailsClient() {
+	_, err := cli.TxRevokeClientExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(),
+		fmt.Sprintf("--fees=%d%s", 1000, s.Config().BondDenom),
+		"--yes",
+		"--serial=1")
+	require.ErrorIs(s.T(), err, certerrors.ErrCertificate)
+	require.Contains(s.T(), err.Error(), "serial 1 does not exist on chain")
+}
+
+func (s *certificateCLISuite) TestGenerateServerNoOverwrite() {
+	result, err := cli.TxGenerateServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(), testHost)
+	require.NoError(s.T(), err)
+	require.NotNil(s.T(), result)
+
+	_, err = cli.TxGenerateServerExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest(), testHost)
+	require.ErrorIs(s.T(), err, certerrors.ErrCertificate)
+	require.Contains(s.T(), err.Error(), "cannot overwrite")
+}
+
+func (s *certificateCLISuite) TestGenerateClientNoOverwrite() {
+	result, err := cli.TxGenerateClientExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest())
+	require.NoError(s.T(), err)
+	require.NotNil(s.T(), result)
+
+	_, err = cli.TxGenerateClientExec(s.GoContextForTest(), s.ContextForTest(), s.WalletForTest())
+	require.ErrorIs(s.T(), err, certerrors.ErrCertificate)
+	require.Contains(s.T(), err.Error(), "cannot overwrite")
+}
+
+func TestCertificateCLI(t *testing.T) {
+	suite.Run(t, &certificateCLISuite{NetworkTestSuite: testutil.NewNetworkTestSuite(nil, &certificateCLISuite{})})
+}
diff --git a/x/deployment/client/cli/cli_test.go b/x/deployment/client/cli/cli_test.go
index e20cdb11..39f39473 100644
--- a/x/deployment/client/cli/cli_test.go
+++ b/x/deployment/client/cli/cli_test.go
@@ -1,6 +1,7 @@
 package cli_test
 
 import (
+	"context"
 	"fmt"
 	"path/filepath"
 	"testing"
@@ -68,7 +69,16 @@ func (s *IntegrationTestSuite) SetupSuite() {
 	clitestutil.ValidateTxSuccessful(s.T(), val.ClientCtx, res.Bytes())
 
 	// Create client certificate
-	_, err = ccli.TxCreateClientExec(
+	_, err = ccli.TxGenerateClientExec(
+		context.Background(),
+		val.ClientCtx,
+		val.Address,
+	)
+	s.Require().NoError(err)
+
+	// Publish client certificate
+	_, err = ccli.TxPublishClientExec(
+		context.Background(),
 		val.ClientCtx,
 		val.Address,
 		fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
diff --git a/x/deployment/client/cli/grpc_rest_test.go b/x/deployment/client/cli/grpc_rest_test.go
index ad47eb78..d5c496da 100644
--- a/x/deployment/client/cli/grpc_rest_test.go
+++ b/x/deployment/client/cli/grpc_rest_test.go
@@ -1,6 +1,7 @@
 package cli_test
 
 import (
+	"context"
 	"fmt"
 	"path/filepath"
 	"testing"
@@ -43,8 +44,18 @@ func (s *GRPCRestTestSuite) SetupSuite() {
 	deploymentPath, err := filepath.Abs("../../testdata/deployment.yaml")
 	s.Require().NoError(err)
 
-	// Create client certificate
-	_, err = ccli.TxCreateClientExec(
+	// Generate client certificate
+	_, err = ccli.TxGenerateClientExec(
+		context.Background(),
+		val.ClientCtx,
+		val.Address,
+	)
+	s.Require().NoError(err)
+	s.Require().NoError(s.network.WaitForNextBlock())
+
+	// Publish client certificate
+	_, err = ccli.TxPublishClientExec(
+		context.Background(),
 		val.ClientCtx,
 		val.Address,
 		fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
diff --git a/x/market/client/cli/cli_test.go b/x/market/client/cli/cli_test.go
index 4da067d8..22be33ba 100644
--- a/x/market/client/cli/cli_test.go
+++ b/x/market/client/cli/cli_test.go
@@ -1,6 +1,7 @@
 package cli_test
 
 import (
+	"context"
 	"fmt"
 	"path/filepath"
 	"testing"
@@ -51,8 +52,17 @@ func (s *IntegrationTestSuite) SetupSuite() {
 
 	val := s.network.Validators[0]
 
-	// Create client certificate
-	_, err = ccli.TxCreateClientExec(
+	// Generate client certificate
+	_, err = ccli.TxGenerateClientExec(
+		context.Background(),
+		val.ClientCtx,
+		val.Address,
+	)
+	s.Require().NoError(err)
+
+	// Publish client certificate
+	_, err = ccli.TxPublishClientExec(
+		context.Background(),
 		val.ClientCtx,
 		val.Address,
 		fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
diff --git a/x/market/client/cli/grpc_rest_test.go b/x/market/client/cli/grpc_rest_test.go
index 4bd4d255..3f359557 100644
--- a/x/market/client/cli/grpc_rest_test.go
+++ b/x/market/client/cli/grpc_rest_test.go
@@ -1,6 +1,7 @@
 package cli_test
 
 import (
+	"context"
 	"fmt"
 	"path/filepath"
 	"testing"
@@ -52,8 +53,17 @@ func (s *GRPCRestTestSuite) SetupSuite() {
 
 	val := s.network.Validators[0]
 
-	// Create client certificate
-	_, err = ccli.TxCreateClientExec(
+	// Generate client certificate
+	_, err = ccli.TxGenerateClientExec(
+		context.Background(),
+		val.ClientCtx,
+		val.Address,
+	)
+	s.Require().NoError(err)
+
+	// Publish client certificate
+	_, err = ccli.TxPublishClientExec(
+		context.Background(),
 		val.ClientCtx,
 		val.Address,
 		fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),