Skip to content

[RFC] Plugins authentication proposal #152

@kad

Description

@kad

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:

Image

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)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions