161 lines
4.6 KiB
Go
161 lines
4.6 KiB
Go
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 <generate-key|activate> [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)
|
|
}
|