feat: add DWH credential administration CLI
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/command"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
os.Exit(command.Run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
//go:build linux
|
||||
|
||||
// Package command implements the deliberately small dwh-auth operator CLI.
|
||||
// It is intentionally independent of flag.CommandLine: command-line values
|
||||
// are never included in diagnostics, and the grammar is closed by default.
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/credential"
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/record"
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/registry"
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/securefile"
|
||||
)
|
||||
|
||||
const (
|
||||
exitOK = 0
|
||||
exitUsage = 2
|
||||
exitNotFound = 3
|
||||
exitIntegrity = 4
|
||||
maxReasonBytes = 160
|
||||
commandServeMsg = "serve is not available in this release"
|
||||
)
|
||||
|
||||
// Run executes one dwh-auth invocation and returns its stable process exit
|
||||
// status. It writes only redacted, deterministic diagnostics to stderr.
|
||||
func Run(ctx context.Context, args []string, _ io.Reader, stdout, stderr io.Writer) int {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if stdout == nil || stderr == nil {
|
||||
return exitIntegrity
|
||||
}
|
||||
root, rest, err := parseRoot(args)
|
||||
if err != nil {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
|
||||
switch rest[0] {
|
||||
case "key":
|
||||
return runKey(ctx, root, rest[1:], stdout, stderr)
|
||||
case "check":
|
||||
return runCheck(ctx, root, rest[1:], stdout, stderr)
|
||||
case "serve":
|
||||
return runServe(root, rest[1:], stderr)
|
||||
default:
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
}
|
||||
|
||||
func parseRoot(args []string) (string, []string, error) {
|
||||
if len(args) < 2 || args[0] != "--registry-root" || args[1] == "" || !filepath.IsAbs(args[1]) {
|
||||
return "", nil, errors.New("invalid registry root")
|
||||
}
|
||||
return filepath.Clean(args[1]), args[2:], nil
|
||||
}
|
||||
|
||||
func runKey(ctx context.Context, root string, args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
switch args[0] {
|
||||
case "create":
|
||||
return runCreate(ctx, root, args[1:], stdout, stderr)
|
||||
case "import":
|
||||
return runImport(ctx, root, args[1:], stdout, stderr)
|
||||
case "list":
|
||||
return runList(ctx, root, args[1:], stdout, stderr)
|
||||
case "status":
|
||||
return runStatus(ctx, root, args[1:], stdout, stderr)
|
||||
case "revoke":
|
||||
return runRevoke(ctx, root, args[1:], stdout, stderr)
|
||||
default:
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
}
|
||||
|
||||
type parsed struct {
|
||||
installationID string
|
||||
description string
|
||||
expiresAt string
|
||||
output string
|
||||
fromFile string
|
||||
keyID string
|
||||
reason string
|
||||
json bool
|
||||
legacyRaw bool
|
||||
}
|
||||
|
||||
func parseFlags(args []string, allowed map[string]bool) (parsed, error) {
|
||||
var value parsed
|
||||
seen := make(map[string]bool)
|
||||
for i := 0; i < len(args); i++ {
|
||||
name := args[i]
|
||||
if !strings.HasPrefix(name, "--") || name == "--" || strings.Contains(name, "=") {
|
||||
return parsed{}, errors.New("invalid option")
|
||||
}
|
||||
if !allowed[name] || seen[name] {
|
||||
return parsed{}, errors.New("invalid option")
|
||||
}
|
||||
seen[name] = true
|
||||
if name == "--json" || name == "--legacy-raw" {
|
||||
switch name {
|
||||
case "--json":
|
||||
value.json = true
|
||||
case "--legacy-raw":
|
||||
value.legacyRaw = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i+1 >= len(args) || strings.HasPrefix(args[i+1], "--") {
|
||||
return parsed{}, errors.New("missing option value")
|
||||
}
|
||||
i++
|
||||
switch name {
|
||||
case "--installation-id":
|
||||
value.installationID = args[i]
|
||||
case "--description":
|
||||
value.description = args[i]
|
||||
case "--expires-at":
|
||||
value.expiresAt = args[i]
|
||||
case "--output":
|
||||
value.output = args[i]
|
||||
case "--from-file":
|
||||
value.fromFile = args[i]
|
||||
case "--key-id":
|
||||
value.keyID = args[i]
|
||||
case "--reason":
|
||||
value.reason = args[i]
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, root string, args []string, stdout, stderr io.Writer) int {
|
||||
options, err := parseFlags(args, map[string]bool{
|
||||
"--installation-id": true,
|
||||
"--description": true,
|
||||
"--expires-at": true,
|
||||
"--output": true,
|
||||
})
|
||||
if err != nil || options.installationID == "" || options.output == "" || !filepath.IsAbs(options.output) || !validInstallationID(options.installationID) || !validMetadata(options.description) {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
expiresAt, err := parseExpiry(options.expiresAt)
|
||||
if err != nil {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
writeLine(stderr, "operation cancelled")
|
||||
return exitIntegrity
|
||||
}
|
||||
|
||||
material, err := credential.Generate(rand.Reader)
|
||||
if err != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
outputFile, err := securefile.CreateSecret(options.output)
|
||||
if err != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
if err := writeAndSync(outputFile, material.Value); err != nil {
|
||||
_ = outputFile.Close()
|
||||
if cleanupOutput(options.output) != nil {
|
||||
writeCleanupFailure(stderr, options.output)
|
||||
return exitIntegrity
|
||||
}
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
if err := outputFile.Close(); err != nil {
|
||||
if cleanupOutput(options.output) != nil {
|
||||
writeCleanupFailure(stderr, options.output)
|
||||
return exitIntegrity
|
||||
}
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
|
||||
store, err := registry.Open(root)
|
||||
if err != nil {
|
||||
if cleanupOutput(options.output) != nil {
|
||||
writeCleanupFailure(stderr, options.output)
|
||||
return exitIntegrity
|
||||
}
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
value := record.Record{
|
||||
SchemaVersion: record.SchemaVersion,
|
||||
Kind: record.KindV1,
|
||||
KeyID: material.KeyID,
|
||||
InstallationID: options.installationID,
|
||||
Description: options.description,
|
||||
SecretSHA256: base64.RawURLEncoding.EncodeToString(material.Digest[:]),
|
||||
CreatedAt: now,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
addErr := store.Add(value)
|
||||
closeErr := store.Close()
|
||||
if addErr != nil || closeErr != nil {
|
||||
if cleanupOutput(options.output) != nil {
|
||||
writeCleanupFailure(stderr, options.output)
|
||||
return exitIntegrity
|
||||
}
|
||||
if errors.Is(addErr, registry.ErrConflict) || errors.Is(addErr, registry.ErrRevoked) {
|
||||
writeLine(stderr, "credential already exists")
|
||||
return exitIntegrity
|
||||
}
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
_, _ = fmt.Fprintf(stdout, "created key_id=%s installation_id=%s output=%s\n", material.KeyID, options.installationID, filepath.Clean(options.output))
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func runImport(ctx context.Context, root string, args []string, stdout, stderr io.Writer) int {
|
||||
options, err := parseFlags(args, map[string]bool{
|
||||
"--legacy-raw": true,
|
||||
"--installation-id": true,
|
||||
"--from-file": true,
|
||||
})
|
||||
if err != nil || !options.legacyRaw || options.installationID != record.LegacyKeyID || options.fromFile == "" || !filepath.IsAbs(options.fromFile) {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
writeLine(stderr, "operation cancelled")
|
||||
return exitIntegrity
|
||||
}
|
||||
value, err := securefile.ReadSecret(options.fromFile, credential.MaxHeaderBytes)
|
||||
if err != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
sum := sha256.Sum256(value)
|
||||
digest := record.Digest(sum)
|
||||
if !credential.VerifyLegacy(value, digest) {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
store, err := registry.Open(root)
|
||||
if err != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
addErr := store.Add(record.Record{
|
||||
SchemaVersion: record.SchemaVersion,
|
||||
Kind: record.KindLegacyRaw,
|
||||
KeyID: record.LegacyKeyID,
|
||||
InstallationID: record.LegacyKeyID,
|
||||
SecretSHA256: base64.RawURLEncoding.EncodeToString(digest[:]),
|
||||
CreatedAt: now,
|
||||
})
|
||||
closeErr := store.Close()
|
||||
if closeErr != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
if addErr != nil {
|
||||
switch {
|
||||
case errors.Is(addErr, registry.ErrConflict), errors.Is(addErr, registry.ErrRevoked):
|
||||
writeLine(stderr, "credential already exists")
|
||||
return exitIntegrity
|
||||
default:
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(stdout, "imported key_id=%s installation_id=%s\n", record.LegacyKeyID, record.LegacyKeyID)
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, root string, args []string, stdout, stderr io.Writer) int {
|
||||
options, err := parseFlags(args, map[string]bool{"--json": true})
|
||||
if err != nil {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
store, err := openCheckedStore(ctx, root)
|
||||
if err != nil {
|
||||
writeStoreError(stderr, err)
|
||||
return storeExit(err)
|
||||
}
|
||||
values, listErr := store.List()
|
||||
closeErr := store.Close()
|
||||
if listErr != nil || closeErr != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
if options.json {
|
||||
writeJSON(stdout, values)
|
||||
return exitOK
|
||||
}
|
||||
for _, value := range values {
|
||||
_, _ = fmt.Fprintf(stdout, "key_id=%s installation_id=%s state=%s\n", value.KeyID, value.InstallationID, value.State)
|
||||
}
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, root string, args []string, stdout, stderr io.Writer) int {
|
||||
options, err := parseFlags(args, map[string]bool{"--key-id": true, "--json": true})
|
||||
if err != nil || options.keyID == "" || !validKeyID(options.keyID) {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
store, err := openCheckedStore(ctx, root)
|
||||
if err != nil {
|
||||
writeStoreError(stderr, err)
|
||||
return storeExit(err)
|
||||
}
|
||||
values, listErr := store.List()
|
||||
closeErr := store.Close()
|
||||
if listErr != nil || closeErr != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
for _, value := range values {
|
||||
if value.KeyID != options.keyID {
|
||||
continue
|
||||
}
|
||||
if options.json {
|
||||
writeJSON(stdout, value)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(stdout, "key_id=%s installation_id=%s state=%s\n", value.KeyID, value.InstallationID, value.State)
|
||||
}
|
||||
return exitOK
|
||||
}
|
||||
writeLine(stderr, "not found")
|
||||
return exitNotFound
|
||||
}
|
||||
|
||||
func runRevoke(ctx context.Context, root string, args []string, stdout, stderr io.Writer) int {
|
||||
options, err := parseFlags(args, map[string]bool{"--key-id": true, "--reason": true})
|
||||
if err != nil || options.keyID == "" || !validKeyID(options.keyID) || options.reason == "" || !validMetadata(options.reason) {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
writeLine(stderr, "operation cancelled")
|
||||
return exitIntegrity
|
||||
}
|
||||
store, err := registry.Open(root)
|
||||
if err != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
err = store.Revoke(options.keyID, options.reason, time.Now().UTC())
|
||||
closeErr := store.Close()
|
||||
if errors.Is(err, registry.ErrNotFound) {
|
||||
writeLine(stderr, "not found")
|
||||
return exitNotFound
|
||||
}
|
||||
if err != nil || closeErr != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
_, _ = fmt.Fprintf(stdout, "revoked key_id=%s\n", options.keyID)
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func runCheck(ctx context.Context, root string, args []string, stdout, stderr io.Writer) int {
|
||||
options, err := parseFlags(args, map[string]bool{"--json": true})
|
||||
if err != nil {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
writeLine(stderr, "operation cancelled")
|
||||
return exitIntegrity
|
||||
}
|
||||
store, err := registry.Open(root)
|
||||
if err != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
checkErr := store.Check()
|
||||
closeErr := store.Close()
|
||||
if checkErr != nil || closeErr != nil {
|
||||
writeLine(stderr, "integrity failure")
|
||||
return exitIntegrity
|
||||
}
|
||||
if options.json {
|
||||
_, _ = io.WriteString(stdout, "{\"status\":\"ok\"}\n")
|
||||
} else {
|
||||
writeLine(stdout, "ok")
|
||||
}
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func runServe(root string, args []string, stderr io.Writer) int {
|
||||
options, err := parseFlags(args, map[string]bool{"--socket": true})
|
||||
if err != nil || options.output != "" {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
// The Unix-socket service is intentionally introduced by Task 4. Keeping
|
||||
// this grammar reserved prevents accidental plaintext or network fallback.
|
||||
for i := range args {
|
||||
if args[i] == "--socket" {
|
||||
if i+1 >= len(args) || !filepath.IsAbs(args[i+1]) {
|
||||
writeLine(stderr, "unsafe invocation")
|
||||
return exitUsage
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = root
|
||||
writeLine(stderr, commandServeMsg)
|
||||
return exitIntegrity
|
||||
}
|
||||
|
||||
func openCheckedStore(ctx context.Context, root string) (*registry.Store, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store, err := registry.Open(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := store.Check(); err != nil {
|
||||
_ = store.Close()
|
||||
return nil, err
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func writeStoreError(stderr io.Writer, err error) {
|
||||
if errors.Is(err, registry.ErrNotFound) {
|
||||
writeLine(stderr, "not found")
|
||||
return
|
||||
}
|
||||
writeLine(stderr, "integrity failure")
|
||||
}
|
||||
|
||||
func storeExit(err error) int {
|
||||
if errors.Is(err, registry.ErrNotFound) {
|
||||
return exitNotFound
|
||||
}
|
||||
return exitIntegrity
|
||||
}
|
||||
|
||||
func writeJSON(writer io.Writer, value any) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = writer.Write(append(data, '\n'))
|
||||
}
|
||||
|
||||
func writeLine(writer io.Writer, value string) {
|
||||
_, _ = io.WriteString(writer, value+"\n")
|
||||
}
|
||||
|
||||
func writeAndSync(file *os.File, value []byte) error {
|
||||
for len(value) > 0 {
|
||||
n, err := file.Write(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
value = value[n:]
|
||||
}
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
func cleanupOutput(path string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
return errors.New("cleanup target is not a regular file")
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCleanupFailure(stderr io.Writer, path string) {
|
||||
_, _ = fmt.Fprintf(stderr, "cleanup failed path=%s\n", filepath.Clean(path))
|
||||
}
|
||||
|
||||
func parseExpiry(value string) (*time.Time, error) {
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil || parsed.Location() != time.UTC {
|
||||
return nil, errors.New("expiry must be UTC RFC3339")
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func validInstallationID(value string) bool {
|
||||
if len(value) < 1 || len(value) > 63 {
|
||||
return false
|
||||
}
|
||||
for i := range value {
|
||||
c := value[i]
|
||||
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (i > 0 && c == '-') {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validMetadata(value string) bool {
|
||||
if len([]rune(value)) > maxReasonBytes {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validKeyID(value string) bool {
|
||||
if value == record.LegacyKeyID {
|
||||
return true
|
||||
}
|
||||
if len(value) != credential.KeyIDEncodedLength {
|
||||
return false
|
||||
}
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(value)
|
||||
return err == nil && len(decoded) == 12 && base64.RawURLEncoding.EncodeToString(decoded) == value
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//go:build linux
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/credential"
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/record"
|
||||
"github.com/aritmolab/thothii/tools/dwh-auth/internal/registry"
|
||||
)
|
||||
|
||||
const sentinelSecret = "SENTINEL-DWH-SECRET-must-never-be-emitted"
|
||||
|
||||
func TestCreateWritesNew0600KeyAndOnlyPublicSummary(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
output := filepath.Join(t.TempDir(), "new-key")
|
||||
stdout, stderr, code := run(t, "--registry-root", root, "key", "create", "--installation-id", "mac-client", "--description", "synthetic", "--output", output)
|
||||
if code != 0 {
|
||||
t.Fatalf("Run() exit = %d, stdout=%q stderr=%q", code, stdout, stderr)
|
||||
}
|
||||
if stderr != "" {
|
||||
t.Fatalf("stderr = %q, want empty", stderr)
|
||||
}
|
||||
if !strings.HasPrefix(stdout, "created key_id=") || !strings.HasSuffix(stdout, " output="+output+"\n") {
|
||||
t.Fatalf("stdout = %q, want only creation summary", stdout)
|
||||
}
|
||||
if strings.Contains(stdout+stderr, sentinelSecret) {
|
||||
t.Fatal("sentinel secret appeared in command output")
|
||||
}
|
||||
value, err := os.ReadFile(output)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(output) error = %v", err)
|
||||
}
|
||||
if len(value) == 0 || !strings.HasPrefix(string(value), credential.Prefix+".") {
|
||||
t.Fatalf("created key does not have canonical prefix: %q", value)
|
||||
}
|
||||
info, err := os.Stat(output)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(output) error = %v", err)
|
||||
}
|
||||
if got, want := info.Mode().Perm(), os.FileMode(0o600); got != want {
|
||||
t.Fatalf("output mode = %04o, want %04o", got, want)
|
||||
}
|
||||
if code := runOnly(t, "--registry-root", root, "key", "create", "--installation-id", "other", "--output", output); code != 4 {
|
||||
t.Fatalf("second create exit = %d, want filesystem/integrity exit 4", code)
|
||||
}
|
||||
if got, err := os.ReadFile(output); err != nil || !bytes.Equal(got, value) {
|
||||
t.Fatalf("existing output changed after rejected create: err=%v value=%q", err, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSupportsExpiryAndImportReads0600WithoutChangingSource(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
keyFile := filepath.Join(t.TempDir(), "legacy-key")
|
||||
if err := os.WriteFile(keyFile, []byte(sentinelSecret), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
original, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile() error = %v", err)
|
||||
}
|
||||
stdout, stderr, code := run(t, "--registry-root", root, "key", "import", "--legacy-raw", "--installation-id", "legacy-shared", "--from-file", keyFile)
|
||||
if code != 0 || stderr != "" || stdout != "imported key_id=legacy-shared installation_id=legacy-shared\n" {
|
||||
t.Fatalf("import result = (%d, %q, %q)", code, stdout, stderr)
|
||||
}
|
||||
got, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(source) error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, original) {
|
||||
t.Fatal("legacy source file was modified")
|
||||
}
|
||||
if strings.Contains(stdout+stderr, sentinelSecret) {
|
||||
t.Fatal("sentinel secret appeared in import output")
|
||||
}
|
||||
if info, err := os.Stat(keyFile); err != nil || info.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("legacy source mode = %v, err=%v; want 0600", info.Mode().Perm(), err)
|
||||
}
|
||||
if code := runOnly(t, "--registry-root", root, "key", "import", "--legacy-raw", "--installation-id", "legacy-shared", "--from-file", keyFile); code != 4 {
|
||||
t.Fatalf("duplicate import exit = %d, want 4", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAndStatusAreRedactedAndJSONIsPristine(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
output := filepath.Join(t.TempDir(), "key")
|
||||
if code := runOnly(t, "--registry-root", root, "key", "create", "--installation-id", "json-client", "--output", output); code != 0 {
|
||||
t.Fatalf("create exit = %d", code)
|
||||
}
|
||||
stdout, stderr, code := run(t, "--registry-root", root, "key", "list", "--json")
|
||||
if code != 0 || stderr != "" {
|
||||
t.Fatalf("list result = (%d, %q, %q)", code, stdout, stderr)
|
||||
}
|
||||
var listed []registry.PublicRecord
|
||||
if err := json.Unmarshal([]byte(stdout), &listed); err != nil {
|
||||
t.Fatalf("list stdout is not pristine JSON: %v (%q)", err, stdout)
|
||||
}
|
||||
if len(listed) != 1 || listed[0].InstallationID != "json-client" {
|
||||
t.Fatalf("list = %#v", listed)
|
||||
}
|
||||
if strings.Contains(stdout, "secret_sha256") || strings.Contains(stdout, sentinelSecret) {
|
||||
t.Fatalf("list exposed secret material: %q", stdout)
|
||||
}
|
||||
keyID := listed[0].KeyID
|
||||
stdout, stderr, code = run(t, "--registry-root", root, "key", "status", "--key-id", keyID, "--json")
|
||||
if code != 0 || stderr != "" {
|
||||
t.Fatalf("status result = (%d, %q, %q)", code, stdout, stderr)
|
||||
}
|
||||
var status registry.PublicRecord
|
||||
if err := json.Unmarshal([]byte(stdout), &status); err != nil {
|
||||
t.Fatalf("status stdout is not pristine JSON: %v", err)
|
||||
}
|
||||
if status.KeyID != keyID || strings.Contains(stdout, "secret_sha256") || strings.Contains(stdout, sentinelSecret) {
|
||||
t.Fatalf("status exposed secret material: %q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeRequiresReasonAndStatusShowsRevokedWithoutDigest(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
output := filepath.Join(t.TempDir(), "key")
|
||||
if code := runOnly(t, "--registry-root", root, "key", "create", "--installation-id", "revoke-client", "--output", output); code != 0 {
|
||||
t.Fatalf("create exit = %d", code)
|
||||
}
|
||||
listOut, _, _ := run(t, "--registry-root", root, "key", "list", "--json")
|
||||
var listed []registry.PublicRecord
|
||||
if err := json.Unmarshal([]byte(listOut), &listed); err != nil || len(listed) != 1 {
|
||||
t.Fatalf("list = %q, err=%v", listOut, err)
|
||||
}
|
||||
keyID := listed[0].KeyID
|
||||
if code := runOnly(t, "--registry-root", root, "key", "revoke", "--key-id", keyID); code != 2 {
|
||||
t.Fatalf("missing reason exit = %d, want usage 2", code)
|
||||
}
|
||||
stdout, stderr, code := run(t, "--registry-root", root, "key", "revoke", "--key-id", keyID, "--reason", "synthetic rotation")
|
||||
if code != 0 || stdout != "revoked key_id="+keyID+"\n" || stderr != "" {
|
||||
t.Fatalf("revoke result = (%d, %q, %q)", code, stdout, stderr)
|
||||
}
|
||||
statusOut, _, statusCode := run(t, "--registry-root", root, "key", "status", "--key-id", keyID, "--json")
|
||||
if statusCode != 0 || strings.Contains(statusOut, "secret_sha256") || strings.Contains(statusOut, sentinelSecret) {
|
||||
t.Fatalf("revoked status = (%d, %q)", statusCode, statusOut)
|
||||
}
|
||||
if !strings.Contains(statusOut, "\"state\":\"revoked\"") {
|
||||
t.Fatalf("status did not show revoked: %q", statusOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckJSONIsPristineAndNotFoundIsExitThree(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
stdout, stderr, code := run(t, "--registry-root", root, "check", "--json")
|
||||
if code != 0 || stderr != "" || stdout != "{\"status\":\"ok\"}\n" {
|
||||
t.Fatalf("check result = (%d, %q, %q)", code, stdout, stderr)
|
||||
}
|
||||
_, stderr, code = run(t, "--registry-root", root, "key", "status", "--key-id", "AAAAAAAAAAAAAAAA")
|
||||
if code != 3 || !strings.Contains(stderr, "not found") {
|
||||
t.Fatalf("not-found result = (%d, %q)", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsafeInvocationRejectsRelativeAndSecretValuedFlags(t *testing.T) {
|
||||
cases := [][]string{
|
||||
{"--registry-root", "relative", "check"},
|
||||
{"--registry-root", "/tmp/registry", "key", "create", "--installation-id", sentinelSecret, "--output", "/tmp/key"},
|
||||
{"--registry-root", "/tmp/registry", "key", "create", "--installation-id", "safe", "--output", "relative"},
|
||||
{"--registry-root", "/tmp/registry", "key", "import", "--legacy-raw", "--installation-id", "legacy-shared", "--from-file", "relative"},
|
||||
{"--registry-root", "/tmp/registry", "key", "status", "--key-id", sentinelSecret},
|
||||
}
|
||||
for _, args := range cases {
|
||||
stdout, stderr, code := run(t, args...)
|
||||
if code != 2 {
|
||||
t.Errorf("args %q exit = %d, want 2 (stdout=%q stderr=%q)", args, code, stdout, stderr)
|
||||
}
|
||||
if strings.Contains(stdout+stderr, sentinelSecret) {
|
||||
t.Errorf("args %q leaked sentinel secret in output", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrityFailureUsesExitFourWithoutDigest(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(root, "active"), 0o750); err != nil {
|
||||
t.Fatalf("Mkdir(active) error = %v", err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(root, "revoked"), 0o750); err != nil {
|
||||
t.Fatalf("Mkdir(revoked) error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "active", "unexpected"), []byte(sentinelSecret), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
stdout, stderr, code := run(t, "--registry-root", root, "check")
|
||||
if code != 4 || stdout != "" || strings.Contains(stderr, sentinelSecret) || strings.Contains(stderr, "sha") {
|
||||
t.Fatalf("integrity result = (%d, %q, %q)", code, stdout, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRejectsInvalidExpiryWithoutWritingOutput(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
output := filepath.Join(t.TempDir(), "key")
|
||||
if code := runOnly(t, "--registry-root", root, "key", "create", "--installation-id", "client", "--expires-at", "not-time", "--output", output); code != 2 {
|
||||
t.Fatalf("invalid expiry exit = %d, want 2", code)
|
||||
}
|
||||
if _, err := os.Stat(output); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("invalid expiry output stat error = %v, want absent", err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(t *testing.T, args ...string) (string, string, int) {
|
||||
t.Helper()
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := Run(context.Background(), args, strings.NewReader(""), &stdout, &stderr)
|
||||
return stdout.String(), stderr.String(), code
|
||||
}
|
||||
|
||||
func runOnly(t *testing.T, args ...string) int {
|
||||
t.Helper()
|
||||
_, _, code := run(t, args...)
|
||||
return code
|
||||
}
|
||||
|
||||
// Keep imports and fixtures honest if this test file is extended with direct records.
|
||||
var _ = base64.RawURLEncoding
|
||||
var _ = sha256.Sum256
|
||||
var _ = io.EOF
|
||||
var _ = time.Time{}
|
||||
var _ record.Record
|
||||
Reference in New Issue
Block a user