Compare commits

..

83 Commits

Author SHA1 Message Date
Wade Simmons a5cb635db4 use P256 as the default curve for fips140 mode 2026-07-29 16:04:25 -04:00
Wade Simmons bf2f26b66f clean up empty line 2026-07-29 14:40:11 -04:00
Wade Simmons 7c00c74550 complain earlier if in FIPS-140 enforced mode 2026-07-29 14:37:21 -04:00
Wade Simmons 738b4fa044 cleanup 2026-07-24 10:26:38 -04:00
Wade Simmons 0e1f5342ac test fips140.Enforced mode 2026-07-24 10:03:22 -04:00
Wade Simmons 00b498f9f9 self-test the reflect code at init 2026-07-24 09:11:53 -04:00
Wade Simmons 69e5e8eeb4 cleanup 2026-07-24 08:22:58 -04:00
Wade Simmons fc950cf765 one var for default fips version 2026-07-24 08:21:22 -04:00
Wade Simmons 0d91e3e4d8 update PHONY 2026-07-24 08:19:33 -04:00
Wade Simmons e8322505ab fix fips140-all 2026-07-24 08:17:52 -04:00
Wade Simmons 009d42b4f5 Merge remote-tracking branch 'origin/master' into fips140 2026-07-23 11:37:11 -04:00
Wade Simmons 8d8cc4f73d note that boringcrypto is deprecated 2026-07-23 10:55:40 -04:00
Wade Simmons e16a31ff6a return err instead of panic 2026-07-23 10:52:06 -04:00
Wade Simmons 6be3d2264e cleanup nonce 2026-07-20 10:03:29 -04:00
Wade Simmons 048fa3fa70 Merge remote-tracking branch 'origin/master' into fips140 2026-07-20 09:38:05 -04:00
Wade Simmons 3264ebf953 cleanup 2026-07-08 10:55:29 -04:00
Wade Simmons 3c1dd52953 update README 2026-07-08 10:51:38 -04:00
Wade Simmons 4e8733ca22 run all smoke tests with -race
Why not?
2026-07-08 10:46:21 -04:00
Wade Simmons 6675f5a361 use go:debug to enable fips140=only
This is safer because it doesn't clober other defaults that are set by
Go or the environment.
2026-07-08 10:41:24 -04:00
Wade Simmons 14c9288127 Ensure Curve25519 and ChaChaPoly are not used in fips140 enforced mode
Since these some from golang/x/crypto, there is a change that stdlib
fips140.Enforced gate won't catch their usage.
2026-07-08 10:33:55 -04:00
Wade Simmons 90c7630270 extract the internal FIPS GCM implementation
We do this because the TLS wrapper is not thread safe on Open. instead of locking around it we
can grab the internal implementation that is thread safe. This is the FIPS
module implementation: `crypto/internal/fips140/aes/gcm.GCMWithXORCounterNonce`

- https://github.com/golang/go/blob/go1.26.4/src/crypto/internal/fips140/aes/gcm/gcm_nonces.go#L212-L287

The wrapper is struct `crypto/tls.xorNonceAEAD`, with field `aead`:

- https://github.com/golang/go/blob/go1.26.4/src/crypto/tls/cipher_suites.go#L482-L487

This can be cleaned up once these FIPS implementations are exposed directly:

- https://github.com/golang/go/issues/73110
2026-07-08 10:19:48 -04:00
Wade Simmons 81afed439e Merge remote-tracking branch 'origin/master' into fips140 2026-06-18 12:07:19 -04:00
Wade Simmons d42407cf9a dont need race detector on these extra smoke runs
We should catch any issues in the original run
2026-06-10 09:49:09 -04:00
Wade Simmons ac5a18e56a use fips140-all in CI tests 2026-06-10 09:43:41 -04:00
Wade Simmons 2efdcb0366 fips140-all 2026-06-10 09:35:59 -04:00
Wade Simmons 92bd785f2b no mips64le 2026-06-09 17:05:34 -04:00
Wade Simmons 7d087a6d9f add all fips140 platforms 2026-06-09 15:42:05 -04:00
Wade Simmons 67da623e3b cleanup enforcement 2026-06-09 15:33:31 -04:00
Wade Simmons 060b46f4b3 better names 2026-06-09 13:49:55 -04:00
Wade Simmons 69c6132a76 fix the tag 2026-06-09 13:27:57 -04:00
Wade Simmons a383937bde make the build tag clearer
Make it clear this isnt an automatic tag, but one we created for
enforcement
2026-06-09 13:26:36 -04:00
Wade Simmons 3b30526379 boringcrypto cleanup 2026-06-09 13:24:59 -04:00
Wade Simmons cb9547b6a1 remove commented out code 2026-06-09 12:28:02 -04:00
Wade Simmons 69a6db705a cleanup unused test 2026-06-09 12:26:42 -04:00
Wade Simmons 9709893009 use testing log 2026-06-09 12:26:04 -04:00
Wade Simmons 32db819315 cleanup 2026-06-09 12:23:48 -04:00
Wade Simmons f437c7d372 more cleanup 2026-06-09 10:55:57 -04:00
Wade Simmons d725e53072 makefile cleanup 2026-06-09 10:39:09 -04:00
Wade Simmons 06fb503fc3 WIP 2026-06-09 10:31:49 -04:00
Wade Simmons 7cd3875934 fix expected for fips140
We actually set the nonce wrong before this branch, fixing now.
2026-06-08 12:22:25 -04:00
Wade Simmons 90ea6346e9 WIP 2026-06-08 11:41:07 -04:00
Wade Simmons 37b752bb23 WIP 2026-06-08 09:43:28 -04:00
Wade Simmons c7e035479c enforce GODEBUG=fips140=only
This makes it much nicer to prove we are using the fips140 module for
all crypto.
2026-06-02 16:36:58 -04:00
Wade Simmons adb01f66a3 make the test match the code
Ensure we use the correctly AESGCM for fips140
2026-06-01 13:48:34 -04:00
Wade Simmons cf5d73d625 better check 2026-06-01 11:24:09 -04:00
Wade Simmons 56a09b7cbb fix 2026-06-01 11:22:39 -04:00
Wade Simmons 1d17c785a8 fixup tests 2026-06-01 11:19:12 -04:00
Wade Simmons 7c8a70b0cc fix smoke boringcrypto and fips140 2026-06-01 11:03:15 -04:00
Wade Simmons c73245ca5c fix missing import 2026-06-01 10:56:10 -04:00
Wade Simmons f1a8054a9d latest golanglint-ci 2026-06-01 10:53:58 -04:00
Wade Simmons dd081ffeb6 cleanup 2026-06-01 10:51:31 -04:00
Wade Simmons cfcdcb0546 use go1.26 2026-06-01 10:46:48 -04:00
Wade Simmons c3e2a2b3a7 better smoke 2026-06-01 10:38:31 -04:00
Wade Simmons 0897f49576 default GODEBUG=fips140=only 2026-06-01 10:36:05 -04:00
Wade Simmons 8671a4ebbd cleanup 2026-06-01 10:21:34 -04:00
Wade Simmons b5ad62aea1 Merge remote-tracking branch 'origin/master' into fips140 2026-06-01 09:52:57 -04:00
Wade Simmons a1b8954a23 update Makefile 2026-05-01 14:13:38 -04:00
Wade Simmons 441a67fbf9 fix missing space 2026-05-01 13:57:34 -04:00
Wade Simmons 7f6976ba5d Merge remote-tracking branch 'origin/master' into fips140 2026-05-01 13:57:26 -04:00
Wade Simmons 5dd566e220 also support fips140v1.26
This will be inprocess soon
2026-04-30 15:21:58 -04:00
Wade Simmons b79fdc272a better default 2026-04-28 13:12:57 -04:00
Wade Simmons d4d747f219 no longer need go1.26 with this fix 2026-04-28 13:12:22 -04:00
Wade Simmons 4ff9e97dce Revert "need go1.26"
This reverts commit 65450f9d21.
2026-04-28 13:12:10 -04:00
Wade Simmons bdbd483063 Revert "bump linter to support go1.26"
This reverts commit 1403473e4b.
2026-04-28 13:11:36 -04:00
Wade Simmons 1403473e4b bump linter to support go1.26 2026-04-28 12:03:31 -04:00
Wade Simmons ae58085032 cleanup, no GODEBUG needed 2026-04-28 11:58:43 -04:00
Wade Simmons 65450f9d21 need go1.26 2026-04-27 17:18:04 -04:00
Wade Simmons f9532f4244 requires go1.26 2026-04-27 17:16:28 -04:00
Wade Simmons fcfbec4710 log the fips140 mode and version
Requires go1.26 for fips140.Version()
2026-04-27 16:43:35 -04:00
Wade Simmons e6eeb3709b fix smoke tests 2026-04-27 16:27:58 -04:00
Wade Simmons ef8b700474 fix mismerge 2026-04-27 16:24:34 -04:00
Wade Simmons 243cf4a7c5 Revert "cleanup"
This reverts commit 08ee2ab35f.
2026-04-27 16:22:09 -04:00
Wade Simmons 08ee2ab35f cleanup 2026-04-27 14:18:17 -04:00
Wade Simmons 2d5d86f24d Merge remote-tracking branch 'origin/master' into fips140 2026-04-27 14:13:47 -04:00
Wade Simmons 6fa1ecdc29 Merge remote-tracking branch 'origin/master' into fips140 2026-01-30 13:41:49 -05:00
Wade Simmons b418a081a8 cleanup 2025-07-25 14:57:49 -04:00
Wade Simmons fd3fa57e79 comments 2025-07-25 14:42:54 -04:00
Wade Simmons 0eb92dcab4 WIP 2025-07-25 14:32:37 -04:00
Wade Simmons f6b206d96c cleanup 2025-07-25 10:38:52 -04:00
Wade Simmons 31cc3a4169 Merge remote-tracking branch 'origin/master' into fips140 2025-07-24 13:57:12 -04:00
Wade Simmons 6da314aa6b WIP 2025-07-24 13:56:42 -04:00
Wade Simmons 3da3d41fb5 log if fips140 in use 2025-07-24 12:37:33 -04:00
Wade Simmons 4485c47641 WIP support new Go fips140 module
This will replace boring crypto at some point.

