Skip to content

Commit 9e1c618

Browse files
committed
feat: add KMIP commands
Signed-off-by: Pierre-Henri Symoneaux <[email protected]>
1 parent ba60efc commit 9e1c618

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2489
-13
lines changed

.github/workflows/test.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ jobs:
3333
type: mtls
3434
cert: $(pwd)/tls.crt
3535
key: $(pwd)/tls.key
36+
kmip:
37+
endpoint: ${{secrets.KMS_KMIP_ENDPOINT}}
38+
auth:
39+
type: mtls
40+
cert: $(pwd)/tls.crt
41+
key: $(pwd)/tls.key
3642
EOF
3743
- name: Test connectivity to KMS dmain
3844
run: ./okms keys ls -d -c okms.yaml

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[![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)
44

55
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.
6+
It supports both REST API and KMIP protocol.
67

78
> **NOTE:** THIS PROJECT IS CURRENTLY UNDER DEVELOPMENT AND SUBJECT TO BREAKING CHANGES.
89
@@ -89,6 +90,7 @@ Available Commands:
8990
configure Configure CLI options
9091
help Help about any command
9192
keys Manage domain keys
93+
kmip Manage kmip objects
9294
version Print the version information
9395
x509 Generate, and sign x509 certificates
9496
@@ -116,6 +118,13 @@ profiles:
116118
type: mtls # Optional, defaults to "mtls"
117119
cert: /path/to/domain/cert.pem
118120
key: /path/to/domain/key.pem
121+
kmip:
122+
endpoint: myserver.acme.com:5696
123+
ca: /path/to/public-ca.crt # Optional if the CA is in system store
124+
auth:
125+
type: mtls # Optional, defaults to "mtls"
126+
cert: /path/to/domain/cert.pem
127+
key: /path/to/domain/key.pem
119128
```
120129
121130
These settings can be overwritten using environment variables:
@@ -124,12 +133,24 @@ These settings can be overwritten using environment variables:
124133
- KMS_HTTP_CA
125134
- KMS_HTTP_CERT
126135
- KMS_HTTP_KEY
136+
and
137+
- KMS_KMIP_ENDPOINT
138+
- KMS_KMIP_CA
139+
- KMS_KMIP_CERT
140+
- KMS_KMIP_KEY
127141
128142
```bash
129-
export KMS_HTTP_ENDPOINT=https://the-kms.ovh
143+
# REST API
144+
export KMS_HTTP_ENDPOINT=https://myserver.acme.com
130145
export KMS_HTTP_CA=/path/to/certs/ca.crt
131146
export KMS_HTTP_CERT=/path/to/certs/user.crt
132147
export KMS_HTTP_KEY=/path/to/certs/user.key
148+
149+
# KMIP
150+
export KMS_KMIP_ENDPOINT=myserver.acme.com:5696
151+
export KMS_KMIP_CA=/path/to/certs/ca.crt
152+
export KMS_KMIP_CERT=/path/to/certs/user.crt
153+
export KMS_KMIP_KEY=/path/to/certs/user.key
133154
```
134155

135156
but each of them can be overwritten with CLI arguments.

cmd/okms/configure/root.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,16 @@ func CreateCommand() *cobra.Command {
4242
}
4343

4444
func Run(profile string) {
45-
config.ReadUserInput("CA file", "http.ca", profile, config.ValidateFileExists.AllowEmpty())
46-
config.ReadUserInput("Certificate file", "http.auth.cert", profile, config.ValidateFileExists)
47-
config.ReadUserInput("Private key file", "http.auth.key", profile, config.ValidateFileExists)
48-
config.ReadUserInput("Endpoint", "http.endpoint", profile, config.ValidateURL)
45+
choice := exit.OnErr2(pterm.DefaultInteractiveSelect.WithOptions([]string{"HTTP", "KMIP"}).Show("Select a protocol to configure"))
46+
if choice == "HTTP" {
47+
config.ReadUserInput("CA file", "http.ca", profile, config.ValidateFileExists.AllowEmpty())
48+
config.ReadUserInput("Certificate file", "http.auth.cert", profile, config.ValidateFileExists)
49+
config.ReadUserInput("Private key file", "http.auth.key", profile, config.ValidateFileExists)
50+
config.ReadUserInput("Endpoint", "http.endpoint", profile, config.ValidateURL)
51+
} else if choice == "KMIP" {
52+
config.ReadUserInput("CA file", "kmip.ca", profile, config.ValidateFileExists.AllowEmpty())
53+
config.ReadUserInput("Certificate file", "kmip.auth.cert", profile, config.ValidateFileExists)
54+
config.ReadUserInput("Private key file", "kmip.auth.key", profile, config.ValidateFileExists)
55+
config.ReadUserInput("Endpoint", "kmip.endpoint", profile, config.ValidateTCPAddr)
56+
}
4957
}

cmd/okms/kmip/attributes.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package kmip
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"regexp"
8+
9+
"github.com/olekukonko/tablewriter"
10+
"github.com/ovh/kmip-go"
11+
"github.com/ovh/kmip-go/ttlv"
12+
"github.com/ovh/okms-cli/common/flagsmgmt"
13+
"github.com/ovh/okms-cli/common/output"
14+
"github.com/ovh/okms-cli/common/utils/exit"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
var (
19+
attributeValueHdrRegex = regexp.MustCompile(`^AttributeValue \(.+\): `)
20+
attributeValueFieldsRegex = regexp.MustCompile(`(.+) \(.+\): `)
21+
)
22+
23+
func printAttributeTable(attributes []kmip.Attribute) {
24+
table := tablewriter.NewWriter(os.Stdout)
25+
table.SetAutoWrapText(false)
26+
table.SetHeader([]string{"Name", "Value"})
27+
table.SetRowLine(true)
28+
table.SetAlignment(tablewriter.ALIGN_LEFT)
29+
30+
enc := ttlv.NewTextEncoder()
31+
for _, attr := range attributes {
32+
enc.Clear()
33+
enc.TagAny(kmip.TagAttributeValue, attr.AttributeValue)
34+
txt := enc.Bytes()
35+
36+
txt = attributeValueHdrRegex.ReplaceAll(txt, nil)
37+
txt = attributeValueFieldsRegex.ReplaceAll(txt, []byte("$1: "))
38+
txt = bytes.ReplaceAll(txt, []byte("\n "), []byte("\n"))
39+
txt = bytes.TrimSpace(txt)
40+
41+
name := string(attr.AttributeName)
42+
if idx := attr.AttributeIndex; idx != nil && *idx > 0 {
43+
name = fmt.Sprintf("%s [%d]", name, *idx)
44+
}
45+
table.Append([]string{name, string(txt)})
46+
}
47+
48+
table.Render()
49+
}
50+
51+
func getAttributesCommand() *cobra.Command {
52+
return &cobra.Command{
53+
Use: "get ID",
54+
Short: "Get the attributes of an object",
55+
Args: cobra.ExactArgs(1),
56+
Run: func(cmd *cobra.Command, args []string) {
57+
attributes := exit.OnErr2(kmipClient.GetAttributes(args[0]).ExecContext(cmd.Context()))
58+
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
59+
output.JsonPrint(attributes)
60+
return
61+
}
62+
printAttributeTable(attributes.Attribute)
63+
},
64+
}
65+
}
66+
67+
func attributesCommand() *cobra.Command {
68+
cmd := &cobra.Command{
69+
Use: "attributes",
70+
Aliases: []string{"attribute", "attr"},
71+
Short: "Manage an object's attributes",
72+
}
73+
cmd.AddCommand(
74+
getAttributesCommand(),
75+
)
76+
return cmd
77+
}

cmd/okms/kmip/create.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package kmip
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/ovh/kmip-go"
8+
"github.com/ovh/kmip-go/kmipclient"
9+
"github.com/ovh/okms-cli/common/flagsmgmt"
10+
"github.com/ovh/okms-cli/common/flagsmgmt/kmipflags"
11+
"github.com/ovh/okms-cli/common/output"
12+
"github.com/ovh/okms-cli/common/utils/exit"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func createCommand() *cobra.Command {
17+
cmd := &cobra.Command{
18+
Use: "create",
19+
Short: "Create kmip keys",
20+
}
21+
cmd.AddCommand(
22+
createSymmetricKey(),
23+
createKeyPair(),
24+
)
25+
return cmd
26+
}
27+
28+
func createSymmetricKey() *cobra.Command {
29+
cmd := &cobra.Command{
30+
Use: "symmetric",
31+
Aliases: []string{"sym"},
32+
Short: "Create KMIP symmetric key",
33+
Args: cobra.NoArgs,
34+
}
35+
36+
var alg kmipflags.SymmetricAlg
37+
usage := kmipflags.KeyUsageList{kmipflags.ENCRYPT, kmipflags.DECRYPT}
38+
39+
cmd.Flags().Var(&alg, "alg", "Key algorithm")
40+
size := cmd.Flags().Int("size", 0, "Key bit length")
41+
cmd.Flags().Var(&usage, "usage", "Cryptographic usage")
42+
name := cmd.Flags().String("name", "", "Optional key name")
43+
44+
sensitive := cmd.Flags().Bool("sensitive", false, "Set sensitive attribute")
45+
extractable := cmd.Flags().Bool("extractable", true, "Set the extractable attribute")
46+
description := cmd.Flags().String("description", "", "Set the description attribute")
47+
comment := cmd.Flags().String("comment", "", "Set the comment attribute")
48+
49+
_ = cmd.MarkFlagRequired("alg")
50+
_ = cmd.MarkFlagRequired("size")
51+
52+
cmd.Run = func(cmd *cobra.Command, args []string) {
53+
req := kmipClient.Create().
54+
SymmetricKey(kmip.CryptographicAlgorithm(alg), *size, usage.ToCryptographicUsageMask()).
55+
WithAttribute(kmip.AttributeNameExtractable, *extractable).
56+
WithAttribute(kmip.AttributeNameSensitive, *sensitive)
57+
if *name != "" {
58+
req = req.WithName(*name)
59+
}
60+
if *description != "" {
61+
req = req.WithAttribute(kmip.AttributeNameDescription, *description)
62+
}
63+
if *comment != "" {
64+
req = req.WithAttribute(kmip.AttributeNameComment, *comment)
65+
}
66+
67+
resp := exit.OnErr2(req.ExecContext(cmd.Context()))
68+
69+
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
70+
output.JsonPrint(resp)
71+
} else {
72+
fmt.Println("Key created with ID", resp.UniqueIdentifier)
73+
// Print returned attributes if any
74+
if resp.Attributes != nil && len(resp.Attributes.Attribute) > 0 {
75+
printAttributeTable(resp.Attributes.Attribute)
76+
}
77+
}
78+
}
79+
80+
return cmd
81+
}
82+
83+
func createKeyPair() *cobra.Command {
84+
cmd := &cobra.Command{
85+
Use: "key-pair",
86+
Short: "Create an asymmetric key-pair",
87+
Args: cobra.NoArgs,
88+
}
89+
90+
var alg kmipflags.AsymmetricAlg
91+
cmd.Flags().Var(&alg, "alg", "Key-pair algorithm")
92+
size := cmd.Flags().Int("size", 0, "Modulus bit length of the RSA key-pair to generate")
93+
var curve kmipflags.EcCurve
94+
cmd.Flags().Var(&curve, "curve", "Elliptic curve for EC keys")
95+
privateUsage := kmipflags.KeyUsageList{kmipflags.SIGN}
96+
publicUsage := kmipflags.KeyUsageList{kmipflags.VERIFY}
97+
cmd.Flags().Var(&privateUsage, "private-usage", "Private key allowed usage")
98+
cmd.Flags().Var(&publicUsage, "public-usage", "Public key allowed usage")
99+
privateName := cmd.Flags().String("private-name", "", "Optional private key name")
100+
publicName := cmd.Flags().String("public-name", "", "Optional public key name")
101+
102+
privateSensitive := cmd.Flags().Bool("private-sensitive", false, "Set sensitive attribute on the private key")
103+
privateExtractable := cmd.Flags().Bool("private-extractable", true, "Set the extractable attribute on the private key")
104+
description := cmd.Flags().String("description", "", "Set the description attribute on both keys")
105+
comment := cmd.Flags().String("comment", "", "Set the comment attribute on both keys")
106+
107+
_ = cmd.MarkFlagRequired("alg")
108+
cmd.MarkFlagsMutuallyExclusive("curve", "size")
109+
110+
cmd.Run = func(cmd *cobra.Command, args []string) {
111+
var req kmipclient.ExecCreateKeyPairAttr
112+
switch alg {
113+
case kmipflags.RSA:
114+
if *size == 0 {
115+
exit.OnErr(errors.New("Missing --size flag"))
116+
}
117+
req = kmipClient.CreateKeyPair().RSA(*size, privateUsage.ToCryptographicUsageMask(), publicUsage.ToCryptographicUsageMask())
118+
case kmipflags.ECDSA:
119+
if curve == 0 {
120+
exit.OnErr(errors.New("Missing --curve flag"))
121+
}
122+
req = kmipClient.CreateKeyPair().ECDSA(kmip.RecommendedCurve(curve), privateUsage.ToCryptographicUsageMask(), publicUsage.ToCryptographicUsageMask())
123+
}
124+
if *privateName != "" {
125+
req = req.PrivateKey().WithName(*privateName)
126+
}
127+
if *publicName != "" {
128+
req = req.PublicKey().WithName(*publicName)
129+
}
130+
req = req.PrivateKey().
131+
WithAttribute(kmip.AttributeNameExtractable, *privateExtractable).
132+
WithAttribute(kmip.AttributeNameSensitive, *privateSensitive)
133+
if *description != "" {
134+
req = req.Common().WithAttribute(kmip.AttributeNameDescription, *description)
135+
}
136+
if *comment != "" {
137+
req = req.Common().WithAttribute(kmip.AttributeNameComment, *comment)
138+
}
139+
resp := exit.OnErr2(req.ExecContext(cmd.Context()))
140+
141+
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
142+
output.JsonPrint(resp)
143+
} else {
144+
fmt.Println("Pubic Key ID:", resp.PublicKeyUniqueIdentifier)
145+
fmt.Println("Private Key ID:", resp.PrivateKeyUniqueIdentifier)
146+
// Print returned attributes if any
147+
if attrs := resp.PublicKeyTemplateAttribute; attrs != nil && len(attrs.Attribute) > 0 {
148+
fmt.Println("Public Key Attributes:")
149+
printAttributeTable(attrs.Attribute)
150+
}
151+
if attrs := resp.PrivateKeyTemplateAttribute; attrs != nil && len(attrs.Attribute) > 0 {
152+
fmt.Println("Private Key Attributes:")
153+
printAttributeTable(attrs.Attribute)
154+
}
155+
}
156+
}
157+
158+
return cmd
159+
}

cmd/okms/kmip/get.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package kmip
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/ovh/kmip-go"
8+
"github.com/ovh/kmip-go/ttlv"
9+
"github.com/ovh/okms-cli/common/flagsmgmt"
10+
"github.com/ovh/okms-cli/common/output"
11+
"github.com/ovh/okms-cli/common/utils/exit"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
func getCommand() *cobra.Command {
16+
cmd := &cobra.Command{
17+
Use: "get ID",
18+
Short: "Get the materials from a kmip object",
19+
Args: cobra.ExactArgs(1),
20+
}
21+
22+
cmd.Run = func(cmd *cobra.Command, args []string) {
23+
req := kmipClient.Get(args[0])
24+
25+
resp := exit.OnErr2(req.ExecContext(cmd.Context()))
26+
if cmd.Flag("output").Value.String() == string(flagsmgmt.JSON_OUTPUT_FORMAT) {
27+
output.JsonPrint(resp)
28+
return
29+
}
30+
31+
switch obj := resp.Object.(type) {
32+
case *kmip.SecretData:
33+
secret := exit.OnErr2(obj.Data())
34+
os.Stdout.Write(secret)
35+
case *kmip.SymmetricKey:
36+
key := exit.OnErr2(obj.KeyMaterial())
37+
os.Stdout.Write(key)
38+
case *kmip.Certificate:
39+
cert := exit.OnErr2(obj.PemCertificate())
40+
fmt.Println(cert)
41+
case *kmip.PrivateKey:
42+
pem := exit.OnErr2(obj.Pkcs8Pem())
43+
fmt.Println(pem)
44+
case *kmip.PublicKey:
45+
pem := exit.OnErr2(obj.PkixPem())
46+
fmt.Println(pem)
47+
default:
48+
os.Stdout.Write(ttlv.MarshalText(resp))
49+
}
50+
}
51+
52+
return cmd
53+
}

0 commit comments

Comments
 (0)