2017-10-23 05:23:39 -04:00
package trustmanager
2017-09-26 14:33:02 -04:00
import (
"encoding/pem"
"errors"
"io"
"io/ioutil"
"path/filepath"
"sort"
"strings"
"github.com/sirupsen/logrus"
2017-10-30 12:21:41 -04:00
"github.com/theupdateframework/notary"
tufdata "github.com/theupdateframework/notary/tuf/data"
"github.com/theupdateframework/notary/tuf/utils"
2017-09-26 14:33:02 -04:00
)
// Exporter is a simple interface for the two functions we need from the Storage interface
type Exporter interface {
Get ( string ) ( [ ] byte , error )
ListFiles ( ) [ ] string
}
// Importer is a simple interface for the one function we need from the Storage interface
type Importer interface {
Set ( string , [ ] byte ) error
}
// ExportKeysByGUN exports all keys filtered to a GUN
func ExportKeysByGUN ( to io . Writer , s Exporter , gun string ) error {
keys := s . ListFiles ( )
sort . Strings ( keys ) // ensure consistency. ListFiles has no order guarantee
for _ , loc := range keys {
keyFile , err := s . Get ( loc )
if err != nil {
logrus . Warn ( "Could not parse key file at " , loc )
continue
}
block , _ := pem . Decode ( keyFile )
keyGun := block . Headers [ "gun" ]
if keyGun == gun { // must be full GUN match
if err := ExportKeys ( to , s , loc ) ; err != nil {
return err
}
}
}
return nil
}
// ExportKeysByID exports all keys matching the given ID
func ExportKeysByID ( to io . Writer , s Exporter , ids [ ] string ) error {
want := make ( map [ string ] struct { } )
for _ , id := range ids {
want [ id ] = struct { } { }
}
keys := s . ListFiles ( )
for _ , k := range keys {
id := filepath . Base ( k )
if _ , ok := want [ id ] ; ok {
if err := ExportKeys ( to , s , k ) ; err != nil {
return err
}
}
}
return nil
}
// ExportKeys copies a key from the store to the io.Writer
func ExportKeys ( to io . Writer , s Exporter , from string ) error {
// get PEM block
k , err := s . Get ( from )
if err != nil {
return err
}
// parse PEM blocks if there are more than one
for block , rest := pem . Decode ( k ) ; block != nil ; block , rest = pem . Decode ( rest ) {
// add from path in a header for later import
block . Headers [ "path" ] = from
// write serialized PEM
err = pem . Encode ( to , block )
if err != nil {
return err
}
}
return nil
}
// ImportKeys expects an io.Reader containing one or more PEM blocks.
// It reads PEM blocks one at a time until pem.Decode returns a nil
// block.
// Each block is written to the subpath indicated in the "path" PEM
// header. If the file already exists, the file is truncated. Multiple
// adjacent PEMs with the same "path" header are appended together.
func ImportKeys ( from io . Reader , to [ ] Importer , fallbackRole string , fallbackGUN string , passRet notary . PassRetriever ) error {
// importLogic.md contains a small flowchart I made to clear up my understand while writing the cases in this function
// it is very rough, but it may help while reading this piece of code
data , err := ioutil . ReadAll ( from )
if err != nil {
return err
}
var (
writeTo string
toWrite [ ] byte
)
for block , rest := pem . Decode ( data ) ; block != nil ; block , rest = pem . Decode ( rest ) {
handleLegacyPath ( block )
setFallbacks ( block , fallbackGUN , fallbackRole )
loc , err := checkValidity ( block )
if err != nil {
// already logged in checkValidity
continue
}
// the path header is not of any use once we've imported the key so strip it away
delete ( block . Headers , "path" )
// we are now all set for import but let's first encrypt the key
blockBytes := pem . EncodeToMemory ( block )
// check if key is encrypted, note: if it is encrypted at this point, it will have had a path header
if privKey , err := utils . ParsePEMPrivateKey ( blockBytes , "" ) ; err == nil {
// Key is not encrypted- ask for a passphrase and encrypt this key
var chosenPassphrase string
for attempts := 0 ; ; attempts ++ {
var giveup bool
chosenPassphrase , giveup , err = passRet ( loc , block . Headers [ "role" ] , true , attempts )
if err == nil {
break
}
if giveup || attempts > 10 {
return errors . New ( "maximum number of passphrase attempts exceeded" )
}
}
blockBytes , err = utils . ConvertPrivateKeyToPKCS8 ( privKey , tufdata . RoleName ( block . Headers [ "role" ] ) , tufdata . GUN ( block . Headers [ "gun" ] ) , chosenPassphrase )
if err != nil {
return errors . New ( "failed to encrypt key with given passphrase" )
}
}
if loc != writeTo {
// next location is different from previous one. We've finished aggregating
// data for the previous file. If we have data, write the previous file,
// clear toWrite and set writeTo to the next path we're going to write
if toWrite != nil {
if err = importToStores ( to , writeTo , toWrite ) ; err != nil {
return err
}
}
// set up for aggregating next file's data
toWrite = nil
writeTo = loc
}
toWrite = append ( toWrite , blockBytes ... )
}
if toWrite != nil { // close out final iteration if there's data left
return importToStores ( to , writeTo , toWrite )
}
return nil
}
func handleLegacyPath ( block * pem . Block ) {
// if there is a legacy path then we set the gun header from this path
// this is the case when a user attempts to import a key bundle generated by an older client
if rawPath := block . Headers [ "path" ] ; rawPath != "" && rawPath != filepath . Base ( rawPath ) {
// this is a legacy filepath and we should try to deduce the gun name from it
pathWOFileName := filepath . Dir ( rawPath )
if strings . HasPrefix ( pathWOFileName , notary . NonRootKeysSubdir ) {
// remove the notary keystore-specific segment of the path, and any potential leading or trailing slashes
gunName := strings . Trim ( strings . TrimPrefix ( pathWOFileName , notary . NonRootKeysSubdir ) , "/" )
if gunName != "" {
block . Headers [ "gun" ] = gunName
}
}
block . Headers [ "path" ] = filepath . Base ( rawPath )
}
}
func setFallbacks ( block * pem . Block , fallbackGUN , fallbackRole string ) {
if block . Headers [ "gun" ] == "" {
if fallbackGUN != "" {
block . Headers [ "gun" ] = fallbackGUN
}
}
if block . Headers [ "role" ] == "" {
if fallbackRole == "" {
block . Headers [ "role" ] = notary . DefaultImportRole
} else {
block . Headers [ "role" ] = fallbackRole
}
}
}
// checkValidity ensures the fields in the pem headers are valid and parses out the location.
// While importing a collection of keys, errors from this function should result in only the
// current pem block being skipped.
func checkValidity ( block * pem . Block ) ( string , error ) {
// A root key or a delegations key should not have a gun
// Note that a key that is not any of the canonical roles (except root) is a delegations key and should not have a gun
switch block . Headers [ "role" ] {
case tufdata . CanonicalSnapshotRole . String ( ) , tufdata . CanonicalTargetsRole . String ( ) , tufdata . CanonicalTimestampRole . String ( ) :
// check if the key is missing a gun header or has an empty gun and error out since we don't know what gun it belongs to
if block . Headers [ "gun" ] == "" {
logrus . Warnf ( "failed to import key (%s) to store: Cannot have canonical role key without a gun, don't know what gun it belongs to" , block . Headers [ "path" ] )
return "" , errors . New ( "invalid key pem block" )
}
default :
delete ( block . Headers , "gun" )
}
loc , ok := block . Headers [ "path" ]
// only if the path isn't specified do we get into this parsing path logic
if ! ok || loc == "" {
// if the path isn't specified, we will try to infer the path rel to trust dir from the role (and then gun)
// parse key for the keyID which we will save it by.
// if the key is encrypted at this point, we will generate an error and continue since we don't know the ID to save it by
decodedKey , err := utils . ParsePEMPrivateKey ( pem . EncodeToMemory ( block ) , "" )
if err != nil {
logrus . Warn ( "failed to import key to store: Invalid key generated, key may be encrypted and does not contain path header" )
return "" , errors . New ( "invalid key pem block" )
}
loc = decodedKey . ID ( )
}
return loc , nil
}
func importToStores ( to [ ] Importer , path string , bytes [ ] byte ) error {
var err error
for _ , i := range to {
if err = i . Set ( path , bytes ) ; err != nil {
logrus . Errorf ( "failed to import key to store: %s" , err . Error ( ) )
continue
}
break
}
return err
}