Validates and activates the key like PUT /api/v1/license (same service call, audited as the system actor) and prints the license state without the key. Exit codes: 2 bad input, 3 key rejected, 4 license server unreachable or untrusted (nothing stored). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
6.8 KiB
Go
191 lines
6.8 KiB
Go
// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
const testKeygenAccount = "9de54794-8e94-4250-83f6-87e3d5686a0b"
|
|
|
|
// fakeKeygen answers the requests of license activation with signed
|
|
// responses: "GOOD-KEY" is valid (no machine activation needed), every
|
|
// other key does not exist.
|
|
func fakeKeygen(t *testing.T) (url string, pub ed25519.PublicKey) {
|
|
t.Helper()
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/v1/accounts/"+testKeygenAccount)
|
|
body, _ := io.ReadAll(r.Body)
|
|
var resp any
|
|
switch {
|
|
case r.Method == http.MethodPost && path == "/licenses/actions/validate-key":
|
|
var req struct {
|
|
Meta struct {
|
|
Key string `json:"key"`
|
|
Nonce int64 `json:"nonce"`
|
|
} `json:"meta"`
|
|
}
|
|
_ = json.Unmarshal(body, &req)
|
|
meta := map[string]any{"valid": false, "code": "NOT_FOUND", "detail": "does not exist", "nonce": req.Meta.Nonce}
|
|
var data any
|
|
if req.Meta.Key == "GOOD-KEY" {
|
|
meta["valid"], meta["code"], meta["detail"] = true, "VALID", "valid"
|
|
data = map[string]any{"id": "lic-1", "type": "licenses", "attributes": map[string]any{
|
|
"name": "CLI Test", "expiry": "2030-01-01T00:00:00Z", "status": "ACTIVE",
|
|
"metadata": map[string]any{"maxAgents": 5},
|
|
}}
|
|
}
|
|
resp = map[string]any{"meta": meta, "data": data}
|
|
case r.Method == http.MethodGet && path == "/licenses/lic-1/entitlements":
|
|
resp = map[string]any{"data": []any{map[string]any{"attributes": map[string]any{"code": "HA"}}}}
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
out, _ := json.Marshal(resp)
|
|
sum := sha256.Sum256(out)
|
|
digest := "sha-256=" + base64.StdEncoding.EncodeToString(sum[:])
|
|
date := time.Now().UTC().Format(http.TimeFormat)
|
|
signing := "(request-target): " + strings.ToLower(r.Method) + " " + r.URL.RequestURI() + "\nhost: " + r.Host +
|
|
"\ndate: " + date + "\ndigest: " + digest
|
|
sig := ed25519.Sign(priv, []byte(signing))
|
|
w.Header().Set("Date", date)
|
|
w.Header().Set("Digest", digest)
|
|
w.Header().Set("Keygen-Signature", `keyid="`+testKeygenAccount+`", algorithm="ed25519", signature="`+
|
|
base64.StdEncoding.EncodeToString(sig)+`", headers="(request-target) host date digest"`)
|
|
w.Header().Set("Content-Type", "application/vnd.api+json")
|
|
_, _ = w.Write(out)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL, pub
|
|
}
|
|
|
|
func TestReadKey(t *testing.T) {
|
|
if k, err := readKey(strings.NewReader(" ABCD-1234 \n")); err != nil || k != "ABCD-1234" {
|
|
t.Errorf("trimmed key: %q %v", k, err)
|
|
}
|
|
for name, in := range map[string]string{
|
|
"empty": "",
|
|
"blank": " \n\t\n",
|
|
"too long": strings.Repeat("A", maxKeyLength+1),
|
|
"oversized": strings.Repeat("A", maxKeyInput+1),
|
|
"two lines": "KEY-1\nKEY-2\n",
|
|
"blank line": "KEY-1\r\nKEY-2",
|
|
} {
|
|
_, err := readKey(strings.NewReader(in))
|
|
ee, ok := err.(*exitError) //nolint:errorlint // readKey returns it unwrapped
|
|
if !ok || ee.code != exitUsage {
|
|
t.Errorf("%s: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLicenseCommandUsage(t *testing.T) {
|
|
var out, errOut bytes.Buffer
|
|
if code := run(context.Background(), []string{"license"}, lookupFrom(nil), nil, &out, &errOut); code != exitUsage {
|
|
t.Errorf("without subcommand: %d", code)
|
|
}
|
|
if code := run(context.Background(), []string{"license", "frob"}, lookupFrom(nil), nil, &out, &errOut); code != exitUsage {
|
|
t.Errorf("unknown subcommand: %d", code)
|
|
}
|
|
if code := run(context.Background(), []string{"license", "set-key", "KEY"}, lookupFrom(nil), nil, &out, &errOut); code != exitUsage ||
|
|
!strings.Contains(errOut.String(), "stdin") {
|
|
t.Errorf("key as argument: %d %q", code, errOut.String())
|
|
}
|
|
out.Reset()
|
|
if code := run(context.Background(), []string{"license", "help"}, lookupFrom(nil), nil, &out, &errOut); code != 0 ||
|
|
!strings.Contains(out.String(), "set-key") {
|
|
t.Errorf("help: %d %q", code, out.String())
|
|
}
|
|
errOut.Reset()
|
|
if code := run(context.Background(), []string{"license", "set-key"}, lookupFrom(nil), strings.NewReader("\n"), &out, &errOut); code != exitUsage ||
|
|
!strings.Contains(errOut.String(), "no license key") {
|
|
t.Errorf("empty stdin: %d %q", code, errOut.String())
|
|
}
|
|
}
|
|
|
|
func TestLicenseSetKey(t *testing.T) {
|
|
dbURL := testDatabase(t)
|
|
dir := t.TempDir()
|
|
keyFile := filepath.Join(dir, "master.key")
|
|
if code := run(context.Background(), []string{"gen-master-key", "-out", keyFile}, lookupFrom(nil), nil, io.Discard, io.Discard); code != 0 {
|
|
t.Fatal("gen-master-key failed")
|
|
}
|
|
keygenURL, pub := fakeKeygen(t)
|
|
env := map[string]string{
|
|
"DDI_DATABASE_URL": dbURL, "DDI_MASTER_KEY_FILE": keyFile, "DDI_STATE_DIR": dir, "DDI_LOG_LEVEL": "error",
|
|
"KEYGEN_URL": keygenURL, "KEYGEN_ACCOUNT_ID": testKeygenAccount, "KEYGEN_PUBLIC_KEY": hex.EncodeToString(pub),
|
|
}
|
|
setKey := func(key string, env map[string]string) (int, string, string) {
|
|
var out, errOut bytes.Buffer
|
|
code := run(context.Background(), []string{"license", "set-key"}, lookupFrom(env), strings.NewReader(key), &out, &errOut)
|
|
return code, out.String(), errOut.String()
|
|
}
|
|
|
|
code, out, errOut := setKey("UNKNOWN-KEY\n", env)
|
|
if code != exitKeyRejected || !strings.Contains(errOut, "license_invalid") || !strings.Contains(errOut, "NOT_FOUND") {
|
|
t.Errorf("unknown key: %d %q", code, errOut)
|
|
}
|
|
if strings.Contains(out+errOut, "UNKNOWN-KEY") {
|
|
t.Error("the key must not be printed")
|
|
}
|
|
|
|
unreachable := map[string]string{}
|
|
for k, v := range env {
|
|
unreachable[k] = v
|
|
}
|
|
unreachable["KEYGEN_URL"] = "http://127.0.0.1:1"
|
|
if code, _, errOut := setKey("GOOD-KEY", unreachable); code != exitLicenseSvc || !strings.Contains(errOut, "license_server_unreachable") {
|
|
t.Errorf("unreachable license server: %d %q", code, errOut)
|
|
}
|
|
|
|
code, out, errOut = setKey(" GOOD-KEY\n", env)
|
|
if code != 0 {
|
|
t.Fatalf("set-key: %d %s", code, errOut)
|
|
}
|
|
for _, want := range []string{"status: valid", "CLI Test (lic-1)", "expires: 2030-01-01T00:00:00Z",
|
|
"max agents: 5", "max addresses: unlimited", "features: HA"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("output lacks %q:\n%s", want, out)
|
|
}
|
|
}
|
|
if strings.Contains(out+errOut, "GOOD-KEY") {
|
|
t.Error("the key must not be printed")
|
|
}
|
|
|
|
conn, err := pgx.Connect(context.Background(), dbURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close(context.Background()) //nolint:errcheck // test
|
|
var actor string
|
|
var n int
|
|
if err := conn.QueryRow(context.Background(),
|
|
`SELECT min(actor_name), count(*) FROM audit_log WHERE action = 'license.set_key'`).Scan(&actor, &n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 || actor != "system" {
|
|
t.Errorf("audit: %d entries by %q", n, actor)
|
|
}
|
|
}
|