Skip to content

Instantly share code, notes, and snippets.

@mlevkov
Created June 4, 2022 07:27
Show Gist options
  • Save mlevkov/8d1a481992494210cb2e5cc3a1c05221 to your computer and use it in GitHub Desktop.
Save mlevkov/8d1a481992494210cb2e5cc3a1c05221 to your computer and use it in GitHub Desktop.
Google Media CDN Signed URLs key helper with PEM converter
package main
import (
"crypto/ed25519"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
"time"
)
// genKeys generates a key pair suitable for request signing
func genKeys(priv, pub io.Writer) error {
pubKey, privKey, err := ed25519.GenerateKey( /*rand=*/ nil)
if err != nil {
return fmt.Errorf("could not generate key: %v", err)
}
if _, err := priv.Write(privKey); err != nil {
return fmt.Errorf("could not write private key: %v", err)
}
if _, err := pub.Write(pubKey); err != nil {
return fmt.Errorf("could not write public key: %v", err)
}
return nil
}
// encodeKey base64 encodes the key for use with our configuration
func encodeKey(key []byte) {
fmt.Fprintln(os.Stdout, base64.RawURLEncoding.EncodeToString(key))
}
// signURL signs the given path using query parameters
func signURL(key []byte, keyset, url string, expires time.Time) {
sep := '?'
if strings.ContainsRune(url, '?') {
sep = '&'
}
toSign := fmt.Sprintf("%s%cExpires=%d&KeyName=%s", url, sep, expires.Unix(), keyset)
sig := ed25519.Sign(key, []byte(toSign))
fmt.Fprintf(os.Stdout, "%s&Signature=%s\n", toSign, base64.RawURLEncoding.EncodeToString(sig))
}
// signPrefixWithQueryParameters signs the given prefix using query parameters
func signPrefixWithQueryParameters(key []byte, keyset, prefix string, expires time.Time) {
toSign := fmt.Sprintf("URLPrefix=%s&Expires=%d&KeyName=%s", base64.RawURLEncoding.EncodeToString([]byte(prefix)), expires.Unix(), keyset)
sig := ed25519.Sign(key, []byte(toSign))
encoded := base64.RawURLEncoding.EncodeToString(sig)
fmt.Fprintf(os.Stdout, "%s&Signature=%s\n", toSign, encoded)
}
// signPrefixWithCookie signs the given prefix using a cookie
func signPrefixWithCookie(key []byte, keyset, prefix string, expires time.Time) {
toSign := fmt.Sprintf("URLPrefix=%s:Expires=%d:KeyName=%s", base64.RawURLEncoding.EncodeToString([]byte(prefix)), expires.Unix(), keyset)
sig := ed25519.Sign(key, []byte(toSign))
fmt.Fprintf(os.Stdout, "Edge-Cache-Cookie=%s:Signature=%s\n", toSign, base64.RawURLEncoding.EncodeToString(sig))
}
// signPrefixWithPathComponent signs the given prefix using path components
func signPrefixWithPathComponent(key []byte, keyset, prefix string, expires time.Time) {
// Remove trailing slashes because the path component format starts
// with a slash and we don't want duplicated slashes in the URL.
prefix = strings.TrimRight(prefix, "/")
toSign := fmt.Sprintf("%s/edge-cache-token=Expires=%d&KeyName=%s", prefix, expires.Unix(), keyset)
sig := ed25519.Sign(key, []byte(toSign))
fmt.Fprintf(os.Stdout, "%s&Signature=%s/\n", toSign, base64.RawURLEncoding.EncodeToString(sig))
}
// ConvertToPEM saves ed25519 keys to disk after
// encoding into PEM format
func ConvertToPEM(priv, pub []byte, privFileBase, pubFileBase string) error {
var (
err error
b []byte
block *pem.Block
newPub ed25519.PublicKey = pub
newPriv ed25519.PrivateKey = priv
)
b, err = x509.MarshalPKCS8PrivateKey(newPriv)
if err != nil {
return err
}
block = &pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
}
err = ioutil.WriteFile(privFileBase+".pem", pem.EncodeToMemory(block), 0600)
if err != nil {
return err
}
// public key
b, err = x509.MarshalPKIXPublicKey(newPub)
if err != nil {
return err
}
block = &pem.Block{
Type: "PUBLIC KEY",
Bytes: b,
}
err = ioutil.WriteFile(pubFileBase+".pem", pem.EncodeToMemory(block), 0644)
return err
}
func main() {
genCmd := flag.NewFlagSet("generate-keys", flag.ExitOnError)
genKey := genCmd.String("key", "private.key", "file name into which to write the generated private key")
genPub := genCmd.String("pub", "public.pub", "file name into which to write the generated public key")
convCmd := flag.NewFlagSet("convert-keys-to-pem", flag.ExitOnError)
convKey := convCmd.String("key", "private.key", "file name into which to write the converted pem private key")
convPub := convCmd.String("pub", "public.pub", "file name into which to write the converted pem public key")
ekCmd := flag.NewFlagSet("encode-key", flag.ExitOnError)
ekPub := ekCmd.String("pub", "public.pub", "file name from which to read the public key")
suCmd := flag.NewFlagSet("sign-url", flag.ExitOnError)
suKey := suCmd.String("key", "private.key", "file name from which to read the private key")
suKeyset := suCmd.String("keyset", "", "the name of the EdgeCacheKeyset to use. Must not be the empty string.")
suURL := suCmd.String("url", "", "the URL to sign, including protocol. Must not be the empty string. For example: http://example.com/path/to/content")
suTTL := suCmd.Duration("ttl", time.Hour, "duration the signed request is valid")
spCmd := flag.NewFlagSet("sign-prefix", flag.ExitOnError)
spKey := spCmd.String("key", "private.key", "file name from which to read the private key")
spKeyset := spCmd.String("keyset", "", "the name of the EdgeCacheKeyset to use. Must not be the empty string.")
spURL := spCmd.String("url-prefix", "", "the URL prefix to sign, including protocol. Must not be the exmpty string. For example: http://example.com/path/ for URLs under /path or http://example.com/path?param=1 for the exact path /path and query parameter with the prefix param=1")
spTTL := spCmd.Duration("ttl", time.Hour, "duration the signed request is valid")
spFmt := spCmd.String("signature-format", "qp", "format to output. Must be one of qp (to output query parameters to add to a URL), cookie (to output the cookie format), or pc (to output a full URL in path component format).")
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "subcommand must be provided\n")
os.Exit(1)
}
switch os.Args[1] {
case "generate-keys":
err := genCmd.Parse(os.Args[2:])
if err != nil {
fmt.Fprintf(os.Stderr, "unable to parse %s arguments: %s\n", os.Args[1], err)
os.Exit(1)
}
// Permission bits are 0600 since private keys should be private
keyFile, err := os.OpenFile(*genKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "could not open private key file for writing: %s\n", err)
os.Exit(1)
}
pubFile, err := os.OpenFile(*genPub, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "could not open public key file for writing: %s\n", err)
os.Exit(1)
}
if err := genKeys(keyFile, pubFile); err != nil {
fmt.Fprintf(os.Stderr, "could not generate keys: %s\n", err)
os.Exit(1)
}
case "convert-keys-to-pem":
err := convCmd.Parse(os.Args[2:])
if err != nil {
fmt.Fprintf(os.Stderr, "unable to parse %s arguments: %s\n", os.Args[1], err)
os.Exit(1)
}
priv, err := os.ReadFile(*convKey)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read private key file: %s\n", err)
os.Exit(1)
}
pub, err := os.ReadFile(*convPub)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read public key file: %s\n", err)
os.Exit(1)
}
err = ConvertToPEM(priv, pub, path.Base(*convKey), path.Base(*convPub))
if err != nil {
fmt.Fprintf(os.Stderr, "could not generate to PEM files: %s\n", err)
os.Exit(1)
}
case "encode-key":
err := ekCmd.Parse(os.Args[2:])
if err != nil {
fmt.Fprintf(os.Stderr, "unable to parse %s arguments: %s\n", os.Args[1], err)
os.Exit(1)
}
pub, err := os.ReadFile(*ekPub)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read public key file: %s\n", err)
os.Exit(1)
}
encodeKey(pub)
case "sign-url":
err := suCmd.Parse(os.Args[2:])
if err != nil {
fmt.Fprintf(os.Stderr, "unable to parse %s arguments: %s\n", os.Args[1], err)
os.Exit(1)
}
if *suKeyset == "" {
fmt.Fprintf(os.Stderr, "a keyset must be provided\n")
os.Exit(1)
}
if *suURL == "" {
fmt.Fprintf(os.Stderr, "a url must be provided\n")
os.Exit(1)
}
key, err := os.ReadFile(*suKey)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read private key file: %s\n", err)
os.Exit(1)
}
expiration := time.Now().Add(*suTTL)
signURL(key, *suKeyset, *suURL, expiration)
case "sign-prefix":
err := spCmd.Parse(os.Args[2:])
if err != nil {
fmt.Fprintf(os.Stderr, "unable to parse %s arguments: %s\n", os.Args[1], err)
os.Exit(1)
}
if *spKeyset == "" {
fmt.Fprintf(os.Stderr, "a keyset must be provided\n")
os.Exit(1)
}
if *spURL == "" {
fmt.Fprintf(os.Stderr, "a url prefix must be provided\n")
os.Exit(1)
}
key, err := os.ReadFile(*spKey)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read private key file: %s\n", err)
os.Exit(1)
}
expiration := time.Now().Add(*spTTL)
switch *spFmt {
case "qp":
signPrefixWithQueryParameters(key, *spKeyset, *spURL, expiration)
case "cookie":
signPrefixWithCookie(key, *spKeyset, *spURL, expiration)
case "pc":
// The path component mode only works if spURL is a path prefix
// since we can't add path components after query parameters.
if strings.ContainsRune(*spURL, '?') {
fmt.Fprintf(os.Stderr, "the pc signature format does not work with query parameter prefixes\n")
os.Exit(1)
}
signPrefixWithPathComponent(key, *spKeyset, *spURL, expiration)
default:
fmt.Fprintf(os.Stderr, "unknown signature-format: %q\n", *spFmt)
os.Exit(1)
}
case "-h", "help", "-help", "--help":
fmt.Fprintf(os.Stdout, "Usage: %s subcommand [subcommand args...]\n\nwhere: subcommand is one of generate-keys, encode-key, sign-url, sign-prefix, help\n", os.Args[0])
os.Exit(0)
default:
fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n", os.Args[1])
fmt.Fprintf(os.Stdout, "try -h, help, -help, --help\n")
os.Exit(1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment