github.com/wostzone/hub/auth@v0.0.0-20220118060317-7bb375743b17/cmd/auth/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"crypto/ecdsa"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/docopt/docopt-go"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/wostzone/hub/auth/pkg/aclstore"
    16  	"github.com/wostzone/hub/auth/pkg/authenticate"
    17  	"github.com/wostzone/hub/auth/pkg/authorize"
    18  	"github.com/wostzone/hub/auth/pkg/unpwstore"
    19  	"github.com/wostzone/hub/lib/client/pkg/certs"
    20  	"github.com/wostzone/hub/lib/client/pkg/config"
    21  	"github.com/wostzone/hub/lib/client/pkg/signing"
    22  	"github.com/wostzone/hub/lib/serve/pkg/certsetup"
    23  	"github.com/wostzone/hub/lib/serve/pkg/hubnet"
    24  )
    25  
    26  //// Commandline commands
    27  //const (
    28  //	CmdCertBundle = "certbundle"
    29  //	CmdClientcert = "clientcert"
    30  //	CmdSetPasswd  = "setpasswd"
    31  //	CmdSetRole    = "setrole"
    32  //)
    33  const Version = `0.3-alpha`
    34  
    35  func main() {
    36  	binFolder := path.Dir(os.Args[0])
    37  	homeFolder := path.Dir(binFolder)
    38  	ParseArgs(homeFolder, os.Args[1:])
    39  }
    40  
    41  // ParseArgs to handle commandline arguments
    42  func ParseArgs(homeFolder string, args []string) {
    43  	// var err error
    44  	configFolder := path.Join(homeFolder, "config")
    45  	certsFolder := path.Join(homeFolder, "certs")
    46  	// configFolder := path.Join(homeFolder, "config")
    47  	// ouRole := certsetup.OUClient
    48  	// genKeys := false
    49  	ifName, mac, ip := hubnet.GetOutboundInterface("")
    50  	_ = ifName
    51  	_ = mac
    52  	sanName := ip.String()
    53  	var optConf struct {
    54  		// commands
    55  		Certbundle  bool
    56  		Clientcert  bool
    57  		Devicecert  bool
    58  		Setpassword bool
    59  		Setrole     bool
    60  		// arguments
    61  		Loginid  string
    62  		Deviceid string
    63  		Groupid  string
    64  		Role     string
    65  		// options
    66  		Aclfile string
    67  		Config  string
    68  		Certs   string
    69  		Hostname  string
    70  		Output  string
    71  		Pubkey  string
    72  		Iter    int
    73  		Verbose bool
    74  	}
    75  	usage := `
    76  Usage:
    77    auth certbundle [-v --certs=CertFolder] [--hostname=hostname]
    78    auth clientcert [-v --certs=CertFolder --pubkey=pubkeyfile] <loginID> 
    79    auth devicecert [-v --certs=CertFolder --pubkey=pubkeyfile] <deviceID> 
    80    auth setpassword [-v -c configFolder] [-i iterations] <loginID> 
    81    auth setrole [-v -c configFolder --aclfile=aclfile] <loginID> <groupID> <role>
    82    auth --help | --version
    83  
    84  Commands:
    85    certbundle   Generate or refresh the Hub certificate bundle, optionally provide a subject hostname or ip
    86    clientcert   Generate a signed client certificate, with pub/private keys if not given
    87    devicecert   Generate a signed device certificate, with pub/private keys if not given
    88    setpassword  Set user password
    89    setrole      Set user role in group
    90  
    91  Arguments:
    92    loginID      used as the certificate CN, login name and certificate filename (loginID-cert.pem)
    93    groupID      group for access control 
    94    role         one of viewer, editor, manager, thing, or none to delete
    95  
    96  Options:
    97    --aclfile=AclFile              use a different acl file instead of the default config/` + aclstore.DefaultAclFile + `
    98    -e --certs=CertFolder      location of Hub certificates [default: ` + certsFolder + `]
    99    -c --config=ConfigFolder   location of Hub config folder [default: ` + configFolder + `]
   100    -o --hostname=Hostname     name or IP address to use on the certificate
   101    -p --pubkey=PubKeyfile     use this public key file to generate certificate, instead of a new key pair
   102  	-i --iter=iterations       Number of iterations for generating password [default: 10]
   103    -h --help                  show this help
   104  	-v --verbose               show info logging
   105    --version                  show app version
   106  `
   107  	opts, err := docopt.ParseArgs(usage, args, Version)
   108  	if err != nil {
   109  		fmt.Printf("Parse Error: %s\n", err)
   110  		os.Exit(1)
   111  	}
   112  
   113  	err = opts.Bind(&optConf)
   114  
   115  	if optConf.Verbose {
   116  		logrus.SetLevel(logrus.InfoLevel)
   117  	} else {
   118  		logrus.SetLevel(logrus.WarnLevel)
   119  	}
   120  
   121  	if err != nil {
   122  		fmt.Printf("Bind Error: %s\n", err)
   123  		os.Exit(1)
   124  	}
   125  	_ = opts
   126  	if optConf.Certbundle {
   127  		if optConf.Hostname != "" {
   128  			sanName = optConf.Hostname
   129  		}
   130  		fmt.Printf("Generating certificate Bundle. Certfolder=%s. names=%s\n", optConf.Certs, sanName)
   131  		err = HandleCreateCertbundle(optConf.Certs, sanName)
   132  	} else if optConf.Clientcert {
   133  		fmt.Printf("Generating Client certificate using CA from %s\n", optConf.Certs)
   134  		err = HandleCreateClientCert(optConf.Certs, optConf.Loginid, optConf.Pubkey)
   135  	} else if optConf.Devicecert {
   136  		fmt.Printf("Generating Thing device certificate using CA from %s\n", optConf.Certs)
   137  		err = HandleCreateDeviceCert(optConf.Certs, optConf.Deviceid, optConf.Pubkey)
   138  	} else if optConf.Setpassword {
   139  		fmt.Printf("Set user password\n")
   140  		err = HandleSetPasswd(optConf.Config, optConf.Loginid, optConf.Iter)
   141  	} else if optConf.Setrole {
   142  		fmt.Printf("Set user role in group\n")
   143  		err = HandleSetRole(optConf.Config, optConf.Loginid, optConf.Groupid, optConf.Role, optConf.Aclfile)
   144  	} else {
   145  		err = fmt.Errorf("invalid command")
   146  	}
   147  	if err != nil {
   148  		fmt.Printf("Error: %s\n", err)
   149  		os.Exit(1)
   150  	}
   151  }
   152  
   153  // CreateKeyPair generate a key pair in PEM format and save it to the cert folder
   154  // as <clientID>-pub.pem and <clientID>-key.pem
   155  // Returns the public key PEM content
   156  func CreateKeyPair(clientID string, certFolder string) (privKey *ecdsa.PrivateKey, err error) {
   157  	privKey = signing.CreateECDSAKeys()
   158  	privKeyFile := path.Join(certFolder, clientID+"-priv.pem")
   159  	pubKeyFile := path.Join(certFolder, clientID+"-pub.pem")
   160  	err = certs.SaveKeysToPEM(privKey, privKeyFile)
   161  	if err == nil {
   162  		pubKeyPem, _ := certs.PublicKeyToPEM(&privKey.PublicKey)
   163  		err = ioutil.WriteFile(pubKeyFile, []byte(pubKeyPem), 0644)
   164  	}
   165  	if err != nil {
   166  		fmt.Printf("Failed saving keys: %s\n", err)
   167  	}
   168  	if err == nil {
   169  		fmt.Printf("Generated public and private key pair as: %s and %s\n", pubKeyFile, privKeyFile)
   170  	}
   171  	return privKey, err
   172  }
   173  
   174  // HandleSetPasswd sets the login name and password for a consumer
   175  func HandleSetPasswd(configFolder string, username string, iterations int) error {
   176  	var pwHash string
   177  	var err error
   178  	var passwd string
   179  	reader := bufio.NewReader(os.Stdin)
   180  	unpwFilePath := path.Join(configFolder, unpwstore.DefaultPasswordFile)
   181  	unpwStore := unpwstore.NewPasswordFileStore(unpwFilePath, "auth.main.HandleSetPasswd")
   182  	err = unpwStore.Open()
   183  	if err == nil {
   184  		fmt.Printf("\nNew Password: ")
   185  		passwd, err = reader.ReadString('\n')
   186  		passwd = strings.Replace(passwd, "\n", "", -1)
   187  		if err != nil {
   188  			return err
   189  		}
   190  		if passwd == "" {
   191  			return fmt.Errorf("missing password")
   192  		}
   193  		// pwHash, err = authen.CreatePasswordHash(passwd, authen.PWHASH_ARGON2id, uint(iterations))
   194  		pwHash, err = authenticate.CreatePasswordHash(passwd, authenticate.PWHASH_ARGON2id, uint(iterations))
   195  	}
   196  	if err == nil {
   197  		err = unpwStore.SetPasswordHash(username, pwHash)
   198  	}
   199  	if err == nil {
   200  		unpwStore.Close()
   201  		fmt.Printf("Password updated for user %s\n", username)
   202  	}
   203  	return err
   204  }
   205  
   206  // HandleCreateCertbundle generates the hub certificate bundle CA, Hub and Plugin keys
   207  // and certificates.
   208  //  If the CA certificate already exist it is NOT updated
   209  //  If the Hub and Plugin certificates already exist, they are renewed
   210  func HandleCreateCertbundle(certsFolder string, sanName string) error {
   211  	err := certsetup.CreateCertificateBundle([]string{sanName}, certsFolder)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	fmt.Printf("Server and Plugin certificates generated in %s\n", certsFolder)
   216  	return nil
   217  }
   218  
   219  // HandleCreateClientCert creates a consumer client certificate and optionally private/public keypair
   220  //  certFolder where to find the CA certificate and key used to sign the client certificate
   221  //  clientID for the CN of the client certificate. Used to identify the consumer.
   222  //  pubKeyFile with path to the client's public key of the certificate
   223  func HandleCreateClientCert(certFolder string, clientID string, pubKeyFile string) error {
   224  	var pubKey *ecdsa.PublicKey
   225  	ou := certsetup.OUClient
   226  	pemPath := path.Join(certFolder, config.DefaultCaCertFile)
   227  	caCert, err := certs.LoadX509CertFromPEM(pemPath)
   228  	if err != nil {
   229  		return err
   230  	}
   231  	pemPath = path.Join(certFolder, config.DefaultCaKeyFile)
   232  	caKey, err := certs.LoadKeysFromPEM(pemPath)
   233  	if err != nil {
   234  		return err
   235  	}
   236  	// If a public key file is given, use it, otherwise generate a pair
   237  	if pubKeyFile != "" {
   238  		fmt.Printf("Using public key file: %s\n", pubKeyFile)
   239  		pubKey, err = certs.LoadPublicKeyFromPEM(pubKeyFile)
   240  		if err != nil {
   241  			return err
   242  		}
   243  	} else {
   244  		fmt.Printf("No public key file was provided. Creating a key pair ") // no newline
   245  		privKey, err := CreateKeyPair(clientID, "")
   246  		pubKey = &privKey.PublicKey
   247  		if err != nil {
   248  			return err
   249  		}
   250  	}
   251  	durationDays := certsetup.DefaultCertDurationDays
   252  	cert, err := certsetup.CreateHubClientCert(
   253  		clientID, ou, pubKey, caCert, caKey, time.Now(), durationDays)
   254  	if err != nil {
   255  		return err
   256  	}
   257  	pemPath = path.Join(".", clientID+"-cert.pem")
   258  	certs.SaveX509CertToPEM(cert, pemPath)
   259  
   260  	fmt.Printf("Client certificate saved at %s\n", pemPath)
   261  	return nil
   262  }
   263  
   264  // HandleCreateDeviceCert creates a device client certificate for a device and save it in the certFolder
   265  // This is similar to creating a consumer certificate
   266  func HandleCreateDeviceCert(certFolder string, deviceID string, pubKeyFile string) error {
   267  	const deviceCertValidityDays = 30
   268  	var pubKey *ecdsa.PublicKey
   269  	pemPath := path.Join(certFolder, config.DefaultCaCertFile)
   270  	caCert, err := certs.LoadX509CertFromPEM(pemPath)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	pemPath = path.Join(certFolder, config.DefaultCaKeyFile)
   275  	caKey, err := certs.LoadKeysFromPEM(pemPath)
   276  	if err != nil {
   277  		return err
   278  	}
   279  	// If a public key file is given, use it, otherwise generate a pair
   280  	if pubKeyFile != "" {
   281  		fmt.Printf("Using public key file: %s\n", pubKeyFile)
   282  		pubKey, err = certs.LoadPublicKeyFromPEM(pubKeyFile)
   283  		if err != nil {
   284  			return err
   285  		}
   286  	} else {
   287  		fmt.Printf("No public key file was provided. Creating a new key pair ") // no newline
   288  		privKey, err := CreateKeyPair(deviceID, "")
   289  		pubKey = &privKey.PublicKey
   290  		if err != nil {
   291  			return err
   292  		}
   293  	}
   294  
   295  	certPEM, err := certsetup.CreateHubClientCert(
   296  		deviceID, certsetup.OUIoTDevice, pubKey,
   297  		caCert, caKey,
   298  		time.Now(), deviceCertValidityDays)
   299  	if err != nil {
   300  		return err
   301  	}
   302  	// save the new certificate
   303  	pemPath = path.Join(".", deviceID+"-cert.pem")
   304  	err = certs.SaveX509CertToPEM(certPEM, pemPath)
   305  
   306  	fmt.Printf("Device certificate saved at %s\n", pemPath)
   307  	return err
   308  }
   309  
   310  // HandleSetRole sets the role of a client in a group.
   311  func HandleSetRole(configFolder string, clientID string, groupID string, role string, aclFile string) error {
   312  	if role != authorize.GroupRoleEditor && role != authorize.GroupRoleViewer &&
   313  		role != authorize.GroupRoleManager && role != authorize.GroupRoleThing && role != authorize.GroupRoleNone {
   314  		err := fmt.Errorf("invalid role '%s'", role)
   315  		return err
   316  	}
   317  	aclFilePath := path.Join(configFolder, aclstore.DefaultAclFile)
   318  	if aclFile != "" {
   319  		// option to specify an acl file wrt home
   320  		aclFilePath = path.Join(path.Dir(configFolder), aclFile)
   321  	}
   322  	aclStore := aclstore.NewAclFileStore(aclFilePath, "author.main.HandleSetRole")
   323  	err := aclStore.Open()
   324  	if err == nil {
   325  		err = aclStore.SetRole(clientID, groupID, role)
   326  	}
   327  	if err == nil {
   328  		fmt.Printf("Client '%s' role set to '%s' for group '%s'\n", clientID, role, groupID)
   329  	}
   330  	aclStore.Close()
   331  	return err
   332  }