diff --git a/README.md b/README.md index dae884e..458999f 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,19 @@ [:book: Docs](https://rotx.dev/docs/)\ [:motorway: Roadmap](https://github.com/orgs/candiddev/projects/6/views/31) -Rot is an open source command line (CLI) tool for managing secrets. +Rot is an open source command line (CLI) tool for managing cryptographic values. -Rot makes encrypting and decrypting secrets easy: +Rot makes cryptography easy: - Generate keys and values using current best encryption -- Easily rekey secrets to the latest encryption standards -- Share your secrets with other users and devices -- One-way encryption for production secrets -- Run commands and scripts with secrets injected via environment variables -- Store your secrets securely in git with human-readable diffs -- Easily generate X.509 certificates and Certificate Authorities +- Rekey encrypted values to the latest encryption standards +- Share your encrypted values with other users and devices +- One-way encryption for zero-knowledge secrets +- Run commands and scripts with encrypted values injected via environment variables +- Store your encrypted values securely in git with human-readable diffs +- Generate and view X.509 certificates and Certificate Authorities +- Generate and view JWTs +- Generate SSH keys and certificates Visit https://rotx.dev for more information. diff --git a/go/cmdAddKey.go b/go/cmdAddKey.go index 720d624..2633bfa 100644 --- a/go/cmdAddKey.go +++ b/go/cmdAddKey.go @@ -12,81 +12,92 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdAddKey(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { - e := c.decryptPrivateKey(ctx) - if e != nil { - if e.Is(errNotInitialized) { - return cmdInit(ctx, args, f, c) - } - - return e +func cmdAddKey() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "key name", + }, + ArgumentsOptional: []string{ + "public key, default: generate a PBKDF-protected asymmetric key", + }, + Usage: "Add a new or existing User key to the configuration keys.", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + e := c.decryptPrivateKey(ctx) + if e != nil { + if e.Is(errNotInitialized) { + return cmdInit().Run(ctx, args, f, c) + } + + return e + } + + var err error + + var p string + + var pub cryptolib.Key[cryptolib.KeyProviderPublic] + + n := args[1] + + if len(args) == 3 { + pub, err = cryptolib.ParseKey[cryptolib.KeyProviderPublic](args[2]) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } else { + var prv cryptolib.Key[cryptolib.KeyProviderPrivate] + + prv, pub, err = cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + prv.ID = n + pub.ID = n + + v, err := cryptolib.KDFSet(cryptolib.Argon2ID, prv.ID, []byte(prv.String()), c.Algorithms.Symmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + if v.Ciphertext == "" { + p = prv.String() + } else { + p = v.String() + } + + f, err := os.OpenFile(c.KeyPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(errors.New("error opening keys file"), err)) + } + + defer f.Close() + + if _, err := f.WriteString(p + "\n"); err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(errors.New("error writing keys file"), err)) + } + } + + v, err := pub.Key.EncryptAsymmetric([]byte(c.privateKey.String()), pub.ID, c.Algorithms.Symmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + sig, err := cryptolib.NewSignature(c.privateKey, []byte(pub.String())) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + v.KeyID = c.privateKey.ID + + c.DecryptKeys[n] = cfgDecryptKey{ + Modified: time.Now(), + PrivateKey: v, + PublicKey: pub, + Signature: sig, + } + + return logger.Error(ctx, c.save(ctx)) + }, } - - var err error - - var p string - - var pub cryptolib.Key[cryptolib.KeyProviderPublic] - - n := args[1] - - if len(args) == 3 { - pub, err = cryptolib.ParseKey[cryptolib.KeyProviderPublic](args[2]) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - } else { - var prv cryptolib.Key[cryptolib.KeyProviderPrivate] - - prv, pub, err = cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - prv.ID = n - pub.ID = n - - v, err := cryptolib.KDFSet(cryptolib.Argon2ID, prv.ID, []byte(prv.String()), c.Algorithms.Symmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - if v.Ciphertext == "" { - p = prv.String() - } else { - p = v.String() - } - - f, err := os.OpenFile(c.KeyPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(errors.New("error opening keys file"), err)) - } - - defer f.Close() - - if _, err := f.WriteString(p + "\n"); err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(errors.New("error writing keys file"), err)) - } - } - - v, err := pub.Key.EncryptAsymmetric([]byte(c.privateKey.String()), pub.ID, c.Algorithms.Symmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - sig, err := cryptolib.NewSignature(c.privateKey, []byte(pub.String())) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - v.KeyID = c.privateKey.ID - - c.DecryptKeys[n] = cfgDecryptKey{ - Modified: time.Now(), - PrivateKey: v, - PublicKey: pub, - Signature: sig, - } - - return logger.Error(ctx, c.save(ctx)) } diff --git a/go/cmdAddPrivateKey.go b/go/cmdAddPrivateKey.go index 6cad99f..1965ae6 100644 --- a/go/cmdAddPrivateKey.go +++ b/go/cmdAddPrivateKey.go @@ -9,22 +9,30 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdAddPrivateKey(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { - if c.PublicKey.IsNil() { - return logger.Error(ctx, errNotInitialized) - } +func cmdAddPrivateKey() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "name", + }, + Usage: "Generate and add a cryptographic private key to the configuration values.", + Run: func(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { + if c.PublicKey.IsNil() { + return logger.Error(ctx, errNotInitialized) + } - prv, pub, err := cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } + prv, pub, err := cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } - prv.ID = args[1] - pub.ID = args[1] + prv.ID = args[1] + pub.ID = args[1] - if err := c.encryptvalue(ctx, []byte(prv.String()), args[1], pub.String()); err != nil { - return logger.Error(ctx, err) - } + if err := c.encryptvalue(ctx, []byte(prv.String()), args[1], pub.String()); err != nil { + return logger.Error(ctx, err) + } - return logger.Error(ctx, c.save(ctx)) + return logger.Error(ctx, c.save(ctx)) + }, + } } diff --git a/go/cmdAddValue.go b/go/cmdAddValue.go index ba4f02c..c043247 100644 --- a/go/cmdAddValue.go +++ b/go/cmdAddValue.go @@ -2,37 +2,74 @@ package main import ( "context" + "fmt" + "strconv" "github.com/candiddev/shared/go/cli" "github.com/candiddev/shared/go/errs" "github.com/candiddev/shared/go/logger" + "github.com/candiddev/shared/go/types" ) -func cmdAddValue(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { - if c.PublicKey.IsNil() { - return logger.Error(ctx, errNotInitialized) - } +func cmdAddValue() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "name", + }, + ArgumentsOptional: []string{ + `comment, default: ""`, + }, + Flags: cli.Flags{ + "d": { + Default: []string{`\n`}, + Usage: "Delimiter", + }, + "l": { + Placeholder: "length", + Usage: "Generate a random string with this length instead of providing a value", + }, + }, + Usage: "Add a value to the configuration values.", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + if c.PublicKey.IsNil() { + return logger.Error(ctx, errNotInitialized) + } - name := args[1] - delimiter := "" - comment := "" + name := args[1] + delimiter := "" + comment := "" - if len(args) >= 3 { - comment = args[2] - } + if len(args) >= 3 { + comment = args[2] + } - if v, ok := f.Value("d"); ok { - delimiter = v - } + if v, ok := f.Value("d"); ok { + delimiter = v + } - v, err := cli.Prompt("Value:", delimiter, true) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } + var b []byte - if err := c.encryptvalue(ctx, v, name, comment); err != nil { - return logger.Error(ctx, err) - } + var err error - return logger.Error(ctx, c.save(ctx)) + if v, ok := f.Value("l"); ok { + l, err := strconv.Atoi(v) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing length: %w", err))) + } + + b = []byte(types.RandString(l)) + } else { + b, err = cli.Prompt("Value:", delimiter, true) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } + + if err := c.encryptvalue(ctx, b, name, comment); err != nil { + return logger.Error(ctx, err) + } + + return logger.Error(ctx, c.save(ctx)) + }, + } } diff --git a/go/cmdDecrypt.go b/go/cmdDecrypt.go index 6ae7fde..5d63ae3 100644 --- a/go/cmdDecrypt.go +++ b/go/cmdDecrypt.go @@ -9,26 +9,34 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdDecrypt(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { - c.decryptKeysEncrypted(ctx) - - value := args[1] - - if value == "-" { - value = string(cli.ReadStdin()) - } - - ev, err := cryptolib.ParseEncryptedValue(value) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) +func cmdDecrypt() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "value, or - for stdin", + }, + Usage: "Decrypt a value and print it to stdout.", + Run: func(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { + c.decryptKeysEncrypted(ctx) + + value := args[1] + + if value == "-" { + value = string(cli.ReadStdin()) + } + + ev, err := cryptolib.ParseEncryptedValue(value) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + v, err := ev.Decrypt(c.keys.Keys()) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + logger.Raw(string(v)) + + return logger.Error(ctx, nil) + }, } - - v, err := ev.Decrypt(c.keys.Keys()) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - logger.Raw(string(v)) - - return logger.Error(ctx, nil) } diff --git a/go/cmdEncrypt.go b/go/cmdEncrypt.go index 2b1972a..aee102f 100644 --- a/go/cmdEncrypt.go +++ b/go/cmdEncrypt.go @@ -9,43 +9,57 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdEncrypt(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { - r := "" - delimiter := "" +func cmdEncrypt() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsOptional: []string{ + "recipient key, optional", + }, + Flags: cli.Flags{ + "d": { + Default: []string{`\n`}, + Usage: "Delimiter", + }, + }, + Usage: "Encrypt a value and print it to stdout. Can specify a recipient key to use asymmetric encryption.", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + r := "" + delimiter := "" - if len(args) >= 2 { - r = args[1] - } + if len(args) >= 2 { + r = args[1] + } - if v, ok := f.Value("d"); ok { - delimiter = v - } + if v, ok := f.Value("d"); ok { + delimiter = v + } - v, err := cli.Prompt("Value:", delimiter, true) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } + v, err := cli.Prompt("Value:", delimiter, true) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } - var ev cryptolib.EncryptedValue - - if r == "" { - ev, err = cryptolib.KDFSet(cryptolib.Argon2ID, "decrypt", v, c.Algorithms.Symmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - } else { - key, err := cryptolib.ParseKey[cryptolib.KeyProviderPublic](r) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - ev, err = key.Key.EncryptAsymmetric(v, key.ID, c.Algorithms.Symmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - } + var ev cryptolib.EncryptedValue - logger.Raw(ev.String() + "\n") + if r == "" { + ev, err = cryptolib.KDFSet(cryptolib.Argon2ID, "decrypt", v, c.Algorithms.Symmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } else { + key, err := cryptolib.ParseKey[cryptolib.KeyProviderPublic](r) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } - return logger.Error(ctx, nil) + ev, err = key.Key.EncryptAsymmetric(v, key.ID, c.Algorithms.Symmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } + + logger.Raw(ev.String() + "\n") + + return logger.Error(ctx, nil) + }, + } } diff --git a/go/cmdGenerateCertificate.go b/go/cmdGenerateCertificate.go index 854d332..13f2752 100644 --- a/go/cmdGenerateCertificate.go +++ b/go/cmdGenerateCertificate.go @@ -3,8 +3,6 @@ package main import ( "context" "crypto/x509" - "errors" - "fmt" "os" "strconv" "strings" @@ -15,99 +13,122 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdGenerateCertificate(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { - pk := args[1] - if pk == "-" { - pk = string(cli.ReadStdin()) +func cmdGenerateCertificate() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "private key value, encrypted value name, or - for stdin", + }, + ArgumentsOptional: []string{ + "public key", + "ca certificate or path", + }, + Flags: cli.Flags{ + "c": { + Usage: "Create a CA certificate", + }, + "d": { + Placeholder: "hostname", + Usage: "DNS hostname (can be used multiple times)", + }, + "e": { + Default: []string{"31536000"}, + Placeholder: "seconds", + Usage: "Expiration in seconds", + }, + "eu": { + Default: []string{"clientAuth", "serverAuth"}, + Placeholder: "extended key usage", + Usage: "Extended key usage, valid values: " + strings.Join(cryptolib.ValidX509ExtKeyUsages(), ", "), + }, + "i": { + Placeholder: "address", + Usage: "IP address (can be used multiple times)", + }, + "ku": { + Default: []string{"digitalSignature"}, + Placeholder: "key usage", + Usage: "Key usage, valid values: " + strings.Join(cryptolib.ValidX509KeyUsages(), ", "), + }, + "n": { + Placeholder: "name", + Usage: "Common Name (CN)", + }, + }, + Usage: "Generate an X.509 certificate and output a PEM-formatted certificate to stdout. Must specify the private key of the signer (for CA signed certificates) or the private key of the certificate (for self-signed certificates). A public key can be specified, otherwise the public key of the private key will be used.", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + pk := args[1] + if pk == "-" { + pk = string(cli.ReadStdin()) + } + + privateKey, errr := c.decryptValuePrivateKey(ctx, pk) + if errr != nil { + return logger.Error(ctx, errr) + } + + var publicKey cryptolib.Key[cryptolib.KeyProviderPublic] + + if len(args) >= 3 { + publicKey, _ = cryptolib.ParseKey[cryptolib.KeyProviderPublic](args[2]) + } + + var ca *x509.Certificate + + if len(args) == 4 { + var key cryptolib.Key[cryptolib.X509Certificate] + + k := args[3] + + f, err := os.ReadFile(k) + if err == nil { + k = string(f) + } + + key, err = cryptolib.ParseKey[cryptolib.X509Certificate](k) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + ca, err = key.Key.Certificate() + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } + + _, isCA := f.Value("c") + + dns, _ := f.Values("d") + + var expires int + + e, _ := f.Value("e") + if i, err := strconv.Atoi(e); err == nil { + expires = i + } + + eu, _ := f.Values("eu") + ku, _ := f.Values("ku") + ips, _ := f.Values("i") + + cn, _ := f.Value("n") + + crt, err := cryptolib.NewX509Certificate(privateKey, publicKey, cn, cryptolib.NewX509CertificateOpts{ + CACertificate: ca, + DNSNames: dns, + ExtKeyUsages: eu, + IPAddresses: ips, + IsCA: isCA, + KeyUsages: ku, + NotAfterSec: expires, + }) + + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + logger.Raw(string(cryptolib.KeyToPEM(crt))) + + return nil + }, } - - if len(strings.Split(pk, ":")) == 1 { - if err := c.decryptPrivateKey(ctx); err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - v, err := c.decryptValue(ctx, pk) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - pk = string(v) - } - - privateKey, err := cryptolib.ParseKey[cryptolib.KeyProviderPrivate](pk) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(errors.New("error parsing private key"), err)) - } - - var publicKey cryptolib.Key[cryptolib.KeyProviderPublic] - - if len(args) >= 3 { - publicKey, _ = cryptolib.ParseKey[cryptolib.KeyProviderPublic](args[2]) - } - - var ca *x509.Certificate - - if len(args) == 4 { - var key cryptolib.Key[cryptolib.X509Certificate] - - k := args[3] - - f, err := os.ReadFile(k) - if err == nil { - k = string(f) - } - - switch { - case strings.HasPrefix(k, "----"): - key, err = cryptolib.PEMToKey[cryptolib.X509Certificate]([]byte(k)) - case len(strings.Split(k, ":")) >= 3: - key, err = cryptolib.ParseKey[cryptolib.X509Certificate](k) - default: - return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing CA certificate: %w", err))) - } - - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - ca, err = key.Key.Certificate() - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - } - - _, isCA := f.Value("c") - - dns, _ := f.Values("d") - - var expires int - - e, _ := f.Value("e") - if i, err := strconv.Atoi(e); err == nil { - expires = i - } - - eu, _ := f.Values("eu") - ku, _ := f.Values("ku") - ips, _ := f.Values("i") - - cn, _ := f.Value("n") - - crt, err := cryptolib.NewX509Certificate(privateKey, publicKey, cn, cryptolib.NewX509CertificateOpts{ - CACertificate: ca, - DNSNames: dns, - ExtKeyUsages: eu, - IPAddresses: ips, - IsCA: isCA, - KeyUsages: ku, - NotAfterSec: expires, - }) - - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - logger.Raw(string(cryptolib.KeyToPEM(crt))) - - return nil } diff --git a/go/cmdGenerateJWT.go b/go/cmdGenerateJWT.go new file mode 100644 index 0000000..5a7b96d --- /dev/null +++ b/go/cmdGenerateJWT.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/candiddev/shared/go/cli" + "github.com/candiddev/shared/go/errs" + "github.com/candiddev/shared/go/jwt" + "github.com/candiddev/shared/go/logger" + "github.com/google/uuid" +) + +func cmdGenerateJWT() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "private key value, encrypted value name, or - for stdin", + }, + Flags: cli.Flags{ + "a": { + Placeholder: "audience", + Usage: "Audience (aud) for JWT. Can be provided multiple times", + }, + "e": { + Default: []string{"3600"}, + Placeholder: "seconds", + Usage: "Expiration (exp) in seconds", + }, + "f": { + Placeholder: "key=value", + Usage: "Add a key and value to the JWT. Will attempt to parse bools and ints unless they are quoted. Can be provided multiple times.", + }, + "id": { + Placeholder: "id", + Usage: "ID (jti) of the JWT, will generate a UUID if not provided", + }, + "is": { + Default: []string{"Rot"}, + Placeholder: "issuer", + Usage: "Issuer (iss) of the JWT", + }, + "s": { + Placeholder: "subject", + Usage: "Subject (sub) of the JWT", + }, + }, + Usage: "Generate a JWT and output it it to stdout. Must specify the private key to sign the JWT.", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + m := map[string]any{} + + aud, _ := f.Values("a") + + var expires time.Time + + e, _ := f.Value("e") + if e != "" { + var err error + + s, err := strconv.Atoi(e) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing -e: %w", err))) + } + + expires = time.Now().Add(time.Second * time.Duration(s)) + } + + id, _ := f.Value("id") + if id == "" { + id = uuid.New().String() + } + + issuer, _ := f.Value("is") + subject, _ := f.Value("s") + + vals, _ := f.Values("f") + + for i := range vals { + s := strings.Split(vals[i], "=") + if len(s) <= 1 { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing field: %s: invalid format, must be key=value", vals[i]))) + } + + value := strings.Join(s[1:], "=") + + if strings.HasPrefix(value, `"`) { + str, err := strconv.Unquote(value) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing field: %s: %w", vals[i], err))) + } + + m[s[0]] = str + } else if b, err := strconv.ParseBool(value); err == nil { + m[s[0]] = b + } else if i, err := strconv.Atoi(value); err == nil { + m[s[0]] = i + } else { + m[s[0]] = value + } + } + + j, _, err := jwt.New(m, expires, aud, id, issuer, subject) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + pk := args[1] + if pk == "-" { + pk = string(cli.ReadStdin()) + } + + privateKey, errr := c.decryptValuePrivateKey(ctx, pk) + if errr != nil { + return logger.Error(ctx, errr) + } + + if err := j.Sign(privateKey); err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + logger.Raw(j.String()) + + return nil + }, + } +} diff --git a/go/cmdGenerateSSH.go b/go/cmdGenerateSSH.go new file mode 100644 index 0000000..ee3ad31 --- /dev/null +++ b/go/cmdGenerateSSH.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + + "github.com/candiddev/shared/go/cli" + "github.com/candiddev/shared/go/cryptolib" + "github.com/candiddev/shared/go/errs" + "github.com/candiddev/shared/go/logger" +) + +func cmdGenerateSSH() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "private key value, encrypted value name, or - for stdin", + "public key value, encrypted value name, or path", + }, + Flags: cli.Flags{ + "c": { + Placeholder: "option=value", + Usage: "Critical options to add to SSH certificate, can be specified multiple times", + }, + "e": { + Placeholder: "extension=value", + Usage: "Extensions to add to SSH certificate, can be specified multiple times", + }, + "h": { + Usage: "Create a host certificate (default: user certificate)", + }, + "i": { + Placeholder: "id", + Usage: "Key ID", + }, + "p": { + Placeholder: "principal", + Usage: "Valid principals to add to SSH certificate, can be specified multiple times", + }, + "v": { + Default: []string{"3600"}, + Placeholder: "seconds", + Usage: "Seconds until certificate expires", + }, + }, + Usage: "Generate SSH certificate and output a SSH formatted certificate. Must specify a Private Key, a Public Key, and a list of principals. Can provide additional fields for the certificate", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + pk := args[1] + if pk == "-" { + pk = string(cli.ReadStdin()) + } + + privateKey, errr := c.decryptValuePrivateKey(ctx, pk) + if errr != nil { + return logger.Error(ctx, errr) + } + + k := args[2] + + fi, err := os.ReadFile(k) + if err == nil { + k = string(fi) + } + + publicKey, err := cryptolib.ParseKey[cryptolib.KeyProviderPublic](k) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + opts := cryptolib.SSHSignOpts{ + CriticalOptions: map[string]string{}, + Extensions: map[string]string{}, + } + + criticalOptions, _ := f.Values("c") + + for _, o := range criticalOptions { + s := strings.Split(o, "=") + v := "" + if len(s) > 1 { + v = strings.Join(s[1:], "=") + } + + opts.CriticalOptions[s[0]] = v + } + + extensions, _ := f.Values("e") + + for _, o := range extensions { + s := strings.Split(o, "=") + v := "" + if len(s) > 1 { + v = strings.Join(s[1:], "=") + } + + opts.Extensions[s[0]] = v + } + + _, opts.TypeHost = f.Values("h") + opts.KeyID, _ = f.Value("i") + opts.ValidPrincipals, _ = f.Values("p") + + valid, _ := f.Value("v") + i, err := strconv.Atoi(valid) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing v flag: %w", err))) + } + + opts.ValidBeforeSec = i + + crt, err := cryptolib.SSHSign(privateKey, publicKey, opts) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + logger.Raw(string(crt)) + + return nil + }, + } +} diff --git a/go/cmdGenerateValue.go b/go/cmdGenerateValue.go deleted file mode 100644 index f204f9d..0000000 --- a/go/cmdGenerateValue.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "context" - "errors" - "strconv" - - "github.com/candiddev/shared/go/cli" - "github.com/candiddev/shared/go/errs" - "github.com/candiddev/shared/go/logger" - "github.com/candiddev/shared/go/types" -) - -func cmdGenerateValue(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { - name := args[1] - length := 20 - comment := "" - - if len(args) >= 3 { - l, err := strconv.Atoi(args[2]) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(errors.New("error parsing length"), err)) - } - - length = l - } - - if v, ok := f.Value("l"); ok { - if i, err := strconv.Atoi(v); err == nil { - length = i - } - } - - if len(args) >= 4 { - comment = args[3] - } - - v := types.RandString(length) - - if err := c.encryptvalue(ctx, []byte(v), name, comment); err != nil { - return logger.Error(ctx, err) - } - - return logger.Error(ctx, c.save(ctx)) -} diff --git a/go/cmdInit.go b/go/cmdInit.go index de68bdf..367f14b 100644 --- a/go/cmdInit.go +++ b/go/cmdInit.go @@ -11,69 +11,80 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdInit(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { - c.DecryptKeys = map[string]cfgDecryptKey{} - c.Values = map[string]cfgValue{} +func cmdInit() cli.Command[*cfg] { //nolint:gocognit + return cli.Command[*cfg]{ + ArgumentsOptional: []string{ + "key name or id of an existing key", + "initial public key, default: generate a PBKDF symmetric key", + }, + Usage: "Initialize a new Rot configuration. Will look for a .rot-keys file and use the first available key if none specified as the initial user key.", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + c.DecryptKeys = map[string]cfgDecryptKey{} + c.Values = map[string]cfgValue{} + + if _, err := os.ReadFile(c.CLI.ConfigPath); err == nil { + b, err := cli.Prompt(fmt.Sprintf("%s aleady exists, overwite (yes/no)?", c.CLI.ConfigPath), "", false) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } - if _, err := os.ReadFile(c.CLI.ConfigPath); err == nil { - b, err := cli.Prompt(fmt.Sprintf("%s aleady exists, overwite (yes/no)?", c.CLI.ConfigPath), "", false) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } + if string(b) != "yes" { + logger.Raw("Canceling...\n") - if string(b) != "yes" { - logger.Raw("Canceling...\n") + return nil + } + } - return nil - } - } + c.decryptKeysEncrypted(ctx) - c.decryptKeysEncrypted(ctx) + var id string - var id string + var key cryptolib.KeyProviderPublic - var key cryptolib.KeyProviderPublic + var err error - var err error + if len(c.keys) > 0 { + if len(args) > 1 { + for i := range c.keys { + if c.keys[i].ID == args[1] { + id = c.keys[i].ID + key, err = c.keys[i].Key.Public() - if len(c.keys) > 0 { - if len(args) > 1 { - for i := range c.keys { - if c.keys[i].ID == args[1] { - id = c.keys[i].ID - key, err = c.keys[i].Key.Public() + break + } + } + } else { + id = c.keys[0].ID + key, err = c.keys[0].Key.Public() + } - break + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) } } - } else { - id = c.keys[0].ID - key, err = c.keys[0].Key.Public() - } - - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - } - if key != nil { - args = []string{ - args[0], - id, - cryptolib.Key[cryptolib.KeyProviderPublic]{ - ID: id, - Key: key, - }.String(), - } - } + if key != nil { + args = []string{ + args[0], + id, + cryptolib.Key[cryptolib.KeyProviderPublic]{ + ID: id, + Key: key, + }.String(), + } + } else if len(args) == 1 { + args = append(args, "rot") + } - prv, pub, err := cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } + prv, pub, err := cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } - c.privateKey = prv - c.PublicKey = pub + c.privateKey = prv + c.PublicKey = pub - return logger.Error(ctx, cmdAddKey(ctx, args, f, c)) + return logger.Error(ctx, cmdAddKey().Run(ctx, args, f, c)) + }, + } } diff --git a/go/cmdPEM.go b/go/cmdPEM.go index 77898d7..03ce3b4 100644 --- a/go/cmdPEM.go +++ b/go/cmdPEM.go @@ -10,33 +10,47 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdPEM(ctx context.Context, args []string, f cli.Flags, _ *cfg) errs.Err { - s := []byte(args[1]) - if args[1] == "-" { - s = cli.ReadStdin() +func cmdPEM() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "key, or - for stdin", + }, + Flags: cli.Flags{ + "i": { + Placeholder: "id", + Usage: "Add id to the key imported from a PEM", + }, + }, + Usage: "Convert Rot keys to PEM, or a PEM keys to Rot, and print it to stdout.", + Run: func(ctx context.Context, args []string, f cli.Flags, _ *cfg) errs.Err { + s := []byte(args[1]) + if args[1] == "-" { + s = cli.ReadStdin() + } + + var out string + + if strings.HasPrefix(string(s), "--") { + s, err := cryptolib.PEMToKey[cryptolib.KeyProvider](s) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + s.ID, _ = f.Value("i") + + out = s.String() + "\n" + } else { + k, err := cryptolib.ParseKey[cryptolib.KeyProvider](string(s)) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + out = string(cryptolib.KeyToPEM(k)) + } + + logger.Raw(out) + + return nil + }, } - - var out string - - if strings.HasPrefix(string(s), "--") { - s, err := cryptolib.PEMToKey[cryptolib.KeyProvider](s) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - s.ID, _ = f.Value("i") - - out = s.String() + "\n" - } else { - k, err := cryptolib.ParseKey[cryptolib.KeyProvider](string(s)) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - out = string(cryptolib.KeyToPEM(k)) - } - - logger.Raw(out) - - return nil } diff --git a/go/cmdRekey.go b/go/cmdRekey.go index 79b1c61..747e540 100644 --- a/go/cmdRekey.go +++ b/go/cmdRekey.go @@ -10,47 +10,52 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdRekey(ctx context.Context, _ []string, _ cli.Flags, c *cfg) errs.Err { - e := c.decryptPrivateKey(ctx) - if e != nil { - return e +func cmdRekey() cli.Command[*cfg] { + return cli.Command[*cfg]{ + Usage: "Rekey all keys and values in a configuration.", + Run: func(ctx context.Context, _ []string, _ cli.Flags, c *cfg) errs.Err { + e := c.decryptPrivateKey(ctx) + if e != nil { + return e + } + + prv, pub, err := cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + c.PublicKey = pub + + for k, v := range c.DecryptKeys { + p, err := c.DecryptKeys[k].PublicKey.Key.EncryptAsymmetric([]byte(prv.String()), c.DecryptKeys[k].PublicKey.ID, c.Algorithms.Symmetric) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + sig, err := cryptolib.NewSignature(prv, []byte(v.PublicKey.String())) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + v.Modified = time.Now() + v.PrivateKey = p + v.Signature = sig + + c.DecryptKeys[k] = v + } + + for k := range c.Values { + v, err := c.decryptValue(ctx, k) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + if err := c.encryptvalue(ctx, v, k, c.Values[k].Comment); err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } + + return logger.Error(ctx, c.save(ctx)) + }, } - - prv, pub, err := cryptolib.NewKeysAsymmetric(c.Algorithms.Asymmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - c.PublicKey = pub - - for k, v := range c.DecryptKeys { - p, err := c.DecryptKeys[k].PublicKey.Key.EncryptAsymmetric([]byte(prv.String()), c.DecryptKeys[k].PublicKey.ID, c.Algorithms.Symmetric) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - sig, err := cryptolib.NewSignature(prv, []byte(v.PublicKey.String())) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - v.Modified = time.Now() - v.PrivateKey = p - v.Signature = sig - - c.DecryptKeys[k] = v - } - - for k := range c.Values { - v, err := c.decryptValue(ctx, k) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - - if err := c.encryptvalue(ctx, v, k, c.Values[k].Comment); err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - } - - return logger.Error(ctx, c.save(ctx)) } diff --git a/go/cmdRemove.go b/go/cmdRemove.go index f31282c..8a32bdf 100644 --- a/go/cmdRemove.go +++ b/go/cmdRemove.go @@ -9,23 +9,36 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdRemove(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { - n := args[1] - - switch args[0] { - case "remove-key": - if _, ok := c.DecryptKeys[n]; !ok { - return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("decryptKey not found with name: %s", n))) - } - - delete(c.DecryptKeys, n) - case "remove-value": - if _, ok := c.Values[n]; !ok { - return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("value not found with name: %s", n))) - } - - delete(c.Values, n) +func cmdRemove(key bool) cli.Command[*cfg] { + t := "value" + if key { + t = "key" } - return c.save(ctx) + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "name", + }, + Usage: fmt.Sprintf("Remove a %s from the configuration.", t), + Run: func(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { + n := args[1] + + switch args[0] { + case "remove-key": + if _, ok := c.DecryptKeys[n]; !ok { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("decryptKey not found with name: %s", n))) + } + + delete(c.DecryptKeys, n) + case "remove-value": + if _, ok := c.Values[n]; !ok { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("value not found with name: %s", n))) + } + + delete(c.Values, n) + } + + return c.save(ctx) + }, + } } diff --git a/go/cmdRun.go b/go/cmdRun.go index 0a7d1d6..6478120 100644 --- a/go/cmdRun.go +++ b/go/cmdRun.go @@ -9,54 +9,62 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdRun(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { - err := c.decryptPrivateKey(ctx) - if err != nil { - return logger.Error(ctx, err) - } +func cmdRun() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "command", + }, + Usage: "Run a command and inject configuration values as environment variables. Values written to stderr/stdout will be masked with ***.", + Run: func(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { + err := c.decryptPrivateKey(ctx) + if err != nil { + return logger.Error(ctx, err) + } - env := []string{} - mask := []string{} + env := []string{} + mask := []string{} - for k := range c.Values { - v, err := c.decryptValue(ctx, k) - if err != nil { - return logger.Error(ctx, err) - } + for k := range c.Values { + v, err := c.decryptValue(ctx, k) + if err != nil { + return logger.Error(ctx, err) + } + + m := true + + for i := range c.Unmask { + if k == c.Unmask[i] { + m = false - m := true + break + } + } - for i := range c.Unmask { - if k == c.Unmask[i] { - m = false + if m { + mask = append(mask, string(v)) + } - break + env = append(env, fmt.Sprintf("%s=%s", k, v)) } - } - if m { - mask = append(mask, string(v)) - } + stderr := logger.NewMaskLogger(logger.Stderr, mask) + stdout := logger.NewMaskLogger(logger.Stdout, mask) - env = append(env, fmt.Sprintf("%s=%s", k, v)) - } + out, err := c.CLI.Run(ctx, cli.RunOpts{ + Args: args[2:], + Command: args[1], + Environment: env, + EnvironmentInherit: true, + Stderr: stderr, + Stdout: stdout, + }) - stderr := logger.NewMaskLogger(logger.Stderr, mask) - stdout := logger.NewMaskLogger(logger.Stdout, mask) - - out, err := c.CLI.Run(ctx, cli.RunOpts{ - Args: args[2:], - Command: args[1], - Environment: env, - EnvironmentInherit: true, - Stderr: stderr, - Stdout: stdout, - }) - - o := out.String() - if o != "" { - logger.Raw(out.String() + "\n") - } + o := out.String() + if o != "" { + logger.Raw(out.String() + "\n") + } - return logger.Error(ctx, err) + return logger.Error(ctx, err) + }, + } } diff --git a/go/cmdSSH.go b/go/cmdSSH.go new file mode 100644 index 0000000..e4d6968 --- /dev/null +++ b/go/cmdSSH.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "strings" + + "github.com/candiddev/shared/go/cli" + "github.com/candiddev/shared/go/cryptolib" + "github.com/candiddev/shared/go/errs" + "github.com/candiddev/shared/go/logger" +) + +func cmdSSH() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "key, or - for stdin", + }, + Flags: cli.Flags{ + "i": { + Placeholder: "id", + Usage: "Add id to the key imported from a SSH key", + }, + }, + Usage: "Convert Rot keys to SSH, or SSH keys to Rot, and print it to stdout.", + Run: func(ctx context.Context, args []string, f cli.Flags, _ *cfg) errs.Err { + s := []byte(args[1]) + if args[1] == "-" { + s = cli.ReadStdin() + } + + var out []byte + + if strings.HasPrefix(string(s), "--") || strings.HasPrefix(string(s), "ssh-") { + s, err := cryptolib.SSHToKey[cryptolib.KeyProvider](s) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + s.ID, _ = f.Value("i") + + out = []byte(s.String() + "\n") + } else { + k, err := cryptolib.ParseKey[cryptolib.KeyProvider](string(s)) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + out, err = cryptolib.KeyToSSH(k) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } + + logger.Raw(string(out)) + + return nil + }, + } +} diff --git a/go/cmdShowAlgorithms.go b/go/cmdShowAlgorithms.go index 16c8898..8d8723b 100644 --- a/go/cmdShowAlgorithms.go +++ b/go/cmdShowAlgorithms.go @@ -8,13 +8,18 @@ import ( "github.com/candiddev/shared/go/errs" ) -func cmdAlgorithms(_ context.Context, _ []string, _ cli.Flags, _ *cfg) errs.Err { - return cli.Print(map[string]any{ - "asymmetric": cryptolib.EncryptionAsymmetric, - "asymmetricBest": cryptolib.BestEncryptionAsymmetric, - "pbkdf": cryptolib.ValidPBKDF, - "pbkdfBest": cryptolib.KDFArgon2ID, - "symmetric": cryptolib.EncryptionSymmetric, - "symmetricBest": cryptolib.BestEncryptionSymmetric, - }) +func cmdAlgorithms() cli.Command[*cfg] { + return cli.Command[*cfg]{ + Usage: "Show algorithms Rot understands.", + Run: func(_ context.Context, _ []string, _ cli.Flags, _ *cfg) errs.Err { + return cli.Print(map[string]any{ + "asymmetric": cryptolib.EncryptionAsymmetric, + "asymmetricBest": cryptolib.BestEncryptionAsymmetric, + "pbkdf": cryptolib.ValidPBKDF, + "pbkdfBest": cryptolib.KDFArgon2ID, + "symmetric": cryptolib.EncryptionSymmetric, + "symmetricBest": cryptolib.BestEncryptionSymmetric, + }) + }, + } } diff --git a/go/cmdShowCertificate.go b/go/cmdShowCertificate.go new file mode 100644 index 0000000..f21fb95 --- /dev/null +++ b/go/cmdShowCertificate.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "crypto/x509" + "fmt" + "os" + + "github.com/candiddev/shared/go/cli" + "github.com/candiddev/shared/go/cryptolib" + "github.com/candiddev/shared/go/errs" + "github.com/candiddev/shared/go/logger" + "github.com/candiddev/shared/go/types" +) + +func cmdShowCertificate() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "Certificate value, path, or - for stdin", + }, + ArgumentsOptional: []string{ + "CA certificate value or path, can be specified multiple times", + }, + Usage: "Show a certificate, optionally validating the certificate with CA public keys.", + Run: func(ctx context.Context, args []string, flags cli.Flags, config *cfg) errs.Err { + cs := args[1] + if cs == "-" { + cs = string(cli.ReadStdin()) + } + + f, err := os.ReadFile(cs) + if err == nil { + cs = string(f) + } + + key, err := cryptolib.ParseKey[cryptolib.X509Certificate](cs) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + crt, err := key.Key.Certificate() + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + var e errs.Err + + if len(args) > 2 { + roots := x509.NewCertPool() + + for i := range args[2:] { + k := args[i+2] + f, err := os.ReadFile(k) + if err == nil { + k = string(f) + } + + key, err = cryptolib.ParseKey[cryptolib.X509Certificate](k) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing CA certificate: %w", err))) + } + + x, err := key.Key.Certificate() + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(fmt.Errorf("error parsing CA certificate: %w", err))) + } + + roots.AddCert(x) + } + + _, err = crt.Verify(x509.VerifyOptions{ + KeyUsages: []x509.ExtKeyUsage{ + x509.ExtKeyUsageAny, + }, + Roots: roots, + }) + + if err != nil { + e = errs.ErrReceiver.Wrap(err) + } + } + + logger.Raw(types.JSONToString(crt)) + + return logger.Error(ctx, e) + }, + } +} diff --git a/go/cmdShowJWT.go b/go/cmdShowJWT.go new file mode 100644 index 0000000..c1cd994 --- /dev/null +++ b/go/cmdShowJWT.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + + "github.com/candiddev/shared/go/cli" + "github.com/candiddev/shared/go/cryptolib" + "github.com/candiddev/shared/go/errs" + "github.com/candiddev/shared/go/jwt" + "github.com/candiddev/shared/go/logger" +) + +func cmdShowJWT() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "JWT, or - for stdin", + }, + ArgumentsOptional: []string{ + "public key value, can be specified multiple times", + }, + Usage: "Show a JWT, optionally validating the signature with a public key.", + Run: func(ctx context.Context, args []string, flags cli.Flags, config *cfg) errs.Err { + j := args[1] + if j == "-" { + j = string(cli.ReadStdin()) + } + + keys := cryptolib.Keys[cryptolib.KeyProviderPublic]{} + + if len(args) > 2 { + for i := range args[2:] { + key, err := cryptolib.ParseKey[cryptolib.KeyProviderPublic](args[i+2]) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + keys = append(keys, key) + } + } + + t, _, err := jwt.Parse(j, keys) + if err != nil { + logger.Error(ctx, errs.ErrReceiver.Wrap(err)) //nolint:errcheck + } + + if t != nil { + s, errr := t.Values() + logger.Raw(s + "\n") + + if errr != nil { + err = errr + } + } + + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + return nil + }, + } +} diff --git a/go/cmdShowPrivateKey.go b/go/cmdShowPrivateKey.go deleted file mode 100644 index a86fe6b..0000000 --- a/go/cmdShowPrivateKey.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "context" - - "github.com/candiddev/shared/go/cli" - "github.com/candiddev/shared/go/errs" - "github.com/candiddev/shared/go/logger" -) - -func cmdShowPrivateKey(ctx context.Context, _ []string, _ cli.Flags, c *cfg) errs.Err { - if err := c.decryptPrivateKey(ctx); err != nil { - return logger.Error(ctx, err) - } - - logger.Raw(c.privateKey.String()) - - return logger.Error(ctx, nil) -} diff --git a/go/cmdShowPublicKey.go b/go/cmdShowPublicKey.go index 526948b..99f764c 100644 --- a/go/cmdShowPublicKey.go +++ b/go/cmdShowPublicKey.go @@ -10,56 +10,64 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdShowPublicKey(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { - var err error +func cmdShowPublicKey() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "name, private key, or - for stdin", + }, + Usage: "Show the public key of a private key.", + Run: func(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { + var err error - var key cryptolib.Key[cryptolib.KeyProviderPrivate] + var key cryptolib.Key[cryptolib.KeyProviderPrivate] - var s string + var s string - switch { - // Stdin - case args[1] == "-": - s = string(cli.ReadStdin()) + switch { + // Stdin + case args[1] == "-": + s = string(cli.ReadStdin()) - fallthrough - // commandline - case len(strings.Split(args[1], ":")) >= 3: - if s == "" { - s = args[1] - } + fallthrough + // commandline + case len(strings.Split(args[1], ":")) >= 3: + if s == "" { + s = args[1] + } - key, err = cryptolib.ParseKey[cryptolib.KeyProviderPrivate](s) - if err != nil { - return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) - } - } + key, err = cryptolib.ParseKey[cryptolib.KeyProviderPrivate](s) + if err != nil { + return logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + } - if key.Key == nil { - c.decryptKeysEncrypted(ctx) + if key.Key == nil { + c.decryptKeysEncrypted(ctx) - n := args[1] + n := args[1] - for i := range c.keys { - if c.keys[i].ID == n { - key = c.keys[i] + for i := range c.keys { + if c.keys[i].ID == n { + key = c.keys[i] - break + break + } + } } - } - } - if key.Key != nil { - pub, err := key.Key.Public() - if err == nil { - logger.Raw(cryptolib.Key[cryptolib.KeyProviderPublic]{ - ID: key.ID, - Key: pub, - }.String() + "\n") + if key.Key != nil { + pub, err := key.Key.Public() + if err == nil { + logger.Raw(cryptolib.Key[cryptolib.KeyProviderPublic]{ + ID: key.ID, + Key: pub, + }.String() + "\n") - return nil - } - } + return nil + } + } - return logger.Error(ctx, errs.ErrReceiver.Wrap(cryptolib.ErrNoPrivateKey)) + return logger.Error(ctx, errs.ErrReceiver.Wrap(cryptolib.ErrNoPrivateKey)) + }, + } } diff --git a/go/cmdShowValue.go b/go/cmdShowValue.go index 7f4a8be..18a4ce8 100644 --- a/go/cmdShowValue.go +++ b/go/cmdShowValue.go @@ -8,37 +8,53 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdShowValue(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { - if _, ok := f.Value("c"); ok { - if v, ok := c.Values[args[1]]; ok { - logger.Raw(v.Comment) - - return nil - } - - return errNotFound - } - - if err := c.decryptPrivateKey(ctx); err != nil { - return logger.Error(ctx, err) - } - - v, err := c.decryptValue(ctx, args[1]) - if err != nil { - return logger.Error(ctx, err) +func cmdShowValue() cli.Command[*cfg] { + return cli.Command[*cfg]{ + ArgumentsRequired: []string{ + "name", + }, + Flags: cli.Flags{ + "c": { + Usage: "Show the comment only", + }, + "v": { + Usage: "Show the value only", + }, + }, + Usage: "Show a decrypted value.", + Run: func(ctx context.Context, args []string, f cli.Flags, c *cfg) errs.Err { + if _, ok := f.Value("c"); ok { + if v, ok := c.Values[args[1]]; ok { + logger.Raw(v.Comment) + + return nil + } + + return errNotFound + } + + if err := c.decryptPrivateKey(ctx); err != nil { + return logger.Error(ctx, err) + } + + v, err := c.decryptValue(ctx, args[1]) + if err != nil { + return logger.Error(ctx, err) + } + + if _, ok := f.Value("v"); ok { + logger.Raw(string(v)) + + return nil + } + + m := map[string]any{ + "comment": c.Values[args[1]].Comment, + "modified": c.Values[args[1]].Modified, + "value": string(v), + } + + return logger.Error(ctx, cli.Print(m)) + }, } - - if _, ok := f.Value("v"); ok { - logger.Raw(string(v)) - - return nil - } - - m := map[string]any{ - "comment": c.Values[args[1]].Comment, - "modified": c.Values[args[1]].Modified, - "value": string(v), - } - - return logger.Error(ctx, cli.Print(m)) } diff --git a/go/cmdShowValues.go b/go/cmdShowValues.go index 6f66c33..375a9d4 100644 --- a/go/cmdShowValues.go +++ b/go/cmdShowValues.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "sort" "github.com/candiddev/shared/go/cli" @@ -9,20 +10,30 @@ import ( "github.com/candiddev/shared/go/logger" ) -func cmdShowKeysValues(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { - v := []string{} - - if args[0] == "show-keys" { - for k := range c.DecryptKeys { - v = append(v, k) - } - } else { - for k := range c.Values { - v = append(v, k) - } +func cmdShowKeysValues(keys bool) cli.Command[*cfg] { + t := "value" + if keys { + t = "key" } - sort.Strings(v) + return cli.Command[*cfg]{ + Usage: fmt.Sprintf("Show %s names in a configuration.", t), + Run: func(ctx context.Context, args []string, _ cli.Flags, c *cfg) errs.Err { + v := []string{} + + if args[0] == "show-keys" { + for k := range c.DecryptKeys { + v = append(v, k) + } + } else { + for k := range c.Values { + v = append(v, k) + } + } - return logger.Error(ctx, cli.Print(v)) + sort.Strings(v) + + return logger.Error(ctx, cli.Print(v)) + }, + } } diff --git a/go/keys.go b/go/keys.go index f832dcf..31983f3 100644 --- a/go/keys.go +++ b/go/keys.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "strings" "time" "github.com/candiddev/shared/go/cryptolib" @@ -98,6 +99,31 @@ func (c *cfg) decryptValue(ctx context.Context, value string) ([]byte, errs.Err) return nil, logger.Error(ctx, errNotFound) } +// decryptValuePrivateKey will lookup a PrivateKey using value. +func (c *cfg) decryptValuePrivateKey(ctx context.Context, privateKey string) (cryptolib.Key[cryptolib.KeyProviderPrivate], errs.Err) { + pk := cryptolib.Key[cryptolib.KeyProviderPrivate]{} + + if len(strings.Split(privateKey, ":")) == 1 { + if err := c.decryptPrivateKey(ctx); err != nil { + return pk, logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + v, err := c.decryptValue(ctx, privateKey) + if err != nil { + return pk, logger.Error(ctx, errs.ErrReceiver.Wrap(err)) + } + + privateKey = string(v) + } + + pk, err := cryptolib.ParseKey[cryptolib.KeyProviderPrivate](privateKey) + if err != nil { + return pk, logger.Error(ctx, errs.ErrReceiver.Wrap(errors.New("error parsing private key"), err)) + } + + return pk, logger.Error(ctx, nil) +} + // encryptValue will encrypt a value and add it to the config. func (c *cfg) encryptvalue(ctx context.Context, value []byte, name, comment string) errs.Err { if err := types.EnvValidate(name); err != nil { diff --git a/go/main.go b/go/main.go index 1be52bd..768e0c4 100644 --- a/go/main.go +++ b/go/main.go @@ -3,7 +3,6 @@ package main import ( "os" - "strings" "github.com/candiddev/shared/go/cli" "github.com/candiddev/shared/go/cryptolib" @@ -15,203 +14,29 @@ func m() errs.Err { return (cli.App[*cfg]{ Commands: map[string]cli.Command[*cfg]{ - "add-key": { - ArgumentsRequired: []string{ - "key name", - }, - ArgumentsOptional: []string{ - "public key, default: generate a PBKDF-protected asymmetric key", - }, - Run: cmdAddKey, - Usage: "Add a key to a configuration", - }, - "add-private-key": { - ArgumentsRequired: []string{ - "name", - }, - Run: cmdAddPrivateKey, - Usage: "Add a private key to a configuration", - }, - "add-value": { - ArgumentsRequired: []string{ - "name", - }, - ArgumentsOptional: []string{ - `comment, default: ""`, - }, - Flags: cli.Flags{ - "d": { - Default: []string{`\n`}, - Usage: "Delimiter", - }, - }, - Run: cmdAddValue, - Usage: "Add a value to a configuration", - }, - "decrypt": { - ArgumentsRequired: []string{ - "value, or - for stdin", - }, - Run: cmdDecrypt, - Usage: "Decrypt a value and print it to stdout", - }, - "encrypt": { - ArgumentsOptional: []string{ - "recipient key, optional", - }, - Flags: cli.Flags{ - "d": { - Default: []string{`\n`}, - Usage: "Delimiter", - }, - }, - Run: cmdEncrypt, - Usage: "Encrypt a value and print it to stdout without adding it to the config. Can specify a recipient key to use asymmetric encryption.", - }, - "generate-certificate": { - ArgumentsRequired: []string{ - "private key value, name, or - for stdin", - }, - ArgumentsOptional: []string{ - "public key", - "ca certificate or path", - }, - Flags: cli.Flags{ - "c": { - Usage: "Create a CA certificate", - }, - "d": { - Placeholder: "hostname", - Usage: "DNS hostname (can be used multiple times)", - }, - "e": { - Default: []string{"31536000"}, - Placeholder: "seconds", - Usage: "Expiration in seconds", - }, - "eu": { - Default: []string{"clientAuth", "serverAuth"}, - Placeholder: "extended key usage", - Usage: "Extended key usage, valid values: " + strings.Join(cryptolib.ValidX509ExtKeyUsages(), ", "), - }, - "i": { - Placeholder: "address", - Usage: "IP address (can be used multiple times)", - }, - "ku": { - Default: []string{"digitalSignature"}, - Placeholder: "key usage", - Usage: "Key usage, valid values: " + strings.Join(cryptolib.ValidX509KeyUsages(), ", "), - }, - "n": { - Placeholder: "name", - Usage: "Common Name (CN)", - }, - }, - Run: cmdGenerateCertificate, - Usage: "Generate X.509 certificates using Private Keys. Must specify the private key of the signer (for CA signed certificates) or the private key of the certificate (for self-signed certificates). A public key can be specified, otherwise the public key of the private key will be used. Outputs a PEM certificate.", - }, - "generate-key": cryptolib.GenerateKeys[*cfg](), - "generate-value": { - ArgumentsRequired: []string{ - "name", - }, - ArgumentsOptional: []string{ - "comment", - }, - Flags: cli.Flags{ - "l": { - Default: []string{"20"}, - Usage: "Length of value", - }, - }, - Run: cmdGenerateValue, - Usage: "Generate a random string and encrypt it", - }, - "init": { - ArgumentsOptional: []string{ - "key name or id of an existing key", - "initial public key, default: generate a PBKDF symmetric key", - }, - Run: cmdInit, - Usage: "Initialize a new Rot configuration. Will look for a .rot-keys file and use the first available key if none specified as the initial user key.", - }, - "pem": { - ArgumentsRequired: []string{ - "key, or - for stdin", - }, - Flags: cli.Flags{ - "i": { - Placeholder: "id", - Usage: "Add id to the key imported from a PEM", - }, - }, - Run: cmdPEM, - Usage: "Convert a key to PEM, or a PEM file to a key", - }, - "rekey": { - Run: cmdRekey, - Usage: "Rekey all keys and values", - }, - "remove-key": { - ArgumentsRequired: []string{ - "name", - }, - Run: cmdRemove, - Usage: "Remove a key from the configuration", - }, - "remove-value": { - ArgumentsRequired: []string{ - "name", - }, - Run: cmdRemove, - Usage: "Remove a value from the configuration", - }, - "run": { - ArgumentsRequired: []string{ - "command", - }, - Run: cmdRun, - Usage: "Run a command and inject values as environment variables. Values written to stderr/stdout will be masked with ***.", - }, - "show-algorithms": { - Run: cmdAlgorithms, - Usage: "Show algorithms Rot understands", - }, - "show-keys": { - Run: cmdShowKeysValues, - Usage: "Show decrypt key names", - }, - "show-private-key": { - Run: cmdShowPrivateKey, - Usage: "Show the decrypted private key", - }, - "show-public-key": { - ArgumentsRequired: []string{ - "name, private key, or - for stdin", - }, - Run: cmdShowPublicKey, - Usage: "Show the public key of a private key using a name, the private key contents, or - for stdin", - }, - "show-value": { - ArgumentsRequired: []string{ - "name", - }, - Flags: cli.Flags{ - "c": { - Usage: "Show the comment only", - }, - "v": { - Usage: "Show the value only", - }, - }, - Run: cmdShowValue, - Usage: "Show a decrypted value", - }, - "show-values": { - Run: cmdShowKeysValues, - Usage: "Show available value names", - }, + "add-key": cmdAddKey(), + "add-private-key": cmdAddPrivateKey(), + "add-value": cmdAddValue(), + "decrypt": cmdDecrypt(), + "encrypt": cmdEncrypt(), + "generate-certificate": cmdGenerateCertificate(), + "generate-jwt": cmdGenerateJWT(), + "generate-key": cryptolib.GenerateKeys[*cfg](), + "generate-ssh": cmdGenerateSSH(), + "init": cmdInit(), + "pem": cmdPEM(), + "rekey": cmdRekey(), + "remove-key": cmdRemove(true), + "remove-value": cmdRemove(false), + "run": cmdRun(), + "show-algorithms": cmdAlgorithms(), + "show-certificate": cmdShowCertificate(), + "show-jwt": cmdShowJWT(), + "show-keys": cmdShowKeysValues(true), + "show-public-key": cmdShowPublicKey(), + "show-value": cmdShowValue(), + "show-values": cmdShowKeysValues(false), + "ssh": cmdSSH(), }, Config: c, Description: "Rot encrypts and decrypts secrets", diff --git a/go/main_test.go b/go/main_test.go index d35f91e..d91cba0 100644 --- a/go/main_test.go +++ b/go/main_test.go @@ -16,6 +16,7 @@ import ( "github.com/candiddev/shared/go/cli" "github.com/candiddev/shared/go/cryptolib" "github.com/candiddev/shared/go/errs" + "github.com/candiddev/shared/go/jwt" ) func TestM(t *testing.T) { @@ -28,7 +29,14 @@ func TestM(t *testing.T) { t.Setenv("ROT_keyPath", "./.rot-keys") // init - out, err := cli.RunMain(m, "\n\n", "init", "test1") + out, err := cli.RunMain(m, "", "init") + assert.HasErr(t, err, nil) + assert.Equal(t, out, "") + + os.Remove("./.rot-keys") + os.Remove("./rot.jsonnet") + + out, err = cli.RunMain(m, "\n\n", "init", "test1") assert.HasErr(t, err, nil) assert.Equal(t, out, "") @@ -77,6 +85,10 @@ func TestM(t *testing.T) { assert.HasErr(t, err, nil) assert.Equal(t, out, "") + out, err = cli.RunMain(m, "", "add-value", "-l", "20", "value", "vc") + assert.HasErr(t, err, nil) + assert.Equal(t, out, "") + // algorithms out, err = cli.RunMain(m, "", "show-algorithms") assert.HasErr(t, err, nil) @@ -91,11 +103,6 @@ func TestM(t *testing.T) { json.Unmarshal([]byte(out), &keys) pk := keys["publicKey"].(string) //nolint:revive - // generate-value - out, err = cli.RunMain(m, "", "generate-value", "value", "20", "vc") - assert.HasErr(t, err, nil) - assert.Equal(t, out, "") - // check config c.Parse(ctx, nil) assert.Equal(t, len(c.Values), 2) @@ -126,10 +133,6 @@ func TestM(t *testing.T) { assert.HasErr(t, err, cryptolib.ErrUnknownEncryption) assert.Equal(t, out != "secret", true) - out, err = cli.RunMain(m, "123\n123\n", "show-private-key") - assert.HasErr(t, err, nil) - assert.Equal(t, strings.Contains(out, "ed25519private"), true) - // show-value out, err = cli.RunMain(m, "123\n123\n", "show-value", "test") assert.HasErr(t, err, nil) @@ -231,6 +234,10 @@ func TestM(t *testing.T) { assert.HasErr(t, err, nil) + out, err = cli.RunMain(m, crtPEM, "show-certificate", "-") + assert.HasErr(t, err, nil) + assert.Equal(t, strings.Contains(out, "localhost"), true) + os.WriteFile("ca.pem", []byte(crtPEM), 0600) cs, err := cli.RunMain(m, crtPEM, "pem", "-") @@ -280,6 +287,55 @@ func TestM(t *testing.T) { assert.Equal(t, x.Issuer.CommonName, "My CA") assert.Equal(t, x.KeyUsage, x509.KeyUsageDigitalSignature) + os.WriteFile("crt.pem", []byte(crtPEM), 0600) + + out, err = cli.RunMain(m, crtPEM, "show-certificate", "crt.pem", "ca.pem") + assert.HasErr(t, err, nil) + assert.Equal(t, strings.Contains(out, "My CA"), true) + + crtPEM, _ = cli.RunMain(m, "", "generate-certificate", prv1, pub.String(), "ca.pem") + os.WriteFile("ca.pem", []byte(crtPEM), 0600) + + out, err = cli.RunMain(m, crtPEM, "show-certificate", "crt.pem", "ca.pem") + assert.HasErr(t, err, errs.ErrReceiver) + assert.Equal(t, strings.Contains(out, "My CA"), true) + + // generate-jwt + j, err := cli.RunMain(m, prv1, "generate-jwt", "-a", "audience", "-e", "60", "-f", "bool=true", "-f", `string="1"`, "-f", "int=1", "-id", "id", "-is", "issuer", "-s", "subject", "-") + assert.HasErr(t, err, nil) + + out, err = cli.RunMain(m, "", "show-jwt", j, pub1) + assert.HasErr(t, err, nil) + assert.Equal(t, strings.Contains(out, `"audience"`), true) + assert.Equal(t, strings.Contains(out, `: true`), true) + assert.Equal(t, strings.Contains(out, `: 1`), true) + assert.Equal(t, strings.Contains(out, `: "1"`), true) + assert.Equal(t, strings.Contains(out, `"id"`), true) + assert.Equal(t, strings.Contains(out, `"issuer"`), true) + assert.Equal(t, strings.Contains(out, `"subject"`), true) + + out, err = cli.RunMain(m, "", "show-jwt", j) + assert.HasErr(t, err, jwt.ErrParseNoPublicKeys) + assert.Equal(t, strings.Contains(out, `"audience"`), true) + + _, jp, _ := cryptolib.NewKeysAsymmetric(cryptolib.AlgorithmBest) + + out, err = cli.RunMain(m, "", "show-jwt", j, jp.String()) + assert.HasErr(t, err, cryptolib.ErrVerify) + assert.Equal(t, strings.Contains(out, `"audience"`), true) + + j, err = cli.RunMain(m, "", "generate-jwt", "hello") + assert.HasErr(t, err, nil) + + _, err = cli.RunMain(m, "", "show-jwt", j, pub1) + assert.HasErr(t, err, nil) + + // ssh + _, err = cli.RunMain(m, "", "generate-ssh", "-c", "source-address=localhost", "-e", "permit-pty", "-h", "-i", "123", "-p", "root", "-v", "360", "hello", pub1) + assert.HasErr(t, err, nil) + _, err = cli.RunMain(m, "", "ssh", pub1) + assert.HasErr(t, err, nil) + // remove out, err = cli.RunMain(m, "123\n123\n", "add-key", "remove", pk) assert.HasErr(t, err, nil) @@ -310,4 +366,5 @@ func TestM(t *testing.T) { os.RemoveAll("rot.jsonnet") os.RemoveAll(".rot-keys") os.RemoveAll("ca.pem") + os.RemoveAll("crt.pem") } diff --git a/hugo/content/about/_index.md b/hugo/content/about/_index.md index c6fa587..1da014a 100644 --- a/hugo/content/about/_index.md +++ b/hugo/content/about/_index.md @@ -1,19 +1,22 @@ --- description: | - About Rot, an easy way to manage secrets. + About Rot, an easy way to manage cryptographic values. menu: {main} title: About type: docs weight: 10 --- -Rot is an open source command line (CLI) tool for managing secrets. +Rot is an open source command line (CLI) tool for managing cryptographic values. + +Rot makes cryptography easy: -Rot makes encrypting and decrypting secrets easy: - Generate keys and values using current best encryption -- Rekey secrets to the latest encryption standards -- Share your secrets with other users and devices -- One-way encryption for production secrets -- Run commands and scripts with secrets injected via environment variables -- Store your secrets securely in git with human-readable diffs -- Easily generate X.509 certificates and Certificate Authorities +- Rekey encrypted values to the latest encryption standards +- Share your encrypted values with other users and devices +- One-way encryption for zero-knowledge secrets +- Run commands and scripts with encrypted values injected via environment variables +- Store your encrypted values securely in git with human-readable diffs +- Generate and view X.509 certificates and Certificate Authorities +- Generate and view JWTs +- Generate SSH keys and certificates diff --git a/hugo/content/about/crypto-agility.md b/hugo/content/about/crypto-agility.md index ae5a3a4..57f7ad4 100644 --- a/hugo/content/about/crypto-agility.md +++ b/hugo/content/about/crypto-agility.md @@ -11,7 +11,7 @@ Rot is designed to prevent cryptographic key "rot": - Constantly decrypting leading to leakage - Encryption algorithms become insecure -Instead of generating secrets once, Rot encourages companies to rekey secrets and reissue keys by making the process as easy as possible: +Instead of generating secrets once, Rot encourages companies to rekey encrypted values and reissue keys by making the process as easy as possible: {{< highlight bash >}} $ ./rot add-key server1 ed25519public:MCowBQYDK2VwAyEAAYkJzjQGb+4I7bfcaq6TnkI6nWJXolUdYSQDKSZIDZU=:AVvPeIzIHg diff --git a/hugo/content/about/crypto-tooling.md b/hugo/content/about/crypto-tooling.md new file mode 100644 index 0000000..be5269f --- /dev/null +++ b/hugo/content/about/crypto-tooling.md @@ -0,0 +1,24 @@ +--- +categories: +- feature +description: Rot is a cryptographic swiss army knife +title: Crypto Tooling +type: docs +--- + +Rot can read, convert, and display various forms of cryptographic keys: + +- JSON Web Tokens (JWTs) +- PEM Keys and Certificates +- SSH Keys and Certificates +- X.509 Certificates + +Using Rot, you can easily manage all of your cryptography needs without additional tooling: + +{{< highlight bash >}} +$ rot show-certificate ca.pem +$ rot show-jwt eyJhbGciOiJFZERTQSIsImtpZCI6IjlzY0lrOW1TaHIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlIiwiZXhwIjoxNzEwMjEyMjE4LCJpYXQiOjE3MTAyMDgyMTgsImlzcyI6Ik15SXNzdWVyIiwianRpIjoiMTIzIiwibmJmIjoxNzEwMjA4MjE4LCJzdWIiOiJFeGFtcGxlIiwidGVzdCI6InllcyJ9.aSPcgRUEmm0g4ak-OjEyyPSn0-_AxRxpFsir_f64UJ_lntR8o6Q3zulUi1IDHDtIYF4hhyutMCzMVIFkS1ufCA ed25519public:MCowBQYDK2VwAyEASI/qzkRrx2hy3GGX1ereMpSw9+Z8KpGJ1HHjv6H+EXs=:9scIk9mShr +$ cat ca.pem | rot pem - > ca.rot +$ cat ca.rot | rot pem - > ca-new.pem +$ cat ~/.ssh/id_rsa.pub | rot ssh - > ssh.rot +{{< /highlight >}} diff --git a/hugo/content/about/secret-storage.md b/hugo/content/about/secret-storage.md index f24a7fe..ddbce20 100644 --- a/hugo/content/about/secret-storage.md +++ b/hugo/content/about/secret-storage.md @@ -1,7 +1,7 @@ --- categories: - feature -description: Rot stores your secrets securely +description: Rot stores your secrets and keys securely title: Secret Storage type: docs --- diff --git a/hugo/content/blog/whats-new-202403.md b/hugo/content/blog/whats-new-202403.md new file mode 100644 index 0000000..31d70db --- /dev/null +++ b/hugo/content/blog/whats-new-202403.md @@ -0,0 +1,29 @@ +--- +author: Mike +date: 2024-02-06 +description: Release notes for Rot v2024.02. +tags: + - release +title: "What's New in Rot: v2024.02" +type: blog +--- + +{{< rot-release version="2024.02" >}} + +## Features + +### JWTs + +Rot can now create JWTs. Visit [Generate JWTs]({{< ref "/docs/guides/generate-jwts" >}}) for more information. + +### SSH + +Rot can now create SSH keys and certificates. Visit [Generate SSH]({{< ref "/docs/guides/generate-ssh" >}}) for more information. + +## Enhancements + +- Added [`show-certificate`]({{< ref "/docs/references/cli#show-certificate" >}}) to display X.509 certificate details. + +## Removals + +- Removed [`generate-value`] to generate random values and add them to the configuration, its functionality has been moved into [`add-value`]. diff --git a/hugo/content/docs/guides/generate-certificates.md b/hugo/content/docs/guides/generate-certificates.md index 54e39c3..f9b01dd 100644 --- a/hugo/content/docs/guides/generate-certificates.md +++ b/hugo/content/docs/guides/generate-certificates.md @@ -65,3 +65,7 @@ $ rot add-private-key example_com $ rot generate-certificate -c -n 'Rot CA' ca > ca.pem $ rot generate-certificate -d www.example.com -n www.example.com ca $(rot show-value -c example_com) ca.pem ``` + +## View Certificates + +You can view the contents of an existing X.509 Certificate as JSON using [`rot show-certificate`]({{< ref "/docs/references/cli#show-certificate" >}}), optionally providing a list of CA certificates to verify it against. diff --git a/hugo/content/docs/guides/generate-jwts.md b/hugo/content/docs/guides/generate-jwts.md new file mode 100644 index 0000000..25ef0e1 --- /dev/null +++ b/hugo/content/docs/guides/generate-jwts.md @@ -0,0 +1,70 @@ +--- +categories: +- guide +description: How to create JSON Web Tokens (JWTs) using Rot +title: Generate JWTs +weight: 50 +--- + +In this guide, we'll go over managing JWTs using Rot. + +## JWT Introduction + +A JSON Web Token (JWT) is a string containing three parts: + +- A JSON header containing algorithm details about the JWT, base64 raw URL encoded +- A JSON payload containing key value pairs of user specified and standard claims, base64 raw URL encoded +- A cryptographic signature of the first two parts + +## Add Private Keys + +You'll need to generate a private key to sign the JWT. The easiest way to do this is using [`rot add-private-key`]({{< ref "/docs/references/cli#add-private-key" >}}) (encrypting the keys into Rot) or [`rot generate-keys`]({{< ref "/docs/references/cli#generate-keys" >}}) (printing the keys to stdout). + +Rot will store the public key in the comment of the encrypted value, we can grab the public key from the comment when we verify the JWT. + +## Generate a JWT + +You can generate a JWT using a private key with [`rot generate-jwt`]({{< ref "/docs/references/cli#generate-jwt" >}}). This command generates a JWT using the options you provide and prints the token to stdout. + +It supports the following flags: + +- `-a `: The audience (aud) for the JWT. Can be provided multiple times. +- `-e `: The expiration (exp) in seconds from now for the JWT. Defaults to 3600/one hour. +- `-f `: Add a key and value to the JWT. Will attempt to parse bools and ints unless they are quoted. Can be provided multiple times. +- `-id `: ID (jti) of the JWT, will generate a UUID if not provided +- `-is `: Issuer (iss) of the JWT, defaults to Rot +- `-s `: Subject (sub) of the JWT + +Here is an example usage: + +```bash +$ rot generate-jwt -a example -e 4000 -f test=yes -id 123 -is MyIssuer -s Example ed25519private:MC4CAQAwBQYDK2VwBCIEIDp+bj8yxdPB7kSUjsqp4WNoHGnSFKeA9opbwGphFm+F:9scIk9mShr +eyJhbGciOiJFZERTQSIsImtpZCI6IjlzY0lrOW1TaHIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlIiwiZXhwIjoxNzEwMjEyMjE4LCJpYXQiOjE3MTAyMDgyMTgsImlzcyI6Ik15SXNzdWVyIiwianRpIjoiMTIzIiwibmJmIjoxNzEwMjA4MjE4LCJzdWIiOiJFeGFtcGxlIiwidGVzdCI6InllcyJ9.aSPcgRUEmm0g4ak-OjEyyPSn0-_AxRxpFsir_f64UJ_lntR8o6Q3zulUi1IDHDtIYF4hhyutMCzMVIFkS1ufCA +``` + +## View JWT + +You can view the contents of a JWT as JSON using [`rot show-jwt`]({{< ref "/docs/references/cli#show-jwt" >}}), optionally providing a list of public keys to verify it against: + +```bash +$ rot show-jwt eyJhbGciOiJFZERTQSIsImtpZCI6IjlzY0lrOW1TaHIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlIiwiZXhwIjoxNzEwMjEyMjE4LCJpYXQiOjE3MTAyMDgyMTgsImlzcyI6Ik15SXNzdWVyIiwianRpIjoiMTIzIiwibmJmIjoxNzEwMjA4MjE4LCJzdWIiOiJFeGFtcGxlIiwidGVzdCI6InllcyJ9.aSPcgRUEmm0g4ak-OjEyyPSn0-_AxRxpFsir_f64UJ_lntR8o6Q3zulUi1IDHDtIYF4hhyutMCzMVIFkS1ufCA ed25519public:MCowBQYDK2VwAyEASI/qzkRrx2hy3GGX1ereMpSw9+Z8KpGJ1HHjv6H+EXs=:9scIk9mShr +{ + "header": { + "alg": "EdDSA", + "kid": "9scIk9mShr", + "typ": "JWT" + }, + "payload": { + "aud": "example", + "exp": 1710212218, + "iat": 1710208218, + "iss": "MyIssuer", + "jti": "123", + "nbf": 1710208218, + "sub": "Example", + "test": "yes" + }, + "signature": "aSPcgRUEmm0g4ak-OjEyyPSn0-_AxRxpFsir_f64UJ_lntR8o6Q3zulUi1IDHDtIYF4hhyutMCzMVIFkS1ufCA" +} + +``` diff --git a/hugo/content/docs/guides/generate-ssh.md b/hugo/content/docs/guides/generate-ssh.md new file mode 100644 index 0000000..52c8e24 --- /dev/null +++ b/hugo/content/docs/guides/generate-ssh.md @@ -0,0 +1,63 @@ +--- +categories: +- guide +description: How to create SSH keys and certificates using Rot +title: Generate SSH +weight: 70 +--- + +In this guide, we'll go over managing a SSH Certificate Authority (CA) using Rot. + +## SSH Certificate Introduction + +OpenSSH can use SSH certificate authorities (CA) to authorize user access to servers and devices without having to add individual public keys to the servers. The user presents a valid certificate to the server which is signed by the CA trusted on the machine and is granted access based on the constraints within the certificate. + +## Add Private Keys + +You'll need to generate a private key to create a SSH keypair and an SSH CA to sign the certificates. The easiest way to do this is using [`rot add-private-key`]({{< ref "/docs/references/cli#add-private-key" >}}) (encrypting the keys into Rot) or [`rot generate-keys`]({{< ref "/docs/references/cli#generate-keys" >}}) (printing the keys to stdout). + +Rot will store the public key in the comment of the encrypted value, we can grab the public key from the comment when we verify the JWT. + +## Generate SSH private keys + +We can use Rot to generate a SSH keypair, similar to `ssh-keygen`: + +```bash +$ rot generate-keys | tee >(rot jq -r .privateKey | rot ssh - > id_ed25519 && chmod 0400 id_ed25519) | rot jq -r .publicKey | rot ssh - > id_ed25519.pub +``` + +This will generate two files, id_ed25519 containing the SSH private key, and id_ed25519.pub containing the SSH public key. + +## Generate SSH certificate private and public keys + +Lets use Rot to generate another keypair, this time for use as the SSH CA: + +```bash +$ rot add-private-key SSH_CA +$ rot show-value -c SSH_CA | rot ssh - +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN+5rkhggPylubB7l9GNhrkuPX+da3iS0g5Vd9ZEhSTf +``` + +The last value returned is the SSH CA. + +## Add the SSH CA to a SSH Server + +The addition of a SSH CA will vary depending on the operating system. The basic steps are: + +- Add the SSH CA value from the above step into a new or existing SSH CA file, such as `/etc/ssh/ssh_ca` +- Reference the SSH CA file in the server's sshd_config file with the line `TrustedUserCAKeys /etc/ssh/ssh_ca` +- Reload or restart the SSH service + +## Sign the SSH public key + +Now we can sign the public key we generated earlier using the SSH CA: + +```bash +$ rot generate-ssh -e permit-pty -p root SSH_CA id_ed25519.pub > id_ed25519-cert.pub +``` + +This command creates a special file, `id_ed25519-cert.pub`, that SSH will automatically present to our server for authentication. Running this command should get us onto the server: + +```bash +$ ssh -i id_ed25519 root@server +``` diff --git a/hugo/content/docs/guides/integration/_index.md b/hugo/content/docs/guides/integration/_index.md index 9bb434c..f43f66a 100644 --- a/hugo/content/docs/guides/integration/_index.md +++ b/hugo/content/docs/guides/integration/_index.md @@ -1,5 +1,6 @@ --- description: Directions for integrating Rot with other products. title: Integrations +weight: 0 --- diff --git a/hugo/content/docs/guides/manage-values.md b/hugo/content/docs/guides/manage-values.md index d9fc869..5aa21d7 100644 --- a/hugo/content/docs/guides/manage-values.md +++ b/hugo/content/docs/guides/manage-values.md @@ -19,7 +19,7 @@ After Rot has been initialized, keys can be added using [`rot add-value`]({{< re ## Generating Values -Rot can generate random, cryptographically secure strings for you (instead of having you provide a value) via [`rot generate-value`]({{< ref "/docs/references/cli#generate-value" >}}). +Rot can generate random, cryptographically secure strings for you, instead of having you provide a value, via [`rot add-value`]({{< ref "/docs/references/cli#add-value" >}}). ## Removing Values diff --git a/hugo/content/docs/guides/run-commands.md b/hugo/content/docs/guides/run-commands.md index 0bc1f6c..eb93abf 100644 --- a/hugo/content/docs/guides/run-commands.md +++ b/hugo/content/docs/guides/run-commands.md @@ -3,7 +3,7 @@ categories: - guide description: How to run commands using Rot title: Run Commands -weight: 50 +weight: 40 --- In this guide, we'll go over running commands with injected secrets via environment variables. diff --git a/hugo/content/docs/references/cli.md b/hugo/content/docs/references/cli.md index dafe44f..1ad6778 100644 --- a/hugo/content/docs/references/cli.md +++ b/hugo/content/docs/references/cli.md @@ -13,6 +13,10 @@ Arguments must be entered before commands. Path to the JSON/Jsonnet [configuration file]({{< ref "/docs/references/config" >}}). +### `-d` + +Disable [Jsonnet]({{< ref "/docs/references/jsonnet" >}}) native functions that can reach external sources like `getPath` and `getRecord`. + ### `-f [format]` Set log format (human, kv, raw, default: human). @@ -31,67 +35,71 @@ Set config key=value (can be provided multiple times) ## Commands -### `add-key [key name] [public key, default: generate a PBKDF-protected asymmetric key]` {#add-key} +### `add-key` Add a key to a configuration. See [Manage Keys]({{< ref "/docs/guides/manage-keys" >}}) for more information. -### `add-private-key [name]` {#add-private-key} +### `add-private-key` -Generate and add a private key to Rot with the specified name. +Generate and add a private key to a configuration with the specified name. -### `add-value [-d delimiter, default: \n] [name] [comment, default: ""]` {#add-value} +### `add-value` -Add a value to a configuration. See [Manage Values]({{< ref "/docs/guides/manage-values" >}}) for more information. +Add a value to a configuration. Can specify an optional length to have Rot randomly generate a value instead of prompting for it. See [Manage Values]({{< ref "/docs/guides/manage-values" >}}) for more information. ### `autocomplete` {{< autocomplete name="Rot" >}} -### `decrypt [value]` {#decrypt} +### `decrypt` Perform ad-hoc decryption of a value using the User Private Keys. -### `encrypt [-d delimiter, default: \n] [recipient key, optional]` {#encrypt} +### `encrypt` Encrypt a value and print it to stdout without adding it to the config. Can specify a recipient key to use asymmetric encryption. -### `generate-certificate [-c, create CA] [-d hostname, add DNS hostname] [-e expiration in seconds] [-eu extended key usage] [-i address, add IP address] [-ku key usage] [-n common name] [private key value, name, or - for stdin] [public key] [ca certificate or path]` {#generate-certificate} +### `generate-certificate` Generate X.509 certificates. Visit [Generating Certificates]({{< ref "/docs/guides/generate-certificates" >}}) for more information. -### `generate-keys [-a algorithm] [name]` {#generate-keys} +### `generate-jwt` + +Generate JSON Web Tokens (JWTs). Visit [Generating JWTs]({{< ref "/docs/guides/generate-jwts" >}}) for more information. + +### `generate-keys` Generate ad-hoc cryptographic keys. -### `generate-value [-l length, default 20] [name] [comment]` {#generate-value} +### `generate-ssh` -Generate a random string of the length provided, encrypt it, and add it to the configuration. See [Manage Values]({{< ref "/docs/guides/manage-values" >}}) for more information. +Generate SSH certificates. Visit [Generating SSH]({{< ref "/docs/guides/generate-ssh" >}}) for more information. -### `init [initial key name] [initial public key, default: generate a PBKDF symmetric key]` {#init} +### `init` Initialize a new Rot configuration. See [Initialize Rot]({{< ref "/docs/guides/initialize-rot" >}}) for more information. -### `jq [jq query options]` {#jq} +### `jq` Query JSON from stdin using jq. Supports standard JQ queries, and the `-r` flag to render raw values. -### `pem [-i id] [key, or - for stdin]` {#pem} +### `pem` -Convert a Rot key to PEM or a PEM key to a Rot key. Can specify an ID for the key when converting from PEM to Rot. +Convert a Rot key to PEM or a PEM key to Rot. Can specify an ID for the key when converting from PEM to Rot. ### `rekey` Rekey a Rot configuration. See [Rekey Rot]({{< ref "/docs/guides/rekey-rot" >}}) for more information. -### `remove-key [name]` {#remove-key} +### `remove-key` Remove a key from a Rot configuration. See [Manage Keys]({{< ref "/docs/guides/manage-keys" >}}) for more information. -### `remove-value [name]` {#remove-value} +### `remove-value` Remove a value from a Rot configuration. See [Manage Values]({{< ref "/docs/guides/manage-values" >}}) for more information. -### `run [command]` {#run} +### `run` Run a command and inject secrets into it via environment variables. See [Run Commands]({{< ref "/docs/guides/run-commands" >}}) for more information. By default, any Value written to stderr/stdout will be masked with `***`. Values can be unmasked using the [`unmask`]({{< ref "/docs/references/config#unmask" >}}) config. @@ -103,19 +111,23 @@ Show algorithms Rot understands Show the rendered config from all sources (file, environment variables, and command line arguments). -### `show-keys` +### `show-certificate` -Show the names of [decryptKeys]({{< ref "/docs/references/config#decryptKeys" >}}) in the configuration. +Show the contents of an X.509 certificate and optionally verify it against a CA certificate. -### `show-private-key` +### `show-jwt` -Show the decrypted Rot Private Key. Useful for piping into [`rot encrypt`](#encrypt) using a temporary key or combined with the [`privateKey`]({{< ref "/docs/references/config#privatekey" >}}) configuration to provide containers and other tools access to a configuration without compromising User Private Keys. +Show the contents of a JWT and optionally verify it against a public key. + +### `show-keys` + +Show the names of [decryptKeys]({{< ref "/docs/references/config#decryptKeys" >}}) in the configuration. ### `show-public-key [name]` Show the public key for a User Private Key. Takes a name of a key that it will lookup from [`keys`]({{< ref "/docs/references/config#keys" >}}) or [`keyPath`]({{< ref "/docs/references/config#keyPath" >}}). Will return the public key of the first key found that matches `name`. -### `show-value [-c, show comment only] [-v, show value only] [name]` +### `show-value` Show a decrypted value from the Rot configuration. See [Manage Values]({{< ref "/docs/guides/manage-values" >}}) for more information. @@ -123,6 +135,10 @@ Show a decrypted value from the Rot configuration. See [Manage Values]({{< ref Show the names of [values]({{< ref "/docs/references/config#values" >}}) in the configuration. +### `ssh` + +Convert a Rot key to SSH or a SSH key to Rot. + ### `version` Print the current version of Rot. diff --git a/hugo/content/docs/references/cryptography.md b/hugo/content/docs/references/cryptography.md index 0e3f9f1..78c99d7 100644 --- a/hugo/content/docs/references/cryptography.md +++ b/hugo/content/docs/references/cryptography.md @@ -7,7 +7,7 @@ title: Cryptography ## Format -Rot formats keys and values in this format: +Rot formats keys and values like this: `::` diff --git a/shared b/shared index 1bc6e22..2cf7ebd 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit 1bc6e2251b60ed8794dc41c557ed072ee450cf04 +Subproject commit 2cf7ebdd391488503ef83fd5bc28107ae8d04700