diff options
| -rw-r--r-- | .github/workflows/test.yml | 22 | ||||
| -rw-r--r-- | Makefile | 12 | ||||
| -rw-r--r-- | exports/.gitignore | 3 | ||||
| -rw-r--r-- | exports/Makefile | 12 | ||||
| -rw-r--r-- | exports/exports.go | 28 | ||||
| -rw-r--r-- | verify.go | 129 | ||||
| -rw-r--r-- | verify_test.go | 74 | ||||
| -rw-r--r-- | wrappers/csharp/.gitignore | 4 | ||||
| -rw-r--r-- | wrappers/csharp/Discovery.cs | 135 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommon.csproj | 27 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommon.sln | 22 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj | 19 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommonTests/VerifyTests.cs | 72 | ||||
| -rw-r--r-- | wrappers/csharp/Makefile | 4 |
14 files changed, 495 insertions, 68 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b695752..b96901f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,10 @@ name: Test -on: [push, pull_request] +on: [ push, pull_request ] jobs: - test: - name: Test + test-go: + name: Test Go runs-on: ubuntu-latest steps: @@ -12,4 +12,18 @@ jobs: - uses: actions/setup-go@v2 with: go-version: ^1.15 - - run: go test + - run: make test-go + + test-wrappers: + name: Test wrappers + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ^1.15 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - run: make test-wrappers diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b11b774 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: build test test-go test-wrappers + +build: + $(MAKE) -C exports build + +test: test-go test-wrappers + +test-go: + go test + +test-wrappers: build + $(MAKE) -C wrappers/csharp test diff --git a/exports/.gitignore b/exports/.gitignore new file mode 100644 index 0000000..a95b306 --- /dev/null +++ b/exports/.gitignore @@ -0,0 +1,3 @@ +*.dll +*.so +*.h diff --git a/exports/Makefile b/exports/Makefile new file mode 100644 index 0000000..94781be --- /dev/null +++ b/exports/Makefile @@ -0,0 +1,12 @@ +.PHONY: build + +ifeq ($(OS),Windows_NT) + lib_suffix := .dll +else + lib_suffix := .so +endif + +build: eduvpn_verify$(lib_suffix) + +eduvpn_verify.dll eduvpn_verify.so: exports.go ../verify.go + go build -o $@ -buildmode=c-shared $< diff --git a/exports/exports.go b/exports/exports.go new file mode 100644 index 0000000..f841eca --- /dev/null +++ b/exports/exports.go @@ -0,0 +1,28 @@ +package main + +import "C" + +import "eduvpn-common" + +// Functions here should not take string parameters, see https://pkg.go.dev/cmd/cgo#hdr-C_references_to_Go + +// Verify verifies a signature on a JSON file. See eduvpn_verify.Verify for more details. +// It returns 0 for a valid signature and a nonzero eduvpn_verify.VerifyErrorCode otherwise. +//export Verify +func Verify(signatureFileContent []byte, signedJson []byte, expectedFileName []byte, minSignTime uint64) int { + valid, err := eduvpn_verify.Verify(string(signatureFileContent), signedJson, string(expectedFileName), minSignTime) + if valid { + return 0 + } else { + return int(err.(eduvpn_verify.VerifyError).Code) + } +} + +// InsecureTestingSetExtraKey adds an extra allowed key for verification with Verify. +// ONLY USE FOR TESTING. Not Thread-safe. Do not call in parallel to Verify. +//export InsecureTestingSetExtraKey +func InsecureTestingSetExtraKey(keyString []byte) { + eduvpn_verify.InsecureTestingSetExtraKey(string(keyString)) +} + +func main() { panic("compile with -buildmode=c-shared") } @@ -3,6 +3,7 @@ package eduvpn_verify import ( "fmt" "github.com/jedisct1/go-minisign" + "os" ) // Verify verifies the signature (.minisig file format) on signedJson. @@ -11,7 +12,7 @@ import ( // minSign must be set to the minimum UNIX timestamp (without milliseconds) for the file version. // This value should not be smaller than the time on the previous document verified. // -// The return value will either be (true, nil) for a valid signature or (false, err) otherwise. +// The return value will either be (true, nil) for a valid signature or (false, VerifyError) otherwise. // // Verify is a wrapper around verifyWithKeys where allowedPublicKeys is set to the list from https://git.sr.ht/~eduvpn/disco.eduvpn.org#public-keys. func Verify(signatureFileContent string, signedJson []byte, expectedFileName string, minSignTime uint64) (bool, error) { @@ -19,11 +20,51 @@ func Verify(signatureFileContent string, signedJson []byte, expectedFileName str "RWRtBSX1alxyGX+Xn3LuZnWUT0w//B6EmTJvgaAxBMYzlQeI+jdrO6KF", // fkooman@tuxed.net, kolla@uninett.no "RWQKqtqvd0R7rUDp0rWzbtYPA3towPWcLDCl7eY9pBMMI/ohCmrS0WiM", // RoSp } + if extraKey != "" { + keyStrs = append(keyStrs, extraKey) + _, err := fmt.Fprintf(os.Stderr, "INSECURE TEST MODE ENABLED WITH KEY %q\n", extraKey) + if err != nil { + panic(err) + } + } valid, err := verifyWithKeys(signatureFileContent, signedJson, expectedFileName, minSignTime, keyStrs) - if err != nil && err.(VerifyError).Code == ErrInvalidPublicKey { - panic(err) // This should not happen + if err != nil { + if err.(detailedVerifyError).Code == errInvalidPublicKey { + panic(err) // This should not happen unless keyStrs has an invalid key + } + return valid, err.(detailedVerifyError).ToVerifyError() } - return valid, err + return valid, nil +} + +// Extra allowed key for testing +var extraKey = "" + +// InsecureTestingSetExtraKey adds an extra allowed key for verification with Verify. +// ONLY USE FOR TESTING. Probably not thread-safe. Do not call in parallel to Verify. +func InsecureTestingSetExtraKey(keyString string) { + extraKey = keyString +} + +type VerifyErrorCode int + +const ( + ErrUnknownExpectedFileName VerifyErrorCode = iota + 1 + ErrInvalidSignature + ErrInvalidSignatureUnknownKey + ErrTooOld +) + +type VerifyError struct { + Code VerifyErrorCode + Detailed detailedVerifyError +} + +func (err VerifyError) Error() string { + return err.Detailed.Error() +} +func (err VerifyError) Unwrap() error { + return err.Detailed } // verifyWithKeys verifies the Minisign signature in signatureFileContent (minisig file format) over the server_list/organization_list JSON in signedJson. @@ -34,29 +75,28 @@ func Verify(signatureFileContent string, signedJson []byte, expectedFileName str // The trusted comment is checked to be of the form "timestamp:<timestamp>\tfile:<expectedFileName>", optionally suffixed by something, e.g. "\thashed". // The signature is checked to have a timestamp with a value of at least minSignTime, which is a UNIX timestamp without milliseconds; // -// The return value will either be (true, nil) on success or (false, err) on failure. +// The return value will either be (true, nil) on success or (false, detailedVerifyError) on failure. func verifyWithKeys(signatureFileContent string, signedJson []byte, expectedFileName string, minSignTime uint64, allowedPublicKeys []string) (bool, error) { switch expectedFileName { case "server_list.json", "organization_list.json": break default: - return false, VerifyError{ErrUnknownExpectedFileName, - fmt.Sprintf("invalid expected file name (%v)", expectedFileName), nil} + return false, detailedVerifyError{errUnknownExpectedFileName, "invalid expected file name", nil} } sig, err := minisign.DecodeSignature(signatureFileContent) if err != nil { - return false, VerifyError{ErrInvalidSignatureFormat, "invalid signature format", err} + return false, detailedVerifyError{errInvalidSignatureFormat, "invalid signature format", err} } if sig.SignatureAlgorithm != [2]byte{'E', 'D'} { - return false, VerifyError{ErrInvalidSignatureAlgorithm, "BLAKE2b-prehashed EdDSA signature required", nil} + return false, detailedVerifyError{errInvalidSignatureAlgorithm, "BLAKE2b-prehashed EdDSA signature required", nil} } for _, keyStr := range allowedPublicKeys { key, err := minisign.NewPublicKey(keyStr) if err != nil { - return false, VerifyError{ErrInvalidPublicKey, "internal error: could not create public key", err} + return false, detailedVerifyError{errInvalidPublicKey, "internal error: could not create public key", err} } if sig.KeyId != key.KeyId { @@ -65,7 +105,7 @@ func verifyWithKeys(signatureFileContent string, signedJson []byte, expectedFile valid, err := key.Verify(signedJson, sig) if !valid { - return false, VerifyError{ErrInvalidSignature, "invalid signature", err} + return false, detailedVerifyError{errInvalidSignature, "invalid signature", err} } var signTime uint64 @@ -73,49 +113,74 @@ func verifyWithKeys(signatureFileContent string, signedJson []byte, expectedFile // sigFileName cannot have spaces _, err = fmt.Sscanf(sig.TrustedComment, "trusted comment: timestamp:%d\tfile:%s", &signTime, &sigFileName) if err != nil { - return false, VerifyError{ErrInvalidTrustedComment, - fmt.Sprintf("failed to interpret trusted comment (%q)", sig.TrustedComment), err} + return false, detailedVerifyError{errInvalidTrustedComment, "failed to interpret trusted comment", err} } if sigFileName != expectedFileName { - return false, VerifyError{ErrWrongFileName, - fmt.Sprintf("signature was on file %q instead of expected %q", sigFileName, expectedFileName), nil} + return false, detailedVerifyError{errWrongFileName, "signature was created for wrong file", nil} } if signTime < minSignTime { - return false, VerifyError{ErrTooOld, - fmt.Sprintf("signature was created at %v < minimum time (%v)", signTime, minSignTime), nil} + return false, detailedVerifyError{errTooOld, "signature was created a time earlier than the minimum time specified", nil} } return true, nil } - return false, VerifyError{ErrWrongKey, "signature was created with an unknown key", nil} + return false, detailedVerifyError{errWrongKey, "signature was created with unknown key", nil} } -type VerifyErrCode int +type detailedVerifyErrorCode int const ( - ErrUnknownExpectedFileName VerifyErrCode = iota - ErrInvalidSignatureFormat - ErrInvalidSignatureAlgorithm - ErrInvalidPublicKey - ErrInvalidSignature - ErrInvalidTrustedComment - ErrWrongFileName - ErrTooOld - ErrWrongKey + errUnknownExpectedFileName detailedVerifyErrorCode = iota + 1 + errInvalidSignatureFormat + errInvalidSignatureAlgorithm + errInvalidPublicKey + errInvalidSignature + errInvalidTrustedComment + errWrongFileName + errTooOld + errWrongKey ) -type VerifyError struct { - Code VerifyErrCode +func (code detailedVerifyErrorCode) ToVerifyErrorCode() VerifyErrorCode { + switch code { + case errUnknownExpectedFileName: + return ErrUnknownExpectedFileName + case errInvalidSignatureFormat: + return ErrInvalidSignature + case errInvalidSignatureAlgorithm: + return ErrInvalidSignature + case errInvalidPublicKey: + panic("errInvalidPublicKey cannot be converted to VerifyErrorCode") + case errInvalidSignature: + return ErrInvalidSignature + case errInvalidTrustedComment: + return ErrInvalidSignature + case errWrongFileName: + return ErrInvalidSignature + case errTooOld: + return ErrTooOld + case errWrongKey: + return ErrInvalidSignatureUnknownKey + } + panic("invalid detailedVerifyErrorCode") +} + +type detailedVerifyError struct { + Code detailedVerifyErrorCode Message string Cause error } -func (err VerifyError) Error() string { +func (err detailedVerifyError) Error() string { return err.Message } -func (err VerifyError) Unwrap() error { +func (err detailedVerifyError) Unwrap() error { return err.Cause } + +func (err detailedVerifyError) ToVerifyError() VerifyError { + return VerifyError{err.Code.ToVerifyErrorCode(), err} +} diff --git a/verify_test.go b/verify_test.go index 3068c96..0689be1 100644 --- a/verify_test.go +++ b/verify_test.go @@ -11,15 +11,25 @@ import ( ) const ( - ok VerifyErrCode = -1 - errAny = -2 + ok = -1 + errAny = -2 ) -func compareResults(t *testing.T, ret bool, err error, expected VerifyErrCode, call func() string) { - if (err == nil) != (expected == ok) || err != nil && expected != errAny && err.(VerifyError).Code != expected { +func compareResults(t *testing.T, ret bool, err error, expected int, call func() string) { + getCode := func(err error) int { + switch e := err.(type) { + case detailedVerifyError: + return int(e.Code) + case VerifyError: + return int(e.Code) + } + panic(nil) + } + + if (err == nil) != (expected == ok) || err != nil && expected != errAny && getCode(err) != expected { var errMsg string if err != nil { - errMsg = fmt.Sprintf("%v %v (cause %v)", err.(VerifyError).Code, err, errors.Unwrap(err)) + errMsg = fmt.Sprintf("%v %v (cause %v)", getCode(err), err, errors.Unwrap(err)) } else { errMsg = "<ok>" } @@ -31,7 +41,7 @@ func compareResults(t *testing.T, ret bool, err error, expected VerifyErrCode, c case errAny: wantErrCode = "<any>" default: - wantErrCode = strconv.Itoa(int(expected)) + wantErrCode = strconv.Itoa(expected) } t.Errorf("%v\nerror = %v, wantErr %v", call(), errMsg, wantErrCode) @@ -63,7 +73,7 @@ func Test_verifyWithKeys(t *testing.T) { } tests := []struct { - result VerifyErrCode + result detailedVerifyErrorCode testName string signatureFile string jsonFile string @@ -71,46 +81,46 @@ func Test_verifyWithKeys(t *testing.T) { minSignTime uint64 allowedPks []string }{ - {ErrInvalidSignatureAlgorithm, "pure", "server_list.json.pure.minisig", "server_list.json", "server_list.json", 10, pk}, + {errInvalidSignatureAlgorithm, "pure", "server_list.json.pure.minisig", "server_list.json", "server_list.json", 10, pk}, {ok, "valid server_list", "server_list.json.minisig", "server_list.json", "server_list.json", 10, pk}, {ok, "TC no hashed", "server_list.json.tc_nohashed.minisig", "server_list.json", "server_list.json", 10, pk}, {ok, "TC later time", "server_list.json.tc_latertime.minisig", "server_list.json", "server_list.json", 10, pk}, - {ErrWrongFileName, "server_list TC file:organization_list", "server_list.json.tc_orglist.minisig", "server_list.json", "server_list.json", 10, pk}, - {ErrWrongFileName, "organization_list as server_list", "organization_list.json.minisig", "organization_list.json", "server_list.json", 10, pk}, - {ErrWrongFileName, "TC file:otherfile", "server_list.json.tc_otherfile.minisig", "server_list.json", "server_list.json", 10, pk}, - {ErrInvalidTrustedComment, "TC no file", "server_list.json.tc_nofile.minisig", "server_list.json", "server_list.json", 10, pk}, - {ErrInvalidTrustedComment, "TC no time", "server_list.json.tc_notime.minisig", "server_list.json", "server_list.json", 10, pk}, + {errWrongFileName, "server_list TC file:organization_list", "server_list.json.tc_orglist.minisig", "server_list.json", "server_list.json", 10, pk}, + {errWrongFileName, "organization_list as server_list", "organization_list.json.minisig", "organization_list.json", "server_list.json", 10, pk}, + {errWrongFileName, "TC file:otherfile", "server_list.json.tc_otherfile.minisig", "server_list.json", "server_list.json", 10, pk}, + {errInvalidTrustedComment, "TC no file", "server_list.json.tc_nofile.minisig", "server_list.json", "server_list.json", 10, pk}, + {errInvalidTrustedComment, "TC no time", "server_list.json.tc_notime.minisig", "server_list.json", "server_list.json", 10, pk}, {errAny, "TC empty time", "server_list.json.tc_emptytime.minisig", "server_list.json", "server_list.json", 10, pk}, {errAny, "TC empty file", "server_list.json.tc_emptyfile.minisig", "server_list.json", "server_list.json", 10, pk}, - {ErrInvalidTrustedComment, "TC random", "server_list.json.tc_random.minisig", "server_list.json", "server_list.json", 10, pk}, + {errInvalidTrustedComment, "TC random", "server_list.json.tc_random.minisig", "server_list.json", "server_list.json", 10, pk}, {ok, "large time", "server_list.json.large_time.minisig", "server_list.json", "server_list.json", 43e8, pk}, {ok, "lower min time", "server_list.json.minisig", "server_list.json", "server_list.json", 5, pk}, - {ErrTooOld, "higher min time", "server_list.json.minisig", "server_list.json", "server_list.json", 11, pk}, + {errTooOld, "higher min time", "server_list.json.minisig", "server_list.json", "server_list.json", 11, pk}, {ok, "valid organization_list", "organization_list.json.minisig", "organization_list.json", "organization_list.json", 10, pk}, - {ErrWrongFileName, "organization_list TC file:server_list", "organization_list.json.tc_servlist.minisig", "organization_list.json", "organization_list.json", 10, pk}, - {ErrWrongFileName, "server_list as organization_list", "server_list.json.minisig", "server_list.json", "organization_list.json", 10, pk}, + {errWrongFileName, "organization_list TC file:server_list", "organization_list.json.tc_servlist.minisig", "organization_list.json", "organization_list.json", 10, pk}, + {errWrongFileName, "server_list as organization_list", "server_list.json.minisig", "server_list.json", "organization_list.json", 10, pk}, - {ErrUnknownExpectedFileName, "valid other_list", "other_list.json.minisig", "other_list.json", "other_list.json", 10, pk}, - {ErrWrongFileName, "other_list as server_list", "other_list.json.minisig", "other_list.json", "server_list.json", 10, pk}, + {errUnknownExpectedFileName, "valid other_list", "other_list.json.minisig", "other_list.json", "other_list.json", 10, pk}, + {errWrongFileName, "other_list as server_list", "other_list.json.minisig", "other_list.json", "server_list.json", 10, pk}, - {ErrInvalidSignatureFormat, "invalid signature file", "random.txt", "server_list.json", "server_list.json", 10, pk}, - {ErrInvalidSignatureFormat, "empty signature file", "empty", "server_list.json", "server_list.json", 10, pk}, + {errInvalidSignatureFormat, "invalid signature file", "random.txt", "server_list.json", "server_list.json", 10, pk}, + {errInvalidSignatureFormat, "empty signature file", "empty", "server_list.json", "server_list.json", 10, pk}, - {ErrWrongKey, "wrong key", "server_list.json.wrong_key.minisig", "server_list.json", "server_list.json", 10, pk}, + {errWrongKey, "wrong key", "server_list.json.wrong_key.minisig", "server_list.json", "server_list.json", 10, pk}, - {ErrInvalidSignatureAlgorithm, "forged pure signature", "server_list.json.forged_pure.minisig", "server_list.json.blake2b", "server_list.json", 10, pk}, - {ErrInvalidSignature, "forged key ID", "server_list.json.forged_keyid.minisig", "server_list.json", "server_list.json", 10, pk}, + {errInvalidSignatureAlgorithm, "forged pure signature", "server_list.json.forged_pure.minisig", "server_list.json.blake2b", "server_list.json", 10, pk}, + {errInvalidSignature, "forged key ID", "server_list.json.forged_keyid.minisig", "server_list.json", "server_list.json", 10, pk}, - {ErrWrongKey, "no allowed keys", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{}}, + {errWrongKey, "no allowed keys", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{}}, {ok, "multiple allowed keys 1", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{ pk[0], "RWSf0PYToIUJmDlsz21YOXvgQzHj9NSdyJUqEY5ZdfS9GepeXt3+JJRZ", }}, {ok, "multiple allowed keys 2", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{ "RWSf0PYToIUJmDlsz21YOXvgQzHj9NSdyJUqEY5ZdfS9GepeXt3+JJRZ", pk[0], }}, - {ErrInvalidPublicKey, "invalid allowed key", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{"AAA"}}, + {errInvalidPublicKey, "invalid allowed key", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{"AAA"}}, } files := map[string][]byte{} @@ -140,7 +150,7 @@ func Test_verifyWithKeys(t *testing.T) { t.Parallel() valid, err := verifyWithKeys(string(files[tt.signatureFile]), files[tt.jsonFile], tt.expectedFileName, tt.minSignTime, tt.allowedPks) - compareResults(t, valid, err, tt.result, func() string { + compareResults(t, valid, err, int(tt.result), func() string { return fmt.Sprintf("verifyWithKeys(%q, %q, %q, %v, %v)", tt.signatureFile, tt.jsonFile, tt.expectedFileName, tt.minSignTime, tt.allowedPks) }) @@ -151,7 +161,7 @@ func Test_verifyWithKeys(t *testing.T) { func Test_Verify(t *testing.T) { var err error tests := []struct { - result VerifyErrCode + result VerifyErrorCode testName string signatureFile string jsonFile string @@ -159,9 +169,9 @@ func Test_Verify(t *testing.T) { minSignTime uint64 }{ //TODO tests with real valid *prehashed* signatures - {ErrInvalidSignatureAlgorithm, "pure server_list", "server_list-1.json.pure.minisig", "server_list-1.json", "server_list.json", 1636532223}, - {ErrInvalidSignatureAlgorithm, "pure organization_list", "organization_list-1.json.pure.minisig", "organization_list-1.json", "organization_list.json", 1636532223}, - {ErrWrongKey, "wrong key", "../dummy/server_list.json.minisig", "../dummy/server_list.json", "server_list.json", 10}, + {ErrInvalidSignature, "pure server_list", "server_list-1.json.pure.minisig", "server_list-1.json", "server_list.json", 1636532223}, + {ErrInvalidSignature, "pure organization_list", "organization_list-1.json.pure.minisig", "organization_list-1.json", "organization_list.json", 1636532223}, + {ErrInvalidSignatureUnknownKey, "wrong key", "../dummy/server_list.json.minisig", "../dummy/server_list.json", "server_list.json", 10}, } files := map[string][]byte{} @@ -190,7 +200,7 @@ func Test_Verify(t *testing.T) { t.Run(tt.testName, func(t *testing.T) { t.Parallel() valid, err := Verify(string(files[tt.signatureFile]), files[tt.jsonFile], tt.expectedFileName, tt.minSignTime) - compareResults(t, valid, err, tt.result, func() string { + compareResults(t, valid, err, int(tt.result), func() string { return fmt.Sprintf("Verify(%q, %q, %q, %v)", tt.signatureFile, tt.jsonFile, tt.expectedFileName, tt.minSignTime) }) diff --git a/wrappers/csharp/.gitignore b/wrappers/csharp/.gitignore new file mode 100644 index 0000000..115bb34 --- /dev/null +++ b/wrappers/csharp/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +/packages/ +.vs/ diff --git a/wrappers/csharp/Discovery.cs b/wrappers/csharp/Discovery.cs new file mode 100644 index 0000000..0d6fef1 --- /dev/null +++ b/wrappers/csharp/Discovery.cs @@ -0,0 +1,135 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +[assembly: InternalsVisibleTo("EduVpnCommonTests")] + +namespace EduVpnCommon +{ + public static class Discovery + { + /// <summary> + /// 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. + /// </summary> + /// <param name="signatureFileContent">.minisig signature file contents.</param> + /// <param name="signedJson">Signed .json file contents.</param> + /// <param name="expectedFileName">The file type to be verified, one of <c>server_list.json</c> or <c>organization_list.json</c>.</param> + /// <param name="minSignTime">Minimum time for signature. Should be set to at least the time in a previously retrieved file.</param> + /// <exception cref="ArgumentException">If <c>expectedFileName</c> is not one of the allowed valued.</exception> + /// <exception cref="VerifyException">If signature verification fails.</exception> + public static void Verify( + ArraySegment<byte> signatureFileContent, + ArraySegment<byte> signedJson, + string expectedFileName, + DateTimeOffset minSignTime) + { + VerifyReturnCode result; + { + using var signatureHandle = GoSliceHandle.FromArray(signatureFileContent); + using var jsonHandle = GoSliceHandle.FromArray(signedJson); + using var expectedFileHandle = GoSliceHandle.FromString(expectedFileName); + + result = Verify(signatureHandle.Slice, jsonHandle.Slice, expectedFileHandle.Slice, + (ulong) minSignTime.ToUnixTimeSeconds()); + } + + if (result != VerifyReturnCode.Ok) + { + if (result == VerifyReturnCode.ErrUnknownExpectedFileName) + throw new ArgumentException("unknown name", nameof(expectedFileName)); + throw new VerifyException((VerifyErrorCode) result); + } + } + + /// <summary>Use for testing only, see Go documentation.</summary> + internal static void InsecureTestingSetExtraKey(string keyString) + { + using var keyHandle = GoSliceHandle.FromString(keyString); + InsecureTestingSetExtraKey(keyHandle.Slice); + } + + const string VerifyLibName = "eduvpn_verify"; + + [DllImport(VerifyLibName)] + static extern VerifyReturnCode Verify(GoSlice signatureFileContent, GoSlice signedJson, GoSlice expectedFileName, ulong minSignTime); + + [DllImport(VerifyLibName)] static extern void InsecureTestingSetExtraKey(GoSlice keyStr); + + class GoSliceHandle : IDisposable + { + GCHandle gcHandle_; + readonly GoSlice slice_; + + public GoSlice Slice => gcHandle_.IsAllocated + ? slice_ + : throw new InvalidOperationException("Handle was disposed"); + + GoSliceHandle(Array array, int offset, int count) + { + gcHandle_ = GCHandle.Alloc(array, GCHandleType.Pinned); + var elemSize = Marshal.SizeOf(array.GetType().GetElementType()!); + slice_ = new GoSlice(gcHandle_.AddrOfPinnedObject() + offset * elemSize, count * elemSize); + } + + public static GoSliceHandle FromArray<T>(ArraySegment<T> segment) where T : struct => + new GoSliceHandle(segment.Array!, segment.Offset, segment.Count); + + public static GoSliceHandle FromString(string str) => + FromArray(new ArraySegment<byte>(Encoding.UTF8.GetBytes(str))); + + public void Dispose() => gcHandle_.Free(); + } + + readonly struct GoSlice + { + readonly IntPtr data_; + readonly long len_, cap_; + + public GoSlice(IntPtr data, long len, long cap) + { + data_ = data; + len_ = len; + cap_ = cap; + } + + public GoSlice(IntPtr data, long len) : this(data, len, len) { } + } + } + + public class VerifyException : Exception + { + public VerifyErrorCode Code { get; } + + internal VerifyException(VerifyErrorCode code) : base(GetMessage(code)) => Code = code; + + static string GetMessage(VerifyErrorCode code) => code switch + { + VerifyErrorCode.ErrInvalidSignature => "invalid signature", + VerifyErrorCode.ErrInvalidSignatureUnknownKey => "invalid signature (unknown key)", + VerifyErrorCode.ErrTooOld => "replay of previous signature (rollback)", + _ => $"unknown verify error ({code})" + }; + } + + public enum VerifyErrorCode + { + /// <summary>Signature is invalid (for the expected file type).</summary> + ErrInvalidSignature = VerifyReturnCode.ErrUnknownExpectedFileName + 1, + + /// <summary>Signature was created with an unknown key and has not been verified.</summary> + ErrInvalidSignatureUnknownKey, + + /// <summary>Signature has a timestamp lower than the specified minimum signing time.</summary> + ErrTooOld + } + + enum VerifyReturnCode + { + Ok, + ErrUnknownExpectedFileName + + //... + } +}
\ No newline at end of file diff --git a/wrappers/csharp/EduVpnCommon.csproj b/wrappers/csharp/EduVpnCommon.csproj new file mode 100644 index 0000000..1bbbc23 --- /dev/null +++ b/wrappers/csharp/EduVpnCommon.csproj @@ -0,0 +1,27 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>8</LangVersion> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <Compile Remove="EduVpnCommonTests/**" /> + </ItemGroup> + + <ItemGroup> + <EmbeddedResource Remove="EduVpnCommonTests/**" /> + </ItemGroup> + + <ItemGroup> + <None Remove="EduVpnCommonTests/**" /> + <None Condition="Exists('../../exports/eduvpn_verify.dll')" Include="../../exports/eduvpn_verify.dll"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Condition="Exists('../../exports/eduvpn_verify.so')" Include="../../exports/eduvpn_verify.so"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> diff --git a/wrappers/csharp/EduVpnCommon.sln b/wrappers/csharp/EduVpnCommon.sln new file mode 100644 index 0000000..0d59e2f --- /dev/null +++ b/wrappers/csharp/EduVpnCommon.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EduVpnCommon", "EduVpnCommon.csproj", "{D95F43A0-EF74-41FD-A526-8987151ABB30}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EduVpnCommonTests", "EduVpnCommonTests/EduVpnCommonTests.csproj", "{B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Release|Any CPU.Build.0 = Release|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj b/wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj new file mode 100644 index 0000000..cf58249 --- /dev/null +++ b/wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="NUnit" Version="3.13.1" /> + <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> + <PackageReference Include="coverlet.collector" Version="3.0.2" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../EduVpnCommon.csproj" /> + </ItemGroup> + +</Project> diff --git a/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs b/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs new file mode 100644 index 0000000..933f2bc --- /dev/null +++ b/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Linq; +using EduVpnCommon; +using NUnit.Framework; + +namespace EduVpnCommonTests +{ + [TestFixture(TestOf = typeof(Discovery)), Parallelizable] + public class VerifyTests + { + // Relative to e.g. EduVpnCommonTests/bin/Debug/net5.0 + readonly string testDataDir_ = $"{TestContext.CurrentContext.TestDirectory}/../../../../../../test_data"; + + [OneTimeSetUp] + public void OneTimeSetUp() => + Discovery.InsecureTestingSetExtraKey(File.ReadLines($"{testDataDir_}/dummy/public.key").Last()); + + [Test] + [TestCase("dummy/server_list.json.minisig", "dummy/server_list.json", "server_list.json")] + [TestCase("dummy/organization_list.json.minisig", "dummy/organization_list.json", "organization_list.json")] + public void TestValid(string sigFile, string jsonFile, string expectedFileName) => + Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch); + + [Test] + [TestCase("dummy/random.txt", "dummy/server_list.json", "server_list.json")] + public void TestInvalidSignature(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws(Is.TypeOf<VerifyException>() + .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrInvalidSignature), + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch)); + + [Test] + [TestCase("dummy/server_list.json.wrong_key.minisig", "dummy/server_list.json", "server_list.json")] + public void TestWrongKey(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws(Is.TypeOf<VerifyException>() + .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrInvalidSignatureUnknownKey), + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch)); + + [Test] + [TestCase("dummy/server_list.json.minisig", "dummy/server_list.json", "server_list.json")] + public void TestOldSignature(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws(Is.TypeOf<VerifyException>() + .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrTooOld), + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.MaxValue)); + + [Test] + [TestCase("dummy/other_list.json.minisig", "dummy/other_list.json", "other_list.json")] + public void TestUnknownExpectedFile(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws<ArgumentException>( + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch)); + } +}
\ No newline at end of file diff --git a/wrappers/csharp/Makefile b/wrappers/csharp/Makefile new file mode 100644 index 0000000..293a7a2 --- /dev/null +++ b/wrappers/csharp/Makefile @@ -0,0 +1,4 @@ +.PHONY: test + +test: + dotnet test |
