From 1ff7f41a61ef4ac4805299e8a66e9c4203a73d7b Mon Sep 17 00:00:00 2001 From: keliramu Date: Mon, 20 Jan 2025 14:30:21 +0200 Subject: [PATCH] Add initial libvinis integration Signed-off-by: keliramu --- .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..be27a5d4 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 = ../../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..8aca33ad --- /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://" +) + +// 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