diff --git a/.github/workflows/address-sanitizer.yml b/.github/workflows/address-sanitizer.yml new file mode 100644 index 0000000..44fb727 --- /dev/null +++ b/.github/workflows/address-sanitizer.yml @@ -0,0 +1,78 @@ +name: AddressSanitizer Build and Test + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_asan: + strategy: + fail-fast: false + matrix: + os: [ ubuntu-24.04 ] + go_version: [ '1.23' ] + wolfssl_configure: [ + '--enable-all --enable-opensslall --enable-opensslextra --enable-debug', + ] + name: ${{ matrix.os }} (Go ${{ matrix.go_version }}, ASan, ${{ matrix.wolfssl_configure }}) + if: github.repository_owner == 'wolfssl' + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go_version }} + + - name: Install build dependencies + run: sudo apt-get update && sudo apt-get install -y autoconf automake libtool + + - name: Cache wolfSSL ASan build + id: cache-wolfssl-asan + uses: actions/cache@v4 + with: + path: /tmp/wolfssl-install + key: wolfssl-asan-${{ runner.os }}-${{ hashFiles('.github/workflows/address-sanitizer.yml') }}-${{ matrix.wolfssl_configure }} + + - name: Build native wolfSSL with AddressSanitizer + if: steps.cache-wolfssl-asan.outputs.cache-hit != 'true' + run: | + git clone --depth 1 https://github.com/wolfSSL/wolfssl.git /tmp/wolfssl + cd /tmp/wolfssl + ./autogen.sh + ./configure --prefix=/tmp/wolfssl-install ${{ matrix.wolfssl_configure }} \ + CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \ + LDFLAGS="-fsanitize=address" + make -j$(nproc) + make install + + - name: Install wolfSSL system-wide + run: | + sudo cp -r /tmp/wolfssl-install/lib/* /usr/local/lib/ + sudo cp -r /tmp/wolfssl-install/include/* /usr/local/include/ + sudo ldconfig + + - name: Run unit tests with ASan instrumentation + env: + # Propagate ASan flags through cgo so the test binary links the + # ASan runtime alongside libwolfssl. -I/-L paths are explicit so this + # doesn't depend on /usr/local being in gcc/ld's default search list. + CGO_CFLAGS: "-I/usr/local/include -fsanitize=address -fno-omit-frame-pointer -g -O1" + CGO_LDFLAGS: "-L/usr/local/lib -fsanitize=address" + # Detect leaks at exit; abort on any error. + ASAN_OPTIONS: "detect_leaks=1:halt_on_error=1:strict_string_checks=1:print_stacktrace=1" + run: go test -count=1 -v . ./handles ./wolftls ./wolfx509 + + - name: Show logs on failure + if: failure() || cancelled() + run: | + echo "AddressSanitizer test failed" + go env diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca76665..3fe04c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,8 @@ jobs: go_version: [ '1.21', '1.22', '1.23' ] wolfssl_configure: [ '--enable-all --enable-opensslall --enable-opensslextra', + '--enable-tls13 --enable-curve25519 --enable-chacha --enable-poly1305 --enable-opensslall --enable-opensslextra', + '--enable-dtls --enable-dtls13 --enable-opensslall --enable-opensslextra', ] name: ${{ matrix.os }} (Go ${{ matrix.go_version }}, ${{ matrix.wolfssl_configure}}) if: github.repository_owner == 'wolfssl' @@ -43,7 +45,7 @@ jobs: uses: actions/cache@v4 with: path: /tmp/wolfssl-install - key: wolfssl-${{ runner.os }}-${{ hashFiles('.github/workflows/build.yml') }} + key: wolfssl-${{ runner.os }}-${{ hashFiles('.github/workflows/build.yml') }}-${{ matrix.wolfssl_configure }} - name: Build native wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -61,17 +63,20 @@ jobs: sudo cp -r /tmp/wolfssl-install/include/* /usr/local/include/ sudo ldconfig - - name: Install Go dependencies + - name: Verify go.mod / go.sum integrity run: | - go get golang.org/x/term - go mod tidy + go mod download + go mod verify - name: Build go-wolfssl library - run: go build . + run: go build . ./handles ./wolftls ./wolfx509 - name: Run go vet run: go vet . + - name: Run unit tests + run: go test -count=1 -timeout=120s . ./handles ./wolftls ./wolfx509 + - name: Build examples run: | # Each example dir has multiple main packages, so build each .go file individually diff --git a/.github/workflows/lint.yml b/.github/workflows/linters.yml similarity index 54% rename from .github/workflows/lint.yml rename to .github/workflows/linters.yml index 54c235c..7eccc4b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/linters.yml @@ -1,6 +1,5 @@ -name: Go Lint and Vet +name: Linters -# START OF COMMON SECTION on: push: branches: [ 'master', 'main', 'release/**' ] @@ -10,21 +9,15 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -# END OF COMMON SECTION jobs: - go_vet: - strategy: - fail-fast: false - matrix: - os: [ ubuntu-24.04 ] - wolfssl_configure: [ - '--enable-all --enable-opensslall --enable-opensslextra', - ] - name: ${{ matrix.os }} go vet + # go vet and staticcheck need cgo to resolve wolfSSL types/symbols. examples/ + # is excluded because it contains multiple `package main` files per directory + # and would produce false-positive redeclaration errors at package level. + vet_and_staticcheck: + name: go vet + staticcheck if: github.repository_owner == 'wolfssl' - runs-on: ${{ matrix.os }} - # This should be a safe limit for the tests to run. + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -42,7 +35,7 @@ jobs: uses: actions/cache@v4 with: path: /tmp/wolfssl-install - key: wolfssl-${{ runner.os }}-${{ hashFiles('.github/workflows/lint.yml') }} + key: wolfssl-${{ runner.os }}-${{ hashFiles('.github/workflows/linters.yml') }}-enable-all - name: Build native wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -50,7 +43,7 @@ jobs: git clone --depth 1 https://github.com/wolfSSL/wolfssl.git /tmp/wolfssl cd /tmp/wolfssl ./autogen.sh - ./configure --prefix=/tmp/wolfssl-install ${{ matrix.wolfssl_configure }} + ./configure --prefix=/tmp/wolfssl-install --enable-all --enable-opensslall --enable-opensslextra make -j$(nproc) make install @@ -60,13 +53,18 @@ jobs: sudo cp -r /tmp/wolfssl-install/include/* /usr/local/include/ sudo ldconfig - - name: Install Go dependencies - run: | - go get golang.org/x/term - go mod tidy + - name: Build + run: go build . ./handles ./wolftls ./wolfx509 - name: Run go vet - run: go vet . + # wolftls is excluded for a pre-existing unsafe.Pointer(uintptr) pattern + # that's a separate cleanup ticket. + run: go vet . ./handles ./wolfx509 + + - name: Install staticcheck + # v0.6.1 is the last release that supports Go 1.21–1.24 natively; + # pinning avoids a silent toolchain auto-upgrade to Go 1.25. + run: go install honnef.co/go/tools/cmd/staticcheck@v0.6.1 - - name: Check build - run: go build . + - name: Run staticcheck + run: $(go env GOPATH)/bin/staticcheck . ./handles ./wolfx509 diff --git a/aes_test.go b/aes_test.go new file mode 100644 index 0000000..d3f00be --- /dev/null +++ b/aes_test.go @@ -0,0 +1,217 @@ +/* aes_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +// allocAes returns a heap-aligned Aes context with t.Cleanup registered. +func allocAes(t *testing.T) *Aes { + t.Helper() + aes := Wc_AesAllocAligned() + if aes == nil { + t.Skip("AES not compiled in (Wc_AesAllocAligned returned nil)") + } + ret := Wc_AesInit(aes, nil, INVALID_DEVID) + if ret == notCompiledIn { + Wc_AesFreeAllocAligned(aes) + t.Skip("AES not compiled in") + } + if ret != 0 { + Wc_AesFreeAllocAligned(aes) + t.Fatalf("Wc_AesInit returned %d", ret) + } + t.Cleanup(func() { + Wc_AesFree(aes) + Wc_AesFreeAllocAligned(aes) + }) + return aes +} + +func TestAesCbc_RoundTrip(t *testing.T) { + aes := allocAes(t) + key := bytes.Repeat([]byte{0x10}, AES_256_KEY_SIZE) + iv := bytes.Repeat([]byte{0x20}, AES_IV_SIZE) + plaintext := bytes.Repeat([]byte("ABCD1234"), 4) // 32 bytes (block-aligned) + + if ret := Wc_AesSetKey(aes, key, AES_256_KEY_SIZE, iv, AES_ENCRYPTION); ret != 0 { + t.Fatalf("AesSetKey enc: %d", ret) + } + cipher := make([]byte, len(plaintext)) + ret := Wc_AesCbcEncrypt(aes, cipher, plaintext, len(plaintext)) + skipIfNotCompiledIn(t, ret, "AES-CBC") + if ret != 0 { + t.Fatalf("AesCbcEncrypt: %d", ret) + } + if bytes.Equal(cipher, plaintext) { + t.Fatal("ciphertext equals plaintext") + } + + if ret := Wc_AesSetKey(aes, key, AES_256_KEY_SIZE, iv, AES_DECRYPTION); ret != 0 { + t.Fatalf("AesSetKey dec: %d", ret) + } + decrypted := make([]byte, len(plaintext)) + if ret := Wc_AesCbcDecrypt(aes, decrypted, cipher, len(cipher)); ret != 0 { + t.Fatalf("AesCbcDecrypt: %d", ret) + } + if !bytes.Equal(decrypted, plaintext) { + t.Fatalf("round-trip mismatch:\n got: %x\n want: %x", decrypted, plaintext) + } +} + +// Boundary: oversized sz must not let wc read past Go slice. +func TestAesCbc_OversizedSz_Rejected(t *testing.T) { + aes := allocAes(t) + key := bytes.Repeat([]byte{0x33}, AES_256_KEY_SIZE) + iv := bytes.Repeat([]byte{0x44}, AES_IV_SIZE) + if ret := Wc_AesSetKey(aes, key, AES_256_KEY_SIZE, iv, AES_ENCRYPTION); ret != 0 { + t.Fatalf("AesSetKey: %d", ret) + } + in := make([]byte, 16) + out := make([]byte, 16) + if ret := Wc_AesCbcEncrypt(aes, out, in, 1<<20); ret == 0 { + t.Fatal("expected error for sz > len(in)") + } +} + +func TestAesGcm_RawWrapper_RoundTrip(t *testing.T) { + aes := allocAes(t) + key := bytes.Repeat([]byte{0x55}, AES_256_KEY_SIZE) + iv := bytes.Repeat([]byte{0x66}, AES_IV_SIZE) + plaintext := []byte("AES-GCM round trip via raw wrapper") + aad := []byte("aad") + + ret := Wc_AesGcmSetKey(aes, key, AES_256_KEY_SIZE) + skipIfNotCompiledIn(t, ret, "AES-GCM") + if ret != 0 { + t.Fatalf("AesGcmSetKey: %d", ret) + } + cipher := make([]byte, len(plaintext)) + tag := make([]byte, AES_BLOCK_SIZE) + if ret := Wc_AesGcmEncrypt(aes, cipher, plaintext, iv, tag, aad); ret != 0 { + t.Fatalf("AesGcmEncrypt: %d", ret) + } + + decrypted := make([]byte, len(plaintext)) + if ret := Wc_AesGcmDecrypt(aes, decrypted, cipher, iv, tag, aad); ret != 0 { + t.Fatalf("AesGcmDecrypt: %d", ret) + } + if !bytes.Equal(decrypted, plaintext) { + t.Fatalf("mismatch: got %q, want %q", decrypted, plaintext) + } +} + +func TestAesGcm_TamperedCipher_FailsAuth(t *testing.T) { + aes := allocAes(t) + key := bytes.Repeat([]byte{0x77}, AES_256_KEY_SIZE) + iv := bytes.Repeat([]byte{0x88}, AES_IV_SIZE) + plain := []byte("authenticate me") + ret := Wc_AesGcmSetKey(aes, key, AES_256_KEY_SIZE) + skipIfNotCompiledIn(t, ret, "AES-GCM") + if ret != 0 { + t.Fatalf("AesGcmSetKey: %d", ret) + } + cipher := make([]byte, len(plain)) + tag := make([]byte, AES_BLOCK_SIZE) + if ret := Wc_AesGcmEncrypt(aes, cipher, plain, iv, tag, nil); ret != 0 { + t.Fatalf("AesGcmEncrypt: %d", ret) + } + cipher[0] ^= 1 + out := make([]byte, len(plain)) + if ret := Wc_AesGcmDecrypt(aes, out, cipher, iv, tag, nil); ret == 0 { + t.Fatal("tampered ciphertext must fail authentication") + } +} + +// Appended-tag short-cipher: must error, not panic on inCipher[len-N:]. +func TestAesGcm_AppendedTagDecrypt_ShortCipher(t *testing.T) { + aes := allocAes(t) + key := bytes.Repeat([]byte{0x99}, AES_256_KEY_SIZE) + iv := bytes.Repeat([]byte{0xaa}, AES_IV_SIZE) + ret := Wc_AesGcmSetKey(aes, key, AES_256_KEY_SIZE) + skipIfNotCompiledIn(t, ret, "AES-GCM") + if ret != 0 { + t.Fatalf("AesGcmSetKey: %d", ret) + } + defer func() { + if r := recover(); r != nil { + t.Fatalf("short cipher should error, not panic: %v", r) + } + }() + out := make([]byte, 16) + if ret := Wc_AesGcm_Appended_Tag_Decrypt(aes, out, []byte{1, 2, 3}, iv, nil); ret == 0 { + t.Fatal("expected error for cipher shorter than AES_BLOCK_SIZE") + } +} + +// PBKDF2 with separate password/output buffers must be deterministic. +func TestPBKDF2_DerivesDeterministicKey(t *testing.T) { + password := []byte("password123") + salt := []byte("saltsaltsaltsalt") + out1 := make([]byte, 32) + out2 := make([]byte, 32) + ret := Wc_PBKDF2(out1, password, len(password), salt, len(salt), 1000, len(out1), WC_SHA256) + skipIfNotCompiledIn(t, ret, "PBKDF2") + if ret != 0 { + t.Fatalf("Wc_PBKDF2 first call: %d", ret) + } + if ret := Wc_PBKDF2(out2, password, len(password), salt, len(salt), 1000, len(out2), WC_SHA256); ret != 0 { + t.Fatalf("Wc_PBKDF2 second call: %d", ret) + } + if !bytes.Equal(out1, out2) { + t.Fatal("PBKDF2 not deterministic") + } + if bytes.Equal(out1, make([]byte, 32)) { + t.Fatal("PBKDF2 output is all zeros") + } +} + +// Empty plaintext: wolfSSL accepts (tag-only) or rejects with BAD_FUNC_ARG +// depending on build options; both are valid contract-wise. +func TestAesGcm_EmptyPlaintext_MatchesWcBehavior(t *testing.T) { + aes := allocAes(t) + key := bytes.Repeat([]byte{0xab}, AES_256_KEY_SIZE) + iv := bytes.Repeat([]byte{0xcd}, AES_IV_SIZE) + ret := Wc_AesGcmSetKey(aes, key, AES_256_KEY_SIZE) + skipIfNotCompiledIn(t, ret, "AES-GCM") + if ret != 0 { + t.Fatalf("AesGcmSetKey: %d", ret) + } + tag := make([]byte, AES_BLOCK_SIZE) + got := Wc_AesGcmEncrypt(aes, []byte{}, []byte{}, iv, tag, nil) + if got == notCompiledIn { + t.Skip("AES-GCM empty-plaintext path not compiled in") + } + if got != 0 && got != BAD_FUNC_ARG { + t.Fatalf("empty plaintext: want 0 or BAD_FUNC_ARG, got %d", got) + } +} + +// Boundary: kLen > len(out) must error. +func TestPBKDF2_OversizedKLen_Rejected(t *testing.T) { + out := make([]byte, 16) + if ret := Wc_PBKDF2(out, []byte("p"), 1, []byte("s"), 1, 1, 1<<20, WC_SHA256); ret == 0 { + t.Fatal("expected error for kLen > len(out)") + } +} diff --git a/blake2_test.go b/blake2_test.go new file mode 100644 index 0000000..5b4c7cd --- /dev/null +++ b/blake2_test.go @@ -0,0 +1,113 @@ +/* blake2_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +func TestBlake2s_KnownAnswer(t *testing.T) { + var ctx Blake2s + if ret := Wc_InitBlake2s(&ctx, WC_BLAKE2S_256_DIGEST_SIZE); ret != 0 { + if ret == notCompiledIn { + t.Skip("Blake2 not compiled in") + } + t.Fatalf("InitBlake2s: %d", ret) + } + + in := []byte("abc") + if ret := Wc_Blake2sUpdate(&ctx, in, len(in)); ret != 0 { + t.Fatalf("Update: %d", ret) + } + out := make([]byte, WC_BLAKE2S_256_DIGEST_SIZE) + if ret := Wc_Blake2sFinal(&ctx, out, WC_BLAKE2S_256_DIGEST_SIZE); ret != 0 { + t.Fatalf("Final: %d", ret) + } + // Known Blake2s-256("abc"): + want := []byte{ + 0x50, 0x8c, 0x5e, 0x8c, 0x32, 0x7c, 0x14, 0xe2, + 0xe1, 0xa7, 0x2b, 0xa3, 0x4e, 0xeb, 0x45, 0x2f, + 0x37, 0x45, 0x8b, 0x20, 0x9e, 0xd6, 0x3a, 0x29, + 0x4d, 0x99, 0x9b, 0x4c, 0x86, 0x67, 0x59, 0x82, + } + if !bytes.Equal(out, want) { + t.Fatalf("Blake2s(\"abc\") mismatch:\n got: %x\n want: %x", out, want) + } +} + +// blake2sBuiltIn returns true if Blake2 is compiled into the linked wolfSSL. +func blake2sBuiltIn() bool { + var probe Blake2s + return Wc_InitBlake2s(&probe, WC_BLAKE2S_256_DIGEST_SIZE) == 0 +} + +// Blake2s_HMAC must produce non-zero output for valid inputs. +func TestBlake2s_HMAC_ProducesNonZeroOutput(t *testing.T) { + if !blake2sBuiltIn() { + t.Skip("Blake2 not compiled in") + } + out := make([]byte, WC_BLAKE2S_256_DIGEST_SIZE) + in := []byte("message to MAC") + key := []byte("secret key") + defer func() { + if r := recover(); r != nil { + t.Fatalf("Blake2s_HMAC panicked: %v", r) + } + }() + Wc_Blake2s_HMAC(out, in, key, WC_BLAKE2S_256_DIGEST_SIZE) + if bytes.Equal(out, make([]byte, WC_BLAKE2S_256_DIGEST_SIZE)) { + t.Fatal("Blake2s_HMAC returned all zeros") + } +} + +// Different keys must produce different MACs. +func TestBlake2s_HMAC_KeyDependent(t *testing.T) { + if !blake2sBuiltIn() { + t.Skip("Blake2 not compiled in") + } + in := []byte("same message") + out1 := make([]byte, WC_BLAKE2S_256_DIGEST_SIZE) + out2 := make([]byte, WC_BLAKE2S_256_DIGEST_SIZE) + defer func() { + if r := recover(); r != nil { + t.Fatalf("Blake2s_HMAC panicked: %v", r) + } + }() + Wc_Blake2s_HMAC(out1, in, []byte("key1"), WC_BLAKE2S_256_DIGEST_SIZE) + Wc_Blake2s_HMAC(out2, in, []byte("key2"), WC_BLAKE2S_256_DIGEST_SIZE) + if bytes.Equal(out1, out2) { + t.Fatal("different keys produced identical MACs") + } +} + +// Wc_Blake2s_HMAC with invalid outlen should not panic or write garbage. +func TestBlake2s_HMAC_InvalidOutlen_NoPanic(t *testing.T) { + out := make([]byte, WC_BLAKE2S_256_DIGEST_SIZE) + defer func() { + if r := recover(); r != nil { + t.Fatalf("invalid outlen should be handled, not panic: %v", r) + } + }() + // requestSz=0 should not produce useful output but must not panic. + Wc_Blake2s_HMAC(out, []byte("data"), []byte("key"), 0) +} diff --git a/boundary_test.go b/boundary_test.go new file mode 100644 index 0000000..2281af5 --- /dev/null +++ b/boundary_test.go @@ -0,0 +1,227 @@ +/* boundary_test.go — wrappers must reject empty/oversized slice arguments + * with a Go error rather than panic, since wc can't see Go slice lengths. + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "testing" +) + +// assertNoPanic runs fn and fails the test if fn panics. +func assertNoPanic(t *testing.T, name string, fn func()) { + t.Helper() + defer func() { + if r := recover(); r != nil { + t.Fatalf("%s: panicked instead of returning error: %v", name, r) + } + }() + fn() +} + +// Every public wrapper must return an error on bad inputs, never panic. +func TestWrapperNilSliceGuards(t *testing.T) { + t.Run("Sha256Hash_emptyOutput", func(t *testing.T) { + var ret int + assertNoPanic(t, "Wc_Sha256Hash", func() { + ret = Wc_Sha256Hash([]byte("data"), 4, []byte{}) + }) + if ret == 0 { + t.Fatal("empty output should error") + } + }) + + t.Run("Sha256Hash_oversizedInputSz", func(t *testing.T) { + var ret int + assertNoPanic(t, "Wc_Sha256Hash", func() { + ret = Wc_Sha256Hash([]byte{1}, 1<<20, make([]byte, WC_SHA256_DIGEST_SIZE)) + }) + if ret == 0 { + t.Fatal("oversized inputSz should error") + } + }) + + t.Run("HKDF_emptyOutput", func(t *testing.T) { + var ret int + assertNoPanic(t, "Wc_HKDF", func() { + ret = Wc_HKDF(WC_SHA256, []byte("ikm"), 3, nil, 0, nil, 0, []byte{}, 0) + }) + if ret == 0 { + t.Fatal("empty output should error") + } + }) + + t.Run("HKDF_oversizedOutSz", func(t *testing.T) { + var ret int + assertNoPanic(t, "Wc_HKDF", func() { + ret = Wc_HKDF(WC_SHA256, []byte("ikm"), 3, nil, 0, nil, 0, make([]byte, 8), 1<<20) + }) + if ret == 0 { + t.Fatal("oversized outSz should error") + } + }) + + t.Run("HmacSetKey_oversizedKeySz", func(t *testing.T) { + hmac := Wc_HmacAllocAligned() + if hmac == nil { + t.Skip("HMAC not compiled in") + } + defer Wc_HmacFreeAllocAligned(hmac) + switch ret := Wc_HmacInit(hmac, nil, INVALID_DEVID); ret { + case 0: + case notCompiledIn: + t.Skip("HMAC not compiled in") + default: + t.Fatalf("Wc_HmacInit: %d", ret) + } + defer Wc_HmacFree(hmac) + var ret int + assertNoPanic(t, "Wc_HmacSetKey", func() { + ret = Wc_HmacSetKey(hmac, WC_SHA256, []byte{1}, 1<<20) + }) + if ret == 0 { + t.Fatal("oversized keySz should error") + } + }) + + t.Run("HmacFinal_emptyOut", func(t *testing.T) { + hmac := Wc_HmacAllocAligned() + if hmac == nil { + t.Skip("HMAC not compiled in") + } + defer Wc_HmacFreeAllocAligned(hmac) + switch ret := Wc_HmacInit(hmac, nil, INVALID_DEVID); ret { + case 0: + case notCompiledIn: + t.Skip("HMAC not compiled in") + default: + t.Fatalf("Wc_HmacInit: %d", ret) + } + defer Wc_HmacFree(hmac) + Wc_HmacSetKey(hmac, WC_SHA256, []byte("k"), 1) + var ret int + assertNoPanic(t, "Wc_HmacFinal", func() { + ret = Wc_HmacFinal(hmac, []byte{}) + }) + if ret == 0 { + t.Fatal("empty out should error") + } + }) + + t.Run("AesCbcEncrypt_oversizedSz", func(t *testing.T) { + aes := Wc_AesAllocAligned() + if aes == nil { + t.Skip("AES not compiled in") + } + defer Wc_AesFreeAllocAligned(aes) + switch ret := Wc_AesInit(aes, nil, INVALID_DEVID); ret { + case 0: + case notCompiledIn: + t.Skip("AES not compiled in") + default: + t.Fatalf("Wc_AesInit: %d", ret) + } + defer Wc_AesFree(aes) + key := make([]byte, AES_256_KEY_SIZE) + iv := make([]byte, AES_IV_SIZE) + Wc_AesSetKey(aes, key, AES_256_KEY_SIZE, iv, AES_ENCRYPTION) + in := make([]byte, 16) + out := make([]byte, 16) + var ret int + assertNoPanic(t, "Wc_AesCbcEncrypt", func() { + ret = Wc_AesCbcEncrypt(aes, out, in, 1<<20) + }) + if ret == 0 { + t.Fatal("oversized sz should error") + } + }) + + t.Run("AesGcmAppendedTagDecrypt_shortCipher", func(t *testing.T) { + aes := Wc_AesAllocAligned() + if aes == nil { + t.Skip("AES not compiled in") + } + defer Wc_AesFreeAllocAligned(aes) + switch ret := Wc_AesInit(aes, nil, INVALID_DEVID); ret { + case 0: + case notCompiledIn: + t.Skip("AES not compiled in") + default: + t.Fatalf("Wc_AesInit: %d", ret) + } + defer Wc_AesFree(aes) + key := make([]byte, AES_256_KEY_SIZE) + Wc_AesGcmSetKey(aes, key, AES_256_KEY_SIZE) + var ret int + assertNoPanic(t, "Wc_AesGcm_Appended_Tag_Decrypt", func() { + ret = Wc_AesGcm_Appended_Tag_Decrypt(aes, make([]byte, 4), []byte{1, 2, 3}, make([]byte, AES_IV_SIZE), nil) + }) + if ret == 0 { + t.Fatal("short cipher should error") + } + }) + + t.Run("ChaChaPoly1305_AppendedTagDecrypt_shortCipher", func(t *testing.T) { + key := make([]byte, CHACHA20_POLY1305_AEAD_KEYSIZE) + iv := make([]byte, CHACHA20_POLY1305_AEAD_IV_SIZE) + var ret int + assertNoPanic(t, "Wc_ChaCha20Poly1305_Appended_Tag_Decrypt", func() { + ret = Wc_ChaCha20Poly1305_Appended_Tag_Decrypt(key, iv, nil, []byte{1, 2, 3}, make([]byte, 4)) + }) + if ret == 0 { + t.Fatal("short cipher should error") + } + }) + + t.Run("ConstantCompare_oversizedLength", func(t *testing.T) { + // ConstantCompare returns 0 (mismatch) on bad length — separate test + // asserts the value; here we verify no-panic only. + assertNoPanic(t, "ConstantCompare", func() { + ConstantCompare([]byte{1, 2}, []byte{1, 2}, 1<<20) + }) + }) + + t.Run("WolfSSL_X509_load_certificate_buffer_emptyBuff", func(t *testing.T) { + // Returns *WOLFSSL_X509 (not int); empty buf must yield nil, not panic. + var got *WOLFSSL_X509 + assertNoPanic(t, "WolfSSL_X509_load_certificate_buffer", func() { + got = WolfSSL_X509_load_certificate_buffer([]byte{}, 0, SSL_FILETYPE_PEM) + }) + if got != nil { + t.Fatal("empty buffer should return nil") + } + }) +} + +// ConstantCompare must reject attacker-controlled oversized/negative lengths. +func TestConstantCompare_AttackerLength_ReturnsZero(t *testing.T) { + a := []byte{0xaa, 0xbb} + b := []byte{0xaa, 0xbb} + if got := ConstantCompare(a, b, 999); got != 0 { + t.Fatalf("oversized length should return 0 (not match), got %d", got) + } + if got := ConstantCompare(a, b, -1); got != 0 { + t.Fatalf("negative length should return 0, got %d", got) + } + if got := ConstantCompare(a, b, 2); got != 1 { + t.Fatalf("equal-length match should return 1, got %d", got) + } +} diff --git a/chacha_poly_test.go b/chacha_poly_test.go new file mode 100644 index 0000000..8e8bb8a --- /dev/null +++ b/chacha_poly_test.go @@ -0,0 +1,198 @@ +/* chacha_poly_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +func chachaKeyAndIV() (key, iv []byte) { + key = make([]byte, CHACHA20_POLY1305_AEAD_KEYSIZE) + for i := range key { + key[i] = byte(i) + } + iv = make([]byte, CHACHA20_POLY1305_AEAD_IV_SIZE) + for i := range iv { + iv[i] = byte(i + 100) + } + return key, iv +} + +func TestChaCha20Poly1305_RoundTrip(t *testing.T) { + key, iv := chachaKeyAndIV() + aad := []byte("authenticated additional data") + plaintext := []byte("ChaCha20-Poly1305 round-trip test message") + + cipher := make([]byte, len(plaintext)) + tag := make([]byte, CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + + ret := Wc_ChaCha20Poly1305_Encrypt(key, iv, aad, plaintext, cipher, tag) + skipIfNotCompiledIn(t, ret, "ChaCha20-Poly1305") + if ret != 0 { + t.Fatalf("Encrypt returned %d", ret) + } + if bytes.Equal(cipher, plaintext) { + t.Fatal("ciphertext equals plaintext (encryption did nothing)") + } + + decrypted := make([]byte, len(plaintext)) + if ret := Wc_ChaCha20Poly1305_Decrypt(key, iv, aad, cipher, tag, decrypted); ret != 0 { + t.Fatalf("Decrypt returned %d", ret) + } + if !bytes.Equal(decrypted, plaintext) { + t.Fatalf("decrypted mismatch:\n got: %q\n want: %q", decrypted, plaintext) + } +} + +// Empty plaintext: wolfSSL accepts (tag-only) or rejects with BAD_FUNC_ARG +// depending on build options; both are valid contract-wise. +func TestChaCha20Poly1305_EmptyPlaintext_MatchesWcBehavior(t *testing.T) { + key, iv := chachaKeyAndIV() + cipher := []byte{} + tag := make([]byte, CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + ret := Wc_ChaCha20Poly1305_Encrypt(key, iv, nil, []byte{}, cipher, tag) + skipIfNotCompiledIn(t, ret, "ChaCha20-Poly1305") + if ret != 0 && ret != BAD_FUNC_ARG { + t.Fatalf("empty plaintext: want 0 or BAD_FUNC_ARG, got %d", ret) + } +} + +// Tampered ciphertext must fail Open (authenticity). +func TestChaCha20Poly1305_TamperedCiphertext_Fails(t *testing.T) { + key, iv := chachaKeyAndIV() + plaintext := []byte("important message") + cipher := make([]byte, len(plaintext)) + tag := make([]byte, CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + + ret := Wc_ChaCha20Poly1305_Encrypt(key, iv, nil, plaintext, cipher, tag) + skipIfNotCompiledIn(t, ret, "ChaCha20-Poly1305") + if ret != 0 { + t.Fatalf("Encrypt: %d", ret) + } + + // Flip a bit in the ciphertext. + cipher[0] ^= 0x01 + + decrypted := make([]byte, len(plaintext)) + if ret := Wc_ChaCha20Poly1305_Decrypt(key, iv, nil, cipher, tag, decrypted); ret == 0 { + t.Fatal("decrypting tampered ciphertext should fail authentication") + } +} + +// Tampered tag must fail Open. +func TestChaCha20Poly1305_TamperedTag_Fails(t *testing.T) { + key, iv := chachaKeyAndIV() + plaintext := []byte("important message") + cipher := make([]byte, len(plaintext)) + tag := make([]byte, CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + + ret := Wc_ChaCha20Poly1305_Encrypt(key, iv, nil, plaintext, cipher, tag) + skipIfNotCompiledIn(t, ret, "ChaCha20-Poly1305") + if ret != 0 { + t.Fatalf("Encrypt: %d", ret) + } + tag[0] ^= 0x01 + + decrypted := make([]byte, len(plaintext)) + if ret := Wc_ChaCha20Poly1305_Decrypt(key, iv, nil, cipher, tag, decrypted); ret == 0 { + t.Fatal("decrypting with tampered tag should fail authentication") + } +} + +// Wrong AAD must fail Open. +func TestChaCha20Poly1305_WrongAAD_Fails(t *testing.T) { + key, iv := chachaKeyAndIV() + plaintext := []byte("aad-bound message") + cipher := make([]byte, len(plaintext)) + tag := make([]byte, CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + + ret := Wc_ChaCha20Poly1305_Encrypt(key, iv, []byte("aad-A"), plaintext, cipher, tag) + skipIfNotCompiledIn(t, ret, "ChaCha20-Poly1305") + if ret != 0 { + t.Fatalf("Encrypt: %d", ret) + } + decrypted := make([]byte, len(plaintext)) + if ret := Wc_ChaCha20Poly1305_Decrypt(key, iv, []byte("aad-B"), cipher, tag, decrypted); ret == 0 { + t.Fatal("decrypting with wrong AAD should fail") + } +} + +// Short ciphertext (< tag size) for Appended_Tag_Decrypt must error, not panic. +func TestChaCha20Poly1305_AppendedTagDecrypt_ShortCipher(t *testing.T) { + key, iv := chachaKeyAndIV() + defer func() { + if r := recover(); r != nil { + t.Fatalf("short cipher should error, not panic: %v", r) + } + }() + out := make([]byte, 32) + short := []byte{1, 2, 3} // shorter than AUTHTAG_SIZE (16) + if ret := Wc_ChaCha20Poly1305_Appended_Tag_Decrypt(key, iv, nil, short, out); ret == 0 { + t.Fatal("expected error on short ciphertext") + } +} + +func TestChaCha20Poly1305_AppendedTag_RoundTrip(t *testing.T) { + key, iv := chachaKeyAndIV() + plaintext := []byte("appended-tag message") + + out, ret := Wc_ChaCha20Poly1305_Appended_Tag_Encrypt(key, iv, nil, plaintext, nil) + skipIfNotCompiledIn(t, ret, "ChaCha20-Poly1305") + if ret != 0 { + t.Fatalf("Appended_Tag_Encrypt: %d", ret) + } + if len(out) != len(plaintext)+CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE { + t.Fatalf("output length %d, want %d", len(out), len(plaintext)+CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + } + + decrypted := make([]byte, len(plaintext)) + if ret := Wc_ChaCha20Poly1305_Appended_Tag_Decrypt(key, iv, nil, out, decrypted); ret != 0 { + t.Fatalf("Appended_Tag_Decrypt: %d", ret) + } + if !bytes.Equal(decrypted, plaintext) { + t.Fatalf("round-trip mismatch: got %q, want %q", decrypted, plaintext) + } +} + +// XChaCha20-Poly1305 round-trip with non-empty plaintext. +func TestXChaCha20Poly1305_RoundTrip(t *testing.T) { + key := make([]byte, CHACHA20_POLY1305_AEAD_KEYSIZE) + for i := range key { + key[i] = byte(i) + } + iv := make([]byte, XCHACHA20_POLY1305_AEAD_NONCE_SIZE) + plaintext := []byte("XChaCha20-Poly1305 round-trip") + cipher := make([]byte, len(plaintext)+CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + ret := Wc_XChaCha20Poly1305_Encrypt(cipher, plaintext, nil, iv, key) + skipIfNotCompiledIn(t, ret, "XChaCha20-Poly1305") + if ret != 0 { + t.Fatalf("Encrypt: %d", ret) + } + out := make([]byte, len(plaintext)) + if ret := Wc_XChaCha20Poly1305_Decrypt(out, cipher, nil, iv, key); ret != 0 { + t.Fatalf("Decrypt: %d", ret) + } + if !bytes.Equal(out, plaintext) { + t.Fatalf("round-trip mismatch: got %q, want %q", out, plaintext) + } +} diff --git a/correctness_test.go b/correctness_test.go new file mode 100644 index 0000000..291ad3b --- /dev/null +++ b/correctness_test.go @@ -0,0 +1,312 @@ +/* correctness_test.go — differential tests asserting crypto invariants + * (different inputs/keys must produce different outputs). + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +// Different inputs must produce different SHA-256 digests. +func TestSha256_DifferentInputs_DifferentDigests(t *testing.T) { + a := make([]byte, WC_SHA256_DIGEST_SIZE) + b := make([]byte, WC_SHA256_DIGEST_SIZE) + ret := Wc_Sha256Hash([]byte("hello"), 5, a) + skipIfNotCompiledIn(t, ret, "SHA-256") + if ret != 0 { + t.Fatalf("Sha256 a: %d", ret) + } + if ret := Wc_Sha256Hash([]byte("world"), 5, b); ret != 0 { + t.Fatalf("Sha256 b: %d", ret) + } + if bytes.Equal(a, b) { + t.Fatal("different inputs produced identical digests") + } +} + +// HMAC must be key-dependent: same message + different key = different MAC. +func TestHmac_KeyDependent(t *testing.T) { + msg := []byte("same message") + mac1 := hmacOneShot(t, WC_SHA256, []byte("key-A"), msg, WC_SHA256_DIGEST_SIZE) + mac2 := hmacOneShot(t, WC_SHA256, []byte("key-B"), msg, WC_SHA256_DIGEST_SIZE) + if bytes.Equal(mac1, mac2) { + t.Fatal("different keys produced identical MACs (HMAC is supposed to be keyed)") + } +} + +// HMAC must be message-dependent: same key + different message = different MAC. +func TestHmac_MessageDependent(t *testing.T) { + key := []byte("the-key") + mac1 := hmacOneShot(t, WC_SHA256, key, []byte("message-A"), WC_SHA256_DIGEST_SIZE) + mac2 := hmacOneShot(t, WC_SHA256, key, []byte("message-B"), WC_SHA256_DIGEST_SIZE) + if bytes.Equal(mac1, mac2) { + t.Fatal("different messages produced identical MACs") + } +} + +// HKDF must produce different output for different inputs across all 3 params. +func TestHKDF_Differential(t *testing.T) { + first := true + out := func(ikm, salt, info []byte) []byte { + o := make([]byte, 32) + ret := Wc_HKDF(WC_SHA256, ikm, len(ikm), salt, len(salt), info, len(info), o, 32) + if first { + skipIfNotCompiledIn(t, ret, "HKDF") + first = false + } + if ret != 0 { + t.Fatalf("HKDF: %d", ret) + } + return o + } + base := out([]byte("ikm"), []byte("salt"), []byte("info")) + if bytes.Equal(base, out([]byte("IKM"), []byte("salt"), []byte("info"))) { + t.Fatal("HKDF: different IKM should give different output") + } + if bytes.Equal(base, out([]byte("ikm"), []byte("SALT"), []byte("info"))) { + t.Fatal("HKDF: different salt should give different output") + } + if bytes.Equal(base, out([]byte("ikm"), []byte("salt"), []byte("INFO"))) { + t.Fatal("HKDF: different info should give different output") + } +} + +// PBKDF2 differential across password/salt/iterations. +func TestPBKDF2_Differential(t *testing.T) { + first := true + derive := func(pwd, salt []byte, iter int) []byte { + o := make([]byte, 32) + ret := Wc_PBKDF2(o, pwd, len(pwd), salt, len(salt), iter, 32, WC_SHA256) + if first { + skipIfNotCompiledIn(t, ret, "PBKDF2") + first = false + } + if ret != 0 { + t.Fatalf("PBKDF2: %d", ret) + } + return o + } + base := derive([]byte("pwd"), []byte("salt-16-bytes!!!"), 1000) + if bytes.Equal(base, derive([]byte("PWD"), []byte("salt-16-bytes!!!"), 1000)) { + t.Fatal("PBKDF2: different password should give different key") + } + if bytes.Equal(base, derive([]byte("pwd"), []byte("SALT-16-bytes!!!"), 1000)) { + t.Fatal("PBKDF2: different salt should give different key") + } + if bytes.Equal(base, derive([]byte("pwd"), []byte("salt-16-bytes!!!"), 2000)) { + t.Fatal("PBKDF2: different iterations should give different key") + } +} + +// AES-CBC must produce different ciphertext for different keys. +func TestAesCbc_KeyDependent(t *testing.T) { + first := true + encrypt := func(key []byte) []byte { + aes := allocAes(t) + iv := bytes.Repeat([]byte{0x01}, AES_IV_SIZE) + if ret := Wc_AesSetKey(aes, key, AES_256_KEY_SIZE, iv, AES_ENCRYPTION); ret != 0 { + t.Fatalf("AesSetKey: %d", ret) + } + plain := bytes.Repeat([]byte("X"), 32) + out := make([]byte, 32) + ret := Wc_AesCbcEncrypt(aes, out, plain, 32) + if first { + skipIfNotCompiledIn(t, ret, "AES-CBC") + first = false + } + if ret != 0 { + t.Fatalf("AesCbcEncrypt: %d", ret) + } + return out + } + a := encrypt(bytes.Repeat([]byte{0xaa}, AES_256_KEY_SIZE)) + b := encrypt(bytes.Repeat([]byte{0xbb}, AES_256_KEY_SIZE)) + if bytes.Equal(a, b) { + t.Fatal("AES-CBC: different keys produced identical ciphertext") + } +} + +// AES-CBC must produce different ciphertext for different IVs (with same key). +func TestAesCbc_IVDependent(t *testing.T) { + first := true + encrypt := func(iv []byte) []byte { + aes := allocAes(t) + key := bytes.Repeat([]byte{0xcc}, AES_256_KEY_SIZE) + if ret := Wc_AesSetKey(aes, key, AES_256_KEY_SIZE, iv, AES_ENCRYPTION); ret != 0 { + t.Fatalf("AesSetKey: %d", ret) + } + plain := bytes.Repeat([]byte("Y"), 32) + out := make([]byte, 32) + ret := Wc_AesCbcEncrypt(aes, out, plain, 32) + if first { + skipIfNotCompiledIn(t, ret, "AES-CBC") + first = false + } + if ret != 0 { + t.Fatalf("AesCbcEncrypt: %d", ret) + } + return out + } + a := encrypt(bytes.Repeat([]byte{0x01}, AES_IV_SIZE)) + b := encrypt(bytes.Repeat([]byte{0x02}, AES_IV_SIZE)) + if bytes.Equal(a, b) { + t.Fatal("AES-CBC: different IVs produced identical ciphertext") + } +} + +// AES-GCM with wrong key must fail authentication. +func TestAesGcm_WrongKey_FailsAuth(t *testing.T) { + keyA := bytes.Repeat([]byte{0x11}, AES_256_KEY_SIZE) + keyB := bytes.Repeat([]byte{0x22}, AES_256_KEY_SIZE) + iv := bytes.Repeat([]byte{0x33}, AES_IV_SIZE) + plain := []byte("auth-bound message") + + encAes := allocAes(t) + ret := Wc_AesGcmSetKey(encAes, keyA, AES_256_KEY_SIZE) + skipIfNotCompiledIn(t, ret, "AES-GCM") + if ret != 0 { + t.Fatalf("SetKey A: %d", ret) + } + cipher := make([]byte, len(plain)) + tag := make([]byte, AES_BLOCK_SIZE) + if ret := Wc_AesGcmEncrypt(encAes, cipher, plain, iv, tag, nil); ret != 0 { + t.Fatalf("Encrypt A: %d", ret) + } + + decAes := allocAes(t) + if ret := Wc_AesGcmSetKey(decAes, keyB, AES_256_KEY_SIZE); ret != 0 { + t.Fatalf("SetKey B: %d", ret) + } + out := make([]byte, len(plain)) + if ret := Wc_AesGcmDecrypt(decAes, out, cipher, iv, tag, nil); ret == 0 { + t.Fatal("AES-GCM: decryption with wrong key should fail authentication") + } +} + +// AES-GCM with wrong IV must fail authentication. +func TestAesGcm_WrongIV_FailsAuth(t *testing.T) { + aes := allocAes(t) + key := bytes.Repeat([]byte{0x44}, AES_256_KEY_SIZE) + ret := Wc_AesGcmSetKey(aes, key, AES_256_KEY_SIZE) + skipIfNotCompiledIn(t, ret, "AES-GCM") + if ret != 0 { + t.Fatalf("SetKey: %d", ret) + } + plain := []byte("data") + cipher := make([]byte, len(plain)) + tag := make([]byte, AES_BLOCK_SIZE) + ivEnc := bytes.Repeat([]byte{0x55}, AES_IV_SIZE) + if ret := Wc_AesGcmEncrypt(aes, cipher, plain, ivEnc, tag, nil); ret != 0 { + t.Fatalf("Encrypt: %d", ret) + } + ivDec := bytes.Repeat([]byte{0x55}, AES_IV_SIZE) + ivDec[0] ^= 0x01 // flip a single bit + out := make([]byte, len(plain)) + if ret := Wc_AesGcmDecrypt(aes, out, cipher, ivDec, tag, nil); ret == 0 { + t.Fatal("AES-GCM: decryption with wrong IV should fail authentication") + } +} + +// ChaCha20-Poly1305 with wrong key must fail authentication. +func TestChaCha20Poly1305_WrongKey_FailsAuth(t *testing.T) { + _, iv := chachaKeyAndIV() + keyA := bytes.Repeat([]byte{0x77}, CHACHA20_POLY1305_AEAD_KEYSIZE) + keyB := bytes.Repeat([]byte{0x88}, CHACHA20_POLY1305_AEAD_KEYSIZE) + plain := []byte("data") + cipher := make([]byte, len(plain)) + tag := make([]byte, CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE) + ret := Wc_ChaCha20Poly1305_Encrypt(keyA, iv, nil, plain, cipher, tag) + skipIfNotCompiledIn(t, ret, "ChaCha20-Poly1305") + if ret != 0 { + t.Fatalf("Encrypt: %d", ret) + } + out := make([]byte, len(plain)) + if ret := Wc_ChaCha20Poly1305_Decrypt(keyB, iv, nil, cipher, tag, out); ret == 0 { + t.Fatal("ChaCha20Poly1305: wrong key should fail authentication") + } +} + +// Curve25519 ECDH: third-party key cannot derive the same shared secret. +func TestCurve25519_ECDH_ThirdPartyMismatch(t *testing.T) { + alice := makeCurve25519Key(t) + bob := makeCurve25519Key(t) + eve := makeCurve25519Key(t) + + aliceBob := make([]byte, curve25519KeySize) + aliceEve := make([]byte, curve25519KeySize) + if ret := Wc_curve25519_shared_secret(alice, bob, aliceBob); ret != 0 { + t.Fatalf("alice<->bob: %d", ret) + } + if ret := Wc_curve25519_shared_secret(alice, eve, aliceEve); ret != 0 { + t.Fatalf("alice<->eve: %d", ret) + } + if bytes.Equal(aliceBob, aliceEve) { + t.Fatal("Curve25519: shared secrets with different peers should differ") + } +} + +// ECC: signature from one key cannot be verified by another key. +func TestEcc_WrongKey_FailsVerify(t *testing.T) { + keyA := makeEccKey(t) + keyB := makeEccKey(t) + rng := newRng(t) + hash := bytes.Repeat([]byte{0x42}, WC_SHA256_DIGEST_SIZE) + + sig := make([]byte, ECC_MAX_SIG_SIZE) + sigLen := ECC_MAX_SIG_SIZE + if ret := Wc_ecc_sign_hash(hash, len(hash), sig, &sigLen, rng, keyA); ret != 0 { + t.Fatalf("sign with keyA: %d", ret) + } + + res := 0 + ret := Wc_ecc_verify_hash(sig[:sigLen], sigLen, hash, len(hash), &res, keyB) + if ret == 0 && res == 1 { + t.Fatal("ECC: signature verified under wrong key") + } +} + +// PBKDF2 output is non-zero (catches accidentally-no-op implementation). +func TestPBKDF2_OutputNonZero(t *testing.T) { + out := make([]byte, 32) + ret := Wc_PBKDF2(out, []byte("p"), 1, []byte("salt"), 4, 100, 32, WC_SHA256) + skipIfNotCompiledIn(t, ret, "PBKDF2") + if ret != 0 { + t.Fatalf("PBKDF2: %d", ret) + } + if bytes.Equal(out, make([]byte, 32)) { + t.Fatal("PBKDF2 output is all zeros") + } +} + +// SHA-256 output is non-zero for non-zero input. +func TestSha256_OutputNonZero(t *testing.T) { + out := make([]byte, WC_SHA256_DIGEST_SIZE) + ret := Wc_Sha256Hash([]byte("any non-empty"), 13, out) + skipIfNotCompiledIn(t, ret, "SHA-256") + if ret != 0 { + t.Fatalf("Sha256: %d", ret) + } + if bytes.Equal(out, make([]byte, WC_SHA256_DIGEST_SIZE)) { + t.Fatal("SHA-256 output is all zeros") + } +} diff --git a/curve25519.go b/curve25519.go index 7266247..51cc309 100644 --- a/curve25519.go +++ b/curve25519.go @@ -21,6 +21,7 @@ package wolfSSL +// #include // #include // #include // #include @@ -72,6 +73,31 @@ import ( type Curve25519_key = C.struct_curve25519_key +// Wc_Curve25519_AllocKey returns a zero-initialized curve25519_key on the C heap. +// wolfSSL populates pointer-typed fields (dp, heap) during init/make_key; in +// some configures (observed under --enable-tls13 --enable-curve25519) those +// values fall in Go's heap range and cgo's runtime pointer check panics with +// "Go pointer to unpinned Go pointer" if the struct lives on Go's heap. C-heap +// allocation avoids it. Pair with Wc_Curve25519_FreeKey. +func Wc_Curve25519_AllocKey() *C.struct_curve25519_key { + p := C.calloc(1, C.size_t(C.sizeof_struct_curve25519_key)) + if p == nil { + return nil + } + return (*C.struct_curve25519_key)(p) +} + +// Wc_Curve25519_FreeKey releases internal wolfSSL state (via wc_curve25519_free) +// and the C-heap allocation. Safe to call after Wc_curve25519_init returned an +// error or notCompiledIn (the key is still zeroed). +func Wc_Curve25519_FreeKey(key *C.struct_curve25519_key) { + if key == nil { + return + } + C.wc_curve25519_free(key) + C.free(unsafe.Pointer(key)) +} + func Wc_curve25519_init(key *C.struct_curve25519_key) int { return int(C.wc_curve25519_init(key)) } diff --git a/curve25519_test.go b/curve25519_test.go new file mode 100644 index 0000000..eaeb566 --- /dev/null +++ b/curve25519_test.go @@ -0,0 +1,79 @@ +/* curve25519_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +const curve25519KeySize = 32 + +func makeCurve25519Key(t *testing.T) *Curve25519_key { + t.Helper() + key := Wc_Curve25519_AllocKey() + if key == nil { + t.Fatal("Wc_Curve25519_AllocKey returned nil") + } + ret := Wc_curve25519_init(key) + if ret == notCompiledIn { + Wc_Curve25519_FreeKey(key) + t.Skip("curve25519 not compiled in") + } + if ret != 0 { + Wc_Curve25519_FreeKey(key) + t.Fatalf("Wc_curve25519_init: %d", ret) + } + t.Cleanup(func() { Wc_Curve25519_FreeKey(key) }) + + rng := newRng(t) + ret = Wc_curve25519_make_key(rng, curve25519KeySize, key) + if ret == notCompiledIn { + t.Skip("curve25519 not compiled in") + } + if ret != 0 { + t.Fatalf("Wc_curve25519_make_key: %d", ret) + } + return key +} + +// Both peers should derive the same shared secret (X25519 ECDH). +func TestCurve25519_ECDH_AgreedSecret(t *testing.T) { + alice := makeCurve25519Key(t) + bob := makeCurve25519Key(t) + + aliceSecret := make([]byte, curve25519KeySize) + bobSecret := make([]byte, curve25519KeySize) + + if ret := Wc_curve25519_shared_secret(alice, bob, aliceSecret); ret != 0 { + t.Fatalf("alice shared_secret: %d", ret) + } + if ret := Wc_curve25519_shared_secret(bob, alice, bobSecret); ret != 0 { + t.Fatalf("bob shared_secret: %d", ret) + } + if !bytes.Equal(aliceSecret, bobSecret) { + t.Fatalf("shared secrets differ:\n alice: %x\n bob: %x", aliceSecret, bobSecret) + } + if bytes.Equal(aliceSecret, make([]byte, curve25519KeySize)) { + t.Fatal("shared secret is all zeros") + } +} diff --git a/ecc.go b/ecc.go index 0f4ea86..2dae65c 100644 --- a/ecc.go +++ b/ecc.go @@ -115,6 +115,28 @@ type Ecc_key = C.struct_ecc_key const ECC_SECP256R1 = int(C.ECC_SECP256R1) +// Wc_Ecc_AllocKey returns a zero-initialized ecc_key on the C heap. C-heap +// allocation avoids the same cgo pointer-check hazard documented on +// Wc_Curve25519_AllocKey. Pair with Wc_Ecc_FreeKey for cleanup. +func Wc_Ecc_AllocKey() *C.struct_ecc_key { + p := C.calloc(1, C.size_t(C.sizeof_struct_ecc_key)) + if p == nil { + return nil + } + return (*C.struct_ecc_key)(p) +} + +// Wc_Ecc_FreeKey releases internal wolfSSL state (via wc_ecc_free) and the +// C-heap allocation. Safe to call after Wc_ecc_init returned an error or +// notCompiledIn (the key is still zeroed). +func Wc_Ecc_FreeKey(key *C.struct_ecc_key) { + if key == nil { + return + } + C.wc_ecc_free(key) + C.free(unsafe.Pointer(key)) +} + func Wc_ecc_init(key *C.struct_ecc_key) int { return int(C.wc_ecc_init(key)) } diff --git a/ecc_test.go b/ecc_test.go new file mode 100644 index 0000000..9531b07 --- /dev/null +++ b/ecc_test.go @@ -0,0 +1,188 @@ +/* ecc_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +const eccP256KeySize = 32 + +func makeEccKey(t *testing.T) *Ecc_key { + t.Helper() + key := Wc_Ecc_AllocKey() + if key == nil { + t.Fatal("Wc_Ecc_AllocKey returned nil") + } + ret := Wc_ecc_init(key) + if ret == notCompiledIn { + Wc_Ecc_FreeKey(key) + t.Skip("ECC not compiled in") + } + if ret != 0 { + Wc_Ecc_FreeKey(key) + t.Fatalf("Wc_ecc_init: %d", ret) + } + t.Cleanup(func() { Wc_Ecc_FreeKey(key) }) + + rng := newRng(t) + ret = Wc_ecc_make_key(rng, eccP256KeySize, key) + if ret == notCompiledIn { + t.Skip("ECC not compiled in") + } + if ret != 0 { + t.Fatalf("Wc_ecc_make_key: %d", ret) + } + return key +} + +func TestEcc_SignAndVerify_RoundTrip(t *testing.T) { + key := makeEccKey(t) + rng := newRng(t) + + hash := bytes.Repeat([]byte{0xab}, WC_SHA256_DIGEST_SIZE) + sig := make([]byte, ECC_MAX_SIG_SIZE) + sigLen := ECC_MAX_SIG_SIZE + if ret := Wc_ecc_sign_hash(hash, len(hash), sig, &sigLen, rng, key); ret != 0 { + t.Fatalf("Wc_ecc_sign_hash: %d", ret) + } + if sigLen <= 0 || sigLen > ECC_MAX_SIG_SIZE { + t.Fatalf("sigLen %d out of range", sigLen) + } + + res := 0 + if ret := Wc_ecc_verify_hash(sig[:sigLen], sigLen, hash, len(hash), &res, key); ret != 0 { + t.Fatalf("Wc_ecc_verify_hash: %d", ret) + } + if res != 1 { + t.Fatalf("verify result = %d, want 1 (valid)", res) + } +} + +// Tampered hash must fail verification. +func TestEcc_TamperedHash_FailsVerify(t *testing.T) { + key := makeEccKey(t) + rng := newRng(t) + + hash := bytes.Repeat([]byte{0xcd}, WC_SHA256_DIGEST_SIZE) + sig := make([]byte, ECC_MAX_SIG_SIZE) + sigLen := ECC_MAX_SIG_SIZE + if ret := Wc_ecc_sign_hash(hash, len(hash), sig, &sigLen, rng, key); ret != 0 { + t.Fatalf("Wc_ecc_sign_hash: %d", ret) + } + + hash[0] ^= 0x01 + res := 0 + // Verify must report failure as either a non-zero ret OR res == 0. + ret := Wc_ecc_verify_hash(sig[:sigLen], sigLen, hash, len(hash), &res, key) + if ret == 0 && res == 1 { + t.Fatal("tampered hash verified as valid") + } +} + +// Boundary: empty hash (which is meaningless for ECC sign) must error, not panic. +func TestEcc_SignHash_EmptyHash_NoPanic(t *testing.T) { + key := makeEccKey(t) + rng := newRng(t) + defer func() { + if r := recover(); r != nil { + t.Fatalf("empty hash should error, not panic: %v", r) + } + }() + sig := make([]byte, ECC_MAX_SIG_SIZE) + sigLen := ECC_MAX_SIG_SIZE + if ret := Wc_ecc_sign_hash([]byte{}, 0, sig, &sigLen, rng, key); ret == 0 { + t.Fatal("expected error for empty hash") + } +} + +// Boundary: nil outLen must error, not nil-deref panic. +func TestEcc_SignHash_NilOutLen_NoPanic(t *testing.T) { + key := makeEccKey(t) + rng := newRng(t) + defer func() { + if r := recover(); r != nil { + t.Fatalf("nil outLen should error, not panic: %v", r) + } + }() + hash := bytes.Repeat([]byte{0xab}, WC_SHA256_DIGEST_SIZE) + sig := make([]byte, ECC_MAX_SIG_SIZE) + if ret := Wc_ecc_sign_hash(hash, len(hash), sig, nil, rng, key); ret == 0 { + t.Fatal("expected error for nil outLen") + } +} + +// Wc_ecc_negate_private must actually change the private scalar. +func TestEcc_NegatePrivate(t *testing.T) { + key := makeEccKey(t) + + before := make([]byte, eccP256KeySize) + beforeLen := len(before) + if ret := Wc_ecc_export_private_only(key, before, &beforeLen); ret != 0 { + t.Fatalf("export pre-negate: %d", ret) + } + + ret := Wc_ecc_negate_private(key) + if ret == notCompiledIn { + t.Skip("Wc_ecc_negate_private not compiled in (requires WOLFSSL_PUBLIC_MP)") + } + if ret != 0 { + t.Fatalf("Wc_ecc_negate_private returned %d", ret) + } + + after := make([]byte, eccP256KeySize) + afterLen := len(after) + if ret := Wc_ecc_export_private_only(key, after, &afterLen); ret != 0 { + t.Fatalf("export post-negate: %d", ret) + } + if bytes.Equal(before[:beforeLen], after[:afterLen]) { + t.Fatal("negate_private did not change the private scalar") + } +} + +// Round-trip via export/import_x963. +func TestEcc_ExportImportX963_RoundTrip(t *testing.T) { + key := makeEccKey(t) + + pubBuf := make([]byte, 256) + pubLen := len(pubBuf) + if ret := Wc_ecc_export_x963_ex(key, pubBuf, &pubLen, 0); ret != 0 { + t.Fatalf("Wc_ecc_export_x963_ex: %d", ret) + } + if pubLen <= 0 || pubLen > len(pubBuf) { + t.Fatalf("pubLen %d out of range", pubLen) + } + + imported := Wc_Ecc_AllocKey() + if imported == nil { + t.Fatal("Wc_Ecc_AllocKey returned nil") + } + defer Wc_Ecc_FreeKey(imported) + if ret := Wc_ecc_init(imported); ret != 0 { + t.Fatalf("Wc_ecc_init: %d", ret) + } + + if ret := Wc_ecc_import_x963_ex(pubBuf[:pubLen], pubLen, imported, ECC_SECP256R1); ret != 0 { + t.Fatalf("Wc_ecc_import_x963_ex: %d", ret) + } +} diff --git a/go.mod b/go.mod index d758d80..7f85f96 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module github.com/wolfssl/go-wolfssl -go 1.13 +go 1.21 + +require golang.org/x/term v0.29.0 + +require golang.org/x/sys v0.30.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..234e8b5 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= diff --git a/hash_test.go b/hash_test.go new file mode 100644 index 0000000..e361e4f --- /dev/null +++ b/hash_test.go @@ -0,0 +1,144 @@ +/* hash_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "encoding/hex" + "testing" +) + +// Known-answer tests against RFC/FIPS published vectors. +func TestSha256Hash_KnownAnswers(t *testing.T) { + cases := []struct { + name string + input string + want string // hex digest + }{ + {"empty", "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"abc", "abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}, + {"long", "The quick brown fox jumps over the lazy dog", + "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + out := make([]byte, WC_SHA256_DIGEST_SIZE) + input := []byte(tc.input) + ret := Wc_Sha256Hash(input, len(input), out) + skipIfNotCompiledIn(t, ret, "SHA-256") + if ret != 0 { + t.Fatalf("Wc_Sha256Hash returned %d", ret) + } + got := hex.EncodeToString(out) + if got != tc.want { + t.Fatalf("digest mismatch:\n got: %s\n want: %s", got, tc.want) + } + }) + } +} + +func TestSha384Hash_EmptyInput(t *testing.T) { + out := make([]byte, WC_SHA384_DIGEST_SIZE) + ret := Wc_Sha384Hash(nil, 0, out) + if ret == notCompiledIn { + t.Skip("SHA-384 not compiled in") + } + if ret != 0 { + t.Fatalf("Wc_Sha384Hash returned %d", ret) + } + want := "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" + if got := hex.EncodeToString(out); got != want { + t.Fatalf("digest mismatch: got %s, want %s", got, want) + } +} + +func TestSha512Hash_EmptyInput(t *testing.T) { + out := make([]byte, WC_SHA512_DIGEST_SIZE) + ret := Wc_Sha512Hash(nil, 0, out) + if ret == notCompiledIn { + t.Skip("SHA-512 not compiled in") + } + if ret != 0 { + t.Fatalf("Wc_Sha512Hash returned %d", ret) + } + want := "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" + if got := hex.EncodeToString(out); got != want { + t.Fatalf("digest mismatch: got %s, want %s", got, want) + } +} + +// Boundary: output buffer too small must return error, not panic / corrupt memory. +func TestSha256Hash_ShortOutputBuffer(t *testing.T) { + out := make([]byte, WC_SHA256_DIGEST_SIZE-1) + defer func() { + if r := recover(); r != nil { + t.Fatalf("short output should error, not panic: %v", r) + } + }() + if ret := Wc_Sha256Hash([]byte("data"), 4, out); ret == 0 { + t.Fatal("expected error on undersized output buffer") + } +} + +// Boundary: inputSz larger than slice must return error, not let wc read past Go memory. +func TestSha256Hash_OversizedInputSz(t *testing.T) { + in := []byte("hi") + out := make([]byte, WC_SHA256_DIGEST_SIZE) + if ret := Wc_Sha256Hash(in, 1<<20, out); ret == 0 { + t.Fatal("expected error on inputSz > len(input)") + } +} + +// Streaming Sha256 should match one-shot. +func TestSha256_Streaming_MatchesOneShot(t *testing.T) { + chunks := []string{"part one", " part two", " final part"} + full := []byte("part one part two final part") + + oneShot := make([]byte, WC_SHA256_DIGEST_SIZE) + ret := Wc_Sha256Hash(full, len(full), oneShot) + skipIfNotCompiledIn(t, ret, "SHA-256") + if ret != 0 { + t.Fatalf("one-shot Sha256Hash returned %d", ret) + } + + var ctx Wc_Sha256 + ret = Wc_InitSha256_ex(&ctx, nil, INVALID_DEVID) + skipIfNotCompiledIn(t, ret, "SHA-256") + if ret != 0 { + t.Fatalf("InitSha256_ex returned %d", ret) + } + t.Cleanup(func() { Wc_Sha256Free(&ctx) }) + for _, c := range chunks { + b := []byte(c) + if ret := Wc_Sha256Update(&ctx, b, len(b)); ret != 0 { + t.Fatalf("Sha256Update returned %d", ret) + } + } + streaming := make([]byte, WC_SHA256_DIGEST_SIZE) + if ret := Wc_Sha256Final(&ctx, streaming); ret != 0 { + t.Fatalf("Sha256Final returned %d", ret) + } + + if !bytes.Equal(oneShot, streaming) { + t.Fatalf("streaming != one-shot:\n one: %x\n strm: %x", oneShot, streaming) + } +} diff --git a/hmac_test.go b/hmac_test.go new file mode 100644 index 0000000..61b44e0 --- /dev/null +++ b/hmac_test.go @@ -0,0 +1,192 @@ +/* hmac_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "encoding/hex" + "testing" +) + +// hmacOneShot computes HMAC-Hash and returns the digest. +func hmacOneShot(t *testing.T, hashType int, key, msg []byte, digestSize int) []byte { + t.Helper() + hmac := Wc_HmacAllocAligned() + if hmac == nil { + t.Skip("HMAC not compiled in") + } + defer Wc_HmacFreeAllocAligned(hmac) + ret := Wc_HmacInit(hmac, nil, INVALID_DEVID) + skipIfNotCompiledIn(t, ret, "HMAC") + if ret != 0 { + t.Fatalf("HmacInit returned %d", ret) + } + defer Wc_HmacFree(hmac) + ret = Wc_HmacSetKey(hmac, hashType, key, len(key)) + skipIfNotCompiledIn(t, ret, "HMAC hash type") + if ret != 0 { + t.Fatalf("HmacSetKey returned %d", ret) + } + if ret := Wc_HmacUpdate(hmac, msg, len(msg)); ret != 0 { + t.Fatalf("HmacUpdate returned %d", ret) + } + out := make([]byte, digestSize) + if ret := Wc_HmacFinal(hmac, out); ret != 0 { + t.Fatalf("HmacFinal returned %d", ret) + } + return out +} + +// RFC 4231 Test Case 1: HMAC-SHA256 with a digest-sized output buffer. +func TestHmacFinal_AllowsPerHashDigestSize(t *testing.T) { + key := bytes.Repeat([]byte{0x0b}, 20) + msg := []byte("Hi There") + want := "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" + + got := hmacOneShot(t, WC_SHA256, key, msg, WC_SHA256_DIGEST_SIZE) + if hex.EncodeToString(got) != want { + t.Fatalf("HMAC-SHA256 mismatch:\n got: %x\n want: %s", got, want) + } +} + +// HMAC-SHA-256 known answer (RFC 4231 test case 2). +func TestHmac_RFC4231_Vectors(t *testing.T) { + cases := []struct { + name string + key string + data string + wantSHA256 string + }{ + { + name: "case2_what_do_ya_want", + key: "Jefe", + data: "what do ya want for nothing?", + wantSHA256: "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := hmacOneShot(t, WC_SHA256, []byte(tc.key), []byte(tc.data), WC_SHA256_DIGEST_SIZE) + if hex.EncodeToString(got) != tc.wantSHA256 { + t.Fatalf("got %x, want %s", got, tc.wantSHA256) + } + }) + } +} + +// HmacFinal must accept an exactly-digest-sized buffer. +func TestHmacFinal_DoesNotRequireMaxDigestSize(t *testing.T) { + hmac := Wc_HmacAllocAligned() + defer Wc_HmacFreeAllocAligned(hmac) + if ret := Wc_HmacInit(hmac, nil, INVALID_DEVID); ret != 0 { + t.Fatalf("HmacInit: %d", ret) + } + defer Wc_HmacFree(hmac) + key := []byte("test-key") + if ret := Wc_HmacSetKey(hmac, WC_SHA256, key, len(key)); ret != 0 { + t.Fatalf("HmacSetKey: %d", ret) + } + if ret := Wc_HmacUpdate(hmac, []byte("data"), 4); ret != 0 { + t.Fatalf("HmacUpdate: %d", ret) + } + // Exactly digest-sized buffer for SHA-256 (32 bytes) — should succeed. + out := make([]byte, WC_SHA256_DIGEST_SIZE) + if ret := Wc_HmacFinal(hmac, out); ret != 0 { + t.Fatalf("HmacFinal with digest-sized buffer should succeed, got %d", ret) + } +} + +// HmacFinal with empty output should not panic. +func TestHmacFinal_EmptyOutput_DoesNotPanic(t *testing.T) { + hmac := Wc_HmacAllocAligned() + defer Wc_HmacFreeAllocAligned(hmac) + if ret := Wc_HmacInit(hmac, nil, INVALID_DEVID); ret != 0 { + t.Fatalf("HmacInit: %d", ret) + } + defer Wc_HmacFree(hmac) + if ret := Wc_HmacSetKey(hmac, WC_SHA256, []byte("k"), 1); ret != 0 { + t.Fatalf("HmacSetKey: %d", ret) + } + defer func() { + if r := recover(); r != nil { + t.Fatalf("empty out should error, not panic: %v", r) + } + }() + if ret := Wc_HmacFinal(hmac, []byte{}); ret == 0 { + t.Fatal("expected error on empty output, got success") + } +} + +// HKDF must accept an empty IKM (RFC 5869; Noise protocol Split relies on it). +func TestHKDF_EmptyIKM_RFC5869(t *testing.T) { + salt := []byte("not-empty-salt") + info := []byte("context") + out := make([]byte, 32) + + ret := Wc_HKDF(WC_SHA256, nil, 0, salt, len(salt), info, len(info), out, len(out)) + skipIfNotCompiledIn(t, ret, "HKDF") + if ret != 0 { + t.Fatalf("HKDF with empty IKM should succeed per RFC 5869, got %d", ret) + } + // Output should be deterministic and non-zero. + if bytes.Equal(out, make([]byte, 32)) { + t.Fatal("HKDF returned all zeros — likely no output produced") + } +} + +// HKDF RFC 5869 Test Case 1: SHA-256 standard. +func TestHKDF_RFC5869_TestCase1(t *testing.T) { + ikm, _ := hex.DecodeString("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b") + salt, _ := hex.DecodeString("000102030405060708090a0b0c") + info, _ := hex.DecodeString("f0f1f2f3f4f5f6f7f8f9") + wantOKM := "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" + + out := make([]byte, 42) + ret := Wc_HKDF(WC_SHA256, ikm, len(ikm), salt, len(salt), info, len(info), out, len(out)) + skipIfNotCompiledIn(t, ret, "HKDF") + if ret != 0 { + t.Fatalf("HKDF returned %d", ret) + } + if hex.EncodeToString(out) != wantOKM { + t.Fatalf("HKDF OKM mismatch:\n got: %x\n want: %s", out, wantOKM) + } +} + +// HKDF with empty salt is also valid per RFC 5869. +func TestHKDF_EmptySalt_OK(t *testing.T) { + ikm := bytes.Repeat([]byte{0xaa}, 22) + out := make([]byte, 32) + ret := Wc_HKDF(WC_SHA256, ikm, len(ikm), nil, 0, nil, 0, out, len(out)) + skipIfNotCompiledIn(t, ret, "HKDF") + if ret != 0 { + t.Fatalf("HKDF with empty salt+info should succeed, got %d", ret) + } +} + +// Boundary: oversized inputKeySz must not let wc read past Go slice. +func TestHKDF_OversizedInputKeySz_Rejected(t *testing.T) { + ikm := []byte("short") + out := make([]byte, 32) + if ret := Wc_HKDF(WC_SHA256, ikm, 1<<20, nil, 0, nil, 0, out, len(out)); ret == 0 { + t.Fatal("expected error on inputKeySz > len(inputKey)") + } +} diff --git a/misc.go b/misc.go index 504e8d5..2f918fe 100644 --- a/misc.go +++ b/misc.go @@ -29,6 +29,7 @@ import "unsafe" const BAD_FUNC_ARG = int(C.BAD_FUNC_ARG) const LENGTH_ONLY_E = int(C.LENGTH_ONLY_E) +const NOT_COMPILED_IN = int(C.NOT_COMPILED_IN) func ConstantCompare(a, b []byte, length int) int { if length < 0 || length > len(a) || length > len(b) { return 0 } diff --git a/misc_test.go b/misc_test.go new file mode 100644 index 0000000..114e7e2 --- /dev/null +++ b/misc_test.go @@ -0,0 +1,83 @@ +/* misc_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +func TestConstantCompare_Match(t *testing.T) { + a := []byte{0x01, 0x02, 0x03} + b := []byte{0x01, 0x02, 0x03} + if got := ConstantCompare(a, b, 3); got != 1 { + t.Fatalf("equal slices: got %d, want 1", got) + } +} + +func TestConstantCompare_Mismatch(t *testing.T) { + a := []byte{0x01, 0x02, 0x03} + b := []byte{0x01, 0x02, 0x04} + if got := ConstantCompare(a, b, 3); got != 0 { + t.Fatalf("different slices: got %d, want 0", got) + } +} + +func TestConstantCompare_PartialLength(t *testing.T) { + // Equal in first 2 bytes, differ in 3rd. + a := []byte{0x01, 0x02, 0x03} + b := []byte{0x01, 0x02, 0x04} + if got := ConstantCompare(a, b, 2); got != 1 { + t.Fatalf("equal-prefix length=2: got %d, want 1", got) + } +} + +// zeroMemory must zero the slice and must not crash on empty input. +func TestZeroMemory_ZerosSlice(t *testing.T) { + buf := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88} + zeroMemory(buf) + for i, b := range buf { + if b != 0 { + t.Fatalf("byte %d not zeroed: %#x", i, b) + } + } +} + +func TestZeroMemory_EmptySlice_NoPanic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("zeroMemory on empty slice should not panic: %v", r) + } + }() + zeroMemory(nil) + zeroMemory([]byte{}) +} + +func TestZeroMemory_LargeSlice(t *testing.T) { + buf := bytes.Repeat([]byte{0xff}, 4096) + zeroMemory(buf) + for i := range buf { + if buf[i] != 0 { + t.Fatalf("byte %d not zeroed in large buffer", i) + } + } +} diff --git a/random_test.go b/random_test.go new file mode 100644 index 0000000..996c161 --- /dev/null +++ b/random_test.go @@ -0,0 +1,89 @@ +/* random_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +func newRng(t *testing.T) *WC_RNG { + t.Helper() + var rng WC_RNG + ret := Wc_InitRng(&rng) + skipIfNotCompiledIn(t, ret, "RNG") + if ret != 0 { + t.Fatalf("Wc_InitRng returned %d", ret) + } + t.Cleanup(func() { Wc_FreeRng(&rng) }) + return &rng +} + +func TestRNG_GeneratesNonZero(t *testing.T) { + rng := newRng(t) + buf := make([]byte, 64) + if ret := Wc_RNG_GenerateBlock(rng, buf, len(buf)); ret != 0 { + t.Fatalf("Wc_RNG_GenerateBlock returned %d", ret) + } + if bytes.Equal(buf, make([]byte, 64)) { + t.Fatal("RNG returned all zeros — implausible (1 in 2^512)") + } +} + +// Two consecutive RNG draws should not match. +func TestRNG_NotRepeating(t *testing.T) { + rng := newRng(t) + a := make([]byte, 32) + b := make([]byte, 32) + if ret := Wc_RNG_GenerateBlock(rng, a, len(a)); ret != 0 { + t.Fatalf("first GenerateBlock: %d", ret) + } + if ret := Wc_RNG_GenerateBlock(rng, b, len(b)); ret != 0 { + t.Fatalf("second GenerateBlock: %d", ret) + } + // Collision probability for two 32-byte draws: 2^-256. + if bytes.Equal(a, b) { + t.Fatal("two RNG draws returned identical 32 bytes — implausible") + } +} + +// sz=0 should return 0 (not error, not panic). +func TestRNG_ZeroSize_ReturnsZero(t *testing.T) { + rng := newRng(t) + defer func() { + if r := recover(); r != nil { + t.Fatalf("sz=0 should not panic: %v", r) + } + }() + if ret := Wc_RNG_GenerateBlock(rng, make([]byte, 4), 0); ret != 0 { + t.Fatalf("expected ret=0 for sz=0, got %d", ret) + } +} + +// Boundary: sz > len(b) must be caught at wrapper (wc can't see Go slice len). +func TestRNG_OversizedSz_Rejected(t *testing.T) { + rng := newRng(t) + b := make([]byte, 16) + if ret := Wc_RNG_GenerateBlock(rng, b, 1<<20); ret == 0 { + t.Fatal("expected error for sz > len(b)") + } +} diff --git a/ssl.go b/ssl.go index bc7959e..7cb0cfb 100644 --- a/ssl.go +++ b/ssl.go @@ -110,6 +110,28 @@ package wolfSSL // return 0; // } // #endif +// #ifndef HAVE_ALPN +// int wolfSSL_UseALPN(WOLFSSL* ssl, char* protocol_name_list, +// unsigned int protocol_name_listSz, unsigned char options) { +// (void)ssl; (void)protocol_name_list; +// (void)protocol_name_listSz; (void)options; +// return NOT_COMPILED_IN; +// } +// int wolfSSL_ALPN_GetProtocol(WOLFSSL* ssl, char** protocol_name, +// unsigned short* size) { +// (void)ssl; (void)protocol_name; (void)size; +// return NOT_COMPILED_IN; +// } +// int wolfSSL_ALPN_GetPeerProtocol(WOLFSSL* ssl, char** list, +// unsigned short* listSz) { +// (void)ssl; (void)list; (void)listSz; +// return NOT_COMPILED_IN; +// } +// int wolfSSL_ALPN_FreePeerProtocol(WOLFSSL* ssl, char** list) { +// (void)ssl; (void)list; +// return NOT_COMPILED_IN; +// } +// #endif import "C" import ( "unsafe" diff --git a/ssl_test.go b/ssl_test.go new file mode 100644 index 0000000..fb93187 --- /dev/null +++ b/ssl_test.go @@ -0,0 +1,59 @@ +/* ssl_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "strings" + "testing" +) + +// ERR_error_string must accept nil data and return a non-empty string. +func TestERR_ErrorString_NoPanicOnEmptyData(t *testing.T) { + got := WolfSSL_ERR_error_string(BAD_FUNC_ARG, nil) + if got == "" { + t.Fatal("expected non-empty error string for BAD_FUNC_ARG") + } +} + +// Same with an empty (non-nil) slice. +func TestERR_ErrorString_NoPanicOnEmptySlice(t *testing.T) { + WolfSSL_ERR_error_string(0, []byte{}) +} + +// ERR_error_string for a known code must return a printable string. +func TestERR_ErrorString_NonEmptyForKnownCode(t *testing.T) { + got := WolfSSL_ERR_error_string(BAD_FUNC_ARG, nil) + if len(got) < 4 { + t.Fatalf("error string too short: %q", got) + } + if strings.Contains(got, "\x00") { + t.Fatalf("error string contains nul: %q", got) + } +} + +// lib_version should return a non-empty version string. +func TestWolfSSL_LibVersion(t *testing.T) { + v := WolfSSL_lib_version() + if v == "" { + t.Fatal("WolfSSL_lib_version returned empty string") + } +} diff --git a/testing_helpers_test.go b/testing_helpers_test.go new file mode 100644 index 0000000..3b56e0b --- /dev/null +++ b/testing_helpers_test.go @@ -0,0 +1,26 @@ +/* testing_helpers_test.go — small shared helpers for tests. + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +package wolfSSL + +import "testing" + +// notCompiledIn aliases the package-level NOT_COMPILED_IN for in-package tests. +const notCompiledIn = NOT_COMPILED_IN + +// skipIfNotCompiledIn skips the test when ret signals a missing wolfSSL feature. +func skipIfNotCompiledIn(t *testing.T, ret int, feature string) { + t.Helper() + if ret == notCompiledIn { + t.Skipf("%s not compiled in", feature) + } +} diff --git a/wolftls/conn.go b/wolftls/conn.go index 84e5015..7c444a4 100644 --- a/wolftls/conn.go +++ b/wolftls/conn.go @@ -39,6 +39,10 @@ import ( wolfSSL "github.com/wolfssl/go-wolfssl" ) +// ErrNotCompiledIn is returned (wrapped) when a feature wolftls tried to use +// is not compiled into the linked wolfSSL library. +var ErrNotCompiledIn = errors.New("wolftls: feature not compiled in wolfSSL") + // recordingConn wraps the underlying net.Conn so wolftls.Conn.Read can // recover the original Go-side Read error after wolfSSL has reduced it to // an error code. net/http's hijack-via-SetReadDeadline path expects Read to @@ -360,6 +364,9 @@ func (c *Conn) doHandshake() error { wolfSSL.WOLFSSL_ALPN_FAILED_ON_MISMATCH) if ret != wolfSSL.WOLFSSL_SUCCESS { c.freeSSL() + if ret == wolfSSL.NOT_COMPILED_IN { + return fmt.Errorf("wolftls: failed to set ALPN: %w", ErrNotCompiledIn) + } return fmt.Errorf("wolftls: failed to set ALPN (%d)", ret) } } diff --git a/wolftls/tls_test.go b/wolftls/tls_test.go index 449f180..195c1fb 100644 --- a/wolftls/tls_test.go +++ b/wolftls/tls_test.go @@ -24,6 +24,7 @@ package wolftls import ( "bytes" "context" + "errors" "fmt" "io" "math/rand" @@ -843,6 +844,9 @@ func TestALPN(t *testing.T) { defer tlsConn.Close() if err := tlsConn.Handshake(); err != nil { + if errors.Is(err, ErrNotCompiledIn) { + t.Skip("ALPN not compiled in") + } t.Fatalf("handshake: %v", err) } diff --git a/wolfx509/certgen_test.go b/wolfx509/certgen_test.go index 7464040..bd9c97e 100644 --- a/wolfx509/certgen_test.go +++ b/wolfx509/certgen_test.go @@ -43,6 +43,7 @@ func TestCreateSelfSignedBasic(t *testing.T) { } der, err := CreateCertificate(tmpl, tmpl, k, k) if err != nil { + skipIfCertGenMissing(t, err) t.Fatalf("CreateCertificate: %v", err) } if len(der) == 0 { @@ -91,6 +92,7 @@ func TestCreateSelfSignedWithSANs(t *testing.T) { } der, err := CreateCertificate(tmpl, tmpl, k, k) if err != nil { + skipIfCertGenMissing(t, err) t.Fatalf("CreateCertificate: %v", err) } @@ -169,6 +171,7 @@ func TestCreateCASignedCert(t *testing.T) { } caDER, err := CreateCertificate(caTmpl, caTmpl, caKey, caKey) if err != nil { + skipIfCertGenMissing(t, err) t.Fatalf("CreateCertificate (CA): %v", err) } caCert, err := ParseCertificate(caDER) @@ -195,6 +198,7 @@ func TestCreateCASignedCert(t *testing.T) { } leafDER, err := CreateCertificate(leafTmpl, caCert, leafKey, caKey) if err != nil { + skipIfCertGenMissing(t, err) t.Fatalf("CreateCertificate (CA-signed leaf): %v", err) } leafCert, err := ParseCertificate(leafDER) @@ -236,6 +240,7 @@ func TestCreateCertificateRequest(t *testing.T) { } der, err := CreateCertificateRequest(tmpl, k) if err != nil { + skipIfCertGenMissing(t, err) t.Fatalf("CreateCertificateRequest: %v", err) } if len(der) == 0 { diff --git a/wolfx509/certgen_wolfcrypt.go b/wolfx509/certgen_wolfcrypt.go index 2c8f366..5936154 100644 --- a/wolfx509/certgen_wolfcrypt.go +++ b/wolfx509/certgen_wolfcrypt.go @@ -42,14 +42,37 @@ package wolfx509 // static int wc_MakeCertReq(Cert* cert, byte* derBuffer, word32 derSz, // RsaKey* rsaKey, ecc_key* eccKey) { // (void)cert; (void)derBuffer; (void)derSz; (void)rsaKey; (void)eccKey; -// return -174; +// return NOT_COMPILED_IN; // } // #endif // #ifndef WOLFSSL_ACME_OID -// static int wc_SetAcmeIdentifierExt(Cert* cert, const char* keyAuth, -// int keyAuthSz) { +// static int wc_SetAcmeIdentifierExt(Cert* cert, const byte* keyAuth, +// word32 keyAuthSz) { // (void)cert; (void)keyAuth; (void)keyAuthSz; -// return -174; +// return NOT_COMPILED_IN; +// } +// #endif +// /* The wolfSSL header declares these unconditionally but the implementations +// * in src/asn.c are gated on WOLFSSL_CERT_GEN. Provide stubs so wolfx509 +// * links cleanly against builds without --enable-certgen. */ +// #ifndef WOLFSSL_CERT_GEN +// int wc_InitCert(Cert* cert) { +// (void)cert; return NOT_COMPILED_IN; +// } +// int wc_MakeCert(Cert* cert, byte* derBuffer, word32 derSz, +// RsaKey* rsaKey, ecc_key* eccKey, WC_RNG* rng) { +// (void)cert; (void)derBuffer; (void)derSz; +// (void)rsaKey; (void)eccKey; (void)rng; +// return NOT_COMPILED_IN; +// } +// int wc_SignCert(int requestSz, int sType, byte* buf, word32 buffSz, +// RsaKey* rsaKey, ecc_key* eccKey, WC_RNG* rng) { +// (void)requestSz; (void)sType; (void)buf; (void)buffSz; +// (void)rsaKey; (void)eccKey; (void)rng; +// return NOT_COMPILED_IN; +// } +// int wc_SetIssuerBuffer(Cert* cert, const byte* der, int derSz) { +// (void)cert; (void)der; (void)derSz; return NOT_COMPILED_IN; // } // #endif import "C" @@ -65,6 +88,11 @@ import ( // TODO: Remove "encoding/asn1" and replace with wolfSSL ASN parsing +// ErrNotCompiledIn is returned by CreateCertificate / CreateCertificateRequest +// when wolfSSL was built without --enable-certgen (or related cert-handling +// features), so the underlying wolfCrypt symbols return NOT_COMPILED_IN. +var ErrNotCompiledIn = errors.New("wolfx509: cert generation not compiled in") + // Key usage bits (RFC 5280) in wolfCrypt's KEYUSE_* layout. These are // the values consumed by wc_MakeCert; translateKeyUsage maps from the // public wolfx509.KeyUsage constants (which mirror crypto/x509's bit @@ -170,6 +198,9 @@ func buildAndSignCert(opts certBuildOpts, parentDER []byte, pubKey, signerKey Ke var cert C.Cert if ret := int(C.wc_InitCert(&cert)); ret != 0 { + if ret == int(C.NOT_COMPILED_IN) { + return nil, ErrNotCompiledIn + } return nil, fmt.Errorf("wolfCrypt: wc_InitCert: %d", ret) } cert.version = 2 @@ -209,6 +240,9 @@ func buildAndSignCert(opts certBuildOpts, parentDER []byte, pubKey, signerKey Ke if len(parentDER) > 0 { if ret := int(C.wc_SetIssuerBuffer(&cert, (*C.byte)(unsafe.Pointer(&parentDER[0])), C.int(len(parentDER)))); ret != 0 { + if ret == int(C.NOT_COMPILED_IN) { + return nil, ErrNotCompiledIn + } return nil, fmt.Errorf("wolfCrypt: wc_SetIssuerBuffer: %d", ret) } } @@ -217,6 +251,9 @@ func buildAndSignCert(opts certBuildOpts, parentDER []byte, pubKey, signerKey Ke if ret := int(C.wc_SetAcmeIdentifierExt(&cert, (*C.byte)(unsafe.Pointer(&opts.AcmeKeyAuth[0])), C.word32(len(opts.AcmeKeyAuth)))); ret != 0 { + if ret == int(C.NOT_COMPILED_IN) { + return nil, ErrNotCompiledIn + } return nil, fmt.Errorf("wolfCrypt: wc_SetAcmeIdentifierExt: %d", ret) } } @@ -238,6 +275,9 @@ func buildAndSignCert(opts certBuildOpts, parentDER []byte, pubKey, signerKey Ke bodySz = int(C.wc_MakeCert(&cert, derPtr, derCap, nil, pubEcc, pubRng)) } if bodySz < 0 { + if bodySz == int(C.NOT_COMPILED_IN) { + return nil, ErrNotCompiledIn + } return nil, fmt.Errorf("wolfCrypt: body build failed: %d", bodySz) } signedSz = int(C.wc_SignCert(cert.bodySz, cert.sigType, derPtr, derCap, @@ -247,6 +287,9 @@ func buildAndSignCert(opts certBuildOpts, parentDER []byte, pubKey, signerKey Ke pubKey.Algorithm()) } if signedSz < 0 { + if signedSz == int(C.NOT_COMPILED_IN) { + return nil, ErrNotCompiledIn + } return nil, fmt.Errorf("wolfCrypt: wc_SignCert: %d", signedSz) } return derOut[:signedSz], nil diff --git a/wolfx509/name.go b/wolfx509/name.go index 267daf9..3c9f221 100644 --- a/wolfx509/name.go +++ b/wolfx509/name.go @@ -22,7 +22,6 @@ package wolfx509 import ( - "bytes" "strings" wolfSSL "github.com/wolfssl/go-wolfssl" @@ -75,12 +74,3 @@ func extractCNFromOneline(oneline string) string { } return rest } - -// equalName reports whether two Names have the same oneline representation. -func equalName(a, b Name) bool { - return a.oneline == b.oneline -} - -// equalRaw reports whether two raw byte slices are equal, used for -// RawSubject/RawIssuer comparison. -func equalRaw(a, b []byte) bool { return bytes.Equal(a, b) } diff --git a/wolfx509/parsecerts_test.go b/wolfx509/parsecerts_test.go index 3dde255..bc642d3 100644 --- a/wolfx509/parsecerts_test.go +++ b/wolfx509/parsecerts_test.go @@ -83,6 +83,7 @@ func TestPublicECCRawXY_RoundTripWithHandles(t *testing.T) { } der, err := CreateCertificate(tmpl, tmpl, k, k) if err != nil { + skipIfCertGenMissing(t, err) t.Fatalf("CreateCertificate: %v", err) } parsed, err := ParseCertificate(der) @@ -122,6 +123,7 @@ func mintCert(t *testing.T, cn string) []byte { } der, err := CreateCertificate(tmpl, tmpl, k, k) if err != nil { + skipIfCertGenMissing(t, err) t.Fatalf("CreateCertificate(%q): %v", cn, err) } return der diff --git a/wolfx509/skip_test.go b/wolfx509/skip_test.go new file mode 100644 index 0000000..2b08d4d --- /dev/null +++ b/wolfx509/skip_test.go @@ -0,0 +1,23 @@ +/* skip_test.go — shared skip helper for builds without --enable-certgen. + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + */ + +package wolfx509 + +import ( + "errors" + "testing" +) + +// skipIfCertGenMissing skips when err is the sentinel returned by +// CreateCertificate / CreateCertificateRequest on builds without +// WOLFSSL_CERT_GEN. +func skipIfCertGenMissing(t *testing.T, err error) { + t.Helper() + if errors.Is(err, ErrNotCompiledIn) { + t.Skip("WOLFSSL_CERT_GEN not compiled in") + } +} diff --git a/x509.go b/x509.go index c535b15..872750a 100644 --- a/x509.go +++ b/x509.go @@ -61,6 +61,15 @@ package wolfSSL // static WOLFSSL_ASN1_OBJECT* wolfSSL_OBJ_txt2obj(const char* s, int no_name) { return NULL; } // static int wolfSSL_OBJ_cmp(const WOLFSSL_ASN1_OBJECT* a, const WOLFSSL_ASN1_OBJECT* b) { return -174; } // #endif +// /* When OPENSSL_ALL is on the header declares wolfSSL_OBJ_txt2obj, but the +// * implementation in src/ssl.c is gated by WOLFSSL_CERT_EXT && WOLFSSL_CERT_GEN. +// * Provide a fallback so configs without certext/certgen still link. */ +// #if defined(OPENSSL_ALL) && (!defined(WOLFSSL_CERT_EXT) || !defined(WOLFSSL_CERT_GEN)) +// WOLFSSL_ASN1_OBJECT* wolfSSL_OBJ_txt2obj(const char* s, int no_name) { +// (void)s; (void)no_name; +// return NULL; /* OBJ_txt2obj returns a pointer; NULL is its error sentinel. */ +// } +// #endif // /* Helper to fetch the ASN1_TIME-printed string for NotBefore/NotAfter via // * a temporary BIO. Returns length written (<= outSz) or 0 on error. */ // #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) diff --git a/x509_test.go b/x509_test.go new file mode 100644 index 0000000..14a63f8 --- /dev/null +++ b/x509_test.go @@ -0,0 +1,126 @@ +/* x509_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "testing" +) + +// BIO must accept a Go slice (wrapper copies to C heap) without cgo violation. +func TestBIO_NewMemBuf_NoCgoViolation(t *testing.T) { + buf := []byte("dummy buffer content for BIO test") + bio := WolfSSL_BIO_new_mem_buf(buf, len(buf)) + if bio == nil { + t.Skip("BIO_new_mem_buf returned NULL (likely build without BIO support)") + } + if ret := WolfSSL_BIO_free(bio); ret < 0 { + t.Fatalf("BIO_free returned %d", ret) + } +} + +// Free-on-nil must be a no-op (matches OpenSSL/wolfSSL convention). +func TestBIO_FreeNil_DoesNotCrash(t *testing.T) { + WolfSSL_BIO_free(nil) +} + +// ASN1_get_object on a Go slice must not trigger cgo's pointer-rule violation. +func TestASN1_GetObject_NoCgoViolation(t *testing.T) { + der := []byte{0x30, 0x03, 0x02, 0x01, 0x05} + in := der + var objLen, tag, cls int + WolfSSL_ASN1_get_object(&in, &objLen, &tag, &cls, len(der)) +} + +// Pre-cgo bounds check: empty / oversized / negative inLen must not panic. +func TestASN1_GetObject_BoundaryInputs_NoPanic(t *testing.T) { + cases := []struct { + name string + in []byte + inLen int + }{ + {"empty", []byte{}, 0}, + {"oversized inLen", []byte{0x30, 0x00}, 1 << 20}, + {"negative inLen", []byte{0x30, 0x00}, -1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("input %q panicked: %v", tc.name, r) + } + }() + in := tc.in + var objLen, tag, cls int + WolfSSL_ASN1_get_object(&in, &objLen, &tag, &cls, tc.inLen) + }) + } +} + +// d2i_ASN1_OBJECT with empty / oversized inputs. +func TestD2I_ASN1_OBJECT_BoundaryInputs_NoPanic(t *testing.T) { + cases := []struct { + name string + der []byte + length int + }{ + {"empty", []byte{}, 0}, + {"oversized length", []byte{0x06, 0x01, 0x01}, 1 << 20}, + {"negative length", []byte{0x06, 0x01, 0x01}, -1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("input %q panicked: %v", tc.name, r) + } + }() + der := tc.der + WolfSSL_d2i_ASN1_OBJECT(nil, &der, tc.length) + }) + } +} + +// X509_load_certificate_buffer with invalid inputs must error/return nil, not panic. +func TestX509_LoadCertBuffer_InvalidInputs_NoPanic(t *testing.T) { + cases := []struct { + name string + buff []byte + buffSz int + }{ + {"empty buffer", []byte{}, 0}, + {"oversized buffSz", []byte{0x01}, 1 << 20}, + {"negative buffSz", []byte{0x01}, -1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("input %q panicked: %v", tc.name, r) + } + }() + if got := WolfSSL_X509_load_certificate_buffer(tc.buff, tc.buffSz, SSL_FILETYPE_PEM); got != nil { + WolfSSL_X509_free(got) + t.Fatalf("expected nil for invalid input, got non-nil") + } + }) + } +}