package main import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "flag" "fmt" "os" "strings" "time" ) const activationSchemaVersion = "rap.installation.activation.v1" func main() { if len(os.Args) < 2 { fail("usage: go run scripts/installation/product-root-tool.go [flags]") } switch os.Args[1] { case "generate-key": generateKey() case "activate": activate(os.Args[2:]) default: fail("unknown command %q", os.Args[1]) } } func generateKey() { publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { fail("generate key: %v", err) } writeJSON(map[string]string{ "key_type": "ed25519", "private_key_b64": base64.StdEncoding.EncodeToString(privateKey), "public_key_b64": base64.StdEncoding.EncodeToString(publicKey), }) } func activate(args []string) { fs := flag.NewFlagSet("activate", flag.ExitOnError) privateKeyB64 := fs.String("private-key-b64", "", "base64 Ed25519 private key") privateKeyFile := fs.String("private-key-file", "", "file with base64 key or generate-key JSON") installID := fs.String("install-id", "", "installation id; generated when empty") ownerEmail := fs.String("owner-email", "", "first owner email") role := fs.String("role", "platform_admin", "platform_admin or platform_recovery_admin") expiresAt := fs.String("expires-at", "", "RFC3339 expiry time") environment := fs.String("environment", "", "optional environment label") nonce := fs.String("nonce", "", "optional nonce") if err := fs.Parse(args); err != nil { fail("parse flags: %v", err) } keyText := strings.TrimSpace(*privateKeyB64) if keyText == "" && strings.TrimSpace(*privateKeyFile) != "" { content, err := os.ReadFile(*privateKeyFile) if err != nil { fail("read private key file: %v", err) } keyText = extractPrivateKeyText(content) } privateKey := decodePrivateKey(keyText) email := strings.ToLower(strings.TrimSpace(*ownerEmail)) if email == "" || !strings.Contains(email, "@") { fail("owner-email is required") } normalizedRole := strings.TrimSpace(*role) if normalizedRole != "platform_admin" && normalizedRole != "platform_recovery_admin" { fail("role must be platform_admin or platform_recovery_admin") } id := strings.TrimSpace(*installID) if id == "" { id = randomID("install") } payload := map[string]any{ "schema_version": activationSchemaVersion, "install_id": id, "owner_email": email, "platform_role": normalizedRole, "issued_at": time.Now().UTC().Format(time.RFC3339), } if strings.TrimSpace(*expiresAt) != "" { if _, err := time.Parse(time.RFC3339, strings.TrimSpace(*expiresAt)); err != nil { fail("expires-at must be RFC3339: %v", err) } payload["expires_at"] = strings.TrimSpace(*expiresAt) } if strings.TrimSpace(*environment) != "" { payload["environment"] = strings.TrimSpace(*environment) } if strings.TrimSpace(*nonce) != "" { payload["nonce"] = strings.TrimSpace(*nonce) } canonical, err := json.Marshal(payload) if err != nil { fail("canonicalize payload: %v", err) } signature := ed25519.Sign(privateKey, canonical) writeJSON(map[string]any{ "activation_payload": payload, "activation_signature": base64.StdEncoding.EncodeToString(signature), }) } func extractPrivateKeyText(content []byte) string { text := strings.TrimSpace(string(content)) var generated struct { PrivateKey string `json:"private_key_b64"` } if err := json.Unmarshal(content, &generated); err == nil && strings.TrimSpace(generated.PrivateKey) != "" { return strings.TrimSpace(generated.PrivateKey) } return text } func decodePrivateKey(value string) ed25519.PrivateKey { if strings.TrimSpace(value) == "" { fail("private key is required") } decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(value)) if err != nil { if raw, rawErr := base64.RawStdEncoding.DecodeString(strings.TrimSpace(value)); rawErr == nil { decoded = raw } else { fail("private key must be base64 encoded: %v", err) } } if len(decoded) != ed25519.PrivateKeySize { fail("private key must decode to %d bytes", ed25519.PrivateKeySize) } return ed25519.PrivateKey(decoded) } func randomID(prefix string) string { var bytes [16]byte if _, err := rand.Read(bytes[:]); err != nil { fail("generate id: %v", err) } return fmt.Sprintf("%s-%s", prefix, hex.EncodeToString(bytes[:])) } func writeJSON(value any) { encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") if err := encoder.Encode(value); err != nil { fail("write json: %v", err) } } func fail(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) os.Exit(1) }