asteris-llc/converge

View on GitHub
keystore/keystore.go

Summary

Maintainability
A
2 hrs
Test Coverage
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package keystore

import (
    "bytes"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "os/user"
    "path"
    "path/filepath"

    "github.com/pkg/errors"
    "golang.org/x/crypto/openpgp"
)

// A Keystore represents a repository of trusted public keys which can be
// used to verify PGP signatures.
type Keystore struct {
    LocalPath  string
    UserPath   string
    SystemPath string
    keyring    openpgp.KeyRing
}

// New returns a new Keystore backed by the provided paths.
func New(localPath, userPath, systemPath string) *Keystore {
    return &Keystore{
        LocalPath:  localPath,
        UserPath:   userPath,
        SystemPath: systemPath,
    }
}

var defaultKeystore *Keystore

// Default returns a keystore backed by the default local, user, and system paths.
func Default() *Keystore {
    if defaultKeystore == nil {
        userPath := ""

        usr, err := user.Current()
        if err == nil {
            userPath = filepath.Join(usr.HomeDir, ".converge/trustedkeys")
        }

        defaultKeystore = &Keystore{
            LocalPath:  "trustedkeys",
            UserPath:   userPath,
            SystemPath: "/usr/lib/converge/trustedkeys",
        }
    }

    return defaultKeystore
}

// StoreTrustedKey stores the contents of the public key.
func (ks *Keystore) StoreTrustedKey(pubkeyBytes []byte) (string, error) {
    if err := os.MkdirAll(ks.UserPath, 0755); err != nil {
        return "", err
    }

    keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(pubkeyBytes))
    if err != nil {
        return "", err
    }

    if len(keyring) < 1 {
        return "", errors.New("cannot store trusted key: empty keyring")
    }

    pubKey := keyring[0].PrimaryKey
    trustedKeyPath := path.Join(ks.UserPath, fmt.Sprintf("%x", pubKey.Fingerprint))
    if err := ioutil.WriteFile(trustedKeyPath, pubkeyBytes, 0644); err != nil {
        return "", err
    }
    return trustedKeyPath, nil
}

// DeleteTrustedKey deletes the trusted key identified by fingerprint.
func (ks *Keystore) DeleteTrustedKey(fingerprint string) error {
    return os.Remove(path.Join(ks.UserPath, fingerprint))
}

// MaskTrustedSystemKey masks the system trusted key identified by fingerprint.
func (ks *Keystore) MaskTrustedSystemKey(fingerprint string) (string, error) {
    dst := path.Join(ks.UserPath, fingerprint)
    return dst, ioutil.WriteFile(dst, []byte(""), 0644)
}

// CheckSignature takes a signed file and a detached signature and verifies if it is signed by a trusted signer.
func (ks *Keystore) CheckSignature(signed, signature io.Reader) error {
    if ks.keyring == nil {
        keyring, err := loadKeyring(ks)
        if err != nil {
            return errors.Wrap(err, "error loading keyring")
        }
        ks.keyring = keyring
    }

    signer, err := openpgp.CheckArmoredDetachedSignature(ks.keyring, signed, signature)

    // openpgp has a weird api so we do some custom error handling.
    if err != nil {
        if err == io.EOF {
            return errors.New("no valid signatures found in signature file")
        }

        return err
    }

    if signer == nil {
        return errors.New("invalid signer")
    }

    return nil
}

func loadKeyring(ks *Keystore) (openpgp.KeyRing, error) {
    var keyring openpgp.EntityList
    trustedKeys := make(map[string]*openpgp.Entity)

    for _, p := range []string{ks.SystemPath, ks.UserPath, ks.LocalPath} {
        files, err := ioutil.ReadDir(p)
        if err != nil {
            if os.IsNotExist(err) {
                continue
            }
            return nil, err
        }

        for _, file := range files {
            if file.Size() == 0 {
                delete(trustedKeys, file.Name())
                continue
            }

            trustedKey, err := os.Open(filepath.Join(p, file.Name()))
            if err != nil {
                return nil, err
            }
            defer trustedKey.Close()

            keys, err := openpgp.ReadArmoredKeyRing(trustedKey)
            if err != nil {
                return nil, err
            }
            if len(keys) < 1 {
                return nil, errors.New("empty keyring")
            }

            fingerprint := fmt.Sprintf("%x", keys[0].PrimaryKey.Fingerprint)
            if file.Name() != fingerprint {
                return nil, fmt.Errorf("fingerprint mismatch: %q:%q", file.Name(), fingerprint)
            }

            trustedKeys[fingerprint] = keys[0]
        }
    }

    for _, v := range trustedKeys {
        keyring = append(keyring, v)
    }
    return keyring, nil
}