diff --git a/quantumlayer/.dockerignore b/quantumlayer/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..de153db3b796413119418ec4b9f82dad2e8cb939 --- /dev/null +++ b/quantumlayer/.dockerignore @@ -0,0 +1 @@ +artifacts diff --git a/quantumlayer/.gitignore b/quantumlayer/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..de153db3b796413119418ec4b9f82dad2e8cb939 --- /dev/null +++ b/quantumlayer/.gitignore @@ -0,0 +1 @@ +artifacts diff --git a/quantumlayer/Dockerfile b/quantumlayer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d638068b767b6a143b3249323f64e2773736ef54 --- /dev/null +++ b/quantumlayer/Dockerfile @@ -0,0 +1,17 @@ +ARG GOLANG_VERSION=1.21 +ARG BUILDARGS + +FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-bookworm as builder +ARG GITLAB_LOGIN +ARG GITLAB_TOKEN +WORKDIR /quantumlayer/ +COPY . . +RUN echo "machine code.fbi.h-da.de login ${GITLAB_LOGIN} password ${GITLAB_TOKEN}" > ~/.netrc +RUN --mount=type=cache,target=/root/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + make build + +FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-bookworm +WORKDIR /app/ +COPY --from=builder /quantumlayer/artifacts/quantumlayer ./quantumlayer +ENTRYPOINT ["./quantumlayer"] diff --git a/quantumlayer/Makefile b/quantumlayer/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..96932cd259b7332d370b66fbb98296f5d401f0ce --- /dev/null +++ b/quantumlayer/Makefile @@ -0,0 +1,19 @@ +MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) +MAKEFILE_DIR := $(dir $(MAKEFILE_PATH)) +GOPATH := $(~/go) + +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +BUILD_ARTIFACTS_PATH=artifacts + +all: build + +pre: + mkdir -p $(BUILD_ARTIFACTS_PATH) + +build: pre + $(GOBUILD) -o $(BUILD_ARTIFACTS_PATH)/quantumlayer ./example/main.go + +container: build + docker buildx build --rm -t quantumlayer --load -f ./Dockerfile . diff --git a/quantumlayer/example/main.go b/quantumlayer/example/main.go new file mode 100644 index 0000000000000000000000000000000000000000..a3860d9d0024772f07d079ff3a5d5707107ff8ac --- /dev/null +++ b/quantumlayer/example/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "log" + "net" + "os" + "os/signal" + "syscall" + + "code.fbi.h-da.de/danet/quantumlayer" + pb "code.fbi.h-da.de/danet/quipsec/gen/go/quipsec" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "gopkg.in/yaml.v3" +) + +type Config struct { + KMSAddr string `yaml:"KMSAddr"` + UDPAddr string `yaml:"UDPAddr"` + PeerUDPAddr string `yaml:"PeerUDPAddr"` + GenerateKeys bool `yaml:"GenerateKeys"` +} + +func main() { + // TODO: flag validation + configPath := flag.String("config", "", "path to the config file") + flag.Parse() + + // unmarshal config + config := &Config{} + file, err := os.ReadFile(*configPath) + if err != nil { + logrus.Fatal(err) + } + if err := yaml.Unmarshal(file, config); err != nil { + logrus.Fatal(err) + } + + newPeerConn, err := grpc.Dial(config.KMSAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatal(err) + return + } + + kmsClient := pb.NewKmsQkdmCommunicationServiceClient(newPeerConn) + + peerUDPAddr, err := net.ResolveUDPAddr("udp", config.PeerUDPAddr) + if err != nil { + log.Fatal(err) + return + } + + stopChan := make(chan os.Signal) + signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM) + + ql := quantumlayer.NewQuantumlayerEmuPRNG(kmsClient, os.Stdout, logrus.GetLevel(), false) + ql.Configure(config.GenerateKeys, config.UDPAddr) + ql.PowerOn() + ql.AddPeer(peerUDPAddr) + + <-stopChan +} diff --git a/quantumlayer/quantumlayer-emu-prng.go b/quantumlayer/quantumlayer-emu-prng.go new file mode 100644 index 0000000000000000000000000000000000000000..d70095adf735004b54a684d632d6f2844470e52f --- /dev/null +++ b/quantumlayer/quantumlayer-emu-prng.go @@ -0,0 +1,374 @@ +package quantumlayer + +/* + * + * This packages "implements", actually it only emulates, the output of a + * quantum link, i.e., the transmitted random numbers from one quantum + * sender to a quantum receiver. + * This relies on crypto/rand to generate the random numbers that will be + * transmitted to the other end. + * + */ +import ( + "context" + "crypto/rand" + "encoding/json" + "errors" + "io" + "math/big" + "net" + "strconv" + "sync" + "time" + + pb "code.fbi.h-da.de/danet/quipsec/gen/go/quipsec" + "github.com/sirupsen/logrus" + logi "github.com/sirupsen/logrus" +) + +type QuantumPayloadElement struct { + BulkKeyId int64 `json:"bulk-key-id"` // the unique ID of this bulk of keys + BulkKeyLength int `json:"bulk-key-length"` // the length, counted in bytes, of bulkKey + BulkKey *[]byte `json:"bulk-key"` // the bulk key +} + +type QuantumlayerEmuPRNG struct { + client pb.KmsQkdmCommunicationServiceClient + configured bool + poweron bool // set to yes if operation, i.e., generating keys + generateKeys bool // set to yes, if this qle should generate random number. + incomingRandNums chan QuantumPayloadElement + peerNumbers *NumberStore + myNumbers *NumberStore + localQLAddress string + udpSrvConn *net.UDPConn + qlPeer string + qlPeerCancel context.CancelFunc + qlLocalPort *net.UDPAddr + qlPeerMutex sync.Mutex +} + +// We use our own logrus instance, as we would like to have different log levels for different parts. +var log = logrus.New() + +func NewQuantumlayerEmuPRNG(client pb.KmsQkdmCommunicationServiceClient, logOutput io.Writer, logLevel logi.Level, logInJson bool) (newql *QuantumlayerEmuPRNG) { + /* + * Setup logging + */ + + //What level + log.SetLevel(logLevel) + // Where to send log out put + log.SetOutput(logOutput) + // and plain-text (standard) or json + if !logInJson { + log.SetFormatter(&logi.TextFormatter{}) + } else { + log.SetFormatter(&logi.JSONFormatter{}) + } + // print code function if level is set to Trace + if logLevel == logi.TraceLevel { + log.SetReportCaller(true) + } else { + log.SetReportCaller(false) + } + + // Return PRNG Quantum Layer + return &QuantumlayerEmuPRNG{ + client: client, + configured: false, + poweron: false, + generateKeys: false, + incomingRandNums: make(chan QuantumPayloadElement), + peerNumbers: NewNumberStore(40000), + myNumbers: NewNumberStore(40000), + qlPeer: "", + } +} + +// Configure the quantum emulation, but do not start if yet +func (qlemuprng *QuantumlayerEmuPRNG) Configure(enableKeyGeneration bool, localQLAddress ...string) { + + // Start receiving numberstores + go qlemuprng.peerNumbers.receiveNumbers(qlemuprng.incomingRandNums, qlemuprng.client) + + // Determine if a local UDP address should be used or not + if len(localQLAddress) == 0 { + // No input + qlemuprng.localQLAddress = ":0" + } else { + qlemuprng.localQLAddress = localQLAddress[0] + } + qlemuprng.generateKeys = enableKeyGeneration + qlemuprng.configured = true +} + +// Power on the quantum layer, i.e., open up the communication ports for the +// other quantum module +func (qlemuprng *QuantumlayerEmuPRNG) PowerOn() { + if !qlemuprng.configured { + // nothing do here move on + log.Errorf("QuantumlayerEmuPRNG: Sorry, the quantum layer is not configured for action. You've missed Configure()") + return + } + //qlemuprng.poweron = false + log.Infof("QuantumlayerEmuPRNG: is powering on...charging.") + + if qlemuprng.generateKeys { + log.Infof("QuantumlayerEmuPRNG: will GENERATE random keys") + } + + if qlemuprng.udpSrvConn == nil { + go func() { + // Get UDP server part going... + log.Debugf("QuantumlayerEmuPRNG: localQLAddress is %s", qlemuprng.localQLAddress) + + // This reads random numbers from other Quantum end + udpSrvPort, err := net.ResolveUDPAddr("udp", qlemuprng.localQLAddress) + if err != nil { + log.Fatalf("QuantumlayerEmuPRNG: UDP failure: %s", err) + return + } + + qlemuprng.udpSrvConn, err = net.ListenUDP("udp", udpSrvPort) + if err != nil { + log.Fatalf("QuantumlayerEmuPRNG: UDP failure: %s", err) + return + } + defer qlemuprng.udpSrvConn.Close() + + // Retrieve local UDP address and store it for further actions. + qlemuprng.qlLocalPort = qlemuprng.udpSrvConn.LocalAddr().(*net.UDPAddr) + + // TODO: This does not seem to be necessary if the gle is not generating rands + // serve UDP incoming + log.Infof("QuantumlayerEmuPRNG: started server, waiting for incoming rands on port %s \n", qlemuprng.udpSrvConn.LocalAddr().(*net.UDPAddr).String()) + inBuffer := make([]byte, 1500) + for { + // Buffer for reading from "Quantum link" + n, addr, err := qlemuprng.udpSrvConn.ReadFromUDP(inBuffer) + if err != nil { + log.Errorf("QuantumlayerEmuPRNG: Could not read from UDP: %s", err) + } else { + log.Debugf("QuantumlayerEmuPRNG: read %d bytes from %s\n", n, addr) + + // Check if sender of datagram is qlPeer + // Warning this is not checking the validity of the sender, i.e., spoofing is possible + if addr.String() == qlemuprng.qlPeer { + log.Debugf("QuantumlayerEmuPRNG: Peer %s listed", addr) + //dumb the received data into the channel and carry on + // TODO/XXX: no vetting for anything + // Unmarshall out of JSON + var inQBuffer QuantumPayloadElement + unmarshallErr := json.Unmarshal(inBuffer[0:n], &inQBuffer) + if unmarshallErr == nil { + qlemuprng.incomingRandNums <- inQBuffer + } + } else { + log.Infof("QuantumlayerEmuPRNG: Peer %s NOT listed", addr) + } + } + } + }() + } + + // Wait for listening UDP socket in the above go-routine to get ready + for qlemuprng.udpSrvConn == nil { + } + + // Ready, set, go! + qlemuprng.poweron = true + + log.Infof("QuantumlayerEmuPRNG: is charged and powered on.") +} + +// Power off the quantum layer, i.e., close the communication ports for the +// other quantum module +func (qlemuprng *QuantumlayerEmuPRNG) PowerOff() { + qlemuprng.poweron = false + log.Println("QuantumlayerEmuPRNG: is powered off...discharging.") +} + +func (qlemuprng *QuantumlayerEmuPRNG) AddPeer(addr *net.UDPAddr) { + if !qlemuprng.poweron { + return + } + //TODO/XXX check the incoming addr + + // Add peer to the .... + qlemuprng.qlPeerMutex.Lock() + qlemuprng.qlPeer = addr.String() + qlemuprng.qlPeerMutex.Unlock() + + // generate only keys if requested to do so. + if qlemuprng.generateKeys { + ctx, cancel := context.WithCancel(context.Background()) + qlemuprng.qlPeerCancel = cancel + + // Start the generation and shipping of random numbers + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + if qlemuprng.poweron { + // retrieve a new back of random numbers + newNumberBatch := qlemuprng.GenerateRandomNumbers() + // TODO: Replace this by some generic encapsulation reader and not just JSON + //Get JSON for transmission ready + qpe := QuantumPayloadElement{time.Now().UnixNano(), len(newNumberBatch), &newNumberBatch} + + // XXX/TODO: error must be handled + jsonPayload, err := json.Marshal(qpe) + if err != nil { + log.Errorf("QuantumlayerEmuPRNG: json.Marshal error %s", err) + } + + _, _, err = qlemuprng.udpSrvConn.WriteMsgUDP(jsonPayload, nil, addr) + if err != nil { + log.Fatalf("QuantumlayerEmuPRNG: WriteMsgUDPAddrPort failed: %s", err) + } + qlemuprng.incomingRandNums <- qpe + } + // TODO: This sleep timer has to replaced by something for clever. + time.Sleep(5 * time.Second) + } + } + }(ctx) + } +} + +func (qlemuprng *QuantumlayerEmuPRNG) RemovePeer() { + if !qlemuprng.poweron { + return + } + + // Stop the generation and shipping of random numbers + qlemuprng.qlPeerCancel() + + // delete peer + qlemuprng.qlPeerMutex.Lock() + qlemuprng.qlPeer = "" + qlemuprng.qlPeerMutex.Unlock() +} + +func (qlemuprng *QuantumlayerEmuPRNG) GetLocalQLPort() (myAddr *net.UDPAddr) { + return qlemuprng.qlLocalPort +} + +func (qlemuprng *QuantumlayerEmuPRNG) GenerateRandomNumbers() (randNums []byte) { + numRands, randError := rand.Int(rand.Reader, big.NewInt(1000)) + if randError != nil { + log.Fatalf("QuantumlayerEmuPRNG: %s", randError) + return + } + + b := make([]byte, numRands.Uint64()) + _, randError = rand.Read(b) + if randError != nil { + log.Fatalf("QuantumlayerEmuPRNG: %s", randError) + return + } + return b +} + +func (qlemuprng *QuantumlayerEmuPRNG) GetKeyBulkPeer() (QuantumLayerBulkKey, error) { + return qlemuprng.peerNumbers.GetBulk() +} + +// GetStatus returns the current status of the QuantumLayerEmuPRNG. This +// includes the information if the QLE is powered, aswell as if the QLE is +// enabled for key generation. +func (qlemuprng *QuantumlayerEmuPRNG) GetStatus() (poweredOn, enabled bool) { + return qlemuprng.poweron, qlemuprng.generateKeys +} + +type NumberStore struct { + mu sync.Mutex + maxBytes int + storage []byte + bulkKeyStorage []QuantumLayerBulkKey + topOfStorage int +} + +// Generates a new store with given maximum number of bytes +func NewNumberStore(maxBytes int) (newNS *NumberStore) { + return &NumberStore{ + maxBytes: maxBytes, + storage: make([]byte, maxBytes), + topOfStorage: 0, + } +} + +func (store *NumberStore) GetBulk() (bulk QuantumLayerBulkKey, err error) { + store.mu.Lock() + defer store.mu.Unlock() + + for nextID := range store.bulkKeyStorage { + next := store.bulkKeyStorage[nextID] + // Preprare to return + a := QuantumLayerBulkKey{ + BulkKeyId: next.BulkKeyId, + BulkKeyLength: next.BulkKeyLength, + BulkKey: nil, + } + + bulkKey := make([]byte, next.BulkKeyLength) + copy(bulkKey, *next.BulkKey) + a.BulkKey = &bulkKey + + // Delete from key store + store.bulkKeyStorage = store.bulkKeyStorage[1:] + // and return + return a, nil + } + returnErr := errors.New("no bulk key to retrieve") + b := QuantumLayerBulkKey{} + return b, returnErr +} + +func (store *NumberStore) GetBatch() (batch []byte) { + store.mu.Lock() + defer store.mu.Unlock() + + if store.topOfStorage != 0 { + log.Debugf("QuantumlayerEmuPRNG: Have Storage in my belly") + } + // prepare to return full batch of numbers + batchReturn := make([]byte, store.topOfStorage) + copy(batchReturn, store.storage) + store.topOfStorage = 0 + + return batchReturn +} + +func (store *NumberStore) receiveNumbers(incoming chan QuantumPayloadElement, client pb.KmsQkdmCommunicationServiceClient) { + for { + receivedNumbers := <-incoming + // add received to our buffer, if buffer still last + store.mu.Lock() + mem := QuantumLayerBulkKey{ + BulkKeyId: receivedNumbers.BulkKeyId, + BulkKeyLength: receivedNumbers.BulkKeyLength, + BulkKey: receivedNumbers.BulkKey, + } + //store.bulkKeyStorage[receivedNumbers.BulkKeyId] = mem + store.bulkKeyStorage = append(store.bulkKeyStorage, mem) + store.mu.Unlock() + + log.Info(mem.BulkKeyId) + + _, err := client.PushKeys(context.Background(), &pb.PushKeysRequest{ + Timestamp: time.Now().Unix(), + KeyBulk: &pb.KeyBulk{ + KeyId: strconv.FormatInt(receivedNumbers.BulkKeyId, 10), + KeyLength: uint64(receivedNumbers.BulkKeyLength), + Keys: *receivedNumbers.BulkKey, + }, + }) + if err != nil { + logi.Error(err) + } + } +} diff --git a/quantumlayer/quantumlayer-emu-prng_test.go b/quantumlayer/quantumlayer-emu-prng_test.go new file mode 100644 index 0000000000000000000000000000000000000000..55854214246986afb98ba77fb37b00bf49ecbf93 --- /dev/null +++ b/quantumlayer/quantumlayer-emu-prng_test.go @@ -0,0 +1,62 @@ +package quantumlayer + +// Some tests + +import ( + "fmt" + "net" + "os" + "testing" + "time" + + logrus "github.com/sirupsen/logrus" +) + +func TestQuantumLayer(t *testing.T) { + + // Generate UDPAddr for ql1 peer + udpQL2AddrString := fmt.Sprintf("127.0.0.1:%d", 5002) + udpQL2Addr, err := net.ResolveUDPAddr("udp", udpQL2AddrString) + if err != nil { + t.Fatalf("QuantumlayerEmuPRNG UDP failure: %s", err) + return + } + + // Generate UDPAddr for ql2 peer + udpQL1AddrString := fmt.Sprintf("127.0.0.1:%d", 5001) + udpQL1Addr, err := net.ResolveUDPAddr("udp", udpQL1AddrString) + if err != nil { + t.Fatalf("QuantumlayerEmuPRNG UDP failure: %s", err) + return + } + + ql1 := NewQuantumlayerEmuPRNG(nil, os.Stdout, logrus.DebugLevel, false) + ql1.Configure(true, udpQL1AddrString) + ql1.PowerOn() // this one generates keys + defer ql1.PowerOff() + + ql2 := NewQuantumlayerEmuPRNG(nil, os.Stdout, logrus.DebugLevel, false) + ql2.Configure(false, udpQL2AddrString) + ql2.PowerOn() // this one does NOT generate keys + defer ql2.PowerOff() + + ql1.AddPeer(udpQL2Addr) + ql2.AddPeer(udpQL1Addr) + + // Wait for key gen to get up and running + time.Sleep(5 * time.Second) + + for n := 0; n < 2; n++ { + resultQl1, err := ql1.GetKeyBulkPeer() + if err == nil { + t.Logf("run %d, *ql1* keyid %d \t keylen %d", n, resultQl1.BulkKeyId, resultQl1.BulkKeyLength) + } else { + t.Fatalf("Couldn't read local ql1 batch with error %s", err) + } + + // TODO: Calculate checksum of BulkKey and double-check + + time.Sleep(5 * time.Second) + + } +} diff --git a/quantumlayer/quantumlayer.go b/quantumlayer/quantumlayer.go new file mode 100644 index 0000000000000000000000000000000000000000..3399b6b956d942e4a71592819dad4a0f1b91af4b --- /dev/null +++ b/quantumlayer/quantumlayer.go @@ -0,0 +1,31 @@ +// This package aims at emulating a quantum link and is extendable to different models +// One can use most of the sourc code of emu-prng and reuse it. +// To add a different quantum module one should only modify the GenerateRandomNumbers function + +package quantumlayer + +type QuantumLayerBulkKey struct { + BulkKeyId int64 // the unique ID of this bulk of keys + BulkKeyLength int // the length, counted in bytes, of bulkKey + // TODO: Pointer of slice should have a well thought reason; + // ask Martin if this is really necessary here + BulkKey *[]byte // the bulk key +} + +type QuantumLayer interface { + Configure(...string) // configure the interface, e.g., used IP/Port config if emulated + PowerOn(enableKeyGeneration bool) // switch on the quantum layer element + PowerOff() // switch off the quantum layer element + GetStatus() (poweredOn bool) // returns true if quantum layer element is powered on + AddPeer() // Adds a Quantum Layer Peer to the peer list + RemovePeer() // Remmoves a Quantum Layer Peer to the peer list + GetLocalQLPort() // Returns the information about the local quantum layer IP and port + GetKeyBulkPeer() (QuantumLayerBulkKey, error) // retrieve the bulk key received from peer + GenerateRandomNumbers() []byte // generate a number of random numbers +} + +type NumberLayer interface { + GetBatch() []byte // allows to retrieve the current available batch of numbers + GetBulk() (QuantumLayerBulkKey, error) + receiveNumbers(chan []byte) +}