2023-08-22 20:35:30 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package distsign implements signature and validation of arbitrary
// distributable files.
//
// There are 3 parties in this exchange:
// - builder, which creates files, signs them with signing keys and publishes
// to server
// - server, which distributes public signing keys, files and signatures
// - client, which downloads files and signatures from server, and validates
// the signatures
//
// There are 2 types of keys:
// - signing keys, that sign individual distributable files on the builder
// - root keys, that sign signing keys and are kept offline
//
// root keys -(sign)-> signing keys -(sign)-> files
//
// All keys are asymmetric Ed25519 key pairs.
//
// The server serves static files under some known prefix. The kinds of files are:
// - distsign.pub - bundle of PEM-encoded public signing keys
// - distsign.pub.sig - signature of distsign.pub using one of the root keys
// - $file - any distributable file
// - $file.sig - signature of $file using any of the signing keys
//
// The root public keys are baked into the client software at compile time.
// These keys are long-lived and prove the validity of current signing keys
// from distsign.pub. To rotate root keys, a new client release must be
// published, they are not rotated dynamically. There are multiple root keys in
// different locations specifically to allow this rotation without using the
// discarded root key for any new signatures.
//
// The signing public keys are fetched by the client dynamically before every
// download and can be rotated more readily, assuming that most deployed
// clients trust the root keys used to issue fresh signing keys.
package distsign
import (
2023-08-28 20:48:33 +00:00
"context"
2023-08-22 20:35:30 +00:00
"crypto/ed25519"
"crypto/rand"
"encoding/binary"
"encoding/pem"
"errors"
"fmt"
"hash"
"io"
2023-08-28 20:48:33 +00:00
"log"
2023-08-22 20:35:30 +00:00
"net/http"
"net/url"
"os"
2023-08-28 20:48:33 +00:00
"time"
2023-08-22 20:35:30 +00:00
"github.com/hdevalence/ed25519consensus"
"golang.org/x/crypto/blake2s"
2023-08-28 20:48:33 +00:00
"tailscale.com/net/tshttpproxy"
"tailscale.com/types/logger"
"tailscale.com/util/must"
2023-08-22 20:35:30 +00:00
)
const (
2023-08-23 23:13:03 +00:00
pemTypeRootPrivate = "ROOT PRIVATE KEY"
pemTypeRootPublic = "ROOT PUBLIC KEY"
pemTypeSigningPrivate = "SIGNING PRIVATE KEY"
pemTypeSigningPublic = "SIGNING PUBLIC KEY"
2023-08-22 20:35:30 +00:00
downloadSizeLimit = 1 << 29 // 512MB
signingKeysSizeLimit = 1 << 20 // 1MB
signatureSizeLimit = ed25519 . SignatureSize
)
2023-08-23 23:13:03 +00:00
// RootKey is a root key used to sign signing keys.
type RootKey struct {
k ed25519 . PrivateKey
}
// GenerateRootKey generates a new root key pair and encodes it as PEM.
func GenerateRootKey ( ) ( priv , pub [ ] byte , err error ) {
2023-08-22 20:35:30 +00:00
pub , priv , err = ed25519 . GenerateKey ( rand . Reader )
if err != nil {
return nil , nil , err
}
return pem . EncodeToMemory ( & pem . Block {
2023-08-23 23:13:03 +00:00
Type : pemTypeRootPrivate ,
2023-08-22 20:35:30 +00:00
Bytes : [ ] byte ( priv ) ,
} ) , pem . EncodeToMemory ( & pem . Block {
2023-08-23 23:13:03 +00:00
Type : pemTypeRootPublic ,
2023-08-22 20:35:30 +00:00
Bytes : [ ] byte ( pub ) ,
} ) , nil
}
2023-08-23 23:13:03 +00:00
// ParseRootKey parses the PEM-encoded private root key. The key must be in the
// same format as returned by GenerateRootKey.
func ParseRootKey ( privKey [ ] byte ) ( * RootKey , error ) {
k , err := parsePrivateKey ( privKey , pemTypeRootPrivate )
if err != nil {
return nil , fmt . Errorf ( "failed to parse root key: %w" , err )
}
return & RootKey { k : k } , nil
}
2023-08-22 20:35:30 +00:00
// SignSigningKeys signs the bundle of public signing keys. The bundle must be
// a sequence of PEM blocks joined with newlines.
2023-08-23 23:13:03 +00:00
func ( r * RootKey ) SignSigningKeys ( pubBundle [ ] byte ) ( [ ] byte , error ) {
2023-08-24 17:54:42 +00:00
if _ , err := ParseSigningKeyBundle ( pubBundle ) ; err != nil {
2023-08-23 23:13:03 +00:00
return nil , err
}
return ed25519 . Sign ( r . k , pubBundle ) , nil
2023-08-22 20:35:30 +00:00
}
2023-08-23 23:13:03 +00:00
// SigningKey is a signing key used to sign packages.
type SigningKey struct {
k ed25519 . PrivateKey
}
// GenerateSigningKey generates a new signing key pair and encodes it as PEM.
func GenerateSigningKey ( ) ( priv , pub [ ] byte , err error ) {
pub , priv , err = ed25519 . GenerateKey ( rand . Reader )
if err != nil {
return nil , nil , err
}
return pem . EncodeToMemory ( & pem . Block {
Type : pemTypeSigningPrivate ,
Bytes : [ ] byte ( priv ) ,
} ) , pem . EncodeToMemory ( & pem . Block {
Type : pemTypeSigningPublic ,
Bytes : [ ] byte ( pub ) ,
} ) , nil
}
// ParseSigningKey parses the PEM-encoded private signing key. The key must be
// in the same format as returned by GenerateSigningKey.
func ParseSigningKey ( privKey [ ] byte ) ( * SigningKey , error ) {
k , err := parsePrivateKey ( privKey , pemTypeSigningPrivate )
if err != nil {
return nil , fmt . Errorf ( "failed to parse root key: %w" , err )
}
return & SigningKey { k : k } , nil
}
2023-08-22 20:35:30 +00:00
// SignPackageHash signs the hash and the length of a package. Use PackageHash
// to compute the inputs.
2023-08-23 23:13:03 +00:00
func ( s * SigningKey ) SignPackageHash ( hash [ ] byte , len int64 ) ( [ ] byte , error ) {
2023-08-22 20:35:30 +00:00
if len <= 0 {
return nil , fmt . Errorf ( "package length must be positive, got %d" , len )
}
msg := binary . LittleEndian . AppendUint64 ( hash , uint64 ( len ) )
2023-08-23 23:13:03 +00:00
return ed25519 . Sign ( s . k , msg ) , nil
2023-08-22 20:35:30 +00:00
}
// PackageHash is a hash.Hash that counts the number of bytes written. Use it
// to get the hash and length inputs to SigningKey.SignPackageHash.
type PackageHash struct {
hash . Hash
len int64
}
// NewPackageHash returns an initialized PackageHash using BLAKE2s.
func NewPackageHash ( ) * PackageHash {
h , err := blake2s . New256 ( nil )
if err != nil {
// Should never happen with a nil key passed to blake2s.
panic ( err )
}
return & PackageHash { Hash : h }
}
func ( ph * PackageHash ) Write ( b [ ] byte ) ( int , error ) {
ph . len += int64 ( len ( b ) )
return ph . Hash . Write ( b )
}
// Reset the PackageHash to its initial state.
func ( ph * PackageHash ) Reset ( ) {
ph . len = 0
ph . Hash . Reset ( )
}
// Len returns the total number of bytes written.
func ( ph * PackageHash ) Len ( ) int64 { return ph . len }
// Client downloads and validates files from a distribution server.
type Client struct {
2023-08-28 20:48:33 +00:00
logf logger . Logf
2023-08-22 20:35:30 +00:00
roots [ ] ed25519 . PublicKey
pkgsAddr * url . URL
}
// NewClient returns a new client for distribution server located at pkgsAddr,
// and uses embedded root keys from the roots/ subdirectory of this package.
2023-08-28 20:48:33 +00:00
func NewClient ( logf logger . Logf , pkgsAddr string ) ( * Client , error ) {
if logf == nil {
logf = log . Printf
}
2023-08-22 20:35:30 +00:00
u , err := url . Parse ( pkgsAddr )
if err != nil {
return nil , fmt . Errorf ( "invalid pkgsAddr %q: %w" , pkgsAddr , err )
}
2023-08-28 20:48:33 +00:00
return & Client { logf : logf , roots : roots ( ) , pkgsAddr : u } , nil
2023-08-22 20:35:30 +00:00
}
func ( c * Client ) url ( path string ) string {
return c . pkgsAddr . JoinPath ( path ) . String ( )
}
// Download fetches a file at path srcPath from pkgsAddr passed in NewClient.
// The file is downloaded to dstPath and its signature is validated using the
// embedded root keys. Download returns an error if anything goes wrong with
// the actual file download or with signature validation.
2023-08-28 20:48:33 +00:00
func ( c * Client ) Download ( ctx context . Context , srcPath , dstPath string ) error {
2023-08-22 20:35:30 +00:00
// Always fetch a fresh signing key.
sigPub , err := c . signingKeys ( )
if err != nil {
return err
}
srcURL := c . url ( srcPath )
sigURL := srcURL + ".sig"
2023-08-28 20:48:33 +00:00
c . logf ( "Downloading %q" , srcURL )
2023-08-22 20:35:30 +00:00
dstPathUnverified := dstPath + ".unverified"
2023-08-28 20:48:33 +00:00
hash , len , err := c . download ( ctx , srcURL , dstPathUnverified , downloadSizeLimit )
2023-08-22 20:35:30 +00:00
if err != nil {
return err
}
2023-08-28 20:48:33 +00:00
c . logf ( "Downloading %q" , sigURL )
2023-08-22 20:35:30 +00:00
sig , err := fetch ( sigURL , signatureSizeLimit )
if err != nil {
// Best-effort clean up of downloaded package.
os . Remove ( dstPathUnverified )
return err
}
msg := binary . LittleEndian . AppendUint64 ( hash , uint64 ( len ) )
2023-08-24 17:54:42 +00:00
if ! VerifyAny ( sigPub , msg , sig ) {
2023-08-22 20:35:30 +00:00
// Best-effort clean up of downloaded package.
os . Remove ( dstPathUnverified )
2023-08-28 17:52:05 +00:00
return fmt . Errorf ( "signature %q for file %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key" , sigURL , srcURL )
2023-08-22 20:35:30 +00:00
}
2023-08-28 20:48:33 +00:00
c . logf ( "Signature OK" )
2023-08-22 20:35:30 +00:00
if err := os . Rename ( dstPathUnverified , dstPath ) ; err != nil {
return fmt . Errorf ( "failed to move %q to %q after signature validation" , dstPathUnverified , dstPath )
}
return nil
}
2023-08-31 19:15:45 +00:00
// ValidateLocalBinary fetches the latest signature associated with the binary
// at srcURLPath and uses it to validate the file located on disk via
// localFilePath. ValidateLocalBinary returns an error if anything goes wrong
// with the signature download or with signature validation.
func ( c * Client ) ValidateLocalBinary ( srcURLPath , localFilePath string ) error {
// Always fetch a fresh signing key.
sigPub , err := c . signingKeys ( )
if err != nil {
return err
}
srcURL := c . url ( srcURLPath )
sigURL := srcURL + ".sig"
localFile , err := os . Open ( localFilePath )
if err != nil {
return err
}
defer localFile . Close ( )
h := NewPackageHash ( )
_ , err = io . Copy ( h , localFile )
if err != nil {
return err
}
hash , hashLen := h . Sum ( nil ) , h . Len ( )
c . logf ( "Downloading %q" , sigURL )
sig , err := fetch ( sigURL , signatureSizeLimit )
if err != nil {
return err
}
msg := binary . LittleEndian . AppendUint64 ( hash , uint64 ( hashLen ) )
if ! VerifyAny ( sigPub , msg , sig ) {
return fmt . Errorf ( "signature %q for file %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key" , sigURL , localFilePath )
}
c . logf ( "Signature OK" )
return nil
}
2023-08-22 20:35:30 +00:00
// signingKeys fetches current signing keys from the server and validates them
// against the roots. Should be called before validation of any downloaded file
// to get the fresh keys.
func ( c * Client ) signingKeys ( ) ( [ ] ed25519 . PublicKey , error ) {
keyURL := c . url ( "distsign.pub" )
sigURL := keyURL + ".sig"
raw , err := fetch ( keyURL , signingKeysSizeLimit )
if err != nil {
return nil , err
}
sig , err := fetch ( sigURL , signatureSizeLimit )
if err != nil {
return nil , err
}
2023-08-24 17:54:42 +00:00
if ! VerifyAny ( c . roots , raw , sig ) {
2023-08-22 20:35:30 +00:00
return nil , fmt . Errorf ( "signature %q for key %q does not validate with any known root key; either you are under attack, or running a very old version of Tailscale with outdated root keys" , sigURL , keyURL )
}
2023-08-24 17:54:42 +00:00
keys , err := ParseSigningKeyBundle ( raw )
2023-08-23 23:13:03 +00:00
if err != nil {
return nil , fmt . Errorf ( "cannot parse signing key bundle from %q: %w" , keyURL , err )
2023-08-22 20:35:30 +00:00
}
return keys , nil
}
// fetch reads the response body from url into memory, up to limit bytes.
func fetch ( url string , limit int64 ) ( [ ] byte , error ) {
resp , err := http . Get ( url )
if err != nil {
return nil , err
}
defer resp . Body . Close ( )
return io . ReadAll ( io . LimitReader ( resp . Body , limit ) )
}
// download writes the response body of url into a local file at dst, up to
// limit bytes. On success, the returned value is a BLAKE2s hash of the file.
2023-08-28 20:48:33 +00:00
func ( c * Client ) download ( ctx context . Context , url , dst string , limit int64 ) ( [ ] byte , int64 , error ) {
tr := http . DefaultTransport . ( * http . Transport ) . Clone ( )
tr . Proxy = tshttpproxy . ProxyFromEnvironment
defer tr . CloseIdleConnections ( )
hc := & http . Client { Transport : tr }
quickCtx , cancel := context . WithTimeout ( ctx , 30 * time . Second )
defer cancel ( )
headReq := must . Get ( http . NewRequestWithContext ( quickCtx , http . MethodHead , url , nil ) )
res , err := hc . Do ( headReq )
2023-08-22 20:35:30 +00:00
if err != nil {
return nil , 0 , err
}
2023-08-28 20:48:33 +00:00
if res . StatusCode != http . StatusOK {
return nil , 0 , fmt . Errorf ( "HEAD %q: %v" , url , res . Status )
}
if res . ContentLength <= 0 {
return nil , 0 , fmt . Errorf ( "HEAD %q: unexpected Content-Length %v" , url , res . ContentLength )
}
c . logf ( "Download size: %v" , res . ContentLength )
2023-08-22 20:35:30 +00:00
2023-08-28 20:48:33 +00:00
dlReq := must . Get ( http . NewRequestWithContext ( ctx , http . MethodGet , url , nil ) )
dlRes , err := hc . Do ( dlReq )
2023-08-22 20:35:30 +00:00
if err != nil {
return nil , 0 , err
}
2023-08-28 20:48:33 +00:00
defer dlRes . Body . Close ( )
// TODO(bradfitz): resume from existing partial file on disk
if dlRes . StatusCode != http . StatusOK {
return nil , 0 , fmt . Errorf ( "GET %q: %v" , url , dlRes . Status )
}
2023-08-22 20:35:30 +00:00
2023-08-28 20:48:33 +00:00
of , err := os . Create ( dst )
if err != nil {
2023-08-22 20:35:30 +00:00
return nil , 0 , err
}
2023-08-28 20:48:33 +00:00
defer of . Close ( )
pw := & progressWriter { total : res . ContentLength , logf : c . logf }
h := NewPackageHash ( )
n , err := io . Copy ( io . MultiWriter ( of , h , pw ) , io . LimitReader ( dlRes . Body , limit ) )
if err != nil {
return nil , n , err
}
if n != res . ContentLength {
return nil , n , fmt . Errorf ( "GET %q: downloaded %v, want %v" , url , n , res . ContentLength )
2023-08-22 20:35:30 +00:00
}
2023-08-28 20:48:33 +00:00
if err := dlRes . Body . Close ( ) ; err != nil {
return nil , n , err
}
if err := of . Close ( ) ; err != nil {
return nil , n , err
}
pw . print ( )
2023-08-22 20:35:30 +00:00
return h . Sum ( nil ) , h . Len ( ) , nil
}
2023-08-28 20:48:33 +00:00
type progressWriter struct {
done int64
total int64
lastPrint time . Time
logf logger . Logf
}
func ( pw * progressWriter ) Write ( p [ ] byte ) ( n int , err error ) {
pw . done += int64 ( len ( p ) )
if time . Since ( pw . lastPrint ) > 2 * time . Second {
pw . print ( )
}
return len ( p ) , nil
}
func ( pw * progressWriter ) print ( ) {
pw . lastPrint = time . Now ( )
pw . logf ( "Downloaded %v/%v (%.1f%%)" , pw . done , pw . total , float64 ( pw . done ) / float64 ( pw . total ) * 100 )
}
2023-08-23 23:13:03 +00:00
func parsePrivateKey ( data [ ] byte , typeTag string ) ( ed25519 . PrivateKey , error ) {
2023-08-22 20:35:30 +00:00
b , rest := pem . Decode ( data )
if b == nil {
return nil , errors . New ( "failed to decode PEM data" )
}
if len ( rest ) > 0 {
return nil , errors . New ( "trailing PEM data" )
}
2023-08-23 23:13:03 +00:00
if b . Type != typeTag {
return nil , fmt . Errorf ( "PEM type is %q, want %q" , b . Type , typeTag )
2023-08-22 20:35:30 +00:00
}
if len ( b . Bytes ) != ed25519 . PrivateKeySize {
return nil , errors . New ( "private key has incorrect length for an Ed25519 private key" )
}
return ed25519 . PrivateKey ( b . Bytes ) , nil
}
2023-08-24 17:54:42 +00:00
// ParseSigningKeyBundle parses the bundle of PEM-encoded public signing keys.
func ParseSigningKeyBundle ( bundle [ ] byte ) ( [ ] ed25519 . PublicKey , error ) {
return parsePublicKeyBundle ( bundle , pemTypeSigningPublic )
}
// ParseRootKeyBundle parses the bundle of PEM-encoded public root keys.
func ParseRootKeyBundle ( bundle [ ] byte ) ( [ ] ed25519 . PublicKey , error ) {
return parsePublicKeyBundle ( bundle , pemTypeRootPublic )
}
func parsePublicKeyBundle ( bundle [ ] byte , typeTag string ) ( [ ] ed25519 . PublicKey , error ) {
2023-08-23 23:13:03 +00:00
var keys [ ] ed25519 . PublicKey
for len ( bundle ) > 0 {
2023-08-24 17:54:42 +00:00
pub , rest , err := parsePublicKey ( bundle , typeTag )
2023-08-23 23:13:03 +00:00
if err != nil {
return nil , err
}
keys = append ( keys , pub )
bundle = rest
}
if len ( keys ) == 0 {
return nil , errors . New ( "no signing keys found in the bundle" )
}
return keys , nil
}
func parseSinglePublicKey ( data [ ] byte , typeTag string ) ( ed25519 . PublicKey , error ) {
pub , rest , err := parsePublicKey ( data , typeTag )
2023-08-22 20:35:30 +00:00
if err != nil {
return nil , err
}
if len ( rest ) > 0 {
return nil , errors . New ( "trailing PEM data" )
}
return pub , err
}
2023-08-23 23:13:03 +00:00
func parsePublicKey ( data [ ] byte , typeTag string ) ( pub ed25519 . PublicKey , rest [ ] byte , retErr error ) {
2023-08-22 20:35:30 +00:00
b , rest := pem . Decode ( data )
if b == nil {
return nil , nil , errors . New ( "failed to decode PEM data" )
}
2023-08-23 23:13:03 +00:00
if b . Type != typeTag {
return nil , nil , fmt . Errorf ( "PEM type is %q, want %q" , b . Type , typeTag )
2023-08-22 20:35:30 +00:00
}
if len ( b . Bytes ) != ed25519 . PublicKeySize {
return nil , nil , errors . New ( "public key has incorrect length for an Ed25519 public key" )
}
return ed25519 . PublicKey ( b . Bytes ) , rest , nil
}
2023-08-24 17:54:42 +00:00
// VerifyAny verifies whether sig is valid for msg using any of the keys.
// VerifyAny will panic if any of the keys have the wrong size for Ed25519.
func VerifyAny ( keys [ ] ed25519 . PublicKey , msg , sig [ ] byte ) bool {
2023-08-22 20:35:30 +00:00
for _ , k := range keys {
if ed25519consensus . Verify ( k , msg , sig ) {
return true
}
}
return false
}