diff --git a/cmd/cunicu/cmd.go b/cmd/cunicu/cmd.go index 4ce277e1..e7ba1f4f 100644 --- a/cmd/cunicu/cmd.go +++ b/cmd/cunicu/cmd.go @@ -13,5 +13,6 @@ import ( _ "github.com/stv0g/cunicu/pkg/daemon/feature/hooks" _ "github.com/stv0g/cunicu/pkg/daemon/feature/hsync" _ "github.com/stv0g/cunicu/pkg/daemon/feature/pdisc" + _ "github.com/stv0g/cunicu/pkg/daemon/feature/pske" _ "github.com/stv0g/cunicu/pkg/daemon/feature/rtsync" ) diff --git a/go.mod b/go.mod index 4f80bc5d..5b59735f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 github.com/go-logr/logr v1.2.3 github.com/go-logr/zapr v1.2.3 + github.com/go-piv/piv-go v1.10.0 github.com/google/nftables v0.0.0-20221002140148-535f5eb8da79 github.com/imdario/mergo v0.3.13 github.com/jpillora/backoff v1.0.0 @@ -21,6 +22,7 @@ require ( github.com/pion/stun v0.3.5 github.com/spf13/cobra v1.5.0 github.com/spf13/pflag v1.0.5 + github.com/symbolicsoft/kyber-k2so v0.2.0 github.com/vishvananda/netlink v1.2.1-beta.2 go.uber.org/zap v1.23.0 golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 diff --git a/go.sum b/go.sum index b25ea318..8c5ab9ff 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= +github.com/go-piv/piv-go v1.10.0 h1:P1Y1VjBI5DnXW0+YkKmTuh5opWnMIrKriUaIOblee9Q= +github.com/go-piv/piv-go v1.10.0/go.mod h1:NZ2zmjVkfFaL/CF8cVQ/pXdXtuj110zEKGdJM6fJZZM= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -379,6 +381,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stv0g/gont v1.6.3 h1:7cIU2+F7iaGOHN9Mu2QKlKqZ4Dzk98Fu/P9gUPtjNFc= github.com/stv0g/gont v1.6.3/go.mod h1:Cl7BLHOE7YhWCPURgG0mvYBQBGGaNNQJeXSWNw+DJXs= +github.com/symbolicsoft/kyber-k2so v0.2.0 h1:JHb3OzwV9xjSH/Sib4jd2swZzp2GNot8gUpdydqQBfs= +github.com/symbolicsoft/kyber-k2so v0.2.0/go.mod h1:sfb01VbnBnAF3NJ0+k+Vh15N2tb0otGDxNfgE8al1is= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -411,6 +415,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 h1:x8vtB3zMecnlqZIwJNUUpwYKYSqCz5jXbiyv0ZJJZeI= golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 9b48e7fb..1921795d 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -51,12 +51,13 @@ var ( } DefaultInterfaceSettings = InterfaceSettings{ - DiscoverPeers: true, - DiscoverEndpoints: true, - SyncConfig: true, - SyncHosts: true, - SyncRoutes: true, - WatchRoutes: true, + DiscoverPeers: true, + DiscoverEndpoints: true, + SyncConfig: true, + SyncHosts: true, + SyncRoutes: true, + WatchRoutes: true, + EstablishPresharedKeys: true, ICE: ICESettings{ URLs: DefaultICEURLs, diff --git a/pkg/config/settings.go b/pkg/config/settings.go index e78ddd77..b6a57755 100644 --- a/pkg/config/settings.go +++ b/pkg/config/settings.go @@ -117,11 +117,12 @@ type InterfaceSettings struct { Peers map[string]PeerSettings `koanf:"peers,omitempty"` // Feature flags - DiscoverEndpoints bool `koanf:"discover_endpoints,omitempty"` - DiscoverPeers bool `koanf:"discover_peers,omitempty"` - SyncConfig bool `koanf:"sync_config,omitempty"` - SyncRoutes bool `koanf:"sync_routes,omitempty"` - SyncHosts bool `koanf:"sync_hosts,omitempty"` + DiscoverEndpoints bool `koanf:"discover_endpoints,omitempty"` + DiscoverPeers bool `koanf:"discover_peers,omitempty"` + SyncConfig bool `koanf:"sync_config,omitempty"` + SyncRoutes bool `koanf:"sync_routes,omitempty"` + SyncHosts bool `koanf:"sync_hosts,omitempty"` + EstablishPresharedKeys bool `koanf:"establish_psk,omitempty"` WatchConfig bool `koanf:"watch_config,omitempty"` WatchRoutes bool `koanf:"watch_routes,omitempty"` diff --git a/pkg/daemon/feature/pske/handlers.go b/pkg/daemon/feature/pske/handlers.go new file mode 100644 index 00000000..f99667d3 --- /dev/null +++ b/pkg/daemon/feature/pske/handlers.go @@ -0,0 +1,17 @@ +package pske + +import ( + "github.com/stv0g/cunicu/pkg/core" + "go.uber.org/zap" +) + +func (i *Interface) OnPeerAdded(cp *core.Peer) { + if cp.PresharedKey().IsSet() { + i.logger.Debug("Ignoring peer as it already has a PSK configured", zap.Any("peer", cp)) + return + } + + i.Peers[cp] = i.NewPeer(cp, i) +} + +func (i *Interface) OnPeerRemoved(p *core.Peer) {} diff --git a/pkg/daemon/feature/pske/peer.go b/pkg/daemon/feature/pske/peer.go new file mode 100644 index 00000000..a1d0be6f --- /dev/null +++ b/pkg/daemon/feature/pske/peer.go @@ -0,0 +1,128 @@ +package pske + +import ( + "context" + + kyber "github.com/symbolicsoft/kyber-k2so" + "go.uber.org/zap" + + "github.com/stv0g/cunicu/pkg/core" + "github.com/stv0g/cunicu/pkg/crypto" + "github.com/stv0g/cunicu/pkg/signaling" + + pskeproto "github.com/stv0g/cunicu/pkg/proto/feature/pske" +) + +type Peer struct { + *core.Peer + + Interface *Interface + + SecretKey KyberSecretKey + + logger *zap.Logger +} + +func (i *Interface) NewPeer(cp *core.Peer, kem *Interface) *Peer { + p := &Peer{ + Peer: cp, + Interface: i, + + logger: i.logger.With(zap.String("peer", cp.String())), + } + + go p.EstablishPSK() + + return p +} + +func (p *Peer) EstablishPSK() { + var err error + + ctx := context.Background() + kp := p.PublicPrivateKeyPair() + + if _, err := p.Interface.Daemon.Backend.Subscribe(ctx, kp, p); err != nil { + p.logger.Error("Failed to subscribe to message", zap.Error(err)) + return + } + + if p.IsControlling() { + var pk KyberPublicKey + + // Generate new key pair + if p.SecretKey, pk, err = kyber.KemKeypair1024(); err != nil { + p.logger.Error("Failed to generate Kyber key pair", zap.Error(err)) + } + + // Send public key + msg := signaling.Message{ + Pske: &pskeproto.PresharedKeyEstablishment{ + PublicKey: pk[:], + }, + } + + if err := p.Interface.Daemon.Backend.Publish(ctx, kp, &msg); err != nil { + p.logger.Error("Failed to publish public key", zap.Error(err)) + } + } +} + +func (p *Peer) OnSignalingMessage(kp *crypto.PublicKeyPair, msg *signaling.Message) { + if msg.Pske == nil { + return + } + + var err error + var psk crypto.Key + + if p.IsControlling() { + if msg.Pske.CipherText == nil { + p.logger.Error("Expected cipher text") + return + } + + if ctLen := len(msg.Pske.CipherText); ctLen != kyber.Kyber1024CTBytes { + p.logger.Error("Invalid cipher text length", zap.Int("len", ctLen)) + return + } + + // Decrypt cipher text + ct := *(*KyberCipherText)(msg.Pske.CipherText) + if psk, err = kyber.KemDecrypt1024(ct, p.SecretKey); err != nil { + p.logger.Error("Failed to decrypt PSK", zap.Error(err)) + return + } + } else { + if msg.Pske.PublicKey == nil { + p.logger.Error("Expected public key") + return + } + + var ct KyberCipherText + + // Encrypt cipher text + pk := *(*KyberPublicKey)(msg.Pske.PublicKey) + if ct, psk, err = kyber.KemEncrypt1024(pk); err != nil { + p.logger.Error("Failed to encrypt cipher text", zap.Error(err)) + return + } + + // Publish cipher text + ctx := context.Background() + kp := p.PublicPrivateKeyPair() + msg := &signaling.Message{ + Pske: &pskeproto.PresharedKeyEstablishment{ + CipherText: ct[:], + }, + } + + if err := p.Interface.Daemon.Backend.Publish(ctx, kp, msg); err != nil { + p.logger.Error("Failed to publish cipher text", zap.Error(err)) + } + } + + if err := p.SetPresharedKey(&psk); err != nil { + p.logger.Error("Failed to update preshared key", zap.Error(err)) + } +} diff --git a/pkg/daemon/feature/pske/piv.go_ b/pkg/daemon/feature/pske/piv.go_ new file mode 100644 index 00000000..a5355145 --- /dev/null +++ b/pkg/daemon/feature/pske/piv.go_ @@ -0,0 +1,130 @@ +package pske + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "os" + "strings" + + "github.com/go-piv/piv-go/piv" +) + +func main() { + if len(os.Args) < 2 { + return + } + + // List all smart cards connected to the system. + cards, err := piv.Cards() + if err != nil { + panic(err) + } + + // Find a YubiKey and open the reader. + var yk *piv.YubiKey + for _, card := range cards { + if strings.Contains(strings.ToLower(card), "yubikey") { + if yk, err = piv.Open(card); err != nil { + panic(err) + } + break + } + } + if yk == nil { + panic("no yubikey found") + } + + defer yk.Close() + + switch os.Args[1] { + case "cert": + cert(yk) + case "decrypt": + decrypt(yk) + case "encrypt": + encrypt(yk) + } +} + +func cert(yk *piv.YubiKey) { + + // sn, _ := yk.Serial() + // fmt.Printf("Version: %d.%d.%d\n", yk.Version().Major, yk.Version().Minor, yk.Version().Patch) + // fmt.Printf("Serial: %d\n", sn) + + crt, err := yk.Certificate(piv.SlotSignature) + if err != nil { + panic(err) + } + + // Print the certificate + // result, err := certinfo.CertificateText(crt) + // if err != nil { + // log.Fatal(err) + // } + // fmt.Print(result) + + pemPayload, _ := x509.MarshalPKIXPublicKey(crt.PublicKey.(*rsa.PublicKey)) + + pem.Encode(os.Stdout, &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pemPayload, + }) +} + +func encrypt(yk *piv.YubiKey) { + pt, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + crt, err := yk.Certificate(piv.SlotSignature) + if err != nil { + panic(err) + } + + pk := crt.PublicKey.(*rsa.PublicKey) + + ct, err := rsa.EncryptPKCS1v15(rand.Reader, pk, pt) + if err != nil { + panic(err) + } + + os.Stdout.Write(ct) +} + +func decrypt(yk *piv.YubiKey) { + ct, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + crt, err := yk.Certificate(piv.SlotSignature) + if err != nil { + panic(err) + } + + sk, err := yk.PrivateKey(piv.SlotSignature, crt.PublicKey, piv.KeyAuth{ + PIN: "0000", + }) + if err != nil { + panic(err) + } + + dec, ok := sk.(crypto.Decrypter) + if !ok { + panic("no rsa key?") + } + + pt, err := dec.Decrypt(rand.Reader, ct, nil) + if err != nil { + panic(err) + } + + fmt.Print(string(pt)) +} diff --git a/pkg/daemon/feature/pske/pske.go b/pkg/daemon/feature/pske/pske.go new file mode 100644 index 00000000..045fc62e --- /dev/null +++ b/pkg/daemon/feature/pske/pske.go @@ -0,0 +1,43 @@ +// Package pske uses the Kyber Key Establishment Mechanism (KEM) to establish Preshared Keys (PSKs) between two WireGuard peers +package pske + +import ( + "github.com/stv0g/cunicu/pkg/core" + "github.com/stv0g/cunicu/pkg/daemon" + "go.uber.org/zap" +) + +func init() { + daemon.RegisterFeature("pske", "Preshared key establishment ", New, 110) +} + +type Interface struct { + *daemon.Interface + + Peers map[*core.Peer]*Peer + + logger *zap.Logger +} + +func New(i *daemon.Interface) (daemon.Feature, error) { + if !i.Settings.EstablishPresharedKeys { + return nil, nil + } + + p := &Interface{ + Interface: i, + Peers: map[*core.Peer]*Peer{}, + + logger: zap.L().Named("pske").With(zap.String("intf", i.Name())), + } + + i.OnPeer(p) + + return p, nil +} + +func (i *Interface) Start() error { + i.logger.Info("Started post-quantum preshared key establishment") + + return nil +} diff --git a/pkg/daemon/feature/pske/types.go b/pkg/daemon/feature/pske/types.go new file mode 100644 index 00000000..a5199e4e --- /dev/null +++ b/pkg/daemon/feature/pske/types.go @@ -0,0 +1,7 @@ +package pske + +import kyber "github.com/symbolicsoft/kyber-k2so" + +type KyberCipherText [kyber.Kyber1024CTBytes]byte +type KyberPublicKey [kyber.Kyber1024PKBytes]byte +type KyberSecretKey [kyber.Kyber1024SKBytes]byte diff --git a/pkg/proto/proto.go b/pkg/proto/proto.go index 59f96bee..d602f8b3 100644 --- a/pkg/proto/proto.go +++ b/pkg/proto/proto.go @@ -5,7 +5,7 @@ package proto //go:generate protoc --proto_path=../../proto --go_out=. --go_opt=paths=import,module=github.com/stv0g/cunicu/pkg/proto core/peer.proto core/interface.proto core/net.proto //go:generate protoc --proto_path=../../proto --go_out=. --go_opt=paths=import,module=github.com/stv0g/cunicu/pkg/proto signaling/signaling.proto signaling/relay.proto //go:generate protoc --proto_path=../../proto --go_out=. --go_opt=paths=import,module=github.com/stv0g/cunicu/pkg/proto rpc/daemon.proto rpc/epdisc.proto rpc/event.proto rpc/signaling.proto rpc/invitation.proto -//go:generate protoc --proto_path=../../proto --go_out=. --go_opt=paths=import,module=github.com/stv0g/cunicu/pkg/proto feature/epdisc.proto feature/epdisc_candidate.proto feature/pdisc.proto feature/pske.proto feature/hooks.proto +//go:generate protoc --proto_path=../../proto --go_out=. --go_opt=paths=import,module=github.com/stv0g/cunicu/pkg/proto feature/epdisc.proto feature/epdisc_candidate.proto feature/pdisc.proto feature/pske.proto feature/hooks.proto feature/pske.proto //go:generate protoc --proto_path=../../proto --go-grpc_out=. --go-grpc_opt=paths=import,module=github.com/stv0g/cunicu/pkg/proto rpc/daemon.proto rpc/epdisc.proto rpc/signaling.proto //go:generate protoc --proto_path=../../proto --go-grpc_out=. --go-grpc_opt=paths=import,module=github.com/stv0g/cunicu/pkg/proto signaling/signaling.proto signaling/relay.proto