Add a new generic container registry client via a new `container` package. Use this to create a command line utility as well as a new upload target for container registries. The code uses the github.com/containers/* project and packages to interact with container registires that is also used by skopeo, podman et al. One if the dependencies is `proglottis/gpgme` that is using cgo to bind libgpgme, so we have to add the corresponding devel package to the BuildRequires as well as installing it on CI. Checks will follow later via an integration test.
255 lines
6.5 KiB
Go
255 lines
6.5 KiB
Go
package libtrust
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// ErrKeyFileDoesNotExist indicates that the private key file does not exist.
|
|
ErrKeyFileDoesNotExist = errors.New("key file does not exist")
|
|
)
|
|
|
|
func readKeyFileBytes(filename string) ([]byte, error) {
|
|
data, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
err = ErrKeyFileDoesNotExist
|
|
} else {
|
|
err = fmt.Errorf("unable to read key file %s: %s", filename, err)
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
/*
|
|
Loading and Saving of Public and Private Keys in either PEM or JWK format.
|
|
*/
|
|
|
|
// LoadKeyFile opens the given filename and attempts to read a Private Key
|
|
// encoded in either PEM or JWK format (if .json or .jwk file extension).
|
|
func LoadKeyFile(filename string) (PrivateKey, error) {
|
|
contents, err := readKeyFileBytes(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var key PrivateKey
|
|
|
|
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") {
|
|
key, err = UnmarshalPrivateKeyJWK(contents)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode private key JWK: %s", err)
|
|
}
|
|
} else {
|
|
key, err = UnmarshalPrivateKeyPEM(contents)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode private key PEM: %s", err)
|
|
}
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// LoadPublicKeyFile opens the given filename and attempts to read a Public Key
|
|
// encoded in either PEM or JWK format (if .json or .jwk file extension).
|
|
func LoadPublicKeyFile(filename string) (PublicKey, error) {
|
|
contents, err := readKeyFileBytes(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var key PublicKey
|
|
|
|
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") {
|
|
key, err = UnmarshalPublicKeyJWK(contents)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode public key JWK: %s", err)
|
|
}
|
|
} else {
|
|
key, err = UnmarshalPublicKeyPEM(contents)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode public key PEM: %s", err)
|
|
}
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// SaveKey saves the given key to a file using the provided filename.
|
|
// This process will overwrite any existing file at the provided location.
|
|
func SaveKey(filename string, key PrivateKey) error {
|
|
var encodedKey []byte
|
|
var err error
|
|
|
|
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") {
|
|
// Encode in JSON Web Key format.
|
|
encodedKey, err = json.MarshalIndent(key, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encode private key JWK: %s", err)
|
|
}
|
|
} else {
|
|
// Encode in PEM format.
|
|
pemBlock, err := key.PEMBlock()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encode private key PEM: %s", err)
|
|
}
|
|
encodedKey = pem.EncodeToMemory(pemBlock)
|
|
}
|
|
|
|
err = ioutil.WriteFile(filename, encodedKey, os.FileMode(0600))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write private key file %s: %s", filename, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SavePublicKey saves the given public key to the file.
|
|
func SavePublicKey(filename string, key PublicKey) error {
|
|
var encodedKey []byte
|
|
var err error
|
|
|
|
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") {
|
|
// Encode in JSON Web Key format.
|
|
encodedKey, err = json.MarshalIndent(key, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encode public key JWK: %s", err)
|
|
}
|
|
} else {
|
|
// Encode in PEM format.
|
|
pemBlock, err := key.PEMBlock()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encode public key PEM: %s", err)
|
|
}
|
|
encodedKey = pem.EncodeToMemory(pemBlock)
|
|
}
|
|
|
|
err = ioutil.WriteFile(filename, encodedKey, os.FileMode(0644))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write public key file %s: %s", filename, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Public Key Set files
|
|
|
|
type jwkSet struct {
|
|
Keys []json.RawMessage `json:"keys"`
|
|
}
|
|
|
|
// LoadKeySetFile loads a key set
|
|
func LoadKeySetFile(filename string) ([]PublicKey, error) {
|
|
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") {
|
|
return loadJSONKeySetFile(filename)
|
|
}
|
|
|
|
// Must be a PEM format file
|
|
return loadPEMKeySetFile(filename)
|
|
}
|
|
|
|
func loadJSONKeySetRaw(data []byte) ([]json.RawMessage, error) {
|
|
if len(data) == 0 {
|
|
// This is okay, just return an empty slice.
|
|
return []json.RawMessage{}, nil
|
|
}
|
|
|
|
keySet := jwkSet{}
|
|
|
|
err := json.Unmarshal(data, &keySet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode JSON Web Key Set: %s", err)
|
|
}
|
|
|
|
return keySet.Keys, nil
|
|
}
|
|
|
|
func loadJSONKeySetFile(filename string) ([]PublicKey, error) {
|
|
contents, err := readKeyFileBytes(filename)
|
|
if err != nil && err != ErrKeyFileDoesNotExist {
|
|
return nil, err
|
|
}
|
|
|
|
return UnmarshalPublicKeyJWKSet(contents)
|
|
}
|
|
|
|
func loadPEMKeySetFile(filename string) ([]PublicKey, error) {
|
|
data, err := readKeyFileBytes(filename)
|
|
if err != nil && err != ErrKeyFileDoesNotExist {
|
|
return nil, err
|
|
}
|
|
|
|
return UnmarshalPublicKeyPEMBundle(data)
|
|
}
|
|
|
|
// AddKeySetFile adds a key to a key set
|
|
func AddKeySetFile(filename string, key PublicKey) error {
|
|
if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") {
|
|
return addKeySetJSONFile(filename, key)
|
|
}
|
|
|
|
// Must be a PEM format file
|
|
return addKeySetPEMFile(filename, key)
|
|
}
|
|
|
|
func addKeySetJSONFile(filename string, key PublicKey) error {
|
|
encodedKey, err := json.Marshal(key)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encode trusted client key: %s", err)
|
|
}
|
|
|
|
contents, err := readKeyFileBytes(filename)
|
|
if err != nil && err != ErrKeyFileDoesNotExist {
|
|
return err
|
|
}
|
|
|
|
rawEntries, err := loadJSONKeySetRaw(contents)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rawEntries = append(rawEntries, json.RawMessage(encodedKey))
|
|
entriesWrapper := jwkSet{Keys: rawEntries}
|
|
|
|
encodedEntries, err := json.MarshalIndent(entriesWrapper, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encode trusted client keys: %s", err)
|
|
}
|
|
|
|
err = ioutil.WriteFile(filename, encodedEntries, os.FileMode(0644))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write trusted client keys file %s: %s", filename, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func addKeySetPEMFile(filename string, key PublicKey) error {
|
|
// Encode to PEM, open file for appending, write PEM.
|
|
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, os.FileMode(0644))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to open trusted client keys file %s: %s", filename, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
pemBlock, err := key.PEMBlock()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encoded trusted key: %s", err)
|
|
}
|
|
|
|
_, err = file.Write(pem.EncodeToMemory(pemBlock))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write trusted keys file: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|