rastrillo / aviso Public

Add aviso-key: prints one private key

Stdout carries the key alone, newline-terminated, so a shell
substitution can capture it into a secret store without parsing. The
public half is derived at boot; there is no second value to keep.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 3c7b03da31ddf2867470d4dcd386045b421d1c3e parent c0b4a14
2 files changed, +61 −0
  • cmd/aviso-key/main.go +33 −0
  • cmd/aviso-key/main_test.go +28 −0
diff --git a/cmd/aviso-key/main.go b/cmd/aviso-key/main.go
new file mode 100644
index 0000000..fbb0897
--- /dev/null
+++ b/cmd/aviso-key/main.go
@@ -0,0 +1,33 @@
+// Command aviso-key prints one VAPID private key, and nothing else, so
+// it can be captured straight into a secret store:
+//
+// APP_VAPID_PRIVATE_KEY="$(go run amadan.net/rastrillo/aviso/cmd/aviso-key)"
+//
+// The public key is derived by aviso.New; there is no second value to
+// keep. Run it once per app: rotating the key means every browser
+// re-enrols on its next visit.
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "amadan.net/rastrillo/aviso"
+)
+
+func run(w io.Writer) error {
+ k, err := aviso.GenerateKey()
+ if err != nil {
+ return err
+ }
+ _, err = fmt.Fprintln(w, k)
+ return err
+}
+
+func main() {
+ if err := run(os.Stdout); err != nil {
+ fmt.Fprintln(os.Stderr, "aviso-key:", err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/aviso-key/main_test.go b/cmd/aviso-key/main_test.go
new file mode 100644
index 0000000..e56aad1
--- /dev/null
+++ b/cmd/aviso-key/main_test.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "bytes"
+ "encoding/base64"
+ "strings"
+ "testing"
+)
+
+// One line, one key, nothing else: a shell substitution captures it
+// without parsing.
+func TestRunPrintsOnePrivateKey(t *testing.T) {
+ var out bytes.Buffer
+ if err := run(&out); err != nil {
+ t.Fatal(err)
+ }
+ if !strings.HasSuffix(out.String(), "\n") || strings.Count(out.String(), "\n") != 1 {
+ t.Fatalf("want exactly one newline-terminated line, got %q", out.String())
+ }
+ s := strings.TrimSpace(out.String())
+ raw, err := base64.RawURLEncoding.DecodeString(s)
+ if err != nil || len(raw) != 32 {
+ t.Fatalf("not a 32-byte base64url key: %q", s)
+ }
+ if strings.ContainsAny(s, "=+/") {
+ t.Fatalf("not unpadded base64url: %q", s)
+ }
+}