// Copyright (c) 2019, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. package crypt import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/pem" "io" "io/ioutil" "github.com/pkg/errors" "github.com/sylabs/sif/pkg/sif" ) var ( ErrEncryptedKeyNotFound = errors.New("encrypted key not found") ErrUnsupportedKeyURI = errors.New("unsupported key URI") ErrNoEncryptedKeyData = errors.New("no encrypted key data") ErrNoPEMData = errors.New("No PEM data") ) const ( Unknown = iota Passphrase PEM ) // KeyInfo contains information for passing around // or extracting a passphrase for an encrypted container type KeyInfo struct { Format int Material string Path string } func getRandomBytes(size int) ([]byte, error) { buf := make([]byte, size) _, err := rand.Read(buf) if err != nil { return nil, err } return buf, nil } func NewPlaintextKey(k KeyInfo) ([]byte, error) { switch k.Format { case PEM: // in this case we will generate a random secret and // encrypt it using the PEM key.use the PEM key to // encrypt a secret return getRandomBytes(64) case Passphrase: // return the original value unmodified return []byte(k.Material), nil default: return nil, ErrUnsupportedKeyURI } } func EncryptKey(k KeyInfo, plaintext []byte) ([]byte, error) { switch k.Format { case PEM: pubKey, err := loadPEMPublicKey(k.Path) if err != nil { return nil, errors.Wrap(err, "loading public key for key encryption") } ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, plaintext, nil) if err != nil { return nil, errors.Wrap(err, "encrypting key") } var buf bytes.Buffer if err := savePEMMessage(&buf, ciphertext); err != nil { return nil, errors.Wrap(err, "serializing encrypted key") } return buf.Bytes(), nil case Passphrase: return nil, nil default: return nil, ErrUnsupportedKeyURI } } func PlaintextKey(k KeyInfo, image string) ([]byte, error) { switch k.Format { case PEM: privateKey, err := loadPEMPrivateKey(k.Path) if err != nil { return nil, errors.Wrap(err, "loading private key for key decryption") } pemKey, err := getEncryptionKeyFromImage(image) if err != nil { return nil, errors.Wrapf(err, "loading encrypted key SIF image %s", image) } pemBuf := bytes.NewReader(pemKey) encKey, err := loadPEMMessage(pemBuf) if err != nil { return nil, errors.Wrapf(err, "unpacking PEM message from SIF image %s", image) } plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encKey, nil) if err != nil { return nil, errors.Wrapf(err, "decrypting key from image %s", image) } return plaintext, nil case Passphrase: return []byte(k.Material), nil default: return nil, ErrUnsupportedKeyURI } } func loadPEMPrivateKey(fn string) (*rsa.PrivateKey, error) { b, err := ioutil.ReadFile(fn) if err != nil { return nil, err } block, _ := pem.Decode(b) if block == nil { return nil, errors.Wrapf(ErrNoPEMData, "reading %s", fn) } return x509.ParsePKCS1PrivateKey(block.Bytes) } func loadPEMPublicKey(fn string) (*rsa.PublicKey, error) { b, err := ioutil.ReadFile(fn) if err != nil { return nil, err } block, _ := pem.Decode(b) if block == nil { return nil, errors.Wrapf(ErrNoPEMData, "reading %s", fn) } return x509.ParsePKCS1PublicKey(block.Bytes) } func loadPEMMessage(r io.Reader) ([]byte, error) { b, err := ioutil.ReadAll(r) if err != nil { return nil, err } block, _ := pem.Decode(b) if block == nil { return nil, errors.Wrapf(ErrNoPEMData, "reading PEM block") } var buf []byte if _, err := asn1.Unmarshal(block.Bytes, &buf); err != nil { return nil, errors.Wrapf(err, "unmarshalling ASN.1 data") } return buf, nil } func savePEMMessage(w io.Writer, msg []byte) error { asn1Bytes, err := asn1.Marshal(msg) if err != nil { return err } var b = &pem.Block{ Type: "MESSAGE", Bytes: asn1Bytes, } return pem.Encode(w, b) } func getEncryptionKeyFromImage(fn string) ([]byte, error) { img, err := sif.LoadContainer(fn, true) if err != nil { return nil, errors.Wrapf(err, "loading container image from %s", fn) } defer img.UnloadContainer() primDescr, _, err := img.GetPartPrimSys() if err != nil { return nil, errors.Wrapf(err, "retrieving primary system partition from %s", fn) } descr, _, err := img.GetLinkedDescrsByType(primDescr.ID, sif.DataCryptoMessage) if err != nil { return nil, errors.Wrapf(err, "retrieving linked descriptors for primary system partition from %s", fn) } for _, d := range descr { format, err := d.GetFormatType() if err != nil { return nil, errors.Wrapf(err, "while retrieving cryptographic message format") } message, err := d.GetMessageType() if err != nil { return nil, errors.Wrapf(err, "while retrieving cryptographic message type") } // currently only support one type of message if format != sif.FormatPEM || message != sif.MessageRSAOAEP { continue } // TODO(ian): For now, assume the first linked message is what we // are looking for. We should consider what we want to do in the // case of multiple linked messages data := d.GetData(&img) if data == nil { return nil, errors.Wrapf(ErrNoEncryptedKeyData, "retrieving encrypted key data from %s", fn) } key := make([]byte, len(data)) copy(key, data) return key, nil } return nil, errors.Wrapf(ErrEncryptedKeyNotFound, "reading from %s", fn) }