From df019490bcfa97a55c3d2acd020a62fe50fdd3f7 Mon Sep 17 00:00:00 2001
From: keliramu <ramunas.keliuotis@nordsec.com>
Date: Mon, 20 Jan 2025 14:30:21 +0200
Subject: [PATCH] Add initial libvinis integration

Signed-off-by: keliramu <ramunas.keliuotis@nordsec.com>
---
 .env.sample               |   1 +
 .gitmodules               |   3 +
 ci/check_dependencies.sh  |   9 +++
 ci/compile.sh             |   4 ++
 ci/test.sh                |   2 +-
 cmd/daemon/no_vinis.go    |   9 +++
 cmd/daemon/transports.go  |  18 ++---
 cmd/daemon/vinis.go       |  13 ++++
 lib-versions.env          |   3 +-
 request/vinis/cgo.go      |  11 +++
 request/vinis/libvinis.go | 145 ++++++++++++++++++++++++++++++++++++++
 third-party/libvinis-go   |   1 +
 12 files changed, 209 insertions(+), 10 deletions(-)
 create mode 100644 cmd/daemon/no_vinis.go
 create mode 100644 cmd/daemon/vinis.go
 create mode 100644 request/vinis/cgo.go
 create mode 100644 request/vinis/libvinis.go
 create mode 160000 third-party/libvinis-go

diff --git a/.env.sample b/.env.sample
index 990d82c6..8b5ffc31 100644
--- a/.env.sample
+++ b/.env.sample
@@ -6,6 +6,7 @@ SALT=f1nd1ngn3m0
 # * drop
 # * moose - internal builds only
 # * quench - internal builds only
+# * vinis - internal builds only
 # * internal - internal builds only
 FEATURES=telio drop
 # Used for mage targets, when set to 1, fetching docker images will be skipped if it is already fetched.
diff --git a/.gitmodules b/.gitmodules
index 5a7de9c4..61f3581a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -7,3 +7,6 @@
 [submodule "third-party/libquench-go"]
 	url = ../../libquench-go.git
 	path = third-party/libquench-go
+[submodule "third-party/libvinis-go"]
+	path = third-party/libvinis-go
+	url = ../../restricted/libvinis-go.git
diff --git a/ci/check_dependencies.sh b/ci/check_dependencies.sh
index 299e9594..9a52e9ac 100755
--- a/ci/check_dependencies.sh
+++ b/ci/check_dependencies.sh
@@ -27,6 +27,10 @@ libquench_artifact_url="${LIBQUENCH_ARTIFACTS_URL}/${LIBQUENCH_VERSION}/linux.zi
 libquench_zipfile="${temp_dir}/libquench-${LIBQUENCH_VERSION}.zip"
 libquench_dst="${temp_dir}/libquench-${LIBQUENCH_VERSION}"
 
+libvinis_artifact_url="${LIBVINIS_ARTIFACTS_URL}/${LIBVINIS_VERSION}/linux.zip"
+libvinis_zipfile="${temp_dir}/libvinis-${LIBVINIS_VERSION}.zip"
+libvinis_dst="${temp_dir}/libvinis-${LIBVINIS_VERSION}"
+
 lib_versions_file="${WORKDIR}/lib-versions.env"
 checkout_completed_flag_file="${lib_root}/checkout-completed-flag"
 
@@ -114,6 +118,7 @@ if [[ "${FEATURES:-""}" == *internal* ]]; then
   fetch_gitlab_artifact "${libmoose_nordvpnapp_artifact_url}" "${libmoose_nordvpnapp_zipfile}"
   fetch_gitlab_artifact "${libmoose_worker_artifact_url}" "${libmoose_worker_zipfile}"
   fetch_gitlab_artifact "${libquench_artifact_url}" "${libquench_zipfile}"
+  fetch_gitlab_artifact "${libvinis_artifact_url}" "${libvinis_zipfile}"
 fi
 
 # ====================[  Unzip files ]=========================
@@ -125,6 +130,7 @@ if [[ "${FEATURES:-""}" == *internal* ]]; then
   unzip -o "${libmoose_nordvpnapp_zipfile}" -d "${temp_dir}" && mv "${temp_dir}/out" "${libmoose_nordvpnapp_dst}"
   unzip -o "${libmoose_worker_zipfile}" -d "${temp_dir}" && mv "${temp_dir}/out" "${libmoose_worker_dst}"
   unzip -o "${libquench_zipfile}" -d "${temp_dir}" && mv "${temp_dir}/dist" "${libquench_dst}"
+  unzip -o "${libvinis_zipfile}" -d "${temp_dir}" && mv "${temp_dir}/dist" "${libvinis_dst}"
 fi
 
 # ====================[  Copy to bin/deps/libs ]=========================
@@ -149,6 +155,9 @@ if [[ "${FEATURES:-""}" == *internal* ]]; then
 
   # libquench
   copy_to_libs "${libquench_dst}/linux/release" "libquench.so"
+
+  # libvinis
+  copy_to_libs "${libvinis_dst}/linux/release" "libvinis.so"
 fi
 
 # remove leftovers
diff --git a/ci/compile.sh b/ci/compile.sh
index 0a07ca81..c348f33f 100755
--- a/ci/compile.sh
+++ b/ci/compile.sh
@@ -83,6 +83,10 @@ if [[ $tags == *"quench"* ]]; then
 	source "${WORKDIR}"/ci/add_private_bindings.sh quench ./third-party/libquench-go
 fi
 
+if [[ $tags == *"vinis"* ]]; then
+	source "${WORKDIR}"/ci/add_private_bindings.sh vinis ./third-party/libvinis-go
+fi
+
 for program in ${!names_map[*]}; do # looping over keys
 	pushd "${WORKDIR}/cmd/${program}"
 	# BUILDMODE can be no value and `go` does not like empty parameter ''
diff --git a/ci/test.sh b/ci/test.sh
index d9ea5b3b..54746e78 100755
--- a/ci/test.sh
+++ b/ci/test.sh
@@ -10,7 +10,7 @@ excluded_packages="moose\|cmd\/daemon\|telio\|daemon\/vpn\/openvpn"
 excluded_packages=$excluded_packages"\|meshnet\/mesh\/nordlynx\|fileshare\/drop"
 excluded_packages=$excluded_packages"\|events\/moose"
 excluded_packages=$excluded_packages"\|pb\|magefiles"
-excluded_packages=$excluded_packages"\|daemon\/vpn\/quench"
+excluded_packages=$excluded_packages"\|daemon\/vpn\/quench\/vinis"
 excluded_categories="root,link,firewall,route,file,integration"
 
 tags="internal"
