diff --git a/ziti/cmd/ziti/cmd/edge_controller/verify.go b/ziti/cmd/ziti/cmd/edge_controller/verify.go new file mode 100644 index 000000000..89d6d09bd --- /dev/null +++ b/ziti/cmd/ziti/cmd/edge_controller/verify.go @@ -0,0 +1,48 @@ +/* + Copyright NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package edge_controller + +import ( + "io" + + cmdutil "github.com/netfoundry/ziti-cmd/ziti/cmd/ziti/cmd/factory" + cmdhelper "github.com/netfoundry/ziti-cmd/ziti/cmd/ziti/cmd/helpers" + "github.com/netfoundry/ziti-cmd/ziti/cmd/ziti/util" + "github.com/spf13/cobra" +) + +// newVerifyCmd creates a command object for the "controller verify" command +func newVerifyCmd(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "verify", + Short: "verifies various entities managed by the Ziti Edge Controller", + Long: "Verifies various entities managed by the Ziti Edge Controller", + Run: func(cmd *cobra.Command, args []string) { + err := cmd.Help() + cmdhelper.CheckErr(err) + }, + } + + cmd.AddCommand(newVerifyCaCmd(f, out, errOut)) + + return cmd +} + +// createEntityOfType create an entity of the given type on the Ziti Edge Controller +func verifyEntityOfType(entityType, body, id string, options *commonOptions) error { + return util.EdgeControllerVerify(entityType, id, body, options.Out, options.OutputJSONResponse) +} diff --git a/ziti/cmd/ziti/cmd/edge_controller/verify_ca.go b/ziti/cmd/ziti/cmd/edge_controller/verify_ca.go new file mode 100644 index 000000000..33b364b14 --- /dev/null +++ b/ziti/cmd/ziti/cmd/edge_controller/verify_ca.go @@ -0,0 +1,258 @@ +/* + Copyright NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package edge_controller + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "github.com/netfoundry/ziti-cmd/ziti/cmd/ziti/cmd/common" + cmdutil "github.com/netfoundry/ziti-cmd/ziti/cmd/ziti/cmd/factory" + cmdhelper "github.com/netfoundry/ziti-cmd/ziti/cmd/ziti/cmd/helpers" + "github.com/netfoundry/ziti-cmd/ziti/cmd/ziti/util" + nfpem "github.com/netfoundry/ziti-foundation/util/pem" + "github.com/spf13/cobra" + "gopkg.in/resty.v1" + "io" + "io/ioutil" + "math/big" + "time" +) + +type verifyCaOptions struct { + commonOptions + + certPath string + certPemBytes []byte + + caNameOrId string + caId string + + caCertPath string + caCertPemBytes []byte + + caKeyPath string + caKeyPemBytes []byte + + isGenerateCert bool +} + +// newVerifyCaCmd creates the 'edge controller verify ca' command for the given entity type +func newVerifyCaCmd(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Command { + options := &verifyCaOptions{ + commonOptions: commonOptions{ + CommonOptions: common.CommonOptions{ + Factory: f, + Out: out, + Err: errOut, + }, + }, + } + + cmd := &cobra.Command{ + Use: "ca [ --cert | --cacert --cakey ]", + Short: "verifies a ca managed by the Ziti Edge Controller", + Long: "verifies a ca managed by the Ziti Edge Controller. If --cert is supplied, it is expected that it is a certificate with the " + + "common name set to the proper verificationToken value from the target CA. If not set, --cakey and --cacert can be provided to " + + "generate the signed certificate and submit it.", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("requires at least %d arg(s), only received %d", 1, len(args)) + } + + options.caNameOrId = args[0] + + if options.certPath != "" { + var err error + options.certPemBytes, err = ioutil.ReadFile(options.certPath) + + if err != nil { + return fmt.Errorf("could not read --cert file (%s): %v", options.certPath, err) + } + options.isGenerateCert = false + } else if options.caCertPath != "" && options.caKeyPath != "" { + var err error + options.caKeyPemBytes, err = ioutil.ReadFile(options.caKeyPath) + + if err != nil { + return fmt.Errorf("could not read --cakey file (%s): %v", options.caKeyPath, err) + } + + options.caCertPemBytes, err = ioutil.ReadFile(options.caCertPath) + + if err != nil { + return fmt.Errorf("could not read --cacert file (%s): %v", options.caCertPath, err) + } + options.isGenerateCert = true + } else { + return errors.New("expected either (--cert --cakey ), some options were missing") + } + + return nil + + }, + Run: func(cmd *cobra.Command, args []string) { + options.Cmd = cmd + options.Args = args + err := runValidateCa(options) + cmdhelper.CheckErr(err) + }, + SuggestFor: []string{}, + } + + // allow interspersing positional args and flags + cmd.Flags().SetInterspersed(true) + cmd.Flags().BoolVarP(&options.OutputJSONResponse, "output-json", "j", false, "Output the full JSON response from the Ziti Edge Controller") + cmd.Flags().StringVarP(&options.certPath, "cert", "c", "", "The path to a cert with the CN set as the verification token and signed by the target CA") + cmd.Flags().StringVarP(&options.caKeyPath, "cacert", "a", "", "The path to the CA cert that should be used to generate and sign a verification cert") + cmd.Flags().StringVarP(&options.caKeyPath, "cakey", "k", "", "The path to the CA key that should be used to generate and sign a verification cert") + + return cmd +} + +func runValidateCa(options *verifyCaOptions) error { + var err error + options.caId, err = mapCaNameToID(options.caNameOrId) + + if err != nil { + return err + } + + if options.isGenerateCert { + jsonContainer, err := util.EdgeControllerRequest("ca/"+options.caId, options.Out, options.OutputJSONResponse, func(request *resty.Request, url string) (*resty.Response, error) { return request.Get(url) }) + + if err != nil { + return fmt.Errorf("could not request ca [%s] (%v}", options.caId, err) + } + + token := jsonContainer.Path("data.verificationToken").Data().(string) + + if token == "" { + return fmt.Errorf("could not obtain verification token for ca [%s]", options.caId) + } + + options.certPemBytes, _, err = generateCert(options, token) + + if err != nil { + return fmt.Errorf("could not generate validation cert: %v", err) + } + } + + return util.EdgeControllerVerify("ca", options.caId, string(options.certPemBytes), options.Out, options.OutputJSONResponse) +} + +func generateCert(options *verifyCaOptions, token string) ([]byte, crypto.Signer, error) { + + certBlocks := nfpem.PemToX509(string(options.caCertPemBytes)) + + if len(certBlocks) == 0 { + return nil, nil, errors.New("could not prase cert file, 0 PEM blocks detected1") + } + + caCertBlock := certBlocks[0] + caCert, err := x509.ParseCertificate(caCertBlock.Raw) + + if err != nil { + return nil, nil, fmt.Errorf("could not parse ca cert (%v)", err) + } + + keyBlocks := nfpem.DecodeAll(options.caKeyPemBytes) + + if len(keyBlocks) == 0 { + return nil, nil, errors.New("could not parse key file, 0 PEM blocks detected") + } + + caPrivateKeyBlock := keyBlocks[0] + signerInterface, err := x509.ParsePKCS8PrivateKey(caPrivateKeyBlock.Bytes) + + if err != nil { + return nil, nil, fmt.Errorf("could not parse key file (%s)", err) + } + + caKey := signerInterface.(crypto.Signer) + + if caKey == nil { + return nil, nil, fmt.Errorf("key was not of correct type, could not be used as signer") + } + + id, _ := rand.Int(rand.Reader, big.NewInt(100000000000000000)) + verificationCert := &x509.Certificate{ + SerialNumber: id, + Subject: pkix.Name{ + CommonName: token, + Organization: []string{"Ziti CLI Generated Validation Cert"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Minute * 5), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + verificationKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + + if err != nil { + return nil, nil, fmt.Errorf("could not generate private key for verification certificate (%v)", err) + } + + signedCertBytes, err := x509.CreateCertificate(rand.Reader, verificationCert, caCert, verificationKey.PublicKey, caKey) + + if err != nil { + return nil, nil, fmt.Errorf("could not sign verification certificate with CA (%v)", err) + } + + verificationCert, _ = x509.ParseCertificate(signedCertBytes) + + verificationBlock := &pem.Block{ + Type: "EC CERTIFICATE", + Bytes: verificationCert.Raw, + } + + verificationCertPemBytes := pem.EncodeToMemory(verificationBlock) + + return verificationCertPemBytes, verificationKey, nil +} + +//func runCreateCa(options *createCaOptions) (err error) { +// data := gabs.New() +// setJSONValue(data, options.name, "name") +// setJSONValue(data, options.autoCaEnrollment, "isAutoCaEnrollmentEnabled") +// setJSONValue(data, options.ottCaEnrollment, "isOttCaEnrollmentEnabled") +// setJSONValue(data, options.authEnabled, "isAuthEnabled") +// setJSONValue(data, string(options.caPemBytes), "certPem") +// +// result, err := createEntityOfType("cas", data.String(), &options.commonOptions) +// +// if err != nil { +// panic(err) +// } +// +// id := result.S("data", "id").Data() +// +// if _, err = fmt.Fprintf(options.Out, "%v\n", id); err != nil { +// panic(err) +// } +// +// return err +//} diff --git a/ziti/cmd/ziti/util/rest.go b/ziti/cmd/ziti/util/rest.go index 317955157..6e71aea39 100644 --- a/ziti/cmd/ziti/util/rest.go +++ b/ziti/cmd/ziti/util/rest.go @@ -625,6 +625,41 @@ func EdgeControllerUpdate(entityType string, body string, out io.Writer, put boo return jsonParsed, nil } +// EdgeControllerVerify will create entities of the given type in the given Edge Controller +func EdgeControllerVerify(entityType, id, body string, out io.Writer, logJSON bool) error { + session := &Session{} + if err := session.Load(); err != nil { + return err + } + + client := newClient() + + if session.Cert != "" { + client.SetRootCertificate(session.Cert) + } + resp, err := client. + R(). + SetHeader("Content-Type", "application/json"). + SetHeader(constants.ZitiSession, session.Token). + SetBody(body). + Post(session.Host + "/" + entityType + "/" + id + "/verify") + + if err != nil { + return fmt.Errorf("unable to verify %v instance [%s] in Ziti Edge Controller at %v. Error: %v", entityType, id, session.Host, err) + } + + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("error verifying %v instance (%v) in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", + entityType, id, session.Host, resp.Status(), resp.String()) + } + + if logJSON { + outputJson(out, resp.Body()) + } + + return nil +} + func EdgeControllerRequest(entityType string, out io.Writer, logJSON bool, doRequest func(*resty.Request, string) (*resty.Response, error)) (*gabs.Container, error) { session := &Session{} if err := session.Load(); err != nil {