Skip to content

Commit

Permalink
support for yubikey management key from a file
Browse files Browse the repository at this point in the history
This commit adds the yubikey attribute management-key-source. This
attribute can be used to pass the yubikey management key from a file.

Fixes smallstep/step-kms-plugin#207
  • Loading branch information
maraino committed Jan 14, 2025
1 parent f7b81be commit f3ae27e
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 98 deletions.
19 changes: 17 additions & 2 deletions kms/uri/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,25 @@ func (u *URI) Pin() string {
return ""
}

// Read returns the raw content of the file in the given attribute key. This
// method will return nil if the key is missing.
func (u *URI) Read(key string) ([]byte, error) {
path := u.Get(key)
if path == "" {
return nil, nil
}
return readFile(path)
}

func readFile(path string) ([]byte, error) {
u, err := url.Parse(path)
if err == nil && (u.Scheme == "" || u.Scheme == "file") && u.Path != "" {
path = u.Path
if err == nil && (u.Scheme == "" || u.Scheme == "file") {
switch {
case u.Path != "":
path = u.Path
case u.Opaque != "":
path = u.Opaque
}
}
b, err := os.ReadFile(path)
if err != nil {
Expand Down
186 changes: 92 additions & 94 deletions kms/uri/uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ package uri

import (
"net/url"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func mustParse(t *testing.T, s string) *URI {
t.Helper()
u, err := Parse(s)
require.NoError(t, err)
return u
}

func TestNew(t *testing.T) {
type args struct {
scheme string
Expand Down Expand Up @@ -154,13 +163,6 @@ func TestParseWithScheme(t *testing.T) {
}

func TestURI_Has(t *testing.T) {
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
type args struct {
key string
}
Expand All @@ -170,15 +172,15 @@ func TestURI_Has(t *testing.T) {
args args
want bool
}{
{"ok", mustParse("yubikey:slot-id=9a"), args{"slot-id"}, true},
{"ok empty", mustParse("yubikey:slot-id="), args{"slot-id"}, true},
{"ok query", mustParse("yubikey:pin=123456?slot-id="), args{"slot-id"}, true},
{"ok empty no equal", mustParse("yubikey:slot-id"), args{"slot-id"}, true},
{"ok query no equal", mustParse("yubikey:pin=123456?slot-id"), args{"slot-id"}, true},
{"ok empty no equal other", mustParse("yubikey:slot-id;pin=123456"), args{"slot-id"}, true},
{"ok query no equal other", mustParse("yubikey:pin=123456?slot-id&pin=123456"), args{"slot-id"}, true},
{"fail", mustParse("yubikey:pin=123456"), args{"slot-id"}, false},
{"fail with query", mustParse("yubikey:pin=123456?slot=9a"), args{"slot-id"}, false},
{"ok", mustParse(t, "yubikey:slot-id=9a"), args{"slot-id"}, true},
{"ok empty", mustParse(t, "yubikey:slot-id="), args{"slot-id"}, true},
{"ok query", mustParse(t, "yubikey:pin=123456?slot-id="), args{"slot-id"}, true},
{"ok empty no equal", mustParse(t, "yubikey:slot-id"), args{"slot-id"}, true},
{"ok query no equal", mustParse(t, "yubikey:pin=123456?slot-id"), args{"slot-id"}, true},
{"ok empty no equal other", mustParse(t, "yubikey:slot-id;pin=123456"), args{"slot-id"}, true},
{"ok query no equal other", mustParse(t, "yubikey:pin=123456?slot-id&pin=123456"), args{"slot-id"}, true},
{"fail", mustParse(t, "yubikey:pin=123456"), args{"slot-id"}, false},
{"fail with query", mustParse(t, "yubikey:pin=123456?slot=9a"), args{"slot-id"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -190,13 +192,6 @@ func TestURI_Has(t *testing.T) {
}

func TestURI_Get(t *testing.T) {
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
type args struct {
key string
}
Expand All @@ -206,12 +201,12 @@ func TestURI_Get(t *testing.T) {
args args
want string
}{
{"ok", mustParse("yubikey:slot-id=9a"), args{"slot-id"}, "9a"},
{"ok first", mustParse("yubikey:slot-id=9a;slot-id=9b"), args{"slot-id"}, "9a"},
{"ok multiple", mustParse("yubikey:slot-id=9a;foo=bar"), args{"foo"}, "bar"},
{"ok in query", mustParse("yubikey:slot-id=9a?foo=bar"), args{"foo"}, "bar"},
{"fail missing", mustParse("yubikey:slot-id=9a"), args{"foo"}, ""},
{"fail missing query", mustParse("yubikey:slot-id=9a?bar=zar"), args{"foo"}, ""},
{"ok", mustParse(t, "yubikey:slot-id=9a"), args{"slot-id"}, "9a"},
{"ok first", mustParse(t, "yubikey:slot-id=9a;slot-id=9b"), args{"slot-id"}, "9a"},
{"ok multiple", mustParse(t, "yubikey:slot-id=9a;foo=bar"), args{"foo"}, "bar"},
{"ok in query", mustParse(t, "yubikey:slot-id=9a?foo=bar"), args{"foo"}, "bar"},
{"fail missing", mustParse(t, "yubikey:slot-id=9a"), args{"foo"}, ""},
{"fail missing query", mustParse(t, "yubikey:slot-id=9a?bar=zar"), args{"foo"}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -223,13 +218,6 @@ func TestURI_Get(t *testing.T) {
}

func TestURI_GetBool(t *testing.T) {
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
type args struct {
key string
}
Expand All @@ -239,13 +227,13 @@ func TestURI_GetBool(t *testing.T) {
args args
want bool
}{
{"true", mustParse("azurekms:name=foo;vault=bar;hsm=true"), args{"hsm"}, true},
{"TRUE", mustParse("azurekms:name=foo;vault=bar;hsm=TRUE"), args{"hsm"}, true},
{"tRUe query", mustParse("azurekms:name=foo;vault=bar?hsm=tRUe"), args{"hsm"}, true},
{"false", mustParse("azurekms:name=foo;vault=bar;hsm=false"), args{"hsm"}, false},
{"false query", mustParse("azurekms:name=foo;vault=bar?hsm=false"), args{"hsm"}, false},
{"empty", mustParse("azurekms:name=foo;vault=bar;hsm=?bar=true"), args{"hsm"}, false},
{"missing", mustParse("azurekms:name=foo;vault=bar"), args{"hsm"}, false},
{"true", mustParse(t, "azurekms:name=foo;vault=bar;hsm=true"), args{"hsm"}, true},
{"TRUE", mustParse(t, "azurekms:name=foo;vault=bar;hsm=TRUE"), args{"hsm"}, true},
{"tRUe query", mustParse(t, "azurekms:name=foo;vault=bar?hsm=tRUe"), args{"hsm"}, true},
{"false", mustParse(t, "azurekms:name=foo;vault=bar;hsm=false"), args{"hsm"}, false},
{"false query", mustParse(t, "azurekms:name=foo;vault=bar?hsm=false"), args{"hsm"}, false},
{"empty", mustParse(t, "azurekms:name=foo;vault=bar;hsm=?bar=true"), args{"hsm"}, false},
{"missing", mustParse(t, "azurekms:name=foo;vault=bar"), args{"hsm"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -257,13 +245,6 @@ func TestURI_GetBool(t *testing.T) {
}

func TestURI_GetEncoded(t *testing.T) {
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
type args struct {
key string
}
Expand All @@ -273,15 +254,15 @@ func TestURI_GetEncoded(t *testing.T) {
args args
want []byte
}{
{"ok", mustParse("yubikey:slot-id=9a"), args{"slot-id"}, []byte{0x9a}},
{"ok prefix", mustParse("yubikey:slot-id=0x9a"), args{"slot-id"}, []byte{0x9a}},
{"ok first", mustParse("yubikey:slot-id=9a9b;slot-id=9b"), args{"slot-id"}, []byte{0x9a, 0x9b}},
{"ok percent", mustParse("yubikey:slot-id=9a;foo=%9a%9b%9c"), args{"foo"}, []byte{0x9a, 0x9b, 0x9c}},
{"ok in query", mustParse("yubikey:slot-id=9a?foo=9a"), args{"foo"}, []byte{0x9a}},
{"ok in query percent", mustParse("yubikey:slot-id=9a?foo=%9a"), args{"foo"}, []byte{0x9a}},
{"ok missing", mustParse("yubikey:slot-id=9a"), args{"foo"}, nil},
{"ok missing query", mustParse("yubikey:slot-id=9a?bar=zar"), args{"foo"}, nil},
{"ok no hex", mustParse("yubikey:slot-id=09a?bar=zar"), args{"slot-id"}, []byte{'0', '9', 'a'}},
{"ok", mustParse(t, "yubikey:slot-id=9a"), args{"slot-id"}, []byte{0x9a}},
{"ok prefix", mustParse(t, "yubikey:slot-id=0x9a"), args{"slot-id"}, []byte{0x9a}},
{"ok first", mustParse(t, "yubikey:slot-id=9a9b;slot-id=9b"), args{"slot-id"}, []byte{0x9a, 0x9b}},
{"ok percent", mustParse(t, "yubikey:slot-id=9a;foo=%9a%9b%9c"), args{"foo"}, []byte{0x9a, 0x9b, 0x9c}},
{"ok in query", mustParse(t, "yubikey:slot-id=9a?foo=9a"), args{"foo"}, []byte{0x9a}},
{"ok in query percent", mustParse(t, "yubikey:slot-id=9a?foo=%9a"), args{"foo"}, []byte{0x9a}},
{"ok missing", mustParse(t, "yubikey:slot-id=9a"), args{"foo"}, nil},
{"ok missing query", mustParse(t, "yubikey:slot-id=9a?bar=zar"), args{"foo"}, nil},
{"ok no hex", mustParse(t, "yubikey:slot-id=09a?bar=zar"), args{"slot-id"}, []byte{'0', '9', 'a'}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -294,22 +275,15 @@ func TestURI_GetEncoded(t *testing.T) {
}

func TestURI_Pin(t *testing.T) {
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
tests := []struct {
name string
uri *URI
want string
}{
{"from value", mustParse("pkcs11:id=%72%73?pin-value=0123456789"), "0123456789"},
{"from source", mustParse("pkcs11:id=%72%73?pin-source=testdata/pin.txt"), "trim-this-pin"},
{"from missing", mustParse("pkcs11:id=%72%73"), ""},
{"from source missing", mustParse("pkcs11:id=%72%73?pin-source=testdata/foo.txt"), ""},
{"from value", mustParse(t, "pkcs11:id=%72%73?pin-value=0123456789"), "0123456789"},
{"from source", mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/pin.txt"), "trim-this-pin"},
{"from missing", mustParse(t, "pkcs11:id=%72%73"), ""},
{"from source missing", mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/foo.txt"), ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -321,13 +295,6 @@ func TestURI_Pin(t *testing.T) {
}

func TestURI_String(t *testing.T) {
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
tests := []struct {
name string
uri *URI
Expand All @@ -336,7 +303,7 @@ func TestURI_String(t *testing.T) {
{"ok new", New("yubikey", url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}}), "yubikey:foo=bar;slot-id=9a"},
{"ok newOpaque", NewOpaque("cloudkms", "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"), "cloudkms:projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"},
{"ok newFile", NewFile("/path/to/file.key"), "file:///path/to/file.key"},
{"ok parse", mustParse("yubikey:slot-id=9a;foo=bar?bar=zar"), "yubikey:foo=bar;slot-id=9a?bar=zar"},
{"ok parse", mustParse(t, "yubikey:slot-id=9a;foo=bar?bar=zar"), "yubikey:foo=bar;slot-id=9a?bar=zar"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -349,13 +316,6 @@ func TestURI_String(t *testing.T) {

func TestURI_GetInt(t *testing.T) {
seventy := int64(70)
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
type args struct {
key string
}
Expand All @@ -365,9 +325,9 @@ func TestURI_GetInt(t *testing.T) {
args args
want *int64
}{
{"ok", mustParse("tpmkms:renewal-percentage=70"), args{"renewal-percentage"}, &seventy},
{"ok empty", mustParse("tpmkms:empty"), args{"renewal-percentage"}, nil},
{"ok non-integer", mustParse("tpmkms:renewal-percentage=not-an-integer"), args{"renewal-percentage"}, nil},
{"ok", mustParse(t, "tpmkms:renewal-percentage=70"), args{"renewal-percentage"}, &seventy},
{"ok empty", mustParse(t, "tpmkms:empty"), args{"renewal-percentage"}, nil},
{"ok non-integer", mustParse(t, "tpmkms:renewal-percentage=not-an-integer"), args{"renewal-percentage"}, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -382,12 +342,6 @@ func TestURI_GetInt(t *testing.T) {
}

func TestURI_GetHexEncoded(t *testing.T) {
mustParse := func(t *testing.T, s string) *URI {
t.Helper()
u, err := Parse(s)
require.NoError(t, err)
return u
}
type args struct {
key string
}
Expand Down Expand Up @@ -418,3 +372,47 @@ func TestURI_GetHexEncoded(t *testing.T) {
})
}
}

func TestURI_Read(t *testing.T) {
// Read does not trim the contents of the file
expected := []byte("trim-this-pin \n")

path := filepath.Join(t.TempDir(), "management.key")
require.NoError(t, os.WriteFile(path, expected, 0600))
pinURI := &url.URL{
Scheme: "file",
Path: path,
}
pathURI := &URI{
URL: &url.URL{Scheme: "yubikey"},
Values: url.Values{
"management-key-source": []string{pinURI.String()},
},
}

type args struct {
key string
}
tests := []struct {
name string
uri *URI
args args
want []byte
assertion assert.ErrorAssertionFunc
}{
{"from attribute", mustParse(t, "yubikey:management-key-source=testdata/pin.txt"), args{"management-key-source"}, expected, assert.NoError},
{"from query attribute", mustParse(t, "yubikey:?management-key-source=testdata/pin.txt"), args{"management-key-source"}, expected, assert.NoError},
{"from uri path", pathURI, args{"management-key-source"}, expected, assert.NoError},
{"from uri opaque", mustParse(t, "yubikey:management-key-source=file:testdata/pin.txt"), args{"management-key-source"}, expected, assert.NoError},
{"from empty attribute", mustParse(t, "yubikey:management-source-key="), args{"management-key-source"}, nil, assert.NoError},
{"from missing attribute", mustParse(t, "yubikey:slot-id=82"), args{"management-key-source"}, nil, assert.NoError},
{"from missing file", mustParse(t, "yubikey:management-key-source=testdata/foo.txt"), args{"management-key-source"}, nil, assert.Error},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.uri.Read(tt.args.key)
tt.assertion(t, err)
assert.Equal(t, tt.want, got)
})
}
}
15 changes: 13 additions & 2 deletions kms/yubikey/yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package yubikey

import (
"bytes"
"context"
"crypto"
"crypto/x509"
Expand All @@ -15,6 +16,7 @@ import (
"strconv"
"strings"
"sync"
"unicode"

"github.com/go-piv/piv-go/v2/piv"
"github.com/pkg/errors"
Expand Down Expand Up @@ -85,14 +87,15 @@ func openCard(card string) (pivKey, error) {
// support multiple cards at the same time.
//
// yubikey:management-key=001122334455667788990011223344556677889900112233?pin-value=123456
// yubikey:management-key-source=/var/run/management.key?pin-source=/var/run/yubikey.pin
// yubikey:serial=112233?pin-source=/var/run/yubikey.pin
//
// You can also define a slot id, this will be ignored in this method but can be
// useful on CLI applications.
//
// yubikey:slot-id=9a?pin-value=123456
//
// If the pin or the management-key are not provided, we will use the default
// If the pin or the management key are not provided, we will use the default
// ones.
func New(_ context.Context, opts apiv1.Options) (*YubiKey, error) {
pin := "123456"
Expand All @@ -109,6 +112,14 @@ func New(_ context.Context, opts apiv1.Options) (*YubiKey, error) {
}
if v := u.Get("management-key"); v != "" {
opts.ManagementKey = v
} else if u.Has("management-key-source") {
b, err := u.Read("management-key-source")
if err != nil {
return nil, err
}
if b = bytes.TrimFunc(b, unicode.IsSpace); len(b) > 0 {
opts.ManagementKey = string(b)
}
}
if v := u.Get("serial"); v != "" {
serial = v
Expand All @@ -119,7 +130,7 @@ func New(_ context.Context, opts apiv1.Options) (*YubiKey, error) {
if opts.ManagementKey != "" {
b, err := hex.DecodeString(opts.ManagementKey)
if err != nil {
return nil, errors.Wrap(err, "error decoding managementKey")
return nil, errors.Wrap(err, "error decoding management key")
}
if len(b) != 24 {
return nil, errors.New("invalid managementKey: length is not 24 bytes")
Expand Down
Loading

0 comments on commit f3ae27e

Please sign in to comment.