diff options
| author | StevenWdV <stevenwdv@gmail.com> | 2021-12-14 15:56:10 +0100 |
|---|---|---|
| committer | StevenWdV <stevenwdv@gmail.com> | 2021-12-14 15:56:10 +0100 |
| commit | ae826fde04191d26af68b898cf4b2f537d24a8ec (patch) | |
| tree | c41943907e6c880a4254106942fc2c399c462016 | |
| parent | d9953dcc09ce61e249e40857bbf5cb98e0bb1fbf (diff) | |
Add Swift wrapper, support more platforms in Makefile
| -rw-r--r-- | .github/workflows/test.yml | 3 | ||||
| -rw-r--r-- | exports/.gitignore | 1 | ||||
| -rw-r--r-- | exports/Makefile | 24 | ||||
| -rw-r--r-- | wrappers/swift/.gitignore | 7 | ||||
| -rw-r--r-- | wrappers/swift/CEduVpnCommon/Package.swift | 13 | ||||
| -rw-r--r-- | wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap | 5 | ||||
| -rw-r--r-- | wrappers/swift/Makefile | 31 | ||||
| -rw-r--r-- | wrappers/swift/Package.swift | 24 | ||||
| -rw-r--r-- | wrappers/swift/README.md | 27 | ||||
| -rw-r--r-- | wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift | 77 | ||||
| -rw-r--r-- | wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift | 60 | ||||
| -rw-r--r-- | wrappers/swift/fix-path.cmd | 6 |
12 files changed, 267 insertions, 11 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bdfd962..df83b6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,4 +30,7 @@ jobs: with: distribution: temurin java-version: 11 + - uses: fwal/setup-swift@v1 + with: + swift-version: 5 - run: make test-wrappers diff --git a/exports/.gitignore b/exports/.gitignore index 63ea916..f916bcd 100644 --- a/exports/.gitignore +++ b/exports/.gitignore @@ -1 +1,2 @@ /*/ +*.h diff --git a/exports/Makefile b/exports/Makefile index 087d3a2..9664144 100644 --- a/exports/Makefile +++ b/exports/Makefile @@ -1,23 +1,25 @@ .PHONY: build copy-to clean -lib_prefix_linux = lib -lib_prefix_windows = -lib_prefix_darwin = lib +GOOS != go env GOHOSTOS +GOARCH != go env GOHOSTARCH -lib_suffix_linux = .so -lib_suffix_windows = .dll -lib_suffix_darwin = .dylib - -GOOS ?= $(shell go env GOHOSTOS) -GOARCH ?= $(shell go env GOHOSTARCH) -LIB_PREFIX = $(lib_prefix_$(GOOS)) -LIB_SUFFIX = $(lib_suffix_$(GOOS)) +ifeq (windows,$(GOOS)) +LIB_PREFIX = +LIB_SUFFIX = .dll +else ifeq (darwin,$(GOOS)) +LIB_PREFIX = lib +LIB_SUFFIX = .dylib +else +LIB_PREFIX = lib +LIB_SUFFIX = .so +endif # Creates targets like 'linux/amd64/eduvpn_verify.so' build: $(GOOS)/$(GOARCH)/$(LIB_PREFIX)eduvpn_verify$(LIB_SUFFIX) $(GOOS)/$(GOARCH)/$(LIB_PREFIX)eduvpn_verify$(LIB_SUFFIX): exports.go ../verify.go CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ -buildmode=c-shared $< + cp $(GOOS)/$(GOARCH)/$(LIB_PREFIX)eduvpn_verify.h ./eduvpn_verify.h copy-to: $(GOOS)/$(GOARCH)/$(LIB_PREFIX)eduvpn_verify$(LIB_SUFFIX) install $< -Dt "$(COPY_TARGET)" diff --git a/wrappers/swift/.gitignore b/wrappers/swift/.gitignore new file mode 100644 index 0000000..6481577 --- /dev/null +++ b/wrappers/swift/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.build/ +Packages/ +*.xcodeproj +xcuserdata/ +DerivedData/ +*.xcworkspacedata diff --git a/wrappers/swift/CEduVpnCommon/Package.swift b/wrappers/swift/CEduVpnCommon/Package.swift new file mode 100644 index 0000000..884f042 --- /dev/null +++ b/wrappers/swift/CEduVpnCommon/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version:5.1 + +import PackageDescription + +let package = Package( + name: "CEduVpnCommon", + products: [ + .library(name: "CEduVpnCommon", targets: ["CEduVpnCommon"]), + ], + targets: [ + .systemLibrary(name: "CEduVpnCommon"), + ] +) diff --git a/wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap b/wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap new file mode 100644 index 0000000..f0f47a7 --- /dev/null +++ b/wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap @@ -0,0 +1,5 @@ +module CEduVpnCommon { + header "../../../../../exports/eduvpn_verify.h" + link "eduvpn_verify" + export * +} diff --git a/wrappers/swift/Makefile b/wrappers/swift/Makefile new file mode 100644 index 0000000..cf117f3 --- /dev/null +++ b/wrappers/swift/Makefile @@ -0,0 +1,31 @@ +.PHONY: build test clean + +ifneq (clean,$(MAKECMDGOALS)) +GOOS != go env GOHOSTOS +GOARCH != go env GOHOSTARCH + +ifeq (Windows_NT,$(OS)) +export PATH := $(PATH):$(abspath ../../exports/$(GOOS)/$(GOARCH)) +else +export LD_LIBRARY_PATH := $(LD_LIBRARY_PATH):$(abspath ../../exports/$(GOOS)/$(GOARCH)) +endif +endif + +build: + $(MAKE) -C ../../exports +ifeq (Windows_NT,$(OS)) + "$(COMSPEC)" /c "\".\\fix-path.cmd & swift build --configuration release -Xlinker -L^\"../../exports/$(GOOS)/$(GOARCH)^\"\"" +else + swift build --configuration release -Xlinker -L"../../exports/$(GOOS)/$(GOARCH)" +endif + +test: + $(MAKE) -C ../../exports +ifeq (Windows_NT,$(OS)) + "$(COMSPEC)" /c "\".\\fix-path.cmd & swift test --parallel -Xlinker -L^\"../../exports/$(GOOS)/$(GOARCH)^\"\"" +else + swift test --parallel -Xlinker -L"../../exports/$(GOOS)/$(GOARCH)" +endif + +clean: + rm -rf .build/ diff --git a/wrappers/swift/Package.swift b/wrappers/swift/Package.swift new file mode 100644 index 0000000..bb6f8e9 --- /dev/null +++ b/wrappers/swift/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.1 +//TODO ^ find out minimal version + +import PackageDescription + +let package = Package( + name: "EduVpnCommon", + products: [ + .library( + name: "EduVpnCommon", + targets: ["EduVpnCommon"]), + ], + dependencies: [ + .package(path: "CEduVpnCommon"), + ], + targets: [ + .target( + name: "EduVpnCommon", + dependencies: ["CEduVpnCommon"]), + .testTarget( + name: "EduVpnCommonTests", + dependencies: ["EduVpnCommon"]), + ] +) diff --git a/wrappers/swift/README.md b/wrappers/swift/README.md new file mode 100644 index 0000000..8259592 --- /dev/null +++ b/wrappers/swift/README.md @@ -0,0 +1,27 @@ +# Swift wrapper + +## Requirements + +You will need to install the [Swift SDK](https://www.swift.org/getting-started), which includes the `swift` tool. + +## Build & test + +Build `EduVpnCommon` using shared Go library for current platform: + +```shell +make +``` + +Build `EduVpnCommon` using shared Go library for specified platform, e.g.: + +```shell +make GOOS=linux GOARCH=amd64 +``` + +On Windows, you will also need to generate a .lib for the .dll. + +Test: + +```shell +make test +``` diff --git a/wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift b/wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift new file mode 100644 index 0000000..340e94a --- /dev/null +++ b/wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift @@ -0,0 +1,77 @@ +import Foundation +import CEduVpnCommon + +private extension Data { + /// Execute `body` with `GoSlice` pointing to `self` + /// - Important: `GoSlice` pointer must not be written to + func withSlice<ResultType>(_ body: (GoSlice) throws -> ResultType) rethrows -> ResultType { + // Could also use raw NSData.bytes, but then you have to be careful that the NSData must remain valid during the call + // This closure method guarantees this + try withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> ResultType in + // Note: UnsafeRawBufferPointer.startIndex will always be 0, see docs + // Cast to UnsafeMutableRawPointer, assumes it will not be written to + try body(GoSlice(data: UnsafeMutableRawPointer(mutating: pointer.baseAddress), + len: GoInt(pointer.count), cap: GoInt(pointer.count))) + } + } +} + +private extension String { + /// Execute `body` with `GoSlice` pointing to UTF-8 bytes copied from `self` + func withSlice<ResultType>(_ body: (GoSlice) throws -> ResultType) rethrows -> ResultType { + try data(using: .utf8)!.withSlice(body) + } +} + +public enum VerifyErr: Error, Equatable { + /// Expected file name is not one of the recognized values. + case ErrUnknownExpectedFileName + /// Signature is invalid (for the expected file type). + case ErrInvalidSignature + /// Signature was created with an unknown key and has not been verified. + case ErrInvalidSignatureUnknownKey + /// Signature has a timestamp lower than the specified minimum signing time. + case ErrTooOld + /// Other unknown error + case Unknown(code: GoInt) + + static func fromCode(_ code: GoInt) -> VerifyErr { + precondition(code != 0) + switch code { + case 1: return ErrUnknownExpectedFileName + case 2: return ErrInvalidSignature + case 3: return ErrInvalidSignatureUnknownKey + case 4: return ErrTooOld + default: return Unknown(code: code) + } + } +} + +/// Verifies the signature on the JSON server_list.json/organization_list.json file. +/// If the function returns, the signature is valid for the given file type. +/// +/// - Parameters: +/// - signature: .minisig signature file contents. +/// - signedJson: Signed .json file contents. +/// - expectedFileName: The file type to be verified, one of "server_list.json" or "organization_list.json". +/// - minSignTime: Minimum time for signature. Should be set to at least the time in a previously retrieved file. +/// - Throws: VerifyErr: If signature verification fails or `expectedFileName` is not one of the allowed values. +public func Verify(signature: Data, signedJson: Data, expectedFileName: String, minSignTime: Date) throws { + let result = signature.withSlice { signatureData in + signedJson.withSlice { jsonData in + expectedFileName.withSlice { expectedNameData in + CEduVpnCommon.Verify(signatureData, jsonData, expectedNameData, GoUint64(minSignTime.timeIntervalSince1970)) + } + } + } + if result != 0 { + throw VerifyErr.fromCode(result) + } +} + +/// Use for testing only, see Go documentation. +internal func InsecureTestingSetExtraKey(keyString: String) { + keyString.withSlice { keyData in + CEduVpnCommon.InsecureTestingSetExtraKey(keyData); + } +} diff --git a/wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift b/wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift new file mode 100644 index 0000000..a508023 --- /dev/null +++ b/wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift @@ -0,0 +1,60 @@ +import XCTest +@testable import EduVpnCommon + +final class EduVpnCommonTests: XCTestCase { + private static let testDataDir = "../../test_data" + + override class func setUp() { + // Swift is confused by CRLF, so on some systems we cannot just take the second-to-last element + InsecureTestingSetExtraKey(keyString: try! String(contentsOfFile: "\(testDataDir)/dummy/public.key") + .components(separatedBy: .newlines).last(where: { !$0.isEmpty })!) + } + + func testValid() throws { + try Verify( + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + expectedFileName: "server_list.json", + minSignTime: Date(timeIntervalSince1970: 0)) + } + + func testInvalidSignature() throws { + XCTAssertThrowsError( + try Verify( + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/random.txt")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + expectedFileName: "server_list.json", + minSignTime: Date(timeIntervalSince1970: 0)), + "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrInvalidSignature)}); + } + + func testWrongKey() throws { + XCTAssertThrowsError( + try Verify( + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json.wrong_key.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + expectedFileName: "server_list.json", + minSignTime: Date(timeIntervalSince1970: 0)), + "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrInvalidSignatureUnknownKey)}); + } + + func testOldSignature() throws { + XCTAssertThrowsError( + try Verify( + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + expectedFileName: "server_list.json", + minSignTime: Date(timeIntervalSince1970: TimeInterval(1 << 31))), + "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrTooOld)}); + } + + func testUnknownExpectedFile() throws { + XCTAssertThrowsError( + try Verify( + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/other_list.json.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/other_list.json")), + expectedFileName: "other_list.json", + minSignTime: Date(timeIntervalSince1970: 0)), + "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrUnknownExpectedFileName)}); + } +} diff --git a/wrappers/swift/fix-path.cmd b/wrappers/swift/fix-path.cmd new file mode 100644 index 0000000..d8a1e42 --- /dev/null +++ b/wrappers/swift/fix-path.cmd @@ -0,0 +1,6 @@ +@echo off +:: Rename PATH -> Path +set _p=%PATH% +set PATH= +set Path=%_p% +set _p= |
