Fixes #2022: Running external commands as a key provider (#2023)

Signed-off-by: AbstractionFactory <179820029+abstractionfactory@users.noreply.github.com>
Signed-off-by: ollevche <ollevche@gmail.com>
Co-authored-by: Oleksandr Levchenkov <ollevche@gmail.com>
This commit is contained in:
AbstractionFactory
2025-01-08 18:08:30 +01:00
committed by GitHub
parent ec20752054
commit 5a6d2d3e98
33 changed files with 1218 additions and 17 deletions

View File

@@ -0,0 +1 @@
{"magic":"OpenTofu-External-Key-Provider","version":1}

View File

@@ -0,0 +1,6 @@
{
"external_data": {
"key1": "value1",
"key2": "value2"
}
}

View File

@@ -0,0 +1,12 @@
{
"key": {
"encryption_key": "newly generated base64-encoded encryption key",
"decryption_key": "base64-encoded decryption key, if input meta was present, omitted otherwise"
},
"meta": {
"external_data": {
"key1": "value1",
"key2": "value2"
}
}
}

View File

@@ -0,0 +1,78 @@
package main
import (
"encoding/json"
"io"
"log"
"os"
)
// Header is the initial greeting the key provider sends out.
type Header struct {
// Magic must always be OpenTofu-External-Keyprovider
Magic string `json:"magic"`
// Version must be 1.
Version int `json:"version"`
}
// Metadata describes both the input and the output metadata.
type Metadata struct {
ExternalData map[string]any `json:"external_data"`
}
// Input describes the input data structure. This is nil on input if no existing
// data needs to be decrypted.
type Input *Metadata
// Output describes the output data written to stdout.
type Output struct {
Key struct {
// EncryptionKey must always be provided.
EncryptionKey []byte `json:"encryption_key,omitempty"`
// DecryptionKey must be provided when the input metadata is present.
DecryptionKey []byte `json:"decryption_key,omitempty"`
} `json:"key"`
// Meta contains the metadata to store alongside the encrypted data. You can
// store data here you need to reconstruct the decryption key later.
Meta Metadata `json:"meta"`
}
func main() {
// Write logs to stderr
log.Default().SetOutput(os.Stderr)
// Write the header:
header := Header{
"OpenTofu-External-Key-Provider",
1,
}
marshalledHeader, err := json.Marshal(header)
if err != nil {
log.Fatalf("%v", err)
}
_, _ = os.Stdout.Write(append(marshalledHeader, []byte("\n")...))
// Read the input
input, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("Failed to read stdin: %v", err)
}
var inMeta Input
if err := json.Unmarshal(input, &inMeta); err != nil {
log.Fatalf("Failed to parse stdin: %v", err)
}
// TODO produce the encryption key
if inMeta != nil {
// TODO produce decryption key
}
output := Output{
// TODO: produce output
}
outputData, err := json.Marshal(output)
if err != nil {
log.Fatalf("Failed to encode output: %v", err)
}
_, _ = os.Stdout.Write(outputData)
}

View File

@@ -0,0 +1,47 @@
#!/usr/bin/python
import base64
import json
import sys
if __name__ == "__main__":
# Write the header:
sys.stdout.write((json.dumps(
{"magic": "OpenTofu-External-Key-Provider", "version": 1}) + "\n"
))
# Read the input:
inputData = sys.stdin.read()
data = json.loads(inputData)
# Construct the key:
key = b''
for i in range(1, 17):
key += chr(i).encode('ascii')
# Output the keys:
if data is None:
# No input metadata was passed, we shouldn't output a decryption key.
# If needed, we can produce an output metadata here, which will be
# stored alongside the encrypted data.
outputMeta = {"external_data":{}}
sys.stdout.write(json.dumps({
"keys": {
"encryption_key": base64.b64encode(key).decode('ascii')
},
"meta": outputMeta
}))
else:
# We had some input metadata, output a decryption key. In a real-life
# scenario we would use the metadata for something like pbdkf2.
inputMeta = data["external_data"]
# Do something with the input metadata if needed and produce the output
# metadata:
outputMeta = {"external_data":{}}
sys.stdout.write(json.dumps({
"keys": {
"encryption_key": base64.b64encode(key).decode('ascii'),
"decryption_key": base64.b64encode(key).decode('ascii')
},
"meta": outputMeta
}))

View File

@@ -0,0 +1,38 @@
#!/bin/sh
set -e
# Output the header as a single line:
echo '{"magic":"OpenTofu-External-Key-Provider","version":1}'
# Read the input metadata.
INPUT=$(echo -n $(cat))
if [ "${INPUT}" = "null" ]; then
# We don't have metadata and shouldn't output a decryption key.
cat << EOF
{
"keys":{
"encryption_key":"AQIDBAUGBwgJCgsMDQ4PEA=="
},
"meta":{
"external_data":{}
}
}
EOF
else
# We have metadata and should output a decryption key. In our simplified case
# it is the same as the encryption key.
cat << EOF
{
"keys":{
"encryption_key":"AQIDBAUGBwgJCgsMDQ4PEA==",
"decryption_key":"AQIDBAUGBwgJCgsMDQ4PEA=="
},
"meta":{
"external_data":{}
}
}
EOF
fi

View File

@@ -0,0 +1,7 @@
terraform {
encryption {
key_provider "externalcommand" "foo" {
command = ["./some_program", "some_parameter"]
}
}
}

View File

@@ -4,6 +4,9 @@ terraform {
# Specify a long / complex passphrase (min. 16 characters)
passphrase = "correct-horse-battery-staple"
# Alternatively, receive the passphrase from another key provider:
chain = key_provider.other.provider
# Adjust the key length to the encryption method (default: 32)
key_length = 32