-
Notifications
You must be signed in to change notification settings - Fork 83
Description
In the light of the recent discussions about what plugins are allowed to do or not, I think we need to implement mechanism for plugin authentication to the runtime.
This would allow Runtime to:
- define on per-plugin basis what kind of operations is allowed to be performed to plugin (e.g. disable modifications of hooks, or disable modification of anything else)
- have default security for unauthenticated plugins
- uniquely identify plugin by plugin's public key (similar to Wiregiard peer identification)
- make sure that one plugin does not try to fake other plugin's name during registration
For authentication, I propose to use challenge/response pattern during plugin registration/configuration phases.
Challenge/response data can be encrypted via Curve25519 / ChaCha20-Poly1305 that are available via standard Go libraries, and are quite common asymmetric encryption algorithms, again similar to what is used in Wireguard.
For protocol, we can easily extend current messages, as demonstrated in following sequence diagram:
For protocol changes we can do something similar to this:
diff --git a/pkg/api/api.proto b/pkg/api/api.proto
index 78fa64d..25bb500 100644
--- a/pkg/api/api.proto
+++ b/pkg/api/api.proto
@@ -26,7 +26,7 @@ option go_package = "github.com/containerd/nri/pkg/api;api";
// The rest of the API is defined by the Plugin service.
service Runtime {
// RegisterPlugin registers the plugin with the runtime.
- rpc RegisterPlugin(RegisterPluginRequest) returns (Empty);
+ rpc RegisterPlugin(RegisterPluginRequest) returns (RegisterPluginResponse);
// UpdateContainers requests unsolicited updates to a set of containers.
rpc UpdateContainers(UpdateContainersRequest) returns (UpdateContainersResponse);
}
@@ -36,6 +36,13 @@ message RegisterPluginRequest {
string plugin_name = 1;
// Plugin invocation index. Plugins are called in ascending index order.
string plugin_idx = 2;
+ // Plugin authentication public key
+ string plugin_public_key = 3;
+}
+
+message RegisterPluginResponse {
+ // Enum, descibes sets of capabilities that plugin allowed to use.
+ repeated int32 allowed_capabilities = 1;
}
message UpdateContainersRequest {
@@ -141,12 +148,18 @@ message ConfigureRequest {
int64 registration_timeout = 4;
// Configured request processing timeout in milliseconds.
int64 request_timeout = 5;
+ // Runtime's public key for encrypting authentication challenge response
+ string runtime_public_key = 6;
+ // Authentication challenge
+ string runtime_authentication_challenge = 7;
}
message ConfigureResponse {
// Events to subscribe the plugin for. Each bit set corresponds to an
// enumerated Event.
int32 events = 2;
+ // Authentication response encrypted with runtime public key
+ string runtime_authentication_response = 3;
}
message SynchronizeRequest {
Encrypt/decrypt/key generation functions can be implemented like this:
WARNING: code is not optimal, just for demonstration. Key generation can be done via stdlib: https://pkg.go.dev/crypto/ed25519#GenerateKey and there are other chunks that can be improved.
package main
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
)
// GenerateKeyPair generates a public/private key pair for Curve25519
func GenerateKeyPair() ([32]byte, [32]byte) {
var privateKey [32]byte
_, err := rand.Read(privateKey[:])
if err != nil {
panic(err)
}
// Clamp the private key
privateKey[0] &= 0xF8 // Clear the three most significant bits
privateKey[31] &= 0x7F // Clear the most significant bit
privateKey[31] |= 0x40 // Set the second least significant bit
var publicKey [32]byte
curve25519.ScalarBaseMult(&publicKey, &privateKey)
return privateKey, publicKey
}
// Encrypt encrypts the plaintext using ChaCha20-Poly1305
func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key[:])
if err != nil {
return nil, err
}
// Generate a nonce
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
// Encrypt the plaintext
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
// Decrypt decrypts the ciphertext using ChaCha20-Poly1305
func Decrypt(ciphertext []byte, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key[:])
if err != nil {
return nil, err
}
// Extract the nonce from the ciphertext
nonceSize := aead.NonceSize()
if len(ciphertext) < nonceSize {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
// Decrypt the ciphertext
plaintext, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
func main() {
// Generate key pairs for Plugin and Runtime
PluginPrivate, PluginPublic := GenerateKeyPair()
RuntimePrivate, RuntimePublic := GenerateKeyPair()
// Print keys in Base64 format
fmt.Printf("Plugin's Private Key (Base64): %s\n", base64.StdEncoding.EncodeToString(PluginPrivate[:]))
fmt.Printf("Plugin's Public Key (Base64): %s\n", base64.StdEncoding.EncodeToString(PluginPublic[:]))
fmt.Printf("Runtime's Private Key (Base64): %s\n", base64.StdEncoding.EncodeToString(RuntimePrivate[:]))
fmt.Printf("Runtime's Public Key (Base64): %s\n", base64.StdEncoding.EncodeToString(RuntimePublic[:]))
// Plugin and Runtime exchange public keys and derive a shared secret
sharedSecretPlugin, err := curve25519.X25519(PluginPrivate[:], RuntimePublic[:])
if err != nil {
panic(err)
}
sharedSecretRuntime, err := curve25519.X25519(RuntimePrivate[:], PluginPublic[:])
if err != nil {
panic(err)
}
// Ensure shared secrets match
if !bytes.Equal(sharedSecretPlugin, sharedSecretRuntime) {
panic("shared secrets do not match")
}
// Prepare a message to encrypt
message := []byte("Random challenge / response data")
fmt.Printf("Original message: %s\n", message)
// Encrypt the message
ciphertext, err := Encrypt(message, sharedSecretPlugin[:])
if err != nil {
panic(err)
}
fmt.Printf("Ciphertext (hex): %s\n", hex.EncodeToString(ciphertext))
// Decrypt the message
decryptedMessage, err := Decrypt(ciphertext, sharedSecretPlugin[:])
if err != nil {
panic(err)
}
fmt.Printf("Decrypted message: %s\n", decryptedMessage)
}
