Created
June 4, 2022 07:27
-
-
Save mlevkov/8d1a481992494210cb2e5cc3a1c05221 to your computer and use it in GitHub Desktop.
Google Media CDN Signed URLs key helper with PEM converter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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