diff --git a/cmd/daemon/no_vinis.go b/cmd/daemon/no_vinis.go
new file mode 100644
index 00000000..51a11985
--- /dev/null
+++ b/cmd/daemon/no_vinis.go
@@ -0,0 +1,9 @@
+//go:build !vinis
+
+package main
+
+import "net/http"
+
+func getPinningTransport(inner http.RoundTripper) http.RoundTripper {
+	return inner
+}
diff --git a/cmd/daemon/transports.go b/cmd/daemon/transports.go
index 49be069f..6ada0ed2 100644
--- a/cmd/daemon/transports.go
+++ b/cmd/daemon/transports.go
@@ -141,10 +141,11 @@ func createTimedOutTransport(
 	if containsH1 {
 		h1ReTransport := request.NewHTTPReTransport(createH1Transport(resolver, fwmark))
 		connectSubject.Subscribe(h1ReTransport.NotifyConnect)
-		h1Transport = request.NewPublishingRoundTripper(
-			h1ReTransport,
-			httpCallsSubject,
-		)
+		h1Transport = getPinningTransport(
+			request.NewPublishingRoundTripper(
+				h1ReTransport,
+				httpCallsSubject,
+			))
 		if !containsH3 {
 			return h1Transport
 		}
@@ -158,10 +159,11 @@ func createTimedOutTransport(
 		}
 		h3ReTransport := request.NewQuicTransport(createH3Transport)
 		connectSubject.Subscribe(h3ReTransport.NotifyConnect)
-		h3Transport = request.NewPublishingRoundTripper(
-			h3ReTransport,
-			httpCallsSubject,
-		)
+		h3Transport = getPinningTransport(
+			request.NewPublishingRoundTripper(
+				h3ReTransport,
+				httpCallsSubject,
+			))
 		if !containsH1 {
 			return h3Transport
 		}
diff --git a/cmd/daemon/vinis.go b/cmd/daemon/vinis.go
new file mode 100644
index 00000000..e22a7527
--- /dev/null
+++ b/cmd/daemon/vinis.go
@@ -0,0 +1,13 @@
+//go:build vinis
+
+package main
+
+import (
+	"net/http"
+
+	"github.com/NordSecurity/nordvpn-linux/request/vinis"
+)
+
+func getPinningTransport(inner http.RoundTripper) http.RoundTripper {
+	return vinis.New(inner)
+}
diff --git a/lib-versions.env b/lib-versions.env
index c2dea325..fdb08a26 100644
--- a/lib-versions.env
+++ b/lib-versions.env
@@ -7,4 +7,5 @@ LIBTELIO_VERSION=v5.1.5
 LIBDROP_VERSION=v8.1.1
 LIBMOOSE_NORDVPNAPP_VERSION=v15.0.2-nordVpnApp
 LIBMOOSE_WORKER_VERSION=v14.3.0-worker
-LIBQUENCH_VERSION=v0.0.19
+LIBQUENCH_VERSION=v0.0.20
+LIBVINIS_VERSION=v0.1.5
diff --git a/request/vinis/cgo.go b/request/vinis/cgo.go
new file mode 100644
index 00000000..7f6504b9
--- /dev/null
+++ b/request/vinis/cgo.go
@@ -0,0 +1,11 @@
+//go:build vinis
+
+package vinis
+
+// #cgo amd64 LDFLAGS: -L${SRCDIR}/../../../../bin/deps/lib/amd64/latest -lvinis
+// #cgo 386 LDFLAGS: -L${SRCDIR}/../../../../bin/deps/lib/i386/latest -lvinis
+// #cgo arm LDFLAGS: -L${SRCDIR}/../../../../bin/deps/lib/armel/latest -lvinis
+// #cgo arm LDFLAGS: -L${SRCDIR}/../../../../bin/deps/lib/armhf/latest -lvinis
+// #cgo arm64 LDFLAGS: -L${SRCDIR}/../../../../bin/deps/lib/aarch64/latest -lvinis
+// #cgo LDFLAGS: -ldl -lm
+import "C"
diff --git a/request/vinis/libvinis.go b/request/vinis/libvinis.go
new file mode 100644
index 00000000..f32da4b6
--- /dev/null
+++ b/request/vinis/libvinis.go
@@ -0,0 +1,145 @@
+//go:build vinis
+
+package vinis
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"crypto/x509"
+	"fmt"
+	"net/http"
+	"strings"
+
+	vinisBindings "vinis"
+)
+
+const (
+	// PinURL is the url for certificate pins
+	PinURL = "https://pdp.nordvpn.com"
+)
+
+// PinningTransport implements certificate pinning for HTTP requests
+type PinningTransport struct {
+	inner     http.RoundTripper
+	pdpClient *vinisBindings.PdpUrlConnectionClient
+	pdpCache  *vinisBindings.PdpCache
+}
+
+func New(inner http.RoundTripper) http.RoundTripper {
+	//TODO/FIXME: remove after debug
+	return inner
+	// if inner == nil {
+	// 	inner = http.DefaultTransport
+	// }
+
+	// transport, ok := inner.(*http.Transport)
+	// if !ok {
+	// 	panic("wrapped transport must be *http.Transport")
+	// }
+
+	// pt := &PinningTransport{
+	// 	inner:     inner,
+	// 	pdpClient: &vinisBindings.PdpUrlConnectionClient{Origin: PinURL},
+	// 	pdpCache:  vinisBindings.NewMemoryCache(),
+	// }
+
+	// if transport.TLSClientConfig == nil {
+	// 	transport.TLSClientConfig = &tls.Config{}
+	// }
+
+	// transport.TLSClientConfig.VerifyPeerCertificate = pt.verifyCertificates
+
+	// return pt
+}
+
+// RoundTrip implements the http.RoundTripper interface
+func (t *PinningTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	return t.inner.RoundTrip(req)
+}
+
+func (t *PinningTransport) verifyCertificates(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+	// when a TLS connection is established, the server presents a certificate chain.
+	// verifiedChains represents the validated certificate chains after the standard TLS verification process
+	if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 {
+		return fmt.Errorf("no verified certificate chain")
+	}
+
+	// leaf certificate
+	cert := verifiedChains[0][0]
+
+	// Pins must match exactly the certificates Subject Common Name or Subject Alternative Name
+	domains := NewDomainSet(cert.Subject.CommonName, cert.DNSNames...)
+
+	//fmt.Println("~~~LEAF CERT CN:", domain)
+	// fmt.Println("~~~CERT PUB KEY b64:", calculateEncodeB64Hash(cert.RawSubjectPublicKeyInfo))
+	// fmt.Println("~~~CERT PUB KEY hex:", calculateEncodeHexHash(cert.RawSubjectPublicKeyInfo))
+
+	dmns1 := domains.getDomains()
+
+	fmt.Println("~~~vinisBindings.FindPins domains:", dmns1)
+
+	pins, err := vinisBindings.FindPins(t.pdpCache, t.pdpClient, vinisBindings.PinQuery{Domains: dmns1})
+	if err != nil {
+		return fmt.Errorf("find pins: %w", err)
+	}
+
+	// RawSubjectPublicKeyInfo - DER encoded SubjectPublicKeyInfo.
+	certPubKeyHash := hash(cert.RawSubjectPublicKeyInfo)
+
+	for _, pin := range pins.Domains {
+		for _, fp := range pin.PubKeyFingerprints {
+			if comparePins(certPubKeyHash, fp) {
+				return nil
+			}
+		}
+	}
+
+	return fmt.Errorf("certificate pin not found")
+}
+
+func hash(data []byte) []byte {
+	sum := sha256.Sum256(data)
+	return sum[:]
+}
+
+func comparePins(crrPin, pinnedPin []byte) bool {
+	return bytes.Equal(crrPin, pinnedPin)
+}
+
+type domainSet struct {
+	names map[string]struct{}
+}
+
+func NewDomainSet(name string, names ...string) *domainSet { //TODO/FIXME: add unit tests
+	ds := &domainSet{
+		names: map[string]struct{}{},
+	}
+	ds.addName(name)
+	for _, nm := range names {
+		ds.addName(nm)
+	}
+	return ds
+}
+
+func (ds *domainSet) getDomains() []string {
+	rc := []string{}
+	for k := range ds.names {
+		rc = append(rc, k)
+	}
+	return rc
+}
+
+func (ds *domainSet) addName(nm string) { //TODO/FIXME: add unit tests
+	nmParts := strings.Split(nm, ".")
+	if len(nmParts) == 0 {
+		return
+	}
+	if nmParts[0] == "*" && len(nmParts) > 2 { // avoid `*.com` case, but `*.nordvpn.com` is ok
+		nm1 := strings.Join(nmParts[1:], ".")
+		ds.names[nm1] = struct{}{}
+	}
+	if nmParts[0] != "*" && len(nmParts) >= 2 {
+		nm1 := strings.Join(nmParts[:], ".")
+		ds.names[nm1] = struct{}{}
+	}
+}
diff --git a/third-party/libvinis-go b/third-party/libvinis-go
new file mode 160000
index 00000000..541b34e9
--- /dev/null
+++ b/third-party/libvinis-go
@@ -0,0 +1 @@
+Subproject commit 541b34e9ace67b69fb36806c3241ca9c2092076c