Skip to content

Commit

Permalink
feat: add KMIP commands
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre-Henri Symoneaux <[email protected]>
  • Loading branch information
phsym committed Feb 3, 2025
1 parent c4a1335 commit 2811130
Show file tree
Hide file tree
Showing 28 changed files with 1,339 additions and 9 deletions.
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.
129 changes: 129 additions & 0 deletions cmd/okms/kmip/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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",
}

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")

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

cmd.Run = func(cmd *cobra.Command, args []string) {
req := kmipClient.Create().SymmetricKey(kmip.CryptographicAlgorithm(alg), *size, usage.ToCryptographicUsageMask())
if *name != "" {
req = req.WithName(*name)
}

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",
}

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")

_ = 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)
}
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
}
65 changes: 65 additions & 0 deletions cmd/okms/kmip/get-attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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-attributes 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)
},
}
}
94 changes: 94 additions & 0 deletions cmd/okms/kmip/locate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package kmip

import (
"os"
"strconv"

"github.com/olekukonko/tablewriter"
"github.com/ovh/kmip-go"
"github.com/ovh/kmip-go/payloads"
"github.com/ovh/kmip-go/ttlv"
"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 locateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "locate",
Aliases: []string{"list", "ls"},
Short: "List kmip objects",
}

detailed := cmd.Flags().Bool("details", false, "Display detailed information")
var state kmipflags.State
cmd.Flags().Var(&state, "state", "List only object with the given state")
var objectType kmipflags.ObjectType
cmd.Flags().Var(&objectType, "type", "List only objects of the given type")

cmd.Run = func(cmd *cobra.Command, args []string) {
req := kmipClient.Locate()
if state != 0 {
req = req.WithAttribute(kmip.AttributeNameState, kmip.State(state))
}
if objectType != 0 {
req = req.WithObjectType(kmip.ObjectType(objectType))
}
locateResp := exit.OnErr2(req.ExecContext(cmd.Context()))
if !*detailed {
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
output.JsonPrint(locateResp)
} else {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Id"})
for _, id := range locateResp.UniqueIdentifier {
table.Append([]string{id})
}
table.Render()
}
return
}

attributes := []*payloads.GetAttributesResponsePayload{}
for _, id := range locateResp.UniqueIdentifier {
attributes = append(attributes, exit.OnErr2(kmipClient.GetAttributes(id).ExecContext(cmd.Context())))
}
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
output.JsonPrint(attributes)
} else {
printObjectTable(attributes)
}
}

return cmd
}

func printObjectTable(objects []*payloads.GetAttributesResponsePayload) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "TYPE", "NAME", "STATE", "ALGORITHM", "SIZE"})
for _, attr := range objects {
var row [6]string
row[0] = attr.UniqueIdentifier
for _, v := range attr.Attribute {
if idx := v.AttributeIndex; idx != nil && *idx > 0 {
continue
}
switch v.AttributeName {
case kmip.AttributeNameObjectType:
row[1] = ttlv.EnumStr(v.AttributeValue.(kmip.ObjectType))
case kmip.AttributeNameName:
row[2] = v.AttributeValue.(kmip.Name).NameValue
case kmip.AttributeNameState:
row[3] = ttlv.EnumStr(v.AttributeValue.(kmip.State))
case kmip.AttributeNameCryptographicAlgorithm:
row[4] = ttlv.EnumStr(v.AttributeValue.(kmip.CryptographicAlgorithm))
case kmip.AttributeNameCryptographicLength:
row[5] = strconv.Itoa(int(v.AttributeValue.(int32)))
}
}
table.Append(row[:])
}
table.Render()
}
Loading

0 comments on commit 2811130

Please sign in to comment.