Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VCert CLI] Support for Certificate Provision and more changes #484

Merged
merged 12 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion cmd/vcert/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ const (
commandSshPickupName = "sshpickup"
commandSshEnrollName = "sshenroll"
commandSshGetConfigName = "sshgetconfig"
commandProvisionName = "provision"
subCommandCloudKeystore = "cloudkeystore"
)

var (
flags commandFlags
flags commandFlags
provisionCommands = stringSlice{
subCommandCloudKeystore,
}
)

type commandFlags struct {
Expand Down Expand Up @@ -141,4 +146,11 @@ type commandFlags struct {
sshCertWindows bool
sshFileCertEnroll string
sshFileGetConfig string
certificateID string
keystoreID string
providerName string
keystoreName string
keystoreCertName string
provisionOutputFile string
provisionPickupID string
}
122 changes: 122 additions & 0 deletions cmd/vcert/cmdCloudProviders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package main
luispresuelVenafi marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"log"
"os"
"strings"

"github.com/urfave/cli/v2"

"github.com/Venafi/vcert/v5"
"github.com/Venafi/vcert/v5/pkg/endpoint"
"github.com/Venafi/vcert/v5/pkg/venafi/cloud"
)

var (
commandProvision = &cli.Command{
Before: runBeforeCommand,
Action: doCommandProvision,
Name: commandProvisionName,
Usage: "To provision a certificate from Venafi Platform to a Cloud Keystore",
Subcommands: []*cli.Command{
luispresuelVenafi marked this conversation as resolved.
Show resolved Hide resolved
{
Name: subCommandCloudKeystore,
Flags: provisionFlags,
Usage: "provision certificate from Venafi Platform to Cloud Keystore",
UsageText: `vcert provision cloudkeystore <Required Venafi Control Plane> <Options>

vcert provision cloudkeystore -k <VCP API key> --certificate-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx --keystore-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx --format json
vcert provision cloudkeystore -k <VCP API key> --pickup-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx --provider-name "My GCP Provider"--keystore-name "My GCP provider" --certificate-name "example-venafi-com"
vcert provision cloudkeystore -p vcp -t <VCP access token> --certificate-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx --provider-name "My GCP Provider" --keystore-name "My GCP provider" --file "/path/to/file.txt"`,
Action: doCommandProvisionCloudKeystore,
},
},
}
)

func doCommandProvision(c *cli.Context) error {
return fmt.Errorf("the following subcommand(s) are required: \n%s", createBulletList(provisionCommands))
}

func createBulletList(items []string) string {
var builder strings.Builder
for _, item := range items {
builder.WriteString("• ")
builder.WriteString(item)
builder.WriteString("\n")
}
return builder.String()
}

func doCommandProvisionCloudKeystore(c *cli.Context) error {
err := validateProvisionFlags(c.Command.Name)
if err != nil {
return err
}
err = setTLSConfig()
if err != nil {
return err
}

cfg, err := buildConfig(c, &flags)
if err != nil {
return fmt.Errorf("failed to build vcert config: %s", err)
}

connector, err := vcert.NewClient(&cfg)
if err != nil {
logf("Unable to connect to %s: %s", cfg.ConnectorType, err)
} else {
logf("Successfully connected to %s", cfg.ConnectorType)
}

var req = &endpoint.ProvisioningRequest{}
var options *endpoint.ProvisioningOptions

log.Printf("fetching keystore information for provided keystore information from flags. KeystoreID: %s, KeystoreName: %s, ProviderName: %s", flags.keystoreID, flags.keystoreName, flags.providerName)
getKeystoreReq := buildGetCloudKeystoreRequest(&flags)
cloudKeystore, err := connector.(*cloud.Connector).GetCloudKeystore(getKeystoreReq)
if err != nil {
return err
}
log.Printf("successfully fetched keystore")

if flags.pickupIDFile != "" {
bytes, err := os.ReadFile(flags.pickupIDFile)
if err != nil {
return fmt.Errorf("failed to read Pickup ID value: %s", err)
}
flags.pickupID = strings.TrimSpace(string(bytes))
}

req, options = fillProvisioningRequest(req, *cloudKeystore, &flags)

metadata, err := connector.ProvisionCertificate(req, options)
if err != nil {
return err
}

result := ProvisioningResult{}
switch cloudKeystore.Type {
case cloud.KeystoreTypeACM:
result.ARN = metadata.GetAWSCertificateMetadata().GetARN()
case cloud.KeystoreTypeAKV:
result.AzureID = metadata.GetAzureCertificateMetadata().GetID()
result.AzureName = metadata.GetAzureCertificateMetadata().GetName()
result.AzureVersion = metadata.GetAzureCertificateMetadata().GetVersion()
case cloud.KeystoreTypeGCM:
result.GcpID = metadata.GetGCPCertificateMetadata().GetID()
result.GcpName = metadata.GetGCPCertificateMetadata().GetName()
}

result.MachineIdentityId = metadata.GetMachineIdentityMetadata().GetID()
result.MachineIdentityActionType = metadata.GetMachineIdentityMetadata().GetActionType()

err = result.Flush(flags.format)

if err != nil {
return fmt.Errorf("failed to output the results: %s", err)
}
return nil
}
57 changes: 57 additions & 0 deletions cmd/vcert/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,50 @@ var (
TakesFile: true,
}

