mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
at least initially we don't support using snap commands to create/delete keys under an external keypair manager, organize things such that though we can produce appropriate error messages and we can implement supporting this later if required
299 lines
8.2 KiB
Go
299 lines
8.2 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2021 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package asserts
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
|
|
"golang.org/x/crypto/openpgp/packet"
|
|
|
|
"github.com/snapcore/snapd/strutil"
|
|
)
|
|
|
|
type ExternalKeyInfo struct {
|
|
Name string
|
|
ID string
|
|
}
|
|
|
|
// ExternalKeypairManager is key pair manager implemented via an external program interface.
|
|
// TODO: points to interface docs
|
|
type ExternalKeypairManager struct {
|
|
keyMgrPath string
|
|
nameToID map[string]string
|
|
cache map[string]*cachedExtKey
|
|
}
|
|
|
|
// NewExternalKeypairManager creates a new ExternalKeypairManager using the program at keyMgrPath.
|
|
func NewExternalKeypairManager(keyMgrPath string) (*ExternalKeypairManager, error) {
|
|
em := &ExternalKeypairManager{
|
|
keyMgrPath: keyMgrPath,
|
|
nameToID: make(map[string]string),
|
|
cache: make(map[string]*cachedExtKey),
|
|
}
|
|
if err := em.checkFeatures(); err != nil {
|
|
return nil, err
|
|
}
|
|
return em, nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) keyMgr(op string, args []string, in []byte, out interface{}) error {
|
|
args = append([]string{op}, args...)
|
|
cmd := exec.Command(em.keyMgrPath, args...)
|
|
var outBuf bytes.Buffer
|
|
var errBuf bytes.Buffer
|
|
|
|
if len(in) != 0 {
|
|
cmd.Stdin = bytes.NewBuffer(in)
|
|
}
|
|
cmd.Stdout = &outBuf
|
|
cmd.Stderr = &errBuf
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("external keypair manager %q %v failed: %v (%q)", em.keyMgrPath, args, err, errBuf.Bytes())
|
|
|
|
}
|
|
switch o := out.(type) {
|
|
case *[]byte:
|
|
*o = outBuf.Bytes()
|
|
default:
|
|
if err := json.Unmarshal(outBuf.Bytes(), out); err != nil {
|
|
return fmt.Errorf("cannot decode external keypair manager %q %v output: %v", em.keyMgrPath, args, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) checkFeatures() error {
|
|
var feats struct {
|
|
Signing []string `json:"signing"`
|
|
PublicKeys []string `json:"public-keys"`
|
|
}
|
|
if err := em.keyMgr("features", nil, nil, &feats); err != nil {
|
|
return err
|
|
}
|
|
if !strutil.ListContains(feats.Signing, "RSA-PKCS") {
|
|
return fmt.Errorf("external keypair manager %q missing support for RSA-PKCS signing", em.keyMgrPath)
|
|
}
|
|
if !strutil.ListContains(feats.PublicKeys, "DER") {
|
|
return fmt.Errorf("external keypair manager %q missing support for public key DER output format", em.keyMgrPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) keyNames() ([]string, error) {
|
|
var knames struct {
|
|
Names []string `json:"key-names"`
|
|
}
|
|
if err := em.keyMgr("key-names", nil, nil, &knames); err != nil {
|
|
return nil, fmt.Errorf("cannot get all external keypair manager key names: %v", err)
|
|
}
|
|
return knames.Names, nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) findByName(name string) (PublicKey, *rsa.PublicKey, error) {
|
|
var k []byte
|
|
err := em.keyMgr("get-public-key", []string{"-f", "DER", "-k", name}, nil, &k)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot find external key: %v", err)
|
|
}
|
|
pubk, err := x509.ParsePKIXPublicKey(k)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot decode external key %q: %v", name, err)
|
|
}
|
|
rsaPub, ok := pubk.(*rsa.PublicKey)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("expected RSA public key, got instead: %T", pubk)
|
|
}
|
|
pubKey := RSAPublicKey(rsaPub)
|
|
return pubKey, rsaPub, nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) Export(keyName string) ([]byte, error) {
|
|
pubKey, _, err := em.findByName(keyName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return EncodePublicKey(pubKey)
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) loadKey(name string) (*cachedExtKey, error) {
|
|
id, ok := em.nameToID[name]
|
|
if ok {
|
|
return em.cache[id], nil
|
|
}
|
|
pubKey, rsaPub, err := em.findByName(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id = pubKey.ID()
|
|
em.nameToID[name] = id
|
|
cachedKey := &cachedExtKey{
|
|
pubKey: pubKey,
|
|
signer: &extSigner{
|
|
keyName: name,
|
|
rsaPub: rsaPub,
|
|
// signWith is filled later
|
|
},
|
|
}
|
|
em.cache[id] = cachedKey
|
|
return cachedKey, nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) privateKey(cachedKey *cachedExtKey) PrivateKey {
|
|
if cachedKey.privKey == nil {
|
|
extSigner := cachedKey.signer
|
|
// fill signWith
|
|
extSigner.signWith = em.signWith
|
|
signer := packet.NewSignerPrivateKey(v1FixedTimestamp, extSigner)
|
|
signk := openpgpPrivateKey{privk: signer}
|
|
extKey := &extPGPPrivateKey{
|
|
pubKey: cachedKey.pubKey,
|
|
from: fmt.Sprintf("external keypair manager %q", em.keyMgrPath),
|
|
externalID: extSigner.keyName,
|
|
bitLen: extSigner.rsaPub.N.BitLen(),
|
|
doSign: signk.sign,
|
|
}
|
|
cachedKey.privKey = extKey
|
|
}
|
|
return cachedKey.privKey
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) GetByName(keyName string) (PrivateKey, error) {
|
|
cachedKey, err := em.loadKey(keyName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return em.privateKey(cachedKey), nil
|
|
}
|
|
|
|
// ExternalUnsupportedOpError represents the error situation of operations
|
|
// that are not supported/mediated via ExternalKeypairManager.
|
|
type ExternalUnsupportedOpError struct {
|
|
msg string
|
|
}
|
|
|
|
func (euoe *ExternalUnsupportedOpError) Error() string {
|
|
return euoe.msg
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) Put(privKey PrivateKey) error {
|
|
return &ExternalUnsupportedOpError{"cannot import private key into external keypair manager"}
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) Delete(keyName string) error {
|
|
return &ExternalUnsupportedOpError{"no support to delete external keypair manager keys"}
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) Generate(keyName string) error {
|
|
return &ExternalUnsupportedOpError{"no support to mediate generating an external keypair manager key"}
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) loadAllKeys() ([]string, error) {
|
|
names, err := em.keyNames()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, name := range names {
|
|
if _, err := em.loadKey(name); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) Get(keyID string) (PrivateKey, error) {
|
|
cachedKey, ok := em.cache[keyID]
|
|
if !ok {
|
|
// try to load all keys
|
|
if _, err := em.loadAllKeys(); err != nil {
|
|
return nil, err
|
|
}
|
|
cachedKey, ok = em.cache[keyID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot find external key with id %q", keyID)
|
|
}
|
|
}
|
|
return em.privateKey(cachedKey), nil
|
|
}
|
|
|
|
func (em *ExternalKeypairManager) List() ([]ExternalKeyInfo, error) {
|
|
names, err := em.loadAllKeys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res := make([]ExternalKeyInfo, len(names))
|
|
for i, name := range names {
|
|
res[i].Name = name
|
|
res[i].ID = em.cache[em.nameToID[name]].pubKey.ID()
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// see https://datatracker.ietf.org/doc/html/rfc2313 and more recently
|
|
// and more precisely about SHA-512:
|
|
// https://datatracker.ietf.org/doc/html/rfc3447#section-9.2 Notes 1.
|
|
var digestInfoSHA512Prefix = []byte{0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}
|
|
|
|
func (em *ExternalKeypairManager) signWith(keyName string, digest []byte) (signature []byte, err error) {
|
|
// wrap the digest into the needed DigestInfo, the RSA-PKCS
|
|
// mechanism or equivalent is expected not to do this on its
|
|
// own
|
|
toSign := &bytes.Buffer{}
|
|
toSign.Write(digestInfoSHA512Prefix)
|
|
toSign.Write(digest)
|
|
|
|
err = em.keyMgr("sign", []string{"-m", "RSA-PKCS", "-k", keyName}, toSign.Bytes(), &signature)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return signature, nil
|
|
}
|
|
|
|
type cachedExtKey struct {
|
|
pubKey PublicKey
|
|
signer *extSigner
|
|
privKey PrivateKey
|
|
}
|
|
|
|
type extSigner struct {
|
|
keyName string
|
|
rsaPub *rsa.PublicKey
|
|
signWith func(keyName string, digest []byte) (signature []byte, err error)
|
|
}
|
|
|
|
func (es *extSigner) Public() crypto.PublicKey {
|
|
return es.rsaPub
|
|
}
|
|
|
|
func (es *extSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
|
|
if opts.HashFunc() != crypto.SHA512 {
|
|
return nil, fmt.Errorf("unexpected pgp signature digest")
|
|
}
|
|
|
|
return es.signWith(es.keyName, digest)
|
|
}
|