pkg/container/seal/v2/helpers.go (254 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 v2
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/hmac"
cryptorand "crypto/rand"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/awnumar/memguard"
"google.golang.org/protobuf/proto"
containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1"
"github.com/elastic/harp/pkg/sdk/security"
"golang.org/x/crypto/hkdf"
)
func tryRecipientKeys(derivedKey *[32]byte, recipients []*containerv1.Recipient) (*[32]byte, error) {
// Calculate recipient identifier
identifier, err := keyIdentifierFromDerivedKey(derivedKey)
if err != nil {
return nil, fmt.Errorf("unable to generate identifier: %w", err)
}
// Find matching recipient
for _, r := range recipients {
// Check recipient identifiers
if !security.SecureCompare(identifier, r.Identifier) {
continue
}
// Try to decrypt the secretbox with the derived key.
clearText, err := decrypt(r.Key, derivedKey)
if err != nil {
return nil, fmt.Errorf("invalid recipient encryption key")
}
var payloadKey [32]byte
copy(payloadKey[:], clearText)
// Encryption key found, return no error.
return &payloadKey, nil
}
// No recipient found in list.
return nil, fmt.Errorf("no recipient found")
}
func prepareSignature(rand io.Reader, encryptionKey *[32]byte) (*ecdsa.PrivateKey, []byte, error) {
// Generate ephemeral signing key
sigPriv, err := ecdsa.GenerateKey(signatureCurve, cryptorand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to generate signing keypair")
}
// Compress public key point
sigPub := elliptic.MarshalCompressed(sigPriv.Curve, sigPriv.PublicKey.X, sigPriv.PublicKey.Y)
// Encrypt public signing key
encryptedPubSig, err := encrypt(rand, sigPub, encryptionKey)
if err != nil {
return nil, nil, fmt.Errorf("unable to encrypt public signing key: %w", err)
}
// Cleanup
memguard.WipeBytes(sigPub)
// No error
return sigPriv, encryptedPubSig, nil
}
func signContainer(sigPriv *ecdsa.PrivateKey, headers *containerv1.Header, container *containerv1.Container) (content, containerSig []byte, err error) {
// Serialize protobuf payload
content, err = proto.Marshal(container)
if err != nil {
return nil, nil, fmt.Errorf("unable to encode container content: %w", err)
}
// Compute header hash
headerHash, err := computeHeaderHash(headers)
if err != nil {
return nil, nil, fmt.Errorf("unable to compute header hash: %w", err)
}
// Compute protected content hash
protectedHash := computeProtectedHash(headerHash, content)
// Sign the protected content
r, s, err := ecdsa.Sign(cryptorand.Reader, sigPriv, protectedHash)
if err != nil {
return nil, nil, fmt.Errorf("unable to sign protected content: %w", err)
}
// Container signature
containerSig = append(r.Bytes(), s.Bytes()...)
// No error
return content, containerSig, nil
}
func generatedEncryptionKey(rand io.Reader) (*[32]byte, error) {
// Generate payload encryption key
var payloadKey [encryptionKeySize]byte
if _, err := io.ReadFull(rand, payloadKey[:]); err != nil {
return nil, fmt.Errorf("unable to generate payload key for encryption")
}
// No error
return &payloadKey, nil
}
func encrypt(rand io.Reader, plaintext []byte, key *[32]byte) ([]byte, error) {
// Check cleartext message size.
if len(plaintext) > messageLimit {
return nil, errors.New("value too large")
}
// Generate random nonce
var seed [seedSize]byte
if _, err := io.ReadFull(rand, seed[:]); err != nil {
return nil, fmt.Errorf("unable to generate random encryption nonce: %w", err)
}
// Derive keys from seed and secret key
ek, n2, ak, err := kdf(key, seed[:])
if err != nil {
return nil, fmt.Errorf("unable to derive keys from seed: %w", err)
}
// Prepare an AES-256-CTR stream cipher
block, err := aes.NewCipher(ek)
if err != nil {
return nil, fmt.Errorf("unable to prepare block cipher: %w", err)
}
ciph := cipher.NewCTR(block, n2)
// Encrypt the payload
c := make([]byte, len(plaintext))
ciph.XORKeyStream(c, plaintext)
// Compute MAC
t, err := mac(ak, seed[:], c)
if err != nil {
return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err)
}
// Serialize final payload
// n || c || t
body := append([]byte{}, seed[:]...)
body = append(body, c...)
body = append(body, t...)
// No error
return body, nil
}
func decrypt(ciphertext []byte, key *[32]byte) ([]byte, error) {
// Check arguments
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphered text too short")
}
// Extract components
n := ciphertext[:seedSize]
t := ciphertext[len(ciphertext)-macSize:]
c := ciphertext[seedSize : len(ciphertext)-macSize]
// Derive keys from seed and secret key
ek, n2, ak, err := kdf(key, n)
if err != nil {
return nil, fmt.Errorf("unable to derive keys from seed: %w", err)
}
// Compute MAC
t2, err := mac(ak, n, c)
if err != nil {
return nil, fmt.Errorf("unable to compute MAC: %w", err)
}
// Time-constant compare MAC
if !security.SecureCompare(t, t2) {
return nil, errors.New("invalid pre-authentication header")
}
// Prepare an AES-256-CTR stream cipher
block, err := aes.NewCipher(ek)
if err != nil {
return nil, fmt.Errorf("unable to prepare block cipher: %w", err)
}
ciph := cipher.NewCTR(block, n2)
// Decrypt the payload
m := make([]byte, len(c))
ciph.XORKeyStream(m, c)
// No error
return m, nil
}
func computeHeaderHash(headers *containerv1.Header) ([]byte, error) {
// Check arguments
if headers == nil {
return nil, errors.New("unable process with nil headers")
}
// Prepare signature
header, err := proto.Marshal(headers)
if err != nil {
return nil, fmt.Errorf("unable to marshal container headers")
}
// Hash serialized proto
hash := sha512.Sum512(header)
// No error
return hash[:], nil
}
func computeProtectedHash(headerHash, content []byte) []byte {
// Prepare protected content
protected := bytes.Buffer{}
protected.Write([]byte("harp fips encrypted signature"))
protected.WriteByte(0x00)
protected.Write(headerHash)
contentHash := sha512.Sum512(content)
protected.Write(contentHash[:])
// No error
return protected.Bytes()
}
func packRecipient(rand io.Reader, payloadKey *[32]byte, ephPrivKey *ecdsa.PrivateKey, peerPublicKey *ecdsa.PublicKey) (*containerv1.Recipient, error) {
// Check arguments
if payloadKey == nil {
return nil, fmt.Errorf("unable to proceed with nil payload key")
}
if ephPrivKey == nil {
return nil, fmt.Errorf("unable to proceed with nil private key")
}
if peerPublicKey == nil {
return nil, fmt.Errorf("unable to proceed with nil public key")
}
// Create identifier
recipientKey, err := deriveSharedKeyFromRecipient(peerPublicKey, ephPrivKey)
if err != nil {
return nil, fmt.Errorf("unable to execute key agreement: %w", err)
}
// Calculate identifier
identifier, err := keyIdentifierFromDerivedKey(recipientKey)
if err != nil {
return nil, fmt.Errorf("unable to derive key identifier: %w", err)
}
// Encrypt the payload key
encryptedKey, err := encrypt(rand, payloadKey[:], recipientKey)
if err != nil {
return nil, fmt.Errorf("unable to encrypt payload key for recipient: %w", err)
}
// Pack recipient
recipient := &containerv1.Recipient{
Identifier: identifier,
Key: encryptedKey,
}
// Return recipient
return recipient, nil
}
func deriveSharedKeyFromRecipient(publicKey *ecdsa.PublicKey, privateKey *ecdsa.PrivateKey) (*[32]byte, error) {
// Compute Z - ECDH(localPrivate, remotePublic)
Z, _ := privateKey.Curve.ScalarMult(publicKey.X, publicKey.Y, privateKey.D.Bytes())
// Prepare info: ( AlgorithmID || PartyInfo || KeyLength )
fixedInfo := []byte{}
fixedInfo = append(fixedInfo, lengthPrefixedArray([]byte("A256CTR"))...)
fixedInfo = append(fixedInfo, uint32ToBytes(encryptionKeySize)...)
// HKDF-HMAC-SHA512
kdf := hkdf.New(sha512.New, Z.Bytes(), nil, fixedInfo)
var sharedSecret [encryptionKeySize]byte
if _, err := io.ReadFull(kdf, sharedSecret[:]); err != nil {
return nil, fmt.Errorf("unable to derive shared secret: %w", err)
}
// No error
return &sharedSecret, nil
}
func keyIdentifierFromDerivedKey(derivedKey *[32]byte) ([]byte, error) {
// HMAC-SHA512
h := hmac.New(sha512.New, []byte("harp signcryption box key identifier"))
if _, err := h.Write(derivedKey[:]); err != nil {
return nil, fmt.Errorf("unable to generate recipient identifier")
}
// Return 32 bytes truncated hash.
return h.Sum(nil)[0:encryptionKeySize], nil
}
func lengthPrefixedArray(value []byte) []byte {
if len(value) == 0 {
return []byte{}
}
result := make([]byte, 4)
binary.BigEndian.PutUint32(result, uint32(len(value)))
return append(result, value...)
}
func uint32ToBytes(value uint32) []byte {
result := make([]byte, 4)
binary.BigEndian.PutUint32(result, value)
return result
}
func kdf(key *[32]byte, n []byte) (ek, n2, ak []byte, err error) {
// Check arguments
if key == nil {
return nil, nil, nil, errors.New("unable to derive keys from a nil seed")
}
// Prepare HKDF-HMAC-SHA384
encKDF := hkdf.New(sha512.New384, key[:], nil, append([]byte("harp-encryption-key-v2"), n...))
// Derive encryption key
tmp := make([]byte, encryptionKeySize+nonceSize)
if _, err := io.ReadFull(encKDF, tmp); err != nil {
return nil, nil, nil, fmt.Errorf("unable to generate encryption key from seed: %w", err)
}
// Split encryption key (Ek) and nonce (n2)
ek = tmp[:encryptionKeySize]
n2 = tmp[encryptionKeySize:]
// Derive authentication key
authKDF := hkdf.New(sha512.New384, key[:], nil, append([]byte("harp-auth-key-for-aead"), n...))
// Derive authentication key
ak = make([]byte, nonceSize)
if _, err := io.ReadFull(authKDF, ak); err != nil {
return nil, nil, nil, fmt.Errorf("unable to generate authentication key from seed: %w", err)
}
// No error
return ek, n2, ak, nil
}
func mac(ak, n, c []byte) ([]byte, error) {
// Compute pre-authenticated content
preAuth, err := pae([]byte("harp-authentication-tag-v2"), n, c)
if err != nil {
return nil, err
}
// Compute MAC
mac := hmac.New(sha512.New384, ak)
// Hash pre-authentication content
mac.Write(preAuth)
// No error
return mac.Sum(nil), nil
}
func pae(pieces ...[]byte) ([]byte, error) {
output := &bytes.Buffer{}
// Encode piece count
count := len(pieces)
if err := binary.Write(output, binary.LittleEndian, uint64(count)); err != nil {
return nil, err
}
// For each element
for i := range pieces {
// Encode size
if err := binary.Write(output, binary.LittleEndian, uint64(len(pieces[i]))); err != nil {
return nil, err
}
// Encode data
if _, err := output.Write(pieces[i]); err != nil {
return nil, err
}
}
// No error
return output.Bytes(), nil
}