flagCertificateID = &cli.StringFlag{
Name: "certificate-id",
Usage: "The id of the certificate to be provisioned to a cloud keystore.",
Destination: &flags.certificateID,
}

flagKeystoreID = &cli.StringFlag{
Name: "keystore-id",
Usage: "The id of the cloud keystore where the certificate will be provisioned.",
Destination: &flags.keystoreID,
}

flagKeystoreName = &cli.StringFlag{
Name: "keystore-name",
Usage: "The name of the cloud keystore where the certificate will be provisioned. Must be set along with provider-name flag.",
Destination: &flags.keystoreName,
}

flagProviderName = &cli.StringFlag{
Name: "provider-name",
Usage: "Name of the cloud provider which owns the cloud keystore where the certificate will be provisioned. Must be set along with keystore-name flag.",
Destination: &flags.providerName,
}

flagKeystoreCertName = &cli.StringFlag{
Name: "certificate-name",
Usage: "Use to specify Cloud Keystore Certificate Name if it supports it",
Destination: &flags.keystoreCertName,
}

flagProvisionOutputFile = &cli.StringFlag{
Name: "file",
Usage: "Use to specify a file name and a location where the output should be written. " +
"Example: --file /path-to/provision-output",
Destination: &flags.provisionOutputFile,
TakesFile: true,
}

flagProvisionPickupID = &cli.StringFlag{
Name: "pickup-id",
Usage: "Use to specify the Pickup ID (for VCP is the Request ID) of the certificate to be provisioned.",
Destination: &flags.provisionPickupID,
}

commonFlags = []cli.Flag{flagInsecure, flagVerbose, flagNoPrompt}
keyFlags = []cli.Flag{flagKeyType, flagKeySize, flagKeyCurve, flagKeyFile, flagKeyPassword}
sansFlags = []cli.Flag{flagDNSSans, flagEmailSans, flagIPSans, flagURISans, flagUPNSans}
Expand Down Expand Up @@ -846,6 +890,19 @@ var (
)),
)

provisionFlags = flagsApppend(
credentialsFlags,
flagCertificateID,
flagProvisionPickupID,
flagPickupIDFile,
flagKeystoreCertName,
flagProviderName,
flagKeystoreName,
flagKeystoreID,
flagFormat,
flagProvisionOutputFile, // TODO: implement this flag
)

commonCredFlags = []cli.Flag{flagConfig, flagProfile, flagUrl, flagToken, flagTrustBundle}

getCredFlags = sortedFlags(flagsApppend(
Expand Down
2 changes: 2 additions & 0 deletions cmd/vcert/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func main() {
commandSshEnroll,
commandSshGetConfig,
commandRunPlaybook,
commandProvision,
},
EnableBashCompletion: true, //todo: write BashComplete function for options
Authors: authors,
Expand Down Expand Up @@ -115,6 +116,7 @@ ACTIONS:
retire tpp | vcp To retire a certificate
revoke tpp To revoke a certificate
run tpp | vcp | firefly To retrieve and install certificates using a vcert playbook file
provision vcp To provision a certificate to cloud keystore
luispresuelVenafi marked this conversation as resolved.
Show resolved Hide resolved

getpolicy tpp | vcp To retrieve the certificate policy of a zone
setpolicy tpp | vcp To apply a certificate policy specification to a zone
Expand Down
72 changes: 65 additions & 7 deletions cmd/vcert/result_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ import (
"github.com/Venafi/vcert/v5/pkg/util"
)

const (
formatJson = "json"
)

type Config struct {
Command string
Format string
Expand All @@ -56,6 +60,17 @@ type Result struct {
Config *Config
}

type ProvisioningResult struct {
ARN string `json:"arn,omitempty"`
AzureID string `json:"azureId,omitempty"`
AzureName string `json:"azureName,omitempty"`
AzureVersion string `json:"azureVersion,omitempty"`
GcpID string `json:"gcpId,omitempty"`
GcpName string `json:"gcpName,omitempty"`
MachineIdentityId string `json:"machineIdentityId,omitempty"`
MachineIdentityActionType string `json:"machineIdentityActionType,omitempty"`
}

type Output struct {
Certificate string `json:",omitempty"`
CSR string `json:",omitempty"`
Expand Down Expand Up @@ -315,24 +330,24 @@ func (r *Result) Flush() error {
allFileOutput.Chain = r.Pcc.Chain
allFileOutput.CSR = r.Pcc.CSR

var bytes []byte
var fileBytes []byte
if r.Config.Format == P12Format || r.Config.Format == LegacyP12Format {
bytes, err = allFileOutput.AsPKCS12(r.Config)
fileBytes, err = allFileOutput.AsPKCS12(r.Config)
if err != nil {
return fmt.Errorf("failed to encode pkcs12: %s", err)
}
} else if r.Config.Format == JKSFormat {
bytes, err = allFileOutput.AsJKS(r.Config)
fileBytes, err = allFileOutput.AsJKS(r.Config)
if err != nil {
return err
}
} else {
bytes, err = allFileOutput.Format(r.Config)
fileBytes, err = allFileOutput.Format(r.Config)
if err != nil {
return err
}
}
err = os.WriteFile(r.Config.AllFile, bytes, 0600)
err = os.WriteFile(r.Config.AllFile, fileBytes, 0600)
errors = append(errors, err)
} else {

Expand Down Expand Up @@ -388,11 +403,12 @@ func (r *Result) Flush() error {
}

// and flush the rest to STDOUT
bytes, err := stdOut.Format(r.Config)
configBytes, err := stdOut.Format(r.Config)
if err != nil {
return err // something worse than file permission problem
}
fmt.Fprint(os.Stdout, string(bytes))
_, err = fmt.Fprint(os.Stdout, string(configBytes))
errors = append(errors, err)

var finalError error
for _, e := range errors {
Expand Down Expand Up @@ -430,3 +446,45 @@ func outputJSON(resp interface{}) error {
}
return err
}

func (r *ProvisioningResult) Flush(format string) error {

result, err := r.Format(format)
if err != nil {
return err
}

_, err = fmt.Fprint(os.Stdout, result)
if err != nil {
return fmt.Errorf("failed to print provisioning result to STDOUT: %w", err)
}

return nil
}

func (r *ProvisioningResult) Format(format string) (string, error) {
result := ""
switch strings.ToLower(format) {
case formatJson:
b, err := json.MarshalIndent(r, "", " ")
if err != nil {
return "", fmt.Errorf("failed to construct JSON: %s", err)
}
result = string(b)
default:
if r.ARN != "" {
result += fmt.Sprintf("arn: %s\n", r.ARN)
}
if r.AzureID != "" {
result += fmt.Sprintf("azureId: %s\n", r.AzureID)
result += fmt.Sprintf("azureName: %s\n", r.AzureName)
result += fmt.Sprintf("azureVersion: %s\n", r.AzureVersion)

}
if r.GcpID != "" {
result += fmt.Sprintf("gcpId %s\n", r.GcpID)
result += fmt.Sprintf("gcpName %s\n", r.GcpName)
}
}
return result, nil
}
15 changes: 0 additions & 15 deletions cmd/vcert/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package main

import (
"bytes"
"fmt"
"unicode"
)

Expand Down Expand Up @@ -72,17 +71,3 @@ func wrapArgumentDescriptionText(text string) string {

return buf.String()
}

func showvcertUsage() {
rvelaVenafi marked this conversation as resolved.
Show resolved Hide resolved
fmt.Printf("\tTo obtain a new token for authentication, use the 'getcred' action.\n")
fmt.Printf("\tTo check whether an authentication token is valid, use the 'checkcred' action.\n")
fmt.Printf("\tTo invalidate an authentication token, use the 'voidcred' action.\n")
fmt.Printf("\tTo generate a certificate signing request (CSR), use the 'gencsr' action.\n")
fmt.Printf("\tTo enroll a certificate, use the 'enroll' action.\n")
fmt.Printf("\tTo retrieve a certificate, use the 'pickup' action.\n")
fmt.Printf("\tTo renew a certificate, use the 'renew' action.\n")
fmt.Printf("\tTo revoke a certificate, use the 'revoke' action.\n")
fmt.Printf("\tTo retire a certificate, use the 'retire' action.\n")
fmt.Printf("\tTo retrieve certificate policy, use the 'getpolicy' action.\n")
fmt.Printf("\tTo apply certificate policy, use the 'setpolicy' action.\n")
}
Loading