diff --git a/backend.go b/backend.go index 1830001..f99df47 100644 --- a/backend.go +++ b/backend.go @@ -56,6 +56,7 @@ func Backend() *backend { Help: "", Paths: framework.PathAppend( configPaths(&b), + importPaths(&b), namesPaths(&b), keysPaths(&b), identitiesPaths(&b), diff --git a/path_identities.go b/path_identities.go index 000843b..664f684 100644 --- a/path_identities.go +++ b/path_identities.go @@ -243,40 +243,22 @@ func (b *backend) pathIdentitiesCreate(ctx context.Context, req *logical.Request if err != nil { return nil, err } - publickey, err := pair.PublicKey() - if err != nil { - return nil, err - } - privatekey, err := pair.PrivateKey() - if err != nil { - return nil, err - } - seed, err := pair.Seed() - if err != nil { - return nil, err - } - identityJSON := &Identity{ - PublicKey: publickey, - TrustedKeys: trustedKeys, - PrivateKey: string(privatekey), - Seed: string(seed), - } defer pair.Wipe() - entry, err := logical.StorageEntryJSON(req.Path, identityJSON) + + identity, err := b.storeIdentity(ctx, req, name, pair, trustedKeys) if err != nil { return nil, err } - - err = req.Storage.Put(ctx, entry) + err = b.crossReference(ctx, req, name, identity.PublicKey) if err != nil { return nil, err } - b.crossReference(ctx, req, name, identityJSON.PublicKey) + return &logical.Response{ Data: map[string]interface{}{ - "type": nkeys.Prefix(identityJSON.PublicKey).String(), - "public_key": identityJSON.PublicKey, - "trusted_keys": identityJSON.TrustedKeys, + "type": nkeys.Prefix(identity.PublicKey).String(), + "public_key": identity.PublicKey, + "trusted_keys": identity.TrustedKeys, }, }, nil } @@ -426,13 +408,11 @@ func (b *backend) pathIdentitiesUpdate(ctx context.Context, req *logical.Request if trustedKeysRaw, ok := data.GetOk("trusted_keys"); ok { trustedKeys = trustedKeysRaw.([]string) } - identity.TrustedKeys = trustedKeys - entry, err := logical.StorageEntryJSON(req.Path, identity) + pair, err := nkeys.FromSeed([]byte(identity.Seed)) if err != nil { return nil, err } - - err = req.Storage.Put(ctx, entry) + identity, err = b.storeIdentity(ctx, req, name, pair, trustedKeys) if err != nil { return nil, err } @@ -557,3 +537,36 @@ func (b *backend) pathVerifySignatureByName(ctx context.Context, req *logical.Re }, nil } + +func (b *backend) storeIdentity(ctx context.Context, req *logical.Request, name string, pair nkeys.KeyPair, trustedKeys []string) (*Identity, error) { + publickey, err := pair.PublicKey() + if err != nil { + return nil, err + } + privatekey, err := pair.PrivateKey() + if err != nil { + return nil, err + } + seed, err := pair.Seed() + if err != nil { + return nil, err + } + identity := &Identity{ + PublicKey: publickey, + TrustedKeys: trustedKeys, + PrivateKey: string(privatekey), + Seed: string(seed), + } + path := fmt.Sprintf("identities/%s", name) + + entry, err := logical.StorageEntryJSON(path, identity) + if err != nil { + return nil, err + } + + err = req.Storage.Put(ctx, entry) + if err != nil { + return nil, err + } + return identity, nil +} diff --git a/path_import.go b/path_import.go new file mode 100644 index 0000000..8e94432 --- /dev/null +++ b/path_import.go @@ -0,0 +1,124 @@ +// Copyright © 2018 Immutability, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/nats-io/nkeys" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +const ( + // TypeNkey is a file with the .nk extension + TypeNkey string = ".nk" + // TypeCreds is a file with the .creds extension + TypeCreds string = ".creds" +) + +func importPaths(b *backend) []*framework.Path { + return []*framework.Path{ + &framework.Path{ + Pattern: "import/" + framework.GenericNameRegex("name"), + HelpSynopsis: "Import an nkey from file.", + HelpDescription: ` + +Reads an nkey seed from file. + +`, + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{Type: framework.TypeString}, + "path": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Absolute path to the keystore file - not the parent directory.", + }, + }, + ExistenceCheck: b.pathExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathImportCreate, + }, + }, + } +} + +func (b *backend) pathImportCreate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + _, err := b.configured(ctx, req) + if err != nil { + return nil, err + } + name := data.Get("name").(string) + var identity *Identity + identity, err = b.readIdentity(ctx, req, name) + if err != nil { + return nil, fmt.Errorf("Error reading identity") + } + if identity == nil { + keystorePath := data.Get("path").(string) + fileType := filepath.Ext(keystorePath) + switch fileType { + case TypeNkey: + seed, err := ioutil.ReadFile(keystorePath) + if err != nil { + return nil, err + } + pair, err := nkeys.FromSeed(seed) + if err != nil { + return nil, err + } + identity, err = b.storeIdentity(ctx, req, name, pair, nil) + if err != nil { + return nil, err + } + err = b.crossReference(ctx, req, name, identity.PublicKey) + if err != nil { + return nil, err + } + + case TypeCreds: + _, seed, err := credsFromNkeyFile(keystorePath) + if err != nil { + return nil, err + } + pair, err := nkeys.FromSeed([]byte(seed)) + if err != nil { + return nil, err + } + identity, err = b.storeIdentity(ctx, req, name, pair, nil) + if err != nil { + return nil, err + } + err = b.crossReference(ctx, req, name, identity.PublicKey) + if err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unknown file type") + } + return &logical.Response{ + Data: map[string]interface{}{ + "type": nkeys.Prefix(identity.PublicKey).String(), + "trusted_keys": identity.TrustedKeys, + "public_key": identity.PublicKey, + }, + }, nil + } + return nil, fmt.Errorf("account %s exists", name) +} diff --git a/tests/claims.bats b/tests/claims.bats index d170525..cb2d69d 100644 --- a/tests/claims.bats +++ b/tests/claims.bats @@ -54,6 +54,33 @@ [ "$trusted_keys" = "$account_key" ] } +@test "import ngs account" { + path=$HOME"/.nkeys/synadia/accounts/ngs/ngs.nk" + user="$(vault write -format=json nkey/import/ngs-account path=$path | jq .data)" + type="$(echo $user | jq -r .type)" + [ "$type" = "account" ] +} + +@test "import ngs user" { + path=$HOME"/.nkeys/synadia/accounts/ngs/users/ngs.nk" + account_key="$(vault read -format=json nkey/identities/ngs-account | jq -r .data.public_key)" + user="$(vault write -format=json nkey/import/ngs-user path=$path | jq .data)" + user_update="$(vault write -format=json nkey/identities/ngs-user trusted_keys=$account_key | jq .data)" + type="$(echo $user | jq -r .type)" + [ "$type" = "user" ] +} + +@test "create ngs user claim" { + issuer="$(vault read -format=json nkey/identities/ngs-account | jq -r .data.public_key)" + subject="$(vault read -format=json nkey/identities/ngs-user | jq -r .data.public_key)" + token="$(vault write -format=json nkey/identities/ngs-account/sign-claim subject=$subject type="user" claims=@user.json | jq -r .data.token)" + response="$(vault write -format=json nkey/identities/ngs-user/verify-claim token=$token | jq .data)" + response_issuer="$(echo $response | jq -r .issuer)" + response_subject="$(echo $response | jq -r .public_key)" + [ "$issuer" = "$response_issuer" ] + [ "$subject" = "$response_subject" ] +} + @test "create account claim" { issuer="$(vault read -format=json nkey/identities/operator | jq -r .data.public_key)" subject="$(vault read -format=json nkey/identities/account | jq -r .data.public_key)" diff --git a/util.go b/util.go index bef6f07..f947818 100644 --- a/util.go +++ b/util.go @@ -17,11 +17,18 @@ package main import ( "encoding/json" "fmt" + "io/ioutil" + "regexp" "github.com/nats-io/jwt" "github.com/nats-io/nkeys" ) +const ( + // Empty is an empty string + Empty string = "" +) + // PrefixByteFromString returns a PrefixByte from the stringified value func PrefixByteFromString(p string) nkeys.PrefixByte { switch p { @@ -106,3 +113,44 @@ func encodeClaim(claimsType, claimsData, subject string, keyPair nkeys.KeyPair) } return token, nil } + +var nscDecoratedRe = regexp.MustCompile(`\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}\n))`) + +func credsFromNkeyFile(userFile string) (string, string, error) { + contents, err := ioutil.ReadFile(userFile) + if err != nil { + return "", Empty, fmt.Errorf("nats: %v", err) + } + defer wipeSlice(contents) + + items := nscDecoratedRe.FindAllSubmatch(contents, -1) + if len(items) == 0 { + return "", string(contents), nil + } + // First result should be the user JWT. + // We copy here so that if the file contained a seed file too we wipe appropriately. + var jwt []byte + var nkey []byte + for i, item := range items { + switch i { + case 0: + if len(item) == 2 { + jwt = make([]byte, len(item[1])) + copy(jwt, item[1]) + } + case 1: + if len(item) == 2 { + nkey = make([]byte, len(item[1])) + copy(nkey, item[1]) + } + } + } + return string(jwt), string(nkey), nil +} + +// Just wipe slice with 'x', for clearing contents of nkey seed file. +func wipeSlice(buf []byte) { + for i := range buf { + buf[i] = 'x' + } +}