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

feat: add KMIP commands #32

Merged
merged 2 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ jobs:
type: mtls
cert: $(pwd)/tls.crt
key: $(pwd)/tls.key
kmip:
endpoint: ${{secrets.KMS_KMIP_ENDPOINT}}
auth:
type: mtls
cert: $(pwd)/tls.crt
key: $(pwd)/tls.key
EOF
- name: Test connectivity to KMS dmain
run: ./okms keys ls -d -c okms.yaml
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![license](https://img.shields.io/badge/license-Apache%202.0-red.svg?style=flat)](https://raw.githubusercontent.com/ovh/okms-sdk-go/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/ovh/okms-cli)](https://goreportcard.com/report/github.com/ovh/okms-cli)

The CLI to interact with your [OVHcloud KMS](https://help.ovhcloud.com/csm/en-ie-kms-quick-start?id=kb_article_view&sysparm_article=KB0063362) services.
It supports both REST API and KMIP protocol.

> **NOTE:** THIS PROJECT IS CURRENTLY UNDER DEVELOPMENT AND SUBJECT TO BREAKING CHANGES.

Expand Down Expand Up @@ -89,6 +90,7 @@ Available Commands:
configure Configure CLI options
help Help about any command
keys Manage domain keys
kmip Manage kmip objects
version Print the version information
x509 Generate, and sign x509 certificates

Expand Down Expand Up @@ -116,6 +118,13 @@ profiles:
type: mtls # Optional, defaults to "mtls"
cert: /path/to/domain/cert.pem
key: /path/to/domain/key.pem
kmip:
endpoint: myserver.acme.com:5696
ca: /path/to/public-ca.crt # Optional if the CA is in system store
auth:
type: mtls # Optional, defaults to "mtls"
cert: /path/to/domain/cert.pem
key: /path/to/domain/key.pem
```

These settings can be overwritten using environment variables:
Expand All @@ -124,12 +133,24 @@ These settings can be overwritten using environment variables:
- KMS_HTTP_CA
- KMS_HTTP_CERT
- KMS_HTTP_KEY
and
- KMS_KMIP_ENDPOINT
- KMS_KMIP_CA
- KMS_KMIP_CERT
- KMS_KMIP_KEY

```bash
export KMS_HTTP_ENDPOINT=https://the-kms.ovh
# REST API
export KMS_HTTP_ENDPOINT=https://myserver.acme.com
export KMS_HTTP_CA=/path/to/certs/ca.crt
export KMS_HTTP_CERT=/path/to/certs/user.crt
export KMS_HTTP_KEY=/path/to/certs/user.key

# KMIP
export KMS_KMIP_ENDPOINT=myserver.acme.com:5696
export KMS_KMIP_CA=/path/to/certs/ca.crt
export KMS_KMIP_CERT=/path/to/certs/user.crt
export KMS_KMIP_KEY=/path/to/certs/user.key
```

but each of them can be overwritten with CLI arguments.
16 changes: 12 additions & 4 deletions cmd/okms/configure/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,16 @@ func CreateCommand() *cobra.Command {
}

func Run(profile string) {
config.ReadUserInput("CA file", "http.ca", profile, config.ValidateFileExists.AllowEmpty())
config.ReadUserInput("Certificate file", "http.auth.cert", profile, config.ValidateFileExists)
config.ReadUserInput("Private key file", "http.auth.key", profile, config.ValidateFileExists)
config.ReadUserInput("Endpoint", "http.endpoint", profile, config.ValidateURL)
choice := exit.OnErr2(pterm.DefaultInteractiveSelect.WithOptions([]string{"HTTP", "KMIP"}).Show("Select a protocol to configure"))
if choice == "HTTP" {
config.ReadUserInput("CA file", "http.ca", profile, config.ValidateFileExists.AllowEmpty())
config.ReadUserInput("Certificate file", "http.auth.cert", profile, config.ValidateFileExists)
config.ReadUserInput("Private key file", "http.auth.key", profile, config.ValidateFileExists)
config.ReadUserInput("Endpoint", "http.endpoint", profile, config.ValidateURL)
} else if choice == "KMIP" {
config.ReadUserInput("CA file", "kmip.ca", profile, config.ValidateFileExists.AllowEmpty())
config.ReadUserInput("Certificate file", "kmip.auth.cert", profile, config.ValidateFileExists)
config.ReadUserInput("Private key file", "kmip.auth.key", profile, config.ValidateFileExists)
config.ReadUserInput("Endpoint", "kmip.endpoint", profile, config.ValidateTCPAddr)
}
}
77 changes: 77 additions & 0 deletions cmd/okms/kmip/attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package kmip

import (
"bytes"
"fmt"
"os"
"regexp"

"github.com/olekukonko/tablewriter"
"github.com/ovh/kmip-go"
"github.com/ovh/kmip-go/ttlv"
"github.com/ovh/okms-cli/common/flagsmgmt"
"github.com/ovh/okms-cli/common/output"
"github.com/ovh/okms-cli/common/utils/exit"
"github.com/spf13/cobra"
)

var (
attributeValueHdrRegex = regexp.MustCompile(`^AttributeValue \(.+\): `)
attributeValueFieldsRegex = regexp.MustCompile(`(.+) \(.+\): `)
)

func printAttributeTable(attributes []kmip.Attribute) {
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{"Name", "Value"})
table.SetRowLine(true)
table.SetAlignment(tablewriter.ALIGN_LEFT)

enc := ttlv.NewTextEncoder()
for _, attr := range attributes {
enc.Clear()
enc.TagAny(kmip.TagAttributeValue, attr.AttributeValue)
txt := enc.Bytes()

txt = attributeValueHdrRegex.ReplaceAll(txt, nil)
txt = attributeValueFieldsRegex.ReplaceAll(txt, []byte("$1: "))
txt = bytes.ReplaceAll(txt, []byte("\n "), []byte("\n"))
txt = bytes.TrimSpace(txt)

name := string(attr.AttributeName)
if idx := attr.AttributeIndex; idx != nil && *idx > 0 {
name = fmt.Sprintf("%s [%d]", name, *idx)
}
table.Append([]string{name, string(txt)})
}

table.Render()
}

func getAttributesCommand() *cobra.Command {
return &cobra.Command{
Use: "get ID",
Short: "Get the attributes of an object",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
attributes := exit.OnErr2(kmipClient.GetAttributes(args[0]).ExecContext(cmd.Context()))
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
output.JsonPrint(attributes)
return
}
printAttributeTable(attributes.Attribute)
},
}
}

func attributesCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "attributes",
Aliases: []string{"attribute", "attr"},
Short: "Manage an object's attributes",
}
cmd.AddCommand(
getAttributesCommand(),
)
return cmd
}
159 changes: 159 additions & 0 deletions cmd/okms/kmip/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package kmip

import (
"errors"
"fmt"

"github.com/ovh/kmip-go"
"github.com/ovh/kmip-go/kmipclient"
"github.com/ovh/okms-cli/common/flagsmgmt"
"github.com/ovh/okms-cli/common/flagsmgmt/kmipflags"
"github.com/ovh/okms-cli/common/output"
"github.com/ovh/okms-cli/common/utils/exit"
"github.com/spf13/cobra"
)

func createCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create kmip keys",
}
cmd.AddCommand(
createSymmetricKey(),
createKeyPair(),
)
return cmd
}

func createSymmetricKey() *cobra.Command {
cmd := &cobra.Command{
Use: "symmetric",
Aliases: []string{"sym"},
Short: "Create KMIP symmetric key",
Args: cobra.NoArgs,
}

var alg kmipflags.SymmetricAlg
usage := kmipflags.KeyUsageList{kmipflags.ENCRYPT, kmipflags.DECRYPT}

cmd.Flags().Var(&alg, "alg", "Key algorithm")
size := cmd.Flags().Int("size", 0, "Key bit length")
cmd.Flags().Var(&usage, "usage", "Cryptographic usage")
name := cmd.Flags().String("name", "", "Optional key name")

sensitive := cmd.Flags().Bool("sensitive", false, "Set sensitive attribute")
extractable := cmd.Flags().Bool("extractable", true, "Set the extractable attribute")
description := cmd.Flags().String("description", "", "Set the description attribute")
comment := cmd.Flags().String("comment", "", "Set the comment attribute")

_ = cmd.MarkFlagRequired("alg")
_ = cmd.MarkFlagRequired("size")

cmd.Run = func(cmd *cobra.Command, args []string) {
req := kmipClient.Create().
SymmetricKey(kmip.CryptographicAlgorithm(alg), *size, usage.ToCryptographicUsageMask()).
WithAttribute(kmip.AttributeNameExtractable, *extractable).
WithAttribute(kmip.AttributeNameSensitive, *sensitive)
if *name != "" {
req = req.WithName(*name)
}
if *description != "" {
req = req.WithAttribute(kmip.AttributeNameDescription, *description)
}
if *comment != "" {
req = req.WithAttribute(kmip.AttributeNameComment, *comment)
}

resp := exit.OnErr2(req.ExecContext(cmd.Context()))

if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
output.JsonPrint(resp)
} else {
fmt.Println("Key created with ID", resp.UniqueIdentifier)
// Print returned attributes if any
if resp.Attributes != nil && len(resp.Attributes.Attribute) > 0 {
printAttributeTable(resp.Attributes.Attribute)
}
}
}

return cmd
}

func createKeyPair() *cobra.Command {
cmd := &cobra.Command{
Use: "key-pair",
Short: "Create an asymmetric key-pair",
Args: cobra.NoArgs,
}

var alg kmipflags.AsymmetricAlg
cmd.Flags().Var(&alg, "alg", "Key-pair algorithm")
size := cmd.Flags().Int("size", 0, "Modulus bit length of the RSA key-pair to generate")
var curve kmipflags.EcCurve
cmd.Flags().Var(&curve, "curve", "Elliptic curve for EC keys")
privateUsage := kmipflags.KeyUsageList{kmipflags.SIGN}
publicUsage := kmipflags.KeyUsageList{kmipflags.VERIFY}
cmd.Flags().Var(&privateUsage, "private-usage", "Private key allowed usage")
cmd.Flags().Var(&publicUsage, "public-usage", "Public key allowed usage")
privateName := cmd.Flags().String("private-name", "", "Optional private key name")
publicName := cmd.Flags().String("public-name", "", "Optional public key name")

privateSensitive := cmd.Flags().Bool("private-sensitive", false, "Set sensitive attribute on the private key")
privateExtractable := cmd.Flags().Bool("private-extractable", true, "Set the extractable attribute on the private key")
description := cmd.Flags().String("description", "", "Set the description attribute on both keys")
comment := cmd.Flags().String("comment", "", "Set the comment attribute on both keys")

_ = cmd.MarkFlagRequired("alg")
cmd.MarkFlagsMutuallyExclusive("curve", "size")

cmd.Run = func(cmd *cobra.Command, args []string) {
var req kmipclient.ExecCreateKeyPairAttr
switch alg {
case kmipflags.RSA:
if *size == 0 {
exit.OnErr(errors.New("Missing --size flag"))
}
req = kmipClient.CreateKeyPair().RSA(*size, privateUsage.ToCryptographicUsageMask(), publicUsage.ToCryptographicUsageMask())
case kmipflags.ECDSA:
if curve == 0 {
exit.OnErr(errors.New("Missing --curve flag"))
}
req = kmipClient.CreateKeyPair().ECDSA(kmip.RecommendedCurve(curve), privateUsage.ToCryptographicUsageMask(), publicUsage.ToCryptographicUsageMask())
}
if *privateName != "" {
req = req.PrivateKey().WithName(*privateName)
}
if *publicName != "" {
req = req.PublicKey().WithName(*publicName)
}
req = req.PrivateKey().
WithAttribute(kmip.AttributeNameExtractable, *privateExtractable).
WithAttribute(kmip.AttributeNameSensitive, *privateSensitive)
if *description != "" {
req = req.Common().WithAttribute(kmip.AttributeNameDescription, *description)
}
if *comment != "" {
req = req.Common().WithAttribute(kmip.AttributeNameComment, *comment)
}
resp := exit.OnErr2(req.ExecContext(cmd.Context()))

if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
output.JsonPrint(resp)
} else {
fmt.Println("Pubic Key ID:", resp.PublicKeyUniqueIdentifier)
fmt.Println("Private Key ID:", resp.PrivateKeyUniqueIdentifier)
// Print returned attributes if any
if attrs := resp.PublicKeyTemplateAttribute; attrs != nil && len(attrs.Attribute) > 0 {
fmt.Println("Public Key Attributes:")
printAttributeTable(attrs.Attribute)
}
if attrs := resp.PrivateKeyTemplateAttribute; attrs != nil && len(attrs.Attribute) > 0 {
fmt.Println("Private Key Attributes:")
printAttributeTable(attrs.Attribute)
}
}
}

return cmd
}
53 changes: 53 additions & 0 deletions cmd/okms/kmip/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kmip

import (
"fmt"
"os"

"github.com/ovh/kmip-go"
"github.com/ovh/kmip-go/ttlv"
"github.com/ovh/okms-cli/common/flagsmgmt"
"github.com/ovh/okms-cli/common/output"
"github.com/ovh/okms-cli/common/utils/exit"
"github.com/spf13/cobra"
)

func getCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "get ID",
Short: "Get the materials from a kmip object",
Args: cobra.ExactArgs(1),
}

cmd.Run = func(cmd *cobra.Command, args []string) {
req := kmipClient.Get(args[0])

resp := exit.OnErr2(req.ExecContext(cmd.Context()))
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
output.JsonPrint(resp)
return
}

switch obj := resp.Object.(type) {
case *kmip.SecretData:
secret := exit.OnErr2(obj.Data())
os.Stdout.Write(secret)
case *kmip.SymmetricKey:
key := exit.OnErr2(obj.KeyMaterial())
os.Stdout.Write(key)
case *kmip.Certificate:
cert := exit.OnErr2(obj.PemCertificate())
fmt.Println(cert)
case *kmip.PrivateKey:
pem := exit.OnErr2(obj.Pkcs8Pem())
fmt.Println(pem)
case *kmip.PublicKey:
pem := exit.OnErr2(obj.PkixPem())
fmt.Println(pem)
default:
os.Stdout.Write(ttlv.MarshalText(resp))
}
}

return cmd
}
Loading