add support for multiple secrets (independent agents) on the knock daemon
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This allows you to generate more than one pre-shared secret on the knock daemon so that you can distribute the secret and control revocation at a more granular level. Each additional secret creates one more concurrent authentic knock sequence.
This commit is contained in:
parent
334407e309
commit
2951c1f684
@ -47,9 +47,15 @@ Example Usage:
|
|||||||
|
|
||||||
hypdConfiguration, err := configuration.LoadConfiguration(args[0])
|
hypdConfiguration, err := configuration.LoadConfiguration(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to start packet server: %w", err))
|
panic(fmt.Errorf("failed to load configuration file '%s': %w", args[0], err))
|
||||||
}
|
}
|
||||||
err = server.PacketServer(hypdConfiguration)
|
|
||||||
|
secrets, err := configuration.LoadSecrets(hypdConfiguration.PreSharedKeyDirectory)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to load secrets from directory '%s': %w", hypdConfiguration.PreSharedKeyDirectory, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.PacketServer(hypdConfiguration, secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to start packet server: %w", err))
|
panic(fmt.Errorf("failed to start packet server: %w", err))
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ type HypdConfiguration struct {
|
|||||||
SuccessAction string `json:"successAction"` // The action to take for a successful knock, each argument is a separate string
|
SuccessAction string `json:"successAction"` // The action to take for a successful knock, each argument is a separate string
|
||||||
TimeoutSeconds int `json:"timeoutSeconds"` // If > 0, once a knock sequence has been successful this value will count down and when it reaches 0, it will perform the TimeoutAction on the client
|
TimeoutSeconds int `json:"timeoutSeconds"` // If > 0, once a knock sequence has been successful this value will count down and when it reaches 0, it will perform the TimeoutAction on the client
|
||||||
TimeoutAction string `json:"timeoutAction"` // The action to take after TimeoutSeconds has elapsed. only applicable if TimeoutSeconds is > 0, each argument is a separate string
|
TimeoutAction string `json:"timeoutAction"` // The action to take after TimeoutSeconds has elapsed. only applicable if TimeoutSeconds is > 0, each argument is a separate string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfiguration opens and parses the configuration file into a HypdConfiguration struct
|
// LoadConfiguration opens and parses the configuration file into a HypdConfiguration struct
|
||||||
|
49
hypd/configuration/secrets.go
Normal file
49
hypd/configuration/secrets.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base32"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var secrets [][]byte
|
||||||
|
|
||||||
|
// LoadSecrets processes all files within the specified directory and attempts to
|
||||||
|
// convert the file contents to secrets to by used by hypd
|
||||||
|
func LoadSecrets(preSharedKeyDirectory string) ([][]byte, error) {
|
||||||
|
secrets = make([][]byte, 0)
|
||||||
|
err := filepath.Walk(preSharedKeyDirectory, processSecretFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to walk directory '%s': %w", preSharedKeyDirectory, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processSecretFile is called against each file in the preSharedKeyDirectory
|
||||||
|
// It reads each file and attemts to base32 decode their contents
|
||||||
|
func processSecretFile(path string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process file '%s': %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
secretBytes, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file '%s': %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedSecret, err := base32.StdEncoding.DecodeString(string(secretBytes))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to base32 decode secret '%s': %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = append(secrets, decodedSecret)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -11,7 +11,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -43,26 +42,21 @@ const (
|
|||||||
var (
|
var (
|
||||||
clients map[uint32]*Client // Contains a map of clients, key is IPv4 address
|
clients map[uint32]*Client // Contains a map of clients, key is IPv4 address
|
||||||
knockSequences []KnockSequence // We have 3 valid knock sequences at any time to account for clock skew
|
knockSequences []KnockSequence // We have 3 valid knock sequences at any time to account for clock skew
|
||||||
sharedSecret string // base32 encoded shared secret used for totp
|
|
||||||
serverConfig *configuration.HypdConfiguration
|
serverConfig *configuration.HypdConfiguration
|
||||||
|
sharedSecrets [][]byte // A slice of byte slices, each being a secret key
|
||||||
)
|
)
|
||||||
|
|
||||||
// PacketServer is the main function when operating in server mode
|
// PacketServer is the main function when operating in server mode
|
||||||
// it sets up the pcap on the capture device and starts a goroutine
|
// it sets up the pcap on the capture device and starts a goroutine
|
||||||
// to rotate the knock sequence
|
// to rotate the knock sequence
|
||||||
func PacketServer(config *configuration.HypdConfiguration) error {
|
func PacketServer(config *configuration.HypdConfiguration, secrets [][]byte) error {
|
||||||
serverConfig = config
|
serverConfig = config
|
||||||
|
sharedSecrets = secrets
|
||||||
iface, err := net.InterfaceByName(serverConfig.NetworkInterface)
|
iface, err := net.InterfaceByName(serverConfig.NetworkInterface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("lookup network iface %q: %v", serverConfig.NetworkInterface, err)
|
log.Fatalf("lookup network iface %q: %v", serverConfig.NetworkInterface, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretBytes, err := os.ReadFile("hyp.secret")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to read file 'hyp.secret': %v", err)
|
|
||||||
}
|
|
||||||
sharedSecret = string(secretBytes)
|
|
||||||
|
|
||||||
clients = make(map[uint32]*Client, 0)
|
clients = make(map[uint32]*Client, 0)
|
||||||
knockSequences = []KnockSequence{}
|
knockSequences = []KnockSequence{}
|
||||||
|
|
||||||
@ -182,14 +176,15 @@ func rotateSequence() {
|
|||||||
// Generate new knock sequences with time skew support
|
// Generate new knock sequences with time skew support
|
||||||
t := time.Now().Add(time.Second * -30)
|
t := time.Now().Add(time.Second * -30)
|
||||||
for i := len(knockSequences); i < 3; i++ {
|
for i := len(knockSequences); i < 3; i++ {
|
||||||
portSequence, err := otphyp.GeneratePorts(sharedSecret, t.Add((time.Second * 30 * time.Duration(i))))
|
for _, secret := range sharedSecrets {
|
||||||
|
portSequence, err := otphyp.GeneratePorts(secret, t.Add((time.Second * 30 * time.Duration(i))))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to generate port knock sequence: %v", err)
|
log.Fatalf("failed to generate port knock sequence: %v", err)
|
||||||
}
|
}
|
||||||
knockSequence := KnockSequence{PortSequence: portSequence}
|
knockSequence := KnockSequence{PortSequence: portSequence}
|
||||||
knockSequences = append(knockSequences, knockSequence)
|
knockSequences = append(knockSequences, knockSequence)
|
||||||
}
|
}
|
||||||
fmt.Println("New sequences:", knockSequences)
|
}
|
||||||
|
|
||||||
// Sleep until next 30 second offset
|
// Sleep until next 30 second offset
|
||||||
time.Sleep(time.Until(time.Now().Truncate(time.Second * 30).Add(time.Second * 30)))
|
time.Sleep(time.Until(time.Now().Truncate(time.Second * 30).Add(time.Second * 30)))
|
||||||
|
@ -15,12 +15,7 @@ import (
|
|||||||
|
|
||||||
// A loose implementation of hotp meant for our specific purposes of generating four random port numbers
|
// A loose implementation of hotp meant for our specific purposes of generating four random port numbers
|
||||||
// Accepts a base32 encoded shared secret and a time
|
// Accepts a base32 encoded shared secret and a time
|
||||||
func GeneratePorts(sharedSecret string, t time.Time) (ports [4]uint16, err error) {
|
func GeneratePorts(sharedSecret []byte, t time.Time) (ports [4]uint16, err error) {
|
||||||
|
|
||||||
sharedSecretBytes, err := base32.StdEncoding.DecodeString(sharedSecret)
|
|
||||||
if err != nil {
|
|
||||||
return [4]uint16{0, 0, 0, 0}, fmt.Errorf("failed to base32 decode shared secret string to bytes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 30 second key rotation
|
// 30 second key rotation
|
||||||
movingFactor := uint64(math.Floor(float64(t.Unix()) / float64(30)))
|
movingFactor := uint64(math.Floor(float64(t.Unix()) / float64(30)))
|
||||||
@ -28,7 +23,7 @@ func GeneratePorts(sharedSecret string, t time.Time) (ports [4]uint16, err error
|
|||||||
binary.BigEndian.PutUint64(buf, movingFactor)
|
binary.BigEndian.PutUint64(buf, movingFactor)
|
||||||
|
|
||||||
// calculate hmac and offset
|
// calculate hmac and offset
|
||||||
mac := hmac.New(sha1.New, sharedSecretBytes)
|
mac := hmac.New(sha1.New, sharedSecret)
|
||||||
mac.Write(buf)
|
mac.Write(buf)
|
||||||
sum := mac.Sum(nil)
|
sum := mac.Sum(nil)
|
||||||
offset := sum[len(sum)-1] & 0xf
|
offset := sum[len(sum)-1] & 0xf
|
||||||
|
Loading…
x
Reference in New Issue
Block a user