We should modify our protocol a bit and instead change to
NewGCMWithRandomNonce.
2025-03-31 12:08:58 -04:00
27 changed files with 539 additions and 426 deletions
+10 -30
View File
@@ -25,39 +25,19 @@ jobs:
go-version: '1.26'
check-latest: true
- name: build
run: make bin-docker CGO_ENABLED=1 BUILD_ARGS=-race
- name: Smoke Docker
run: make smoke-docker
- name: setup docker image
working-directory: ./.github/workflows/smoke
run: ./build.sh
- name: Smoke Docker IPv6 overlay
run: make smoke-docker-ipv6
- name: run smoke
working-directory: ./.github/workflows/smoke
run: ./smoke.sh
- name: Smoke Relay Docker
run: make smoke-relay-docker
- name: setup docker image ipv6
working-directory: ./.github/workflows/smoke
run: SMOKE_OVERLAY_IPV6=1 ./build.sh
- name: Smoke Docker boringcrypto
run: make boringcrypto smoke-docker
- name: run smoke ipv6
working-directory: ./.github/workflows/smoke
run: SMOKE_OVERLAY_IPV6=1 ./smoke.sh
- name: setup relay docker image
working-directory: ./.github/workflows/smoke
run: ./build-relay.sh
- name: run smoke relay
working-directory: ./.github/workflows/smoke
run: ./smoke-relay.sh
- name: setup docker image for P256
working-directory: ./.github/workflows/smoke
run: NAME="smoke-p256" CURVE=P256 ./build.sh
- name: run smoke-p256
working-directory: ./.github/workflows/smoke
run: NAME="smoke-p256" ./smoke.sh
- name: Smoke Docker fips140
run: make fips140-all GOALS=smoke-docker
timeout-minutes: 10
+8 -3
View File
@@ -58,9 +58,14 @@ jobs:
e2e-cmd: make e2evv
- name: linux-boringcrypto
os: ubuntu-latest
build-cmd: make bin-boringcrypto
test-cmd: make test-boringcrypto
e2e-cmd: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
build-cmd: make boringcrypto
test-cmd: make boringcrypto test
e2e-cmd: make boringcrypto e2evv
- name: linux-fips140
os: ubuntu-latest
build-cmd: make fips140-all
test-cmd: make fips140-all GOALS=test
e2e-cmd: make fips140-all GOALS=e2evv
- name: linux-pkcs11
os: ubuntu-latest
build-cmd: make bin-pkcs11
-82
View File
@@ -7,88 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.11.0] - 2026-07-23
See the [v1.11.0](https://github.com/slackhq/nebula/milestone/25?closed=1) milestone for a complete list of changes.
### Breaking
- Logging has switched from logrus to Go's structured `slog`. Log output changes: levels are upper case
(`level=INFO`), trace prints as `level=DEBUG-4`, timestamps are always RFC3339Nano and `logging.timestamp_format`
is ignored, and some messages were reworded. Review any log parsing before upgrading. This is also an API break
for embedders, as constructors now take a `*slog.Logger`. (#1672, #1734, #1621)
- `firewall.inbound_action` and `firewall.outbound_action` (used to set reject vs. drop policy) were each being
applied to the opposite direction, that is now corrected. This only affects how blocked packets are answered, not
which packets the firewall allows or denies. If you set either of these you are getting the behavior of the other
one today and likely want to swap them before upgrading. (#1798)
- On Windows, Nebula now installs WFP PERMIT filters for the nebula adapter and the listener port by default. WFP
sits below Windows Defender Firewall, so any WDF inbound rules you rely on for either will no longer apply. Set
`tun.windows_bypass_wdf` and `listen.windows_bypass_wdf` to false to leave WDF in charge. (#1710)
- On Windows, the nebula device is now set to the `private` network category instead of whatever Windows decided,
which is usually `Public`. This makes the host firewall less restrictive on the overlay. Set
`tun.network_category` to `unset` to keep the old behavior. (#1710)
- Reject packets for non-TCP now use ICMP code 13, communication administratively prohibited, instead of code 3,
port unreachable. Anything keying off the old code needs updating. (#1766, #1768)
- The SSH debug server's profiling commands are now confined to `sshd.sandbox_dir`, which defaults to
`$TMP/nebula-debug`. Relative paths resolve inside it and absolute paths outside it are rejected, so anything
scripting `start-cpu-profile`, `save-heap-profile`, or `save-mutex-profile` with a path elsewhere needs the
directory set. The directory is not created for you. (#1622)
### Added
- Sign the Windows release binaries. (#1718)
- Generate IPv6 reject packets, matching the existing IPv4 behavior. (#1766, #1767, #1768)
- Accept `-` in `nebula-cert` to read from stdin or write to stdout. (#1714)
- Search for both `config.yml` and `config.yaml` in service and command line modes. (#1717)
- Add version labels to the Docker/OCI images. (#1772)
- Rebind the listener and re-query lighthouses on macOS when the underlay network changes, so devices moving
between wifi and wired or between networks recover without waiting for dead tunnel detection. Controlled by
`listen.rebind_on_network_change` (default `true`, not reloadable). (#1816)
### Changed
- Reload the firewall when the unsafe networks in the certificate change. (#1719)
- Reconfigure, start, and stop the stats listener on a config reload instead of requiring a restart. (#1670)
- Update a static host's addresses when they change on reload. (#1713)
- Don't require a port on ICMP firewall rules. (#1609)
- Connection track ICMP traffic. (#1602)
- Return `NODATA` instead of `NXDOMAIN` from the DNS server for a name that exists but has no record of the
requested type, so clients that query `AAAA` first (busybox/Alpine) fall through to `A`. (#1668)
- Record the local host's details in the DNS server. (#1716)
- Install Windows unsafe routes as link routes. (#1709)
- Reduce relay handshake log spam, and only log a handshake send error at error level when the remote list
changes. (#1733, #1765, #1810)
- Start, stop, and reload subsystems (DNS, stats, conntrack, ssh, punchy) cleanly without leaking goroutines. (#1640, #1654, #1661, #1667, #1669, #1708, #1806, #1815)
- `Control` is now safe to stop and wait on from any lifecycle state, and a new `Control.Wait` blocks until nebula
has fully stopped and returns the first fatal reader error. Failed starts release the udp sockets and tun fd
instead of leaking them. (#1794)
- Trigger an immediate lighthouse update when reconnecting to or adding a lighthouse instead of waiting for the next update tick. (#1645)
- Bring the Darwin and OpenBSD tun implementations in line with the other BSDs. (#1703)
- Update to build against go v1.26. (#1818)
- Various dependency updates. (#1586, #1587, #1604, #1617, #1618, #1627, #1628, #1629, #1652, #1664, #1665, #1697, #1721, #1732, #1742, #1743, #1750, #1763, #1771, #1782, #1800, #1807)
### Fixed
- Fix a data race on a host's remote address that could send packets to the wrong address during a roam. (#1773)
- Fix tunnels that could permanently escape connection manager monitoring. (#1752)
- Fix a crash when reloading the SSH server's trusted keys. (#1787)
- Fix hostmap corruption when a host has multiple overlay addresses. Each address now gets its own list instead of
a single shared chain, which also fixes two latent bugs on the add and makePrimary paths. (#1788, #1790)
- Apply `remote_allow_list` IPv4 rules to 4-in-6 mapped addresses. (#1786)
- Don't panic in the DNS server on a short or empty query name. (#1635)
- Advance the replay window on relayed packets so a relay drops replayed frames instead of re-forwarding them. (#1751)
- Fix a race in relay state handling. (#1753)
- Lock replay window updates so concurrent readers can't corrupt it. (#1802)
- Reject malformed handshakes more reliably, including invalid ed25519 key lengths. (#1601, #1756)
- Properly handle `closetunnel` packets. (#1638)
- Fix an IPv6 extension-header length overflow that could make the firewall parse the wrong protocol and ports. (#1789)
- Fix relay re-establishment when a handshake arrives over a relay entry that a one-sided teardown left
`Disestablished`, which silently dropped every send until dead tunnel detection forced a re-handshake. (#1805)
- Don't build new relay state on a tunnel that was just discarded. (#1796)
- Don't delete the wrong pending hostinfo in the handshake manager. (#1811)
- Don't call the packet reader after a UDP error on Darwin. (#1755)
- Open the FreeBSD tun device non blocking. (#1666)
## [1.10.3] - 2026-02-06
### Security
+80 -21
View File
@@ -72,6 +72,17 @@ ALL_CROSS_LINUX_ARM = linux-arm-5 linux-arm-6 linux-arm-7 linux-arm64
ALL_CROSS_LINUX_MIPS = linux-mips linux-mipsle linux-mips64 linux-mips64le linux-mips-softfloat
ALL_CROSS_LINUX_OTHER = linux-386 linux-ppc64le linux-riscv64 linux-loong64
# Based on section 2.2 of the Go Cryptographic Module CVMP Security Policy #5247
ALL_FIPS140 = linux-amd64-fips140 \
linux-arm64-fips140 \
windows-amd64-fips140 \
windows-arm64-fips140 \
darwin-arm64-fips140 \
freebsd-amd64-fips140 \
linux-arm-7-fips140 \
linux-mips64-fips140 \
linux-ppc64le-fips140
e2e:
$(TEST_ENV) go test -tags=e2e_testing -count=1 $(TEST_FLAGS) ./e2e
@@ -137,6 +148,8 @@ release-netbsd: $(ALL_NETBSD:%=build/nebula-%.tar.gz)
release-boringcrypto: build/nebula-linux-$(shell go env GOARCH)-boringcrypto.tar.gz
release-fips140: $(ALL_FIPS140:%=build/nebula-%.tar.gz)
BUILD_ARGS += -trimpath
bin-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe
@@ -157,17 +170,20 @@ bin-freebsd-arm64: build/freebsd-arm64/nebula build/freebsd-arm64/nebula-cert
bin-boringcrypto: build/linux-$(shell go env GOARCH)-boringcrypto/nebula build/linux-$(shell go env GOARCH)-boringcrypto/nebula-cert
mv $? .
bin-fips140: build/linux-$(shell go env GOARCH)-fips140/nebula build/linux-$(shell go env GOARCH)-fips140/nebula-cert
mv $? .
bin-pkcs11: BUILD_ARGS += -tags pkcs11
bin-pkcs11: CGO_ENABLED = 1
bin-pkcs11: bin
bin:
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
$(GOENV) go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
$(GOENV) go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
install:
go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ${NEBULA_CMD_PATH}
go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ./cmd/nebula-cert
$(GOENV) go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ${NEBULA_CMD_PATH}
$(GOENV) go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ./cmd/nebula-cert
build/linux-arm-%: GOENV += GOARM=$(word 3, $(subst -, ,$*))
build/linux-mips-%: GOENV += GOMIPS=$(word 3, $(subst -, ,$*))
@@ -178,8 +194,11 @@ build/linux-mips-softfloat/%: LDFLAGS += -s -w
# boringcrypto
build/linux-amd64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
build/linux-arm64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
build/linux-amd64-boringcrypto/%: LDFLAGS += -checklinkname=0
build/linux-arm64-boringcrypto/%: LDFLAGS += -checklinkname=0
# fips140
FIPSVERSION = v1.0.0
$(foreach _rule, $(ALL_FIPS140), build/$(_rule)/%): GOENV += GOFIPS140=$(FIPSVERSION)
$(foreach _rule, $(ALL_FIPS140), build/$(_rule)/%): BUILD_ARGS += -tags fips140enforce
build/%/nebula: .FORCE
GOOS=$(firstword $(subst -, , $*)) \
@@ -210,10 +229,7 @@ vet:
go vet $(VET_FLAGS) -v ./...
test:
go test -v ./...
test-boringcrypto:
GOEXPERIMENT=boringcrypto CGO_ENABLED=1 go test -ldflags "-checklinkname=0" -v ./...
$(TEST_ENV) go test $(TEST_FLAGS) -v ./...
test-pkcs11:
CGO_ENABLED=1 go test -v -tags pkcs11 ./...
@@ -256,29 +272,72 @@ ifeq ($(words $(MAKECMDGOALS)),1)
@$(MAKE) service ${.DEFAULT_GOAL} --no-print-directory
endif
# Useful to chain together, like:
# - make fips140 e2evv
# - make fips140 smoke-docker
# Use `release-fips140` to build release binaries
fips140:
@echo > $(NULL_FILE)
ifeq ($(strip $(GOFIPS140)),)
$(eval GOFIPS140 = $(FIPSVERSION))
endif
$(eval GOENV += GOFIPS140=$(GOFIPS140))
$(eval BUILD_ARGS += -tags fips140enforce)
$(eval TEST_ENV += $(GOENV))
$(eval CURVE = P256)
ifeq ($(words $(MAKECMDGOALS)),1)
@$(MAKE) fips140 GOFIPS140=$(GOFIPS140) ${.DEFAULT_GOAL} --no-print-directory
endif
# To test the future pending module, use like `make fips140-latest test`
ALL_GOFIPS140 = v1.0.0 v1.26.0 latest
define FIPS140_rule
fips140-$(1): GOFIPS140 = $(1)
fips140-$(1): fips140
endef
$(foreach _rule, $(ALL_GOFIPS140), $(eval $(call FIPS140_rule,$(_rule))))
# Iterate and run the goals for all fips versions, like `make fips140-all GOALS=test`
fips140-all:
@$(foreach _v,$(ALL_GOFIPS140),$(MAKE) fips140-$(_v) $(GOALS) &&) true
# Useful to chain together, like:
# - make boringcrypto e2evv
# - make boringcrypto smoke-docker
# Use `release-boringcrypto` or `bin-boringcrypto` to build release binaries
boringcrypto:
@echo > $(NULL_FILE)
$(eval GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1)
$(eval TEST_ENV += $(GOENV))
$(eval CURVE = P256)
ifeq ($(words $(MAKECMDGOALS)),1)
@$(MAKE) boringcrypto ${.DEFAULT_GOAL} --no-print-directory
endif
bin-docker: bin build/linux-amd64/nebula build/linux-amd64/nebula-cert
smoke-docker: BUILD_ARGS += -race
smoke-docker: GOENV += CGO_ENABLED=1
smoke-docker: bin-docker
cd .github/workflows/smoke/ && ./build.sh
cd .github/workflows/smoke/ && ./smoke.sh
cd .github/workflows/smoke/ && NAME="smoke-p256" CURVE="P256" ./build.sh
cd .github/workflows/smoke/ && NAME="smoke-p256" ./smoke.sh
# This is so we can limit `fips140` smoke test to just P256 curve.
if [ "$(CURVE)" != "P256" ]; then cd .github/workflows/smoke/ && $(GOENV) ./build.sh; fi
if [ "$(CURVE)" != "P256" ]; then cd .github/workflows/smoke/ && $(GOENV) ./smoke.sh; fi
cd .github/workflows/smoke/ && $(GOENV) NAME="smoke-p256" CURVE="P256" ./build.sh
cd .github/workflows/smoke/ && $(GOENV) NAME="smoke-p256" ./smoke.sh
smoke-relay-docker: BUILD_ARGS += -race
smoke-relay-docker: GOENV += CGO_ENABLED=1
smoke-relay-docker: bin-docker
cd .github/workflows/smoke/ && ./build-relay.sh
cd .github/workflows/smoke/ && ./smoke-relay.sh
cd .github/workflows/smoke/ && $(GOENV) ./build-relay.sh
cd .github/workflows/smoke/ && $(GOENV) ./smoke-relay.sh
smoke-docker-ipv6: export SMOKE_OVERLAY_IPV6 = 1
smoke-docker-ipv6: smoke-docker
smoke-docker-race: BUILD_ARGS = -race
smoke-docker-race: CGO_ENABLED = 1
smoke-docker-race: smoke-docker
smoke-vagrant/%: bin-docker build/%/nebula
cd .github/workflows/smoke/ && ./build.sh $*
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
.FORCE:
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin bin-windows bin-windows-arm64 bin-darwin bin-freebsd bin-freebsd-arm64 bin-boringcrypto bin-fips140 bin-pkcs11 bin-docker boringcrypto build-test-mobile docker e2e e2ev e2evv e2evvv e2evvvv e2e-bench fips140 fips140-all $(ALL_GOFIPS140:%=fips140-%) install proto release release-linux release-freebsd release-openbsd release-netbsd release-boringcrypto release-fips140 service smoke-docker smoke-relay-docker smoke-docker-ipv6 test test-pkcs11 test-cov-html vet smoke-vagrant/%
.DEFAULT_GOAL := bin
+12 -2
View File
@@ -145,17 +145,27 @@ To build nebula for a specific platform (ex, Windows):
See the [Makefile](Makefile) for more details on build targets
## Curve P256 and BoringCrypto
## Curve P256 and FIPS 140-3 mode
The default curve used for cryptographic handshakes and signatures is Curve25519. This is the recommended setting for most users. If your deployment has certain compliance requirements, you have the option of creating your CA using `nebula-cert ca -curve P256` to use NIST Curve P256. The CA will then sign certificates using ECDSA P256, and any hosts using these certificates will use P256 for ECDH handshakes.
In addition, Nebula can be built using the [BoringCrypto GOEXPERIMENT](https://github.com/golang/go/blob/go1.20/src/crypto/internal/boring/README.md) by running either of the following make targets:
Nebula can be built to support the [FIPS 140-3](https://go.dev/doc/security/fips140) mode of Go by running either of the following make targets. (This sets GOFIPS140=v1.0.0, which must be done at compile time so that the correct AES-GCM can be used for FIPS 140-3 enforcement mode).
```sh
make fips140
make fips140 test
make release-fips140
```
Nebula can also be built using the [BoringCrypto GOEXPERIMENT](https://github.com/golang/go/blob/go1.20/src/crypto/internal/boring/README.md) by running either of the following make targets.
```sh
make bin-boringcrypto
make release-boringcrypto
```
NOTE: boringcrypto support is deprecated and will be removed in the next release. Users should migrate to the native FIPS 140-3 mode described above.
This is not the recommended default deployment, but may be useful based on your compliance requirements.
## Credits
+9 -1
View File
@@ -3,6 +3,7 @@ package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/fips140"
"crypto/rand"
"flag"
"fmt"
@@ -43,6 +44,13 @@ type caFlags struct {
subnets *string
}
func defaultCurve() string {
if fips140.Enforced() {
return "P256"
}
return "25519"
}
func newCaFlags() *caFlags {
cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
cf.set.Usage = func() {}
@@ -59,7 +67,7 @@ func newCaFlags() *caFlags {
cf.argonParallelism = cf.set.Uint("argon-parallelism", 4, "Optional: Argon2 parallelism parameter used for encrypted private key passphrase")
cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)")
cf.curve = cf.set.String("curve", defaultCurve(), "EdDSA/ECDSA Curve (25519, P256)")
cf.p11url = p11Flag(cf.set)
cf.ips = cf.set.String("ips", "", "Deprecated, see -networks")
+5
View File
@@ -0,0 +1,5 @@
//go:build fips140enforce
//go:debug fips140=only
package main
+1 -1
View File
@@ -24,7 +24,7 @@ func newKeygenFlags() *keygenFlags {
cf.set.Usage = func() {}
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
cf.curve = cf.set.String("curve", "25519", "ECDH Curve (25519, P256)")
cf.curve = cf.set.String("curve", defaultCurve(), "ECDH Curve (25519, P256)")
cf.p11url = p11Flag(cf.set)
return &cf
}
+5
View File
@@ -0,0 +1,5 @@
//go:build fips140enforce
//go:debug fips140=only
package main
+5
View File
@@ -0,0 +1,5 @@
//go:build fips140enforce
//go:debug fips140=only
package main
-136
View File
@@ -1,136 +0,0 @@
//go:build e2e_testing
// +build e2e_testing
package e2e
import (
"testing"
"time"
"github.com/slackhq/nebula"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/e2e/router"
"github.com/slackhq/nebula/udp"
)
// TestRecoveryTiming measures how long a tunnel takes to come back after the peer stops accepting our traffic,
// which is what a laptop waking on a new network looks like from the peer's side: its NAT has no state for where
// we are now, so everything we send disappears.
//
// It is a measurement, not a pass/fail assertion. Recovery is timed to the moment the peer punches back at us,
// since that is when its NAT opens and the tunnel is usable again.
//
// go test -tags e2e_testing -v -run TestRecoveryTiming ./e2e/
func TestRecoveryTiming(t *testing.T) {
for _, tc := range []struct {
name string
rebind bool
}{
{"no trigger", false},
{"rebind counter", true},
} {
t.Run(tc.name, func(t *testing.T) {
d, lost := measureRecovery(t, tc.rebind)
t.Logf("RESULT %-16s recovered in %-9v (%d packets lost)", tc.name, d.Round(time.Millisecond), lost)
})
}
}
// measureRecovery returns how long until the peer punched back, and how many of our packets died meanwhile. When
// rebind is true we call RebindUDPServer once the tunnel goes dark, which is what the darwin network change
// monitor does and what iOS has always done. When false, nothing tells nebula anything is wrong.
func measureRecovery(t *testing.T, rebind bool) (time.Duration, int) {
t.Helper()
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
"lighthouse": m{"am_lighthouse": true},
})
peerCfg := m{
"lighthouse": m{
"hosts": []any{lhVpnIpNet[0].Addr().String()},
"interval": 600,
"local_allow_list": m{
"10.0.0.0/24": true,
"::/0": false,
},
},
"static_host_map": m{
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
},
}
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", peerCfg)
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.3/24", peerCfg)
r := router.NewR(t, lhControl, myControl, theirControl)
defer r.RenderFlow()
defer func() {
lhControl.Stop()
myControl.Stop()
theirControl.Stop()
}()
lhControl.Start()
myControl.Start()
theirControl.Start()
r.RouteFor(time.Millisecond * 500)
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("establish")))
r.RouteFor(time.Second)
if myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false) == nil {
t.Fatal("failed to establish the tunnel we are measuring")
}
r.RouteFor(time.Millisecond * 500)
// From here the peer's NAT has no state for us, everything we send it disappears
start := time.Now()
blackholed := 0
var recovered time.Duration
if rebind {
myControl.RebindUDPServer()
}
// Keep the tun busy the way someone retrying a stalled connection would
stop := make(chan struct{})
defer close(stop)
go func() {
tick := time.NewTicker(time.Millisecond * 200)
defer tick.Stop()
for {
select {
case <-stop:
return
case <-tick.C:
myControl.InjectTunPacket(BuildTunUDPPacket(
theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("retry")))
}
}
}()
r.RouteForAllExitFuncOrTimeout(time.Second*30, func(p *udp.Packet, c *nebula.Control) router.ExitType {
if c == theirControl && p.From == myControl.GetUDPAddr() {
blackholed++
return router.Drop
}
// The peer reaching us directly is the moment its NAT opened, whether that is a punch or a handshake
if c == myControl && p.From == theirUdpAddr {
recovered = time.Since(start)
return router.RouteAndExit
}
return router.KeepRouting
})
if recovered == 0 {
t.Fatalf("no recovery within 30s (%d packets blackholed)", blackholed)
}
return recovered, blackholed
}
+2 -19
View File
@@ -153,9 +153,6 @@ const (
ExitNow ExitType = 1
// RouteAndExit routes this packet and exits immediately afterwards
RouteAndExit ExitType = 2
// Drop discards this packet without delivering it and keeps routing. Use it to simulate a blackhole, such as
// a restrictive NAT refusing traffic from an address it has not seen.
Drop ExitType = 3
)
type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
@@ -166,9 +163,7 @@ type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
func NewR(t testing.TB, controls ...*nebula.Control) *R {
ctx, cancel := context.WithCancel(context.Background())
// t.Name() contains a slash for subtests, so the flow log can land in a nested directory
fn := filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name()))
if err := os.MkdirAll(filepath.Dir(fn), 0755); err != nil {
if err := os.MkdirAll("mermaid", 0755); err != nil {
panic(err)
}
@@ -179,7 +174,7 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
outNat: make(map[outNatKey]netip.AddrPort),
flow: []flowEntry{},
ignoreFlows: []ignoreFlow{},
fn: fn,
fn: filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name())),
t: t,
cancelRender: cancel,
}
@@ -692,10 +687,6 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
p.Release()
return
case Drop:
// Record it so the flow log shows the attempt, but never hand it to the receiver
r.unlockedInjectFlow(sender, receiver, p, false)
case KeepRouting:
fp := r.unlockedInjectFlow(sender, receiver, p, false)
receiver.InjectUDPPacket(p)
@@ -788,10 +779,6 @@ func (r *R) RouteForAllExitFuncOrTimeout(timeout time.Duration, whatDo ExitFunc)
p.Release()
return true
case Drop:
// Record it so the flow log shows the attempt, but never hand it to the receiver
r.unlockedInjectFlow(cm[x], receiver, p, false)
case KeepRouting:
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
receiver.InjectUDPPacket(p)
@@ -897,10 +884,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
p.Release()
return
case Drop:
// Record it so the flow log shows the attempt, but never hand it to the receiver
r.unlockedInjectFlow(cm[x], receiver, p, false)
case KeepRouting:
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
receiver.InjectUDPPacket(p)
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/vishvananda/netlink v1.3.1
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.5
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.54.0
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
golang.org/x/net v0.57.0
+2 -2
View File
@@ -155,8 +155,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+4
View File
@@ -2,6 +2,7 @@ package nebula
import (
"context"
"crypto/fips140"
"errors"
"fmt"
"io"
@@ -238,6 +239,9 @@ func (f *Interface) activate() error {
"build", f.version,
"udpAddr", addr,
"boringcrypto", boringEnabled(),
"fips140Version", fips140.Version(),
"fips140Enabled", fips140.Enabled(),
"fips140Enforced", fips140.Enforced(),
)
if f.routines > 1 {
+4 -65
View File
@@ -4,77 +4,16 @@
package noiseutil
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
// unsafe needed for go:linkname
_ "unsafe"
"crypto/boring"
"github.com/flynn/noise"
)
var CipherAESGCM noise.CipherFunc = CipherAESGCMFIPS140
// EncryptLockNeeded indicates if calls to Encrypt need a lock
// This is true for boringcrypto because the Seal function verifies that the
// nonce is strictly increasing.
const EncryptLockNeeded = true
// NewGCMTLS is no longer exposed in go1.19+, so we need to link it in
// See: https://github.com/golang/go/issues/56326
//
// NewGCMTLS is the internal method used with boringcrypto that provides a
// validated mode of AES-GCM which enforces the nonce is strictly
// monotonically increasing. This is the TLS 1.2 specification for nonce
// generation (which also matches the method used by the Noise Protocol)
//
// - https://github.com/golang/go/blob/go1.19/src/crypto/tls/cipher_suites.go#L520-L522
// - https://github.com/golang/go/blob/go1.19/src/crypto/internal/boring/aes.go#L235-L237
// - https://github.com/golang/go/blob/go1.19/src/crypto/internal/boring/aes.go#L250
// - https://github.com/google/boringssl/blob/ae223d6138807a13006342edfeef32e813246b39/include/openssl/aead.h#L379-L381
// - https://github.com/google/boringssl/blob/ae223d6138807a13006342edfeef32e813246b39/crypto/fipsmodule/cipher/e_aes.c#L1082-L1093
//
//go:linkname newGCMTLS crypto/internal/boring.NewGCMTLS
func newGCMTLS(c cipher.Block) (cipher.AEAD, error)
type cipherFn struct {
fn func([32]byte) noise.Cipher
name string
}
func (c cipherFn) Cipher(k [32]byte) noise.Cipher { return c.fn(k) }
func (c cipherFn) CipherName() string { return c.name }
// CipherAESGCM is the AES256-GCM AEAD cipher (using NewGCMTLS when GoBoring is present)
var CipherAESGCM noise.CipherFunc = cipherFn{cipherAESGCMBoring, "AESGCM"}
func cipherAESGCMBoring(k [32]byte) noise.Cipher {
c, err := aes.NewCipher(k[:])
if err != nil {
panic(err)
}
gcm, err := newGCMTLS(c)
if err != nil {
panic(err)
}
return aeadCipher{
gcm,
func(n uint64) []byte {
var nonce [12]byte
binary.BigEndian.PutUint64(nonce[4:], n)
return nonce[:]
},
}
}
type aeadCipher struct {
cipher.AEAD
nonce func(uint64) []byte
}
func (c aeadCipher) Encrypt(out []byte, n uint64, ad, plaintext []byte) []byte {
return c.Seal(out, c.nonce(n), plaintext, ad)
}
func (c aeadCipher) Decrypt(out []byte, n uint64, ad, ciphertext []byte) ([]byte, error) {
return c.Open(out, c.nonce(n), ciphertext, ad)
}
var boringEnabled = boring.Enabled()
-32
View File
@@ -4,8 +4,6 @@
package noiseutil
import (
"crypto/boring"
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
@@ -14,33 +12,3 @@ import (
func TestEncryptLockNeeded(t *testing.T) {
assert.True(t, EncryptLockNeeded)
}
// Ensure NewGCMTLS validates the nonce is non-repeating
func TestNewGCMTLS(t *testing.T) {
assert.True(t, boring.Enabled())
// Test Case 16 from GCM Spec:
// - (now dead link): http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf
// - as listed in boringssl tests: https://github.com/google/boringssl/blob/fips-20220613/crypto/cipher_extra/test/cipher_tests.txt#L412-L418
key, _ := hex.DecodeString("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308")
iv, _ := hex.DecodeString("cafebabefacedbaddecaf888")
plaintext, _ := hex.DecodeString("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39")
aad, _ := hex.DecodeString("feedfacedeadbeeffeedfacedeadbeefabaddad2")
expected, _ := hex.DecodeString("522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662")
expectedTag, _ := hex.DecodeString("76fc6ece0f4e1768cddf8853bb2d551b")
expected = append(expected, expectedTag...)
var keyArray [32]byte
copy(keyArray[:], key)
c := CipherAESGCM.Cipher(keyArray)
aead := c.(aeadCipher).AEAD
dst := aead.Seal([]byte{}, iv, plaintext, aad)
assert.Equal(t, expected, dst)
// We expect this to fail since we are re-encrypting with a repeat IV
assert.PanicsWithError(t, "boringcrypto: EVP_AEAD_CTX_seal failed", func() {
dst = aead.Seal([]byte{}, iv, plaintext, aad)
})
}
+4 -1
View File
@@ -29,8 +29,11 @@ type CipherState interface {
// NewCipherState wraps the post-handshake noise.CipherState in the per-cipher type that matches cipherFunc.
// cipherFunc must be the same cipher used to build the noise CipherSuite that produced s.
func NewCipherState(s *noise.CipherState, cipherFunc noise.CipherFunc) CipherState {
if cs, ok := s.Cipher().(CipherState); ok {
return cs
}
switch cipherFunc.CipherName() {
case CipherAESGCM.CipherName():
case noise.CipherAESGCM.CipherName():
return NewCipherStateAESGCM(s)
case noise.CipherChaChaPoly.CipherName():
return NewCipherStateChaChaPoly(s)
+11 -4
View File
@@ -1,6 +1,7 @@
package noiseutil
import (
"crypto/fips140"
"testing"
"github.com/flynn/noise"
@@ -10,24 +11,30 @@ import (
func TestCipherStateAESGCMRoundtrip(t *testing.T) {
enc, dec := buildCipherStates(t, CipherAESGCM)
roundtrip(t, NewCipherStateAESGCM(enc), NewCipherStateAESGCM(dec))
roundtrip(t, NewCipherState(enc, CipherAESGCM), NewCipherState(dec, CipherAESGCM))
}
func TestCipherStateChaChaPolyRoundtrip(t *testing.T) {
enc, dec := buildCipherStates(t, noise.CipherChaChaPoly)
roundtrip(t, NewCipherStateChaChaPoly(enc), NewCipherStateChaChaPoly(dec))
roundtrip(t, NewCipherState(enc, noise.CipherChaChaPoly), NewCipherState(dec, noise.CipherChaChaPoly))
}
func TestNewCipherStateDispatch(t *testing.T) {
encA, _ := buildCipherStates(t, CipherAESGCM)
encC, _ := buildCipherStates(t, noise.CipherChaChaPoly)
assert.IsType(t, &CipherStateAESGCM{}, NewCipherState(encA, CipherAESGCM))
if !boringEnabled && !fips140.Enabled() {
assert.IsType(t, &CipherStateAESGCM{}, NewCipherState(encA, CipherAESGCM))
} else {
// fips140
assert.IsType(t, encA.Cipher(), NewCipherState(encA, CipherAESGCM))
}
assert.IsType(t, &CipherStateChaChaPoly{}, NewCipherState(encC, noise.CipherChaChaPoly))
}
func TestNewCipherStateUnsupportedPanics(t *testing.T) {
enc, _ := buildCipherStates(t, CipherAESGCM)
enc, _ := buildCipherStates(t, noise.CipherChaChaPoly)
assert.Panics(t, func() {
NewCipherState(enc, fakeCipher{})
})
+179
View File
@@ -0,0 +1,179 @@
package noiseutil
import (
"bytes"
"crypto/cipher"
"crypto/fips140"
"encoding/binary"
"fmt"
"reflect"
"runtime"
"unsafe"
// unsafe needed for go:linkname
_ "crypto/tls"
_ "unsafe"
"github.com/flynn/noise"
)
// TODO: Use NewGCMWithCounterNonce or NewGCMForQUIC once available:
// - https://github.com/golang/go/issues/73110
// - https://github.com/golang/go/issues/79219
// Using tls.aeadAESGCMTLS13 gives us the TLS 1.3 GCM, which also verifies
// that the nonce is strictly increasing. This works for both boringcrypto
// and fips140.
//
//go:linkname aeadAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func aeadAESGCMTLS13(key, noncePrefix []byte) cipher.AEAD
type cipherFn struct {
fn func([32]byte) noise.Cipher
name string
}
func (c cipherFn) Cipher(k [32]byte) noise.Cipher { return c.fn(k) }
func (c cipherFn) CipherName() string { return c.name }
// CipherAESGCMFIPS140 is the AES256-GCM AEAD cipher (using tls.aeadAESGCMTLS13, for both boringcrypto and fips140)
var CipherAESGCMFIPS140 noise.CipherFunc = cipherFn{cipherAESGCMFIPS140, "AESGCM"}
// tls.aeadAESGCMTLS13 uses a 4 byte static prefix and an 8 byte XOR mask
var emptyNonce = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
func cipherAESGCMFIPS140(k [32]byte) noise.Cipher {
gcm := aeadAESGCMTLS13(k[:], emptyNonce)
gcm = extractFIPSAEAD(gcm)
return &aeadGCMFIPS140Cipher{
AEAD: gcm,
}
}
type aeadGCMFIPS140Cipher struct {
cipher.AEAD
ready bool
}
// Extract the internal FIPS GCM implementation from the tls wrapper. The TLS
// wrapper is not thread safe around Open, so instead of locking around it we
// can grab the internal implementation that is thread safe. This is the FIPS
// module implementation: `crypto/internal/fips140/aes/gcm.GCMWithXORCounterNonce`
//
// - https://github.com/golang/go/blob/go1.26.4/src/crypto/internal/fips140/aes/gcm/gcm_nonces.go#L212-L287
//
// The wrapper is struct `crypto/tls.xorNonceAEAD` , with field `aead`:
//
// - https://github.com/golang/go/blob/go1.26.4/src/crypto/tls/cipher_suites.go#L482-L487
//
// This can be cleaned up once these FIPS implementations are exposed directly:
//
// - https://github.com/golang/go/issues/73110
func extractFIPSAEAD(xorNonceAEAD cipher.AEAD) cipher.AEAD {
r := reflect.ValueOf(xorNonceAEAD)
v := r.Elem().FieldByName("aead")
if !v.IsValid() {
// The internal crypto/tls.xorNonceAEAD struct no longer has an `aead`
// field. This can only happen on a Go version this code was not built
// against; the package init() self-test guards against ever reaching
// this at runtime, so this is a defensive fail-fast.
panic(fmt.Sprintf("noiseutil: could not extract FIPS AEAD from %T on %s: no `aead` field (incompatible Go version)", xorNonceAEAD, runtime.Version()))
}
v2 := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
aead, ok := v2.Interface().(cipher.AEAD)
if !ok {
panic(fmt.Sprintf("noiseutil: extracted FIPS `aead` field is %s, not a cipher.AEAD, on %s (incompatible Go version)", v2.Type(), runtime.Version()))
}
return aead
}
func (c *aeadGCMFIPS140Cipher) init(nonce []byte) {
// GCMWithXORCounterNonce expects that the first call to Seal
// is with a counter of `0`, this is how it extracts the nonce mask.
// We can clean this up in the future when NewGCMWithCounterNonce or
// NewGCMForQUIC are available:
if !bytes.Equal(emptyNonce, nonce) {
c.AEAD.Seal([]byte{}, emptyNonce, []byte{}, []byte{})
}
c.ready = true
}
func (c *aeadGCMFIPS140Cipher) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if !c.ready {
c.init(nonce)
}
return c.AEAD.Seal(dst, nonce, plaintext, additionalData)
}
func (c *aeadGCMFIPS140Cipher) Encrypt(out []byte, n uint64, ad, plaintext []byte) []byte {
return c.Seal(out, aeadGCMFIPS140CipherNonce(n), plaintext, ad)
}
func (c *aeadGCMFIPS140Cipher) Decrypt(out []byte, n uint64, ad, ciphertext []byte) ([]byte, error) {
return c.Open(out, aeadGCMFIPS140CipherNonce(n), ciphertext, ad)
}
func (c *aeadGCMFIPS140Cipher) EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error) {
binary.BigEndian.PutUint64(nb[4:], n)
out = c.Seal(out, nb, plaintext, ad)
return out, nil
}
func (c *aeadGCMFIPS140Cipher) DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error) {
binary.BigEndian.PutUint64(nb[4:], n)
return c.Open(out, nb, ciphertext, ad)
}
func aeadGCMFIPS140CipherNonce(n uint64) []byte {
// GCMWithXORCounterNonce uses a 4 byte static prefix and an 8 byte nonce
var nonce [12]byte
binary.BigEndian.PutUint64(nonce[4:], n)
return nonce[:]
}
// init validates the go:linkname + reflection extraction and the nonce-reuse
// protection at startup, in every build. cipherAESGCMFIPS140 relies on unexported
// crypto/tls and crypto/internal/fips140 internals; if a future Go version changes
// those, this fails fast with a clear message instead of panicking per-handshake
// (or, worse, silently losing the strictly-increasing nonce check that is the whole
// point of using this cipher). Because this file has no build tag, this self-test
// runs even in non-FIPS builds, so the default CI lane catches an incompatible Go.
func init() {
var key [32]byte
c := cipherAESGCMFIPS140(key)
// Verify the extracted AEAD produces a working encrypt/decrypt roundtrip.
plaintext := []byte("nebula fips140 self-test")
ad := []byte("ad")
ct := c.Encrypt(nil, 1, ad, plaintext)
pt, err := c.Decrypt(nil, 1, ad, ct)
if err != nil {
panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test roundtrip failed on %s: %v", runtime.Version(), err))
}
if !bytes.Equal(pt, plaintext) {
panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test roundtrip returned wrong plaintext on %s", runtime.Version()))
}
// Verify the nonce-reuse protection still fires: re-encrypting with the same
// counter must panic. This is the guarantee we depend on for nonce safety, so
// if the extraction ever silently yields an AEAD without it, refuse to start.
// The strictly-increasing nonce check only exists under boringcrypto/fips140;
// in a plain build aeadAESGCMTLS13 wraps a standard GCM that does not enforce
// it (and CipherAESGCMFIPS140 is unused there anyway), so only assert it when
// one of those modes is active.
if (boringEnabled || fips140.Enabled()) && !reusePanics(c) {
panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test did not reject a reused nonce on %s; nonce-reuse protection is missing (incompatible Go version)", runtime.Version()))
}
}
// reusePanics reports whether re-encrypting with an already-used counter panics,
// as GCMWithXORCounterNonce is expected to.
func reusePanics(c noise.Cipher) (panicked bool) {
c.Encrypt(nil, 2, nil, nil)
defer func() {
if recover() != nil {
panicked = true
}
}()
c.Encrypt(nil, 2, nil, nil)
return false
}
+48
View File
@@ -0,0 +1,48 @@
package noiseutil
import (
"crypto/cipher"
"crypto/fips140"
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
)
// Ensure NewAESGCM validates the nonce is non-repeating
func TestNewAESGCM(t *testing.T) {
if !boringEnabled && !fips140.Enabled() {
t.Skip("TestNewAESGCM is only for fips140/boringcrypto")
}
key, _ := hex.DecodeString("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308")
iv, _ := hex.DecodeString("00000000facedbaddecaf888")
plaintext, _ := hex.DecodeString("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39")
aad, _ := hex.DecodeString("feedfacedeadbeeffeedfacedeadbeefabaddad2")
expected, _ := hex.DecodeString("6a65c2edd45bd63c7e29f40e3d2ed8ba2b99f4c83135383d5676652f255059ceb24863ff10afb1089db701245da87fb88d3acd5f9dd0770cac220c3c04145caf25e190aeb775e7080401c628")
var keyArray [32]byte
copy(keyArray[:], key)
c := CipherAESGCM.Cipher(keyArray)
aead := c.(cipher.AEAD)
dst := aead.Seal([]byte{}, iv, plaintext, aad)
t.Logf("%x", dst)
assert.Equal(t, expected, dst)
// We expect this to fail since we are re-encrypting with a repeat IV
switch {
case boringEnabled:
assert.PanicsWithError(t, "boringcrypto: EVP_AEAD_CTX_seal failed", func() {
dst = aead.Seal([]byte{}, iv, plaintext, aad)
})
case fips140.Version() == "v1.0.0":
assert.PanicsWithValue(t, "crypto/cipher: counter decreased", func() {
dst = aead.Seal([]byte{}, iv, plaintext, aad)
})
default:
assert.PanicsWithValue(t, "crypto/cipher: counter decreased or remained the same", func() {
dst = aead.Seal([]byte{}, iv, plaintext, aad)
})
}
}
+13
View File
@@ -0,0 +1,13 @@
//go:build fips140enforce
package noiseutil
import (
"crypto/fips140"
)
func init() {
if !fips140.Enforced() {
panic("Nebula compiled with fips140 expects FIPS140 to be enforced. Do not set GODEBUG=fips140, or if you do it must be set as GODEBUG=fips140=only")
}
}
+15 -4
View File
@@ -1,14 +1,25 @@
//go:build !boringcrypto
// +build !boringcrypto
package noiseutil
import (
"crypto/fips140"
"github.com/flynn/noise"
)
// EncryptLockNeeded indicates if calls to Encrypt need a lock
const EncryptLockNeeded = false
var EncryptLockNeeded = fips140.Enabled()
// CipherAESGCM is the standard noise.CipherAESGCM when boringcrypto is not enabled
var CipherAESGCM noise.CipherFunc = noise.CipherAESGCM
var CipherAESGCM noise.CipherFunc = initAESGCM()
func initAESGCM() noise.CipherFunc {
if fips140.Enabled() {
return CipherAESGCMFIPS140
} else {
return noise.CipherAESGCM
}
}
var boringEnabled = false
-14
View File
@@ -1,14 +0,0 @@
//go:build !boringcrypto
// +build !boringcrypto
package noiseutil
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncryptLockNeeded(t *testing.T) {
assert.False(t, EncryptLockNeeded)
}
+20 -5
View File
@@ -1,6 +1,7 @@
package nebula
import (
"crypto/fips140"
"encoding/binary"
"encoding/json"
"errors"
@@ -237,10 +238,17 @@ func (cs *CertState) getCertificate(v cert.Version) cert.Certificate {
return nil
}
func newCipherSuite(curve cert.Curve, pkcs11backed bool, cipher string) (noise.CipherSuite, error) {
// newCipherSuite builds the noise.CipherSuite for the given curve and cipher.
// When fips140Enforced is true (FIPS 140-only mode), non-approved algorithms
// (Curve25519 and ChaChaPoly) are rejected with an error. Callers pass
// fips140.Enforced() for fips140Enforced.
func newCipherSuite(curve cert.Curve, pkcs11backed bool, cipher string, fips140Enforced bool) (noise.CipherSuite, error) {
var dhFunc noise.DHFunc
switch curve {
case cert.Curve_CURVE25519:
if fips140Enforced {
return nil, errors.New("pki: use of Curve25519 is not allowed in FIPS 140-only mode")
}
dhFunc = noise.DH25519
case cert.Curve_P256:
if pkcs11backed {
@@ -253,6 +261,9 @@ func newCipherSuite(curve cert.Curve, pkcs11backed bool, cipher string) (noise.C
}
if cipher == "chachapoly" {
if fips140Enforced {
return nil, errors.New("pki: use of ChaChaPoly is not allowed in FIPS 140-only mode")
}
return noise.NewCipherSuite(dhFunc, noise.CipherChaChaPoly, noise.HashSHA256), nil
}
return noise.NewCipherSuite(dhFunc, noiseutil.CipherAESGCM, noise.HashSHA256), nil
@@ -326,6 +337,10 @@ func newCertStateFromConfig(c *config.C, cipher string) (*CertState, error) {
return nil, err
}
if fips140.Enforced() && crt.Curve() != cert.Curve_P256 {
return nil, fmt.Errorf("pki: use of %s is not allowed in FIPS 140-only mode", crt.Curve())
}
switch crt.Version() {
case cert.Version1:
if v1 != nil {
@@ -405,7 +420,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
} else {
if err := v1.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert")
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert: %w", err)
}
}
@@ -413,7 +428,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
if err != nil {
return nil, fmt.Errorf("error marshalling v1 certificate for handshake: %w", err)
}
ncs, err := newCipherSuite(v1.Curve(), pkcs11backed, cipher)
ncs, err := newCipherSuite(v1.Curve(), pkcs11backed, cipher, fips140.Enforced())
if err != nil {
return nil, err
}
@@ -430,7 +445,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
} else {
if err := v2.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert")
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert: %w", err)
}
}
@@ -438,7 +453,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
if err != nil {
return nil, fmt.Errorf("error marshalling v2 certificate for handshake: %w", err)
}
ncs, err := newCipherSuite(v2.Curve(), pkcs11backed, cipher)
ncs, err := newCipherSuite(v2.Curve(), pkcs11backed, cipher, fips140.Enforced())
if err != nil {
return nil, err
}
+94
View File
@@ -0,0 +1,94 @@
package nebula
import (
"strings"
"testing"
"github.com/slackhq/nebula/cert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCipherSuite(t *testing.T) {
tests := []struct {
name string
curve cert.Curve
cipher string
fips140Enforced bool
wantErr string
// wantName is the full expected CipherSuite name (<DH>_<Cipher>_<Hash>),
// only checked when wantErr is empty. Asserting the whole name makes both
// the curve and cipher selection load-bearing.
wantName string
}{
{
name: "curve25519 aesgcm, not enforced",
curve: cert.Curve_CURVE25519,
cipher: "aesgcm",
wantName: "25519_AESGCM_SHA256",
},
{
name: "curve25519 chachapoly, not enforced",
curve: cert.Curve_CURVE25519,
cipher: "chachapoly",
wantName: "25519_ChaChaPoly_SHA256",
},
{
name: "p256 aesgcm, not enforced",
curve: cert.Curve_P256,
cipher: "aesgcm",
wantName: "P256_AESGCM_SHA256",
},
{
name: "p256 aesgcm, enforced is allowed",
curve: cert.Curve_P256,
cipher: "aesgcm",
fips140Enforced: true,
wantName: "P256_AESGCM_SHA256",
},
{
name: "curve25519 rejected when enforced",
curve: cert.Curve_CURVE25519,
cipher: "aesgcm",
fips140Enforced: true,
wantErr: "pki: use of Curve25519 is not allowed in FIPS 140-only mode",
},
{
name: "chachapoly rejected when enforced",
curve: cert.Curve_P256,
cipher: "chachapoly",
fips140Enforced: true,
wantErr: "pki: use of ChaChaPoly is not allowed in FIPS 140-only mode",
},
{
// Curve is checked before cipher, so a Curve25519+ChaChaPoly
// request reports the Curve25519 rejection.
name: "curve25519 chachapoly rejected on curve when enforced",
curve: cert.Curve_CURVE25519,
cipher: "chachapoly",
fips140Enforced: true,
wantErr: "pki: use of Curve25519 is not allowed in FIPS 140-only mode",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs, err := newCipherSuite(tt.curve, false, tt.cipher, tt.fips140Enforced)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
assert.Nil(t, cs)
return
}
require.NoError(t, err)
require.NotNil(t, cs)
assert.Equal(t, tt.wantName, string(cs.Name()))
})
}
}
func TestNewCipherSuiteUnsupportedCurve(t *testing.T) {
cs, err := newCipherSuite(cert.Curve(99), false, "aesgcm", false)
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), "unsupported curve:"), "got: %v", err)
assert.Nil(t, cs)
}
+7 -3
View File
@@ -2,6 +2,7 @@ package nebula
import (
"context"
"crypto/fips140"
"errors"
"fmt"
"log/slog"
@@ -288,9 +289,12 @@ func (s *statsServer) buildRuntime(cfg statsConfig) ([]func(), *http.Server) {
Name: "info",
Help: "Version information for the Nebula binary",
ConstLabels: prometheus.Labels{
"version": s.buildVersion,
"goversion": runtime.Version(),
"boringcrypto": strconv.FormatBool(boringEnabled()),
"version": s.buildVersion,
"goversion": runtime.Version(),
"boringcrypto": strconv.FormatBool(boringEnabled()),
"fips140Version": fips140.Version(),
"fips140Enabled": strconv.FormatBool(fips140.Enabled()),
"fips140Enforced": strconv.FormatBool(fips140.Enforced()),
},
})
pr.MustRegister(g)