diff --git a/shared/license/keys.go b/shared/license/keys.go new file mode 100644 index 0000000..5787946 --- /dev/null +++ b/shared/license/keys.go @@ -0,0 +1,38 @@ +package license + +import ( + "fmt" + + "github.com/hyperboloide/lk" +) + +// trustedPublicKeys are the keys a licence may be signed with, newest first. +// +// To rotate: prepend the new key, ship a server release that trusts both, then +// reissue. Remove a retired key only once every licence signed with it has +// expired. +// +// This is a slice from day one even though it holds one entry, because +// retrofitting a single-key verifier into a multi-key one during an incident is +// not a thing to plan for. +// +// These are compiled in and deliberately not configurable. A configurable trust +// root is a licensing bypass: a self-hosted operator could point it at a keypair +// they generated themselves. +var trustedPublicKeys = []string{ + // Populated in Task 6 with the real production key. + // Until then this slice is empty and every licence fails to verify, + // which is the correct default for a build with no trust root. +} + +func publicKeys() ([]*lk.PublicKey, error) { + out := make([]*lk.PublicKey, 0, len(trustedPublicKeys)) + for i, s := range trustedPublicKeys { + k, err := lk.PublicKeyFromB32String(s) + if err != nil { + return nil, fmt.Errorf("trusted public key %d is malformed: %w", i, err) + } + out = append(out, k) + } + return out, nil +} diff --git a/shared/license/sign.go b/shared/license/sign.go new file mode 100644 index 0000000..d812ad2 --- /dev/null +++ b/shared/license/sign.go @@ -0,0 +1,43 @@ +//go:build !noSign + +package license + +import ( + "encoding/json" + "fmt" + + "github.com/hyperboloide/lk" +) + +// Sign marshals a licence and signs it, returning the base32 blob. +// +// This file carries the !noSign build tag so the signing path can be compiled +// out of the control plane. The server has no reason to hold signing code and +// no reason to ship it into a customer's data centre. +// +// privateKeyB32 comes from LICENSE_SIGNING_KEY on the issuing side only. +func Sign(l License, privateKeyB32 string) (string, error) { + if privateKeyB32 == "" { + return "", fmt.Errorf("no signing key provided") + } + priv, err := lk.PrivateKeyFromB32String(privateKeyB32) + if err != nil { + return "", fmt.Errorf("parse signing key: %w", err) + } + + data, err := json.Marshal(l) + if err != nil { + return "", fmt.Errorf("marshal licence: %w", err) + } + + signed, err := lk.NewLicense(priv, data) + if err != nil { + return "", fmt.Errorf("sign licence: %w", err) + } + + blob, err := signed.ToB32String() + if err != nil { + return "", fmt.Errorf("encode licence: %w", err) + } + return blob, nil +}