Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ env:
# `make lint` to see where else it needs to be updated as well).
GO_VERSION: 1.25.5

# Pin the Go toolchain to whatever is installed on the runner. This prevents
# a malicious or accidental `toolchain` directive in any (transitive) module
# from causing the `go` command to silently download and execute a different
# Go binary. See https://go.dev/doc/toolchain.
GOTOOLCHAIN: local

# Force CI to use only the public Google-maintained, checksum-immortalized
# module proxy. The `,direct` fallback is intentionally omitted so a failed
# proxy lookup cannot silently fall through to a raw VCS fetch.
GOPROXY: https://proxy.golang.org

# Make CI builds fail loudly if anything tries to mutate go.mod / go.sum.
# The only sanctioned path for module updates is `make tidy-module`.
GOFLAGS: -mod=readonly

jobs:
static-checks:
name: Static Checks
Expand Down Expand Up @@ -75,9 +90,31 @@ jobs:
- name: Check code format
run: make fmt-check

- name: Check go modules tidiness
- name: Check go modules tidiness
run: make tidy-module-check

########################
# Supply-chain integrity checks.
########################
#
# `go mod verify` re-hashes every module in the local module cache and
# compares against the hashes recorded in go.sum. This catches a
# tampered module cache that slipped past the proxy.
- name: Verify go module hashes
run: go mod verify

# `govulncheck` walks the call graph (not just go.sum) and reports
# known CVEs that actually reach our compiled code paths. Pinned to an
# explicit version so the install itself is reproducible.
- name: Check for known Go vulnerabilities
env:
# `go install pkg@version` operates outside the current module,
# so it must be allowed to write to its own (separate) cache.
GOFLAGS: ""
run: |
go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
govulncheck ./...

- name: Lint proto files
run: make protolint

Expand Down
17 changes: 15 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ TOOLS_MOD := $(TOOLS_DIR)/go.mod
GOCC ?= go
PREFIX ?= /usr/local

# Force every `go build`, `go test`, `go install`, etc. to operate in readonly
# mode so they cannot silently mutate go.mod / go.sum just because a developer
# added an import locally. The only sanctioned path for module updates is
# `make tidy-module`, which explicitly clears GOFLAGS for its `go mod tidy`
# invocation.
export GOFLAGS := -mod=readonly $(GOFLAGS)

# Pin the Go toolchain to whatever is already installed so a malicious or
# accidental `toolchain` directive in any (transitive) module cannot cause the
# `go` command to silently download and execute a different Go binary. See
# https://go.dev/doc/toolchain.
export GOTOOLCHAIN := local

GOTOOL := GOWORK=off $(GOCC) tool -modfile=$(TOOLS_MOD)


Expand Down Expand Up @@ -406,7 +419,7 @@ protolint:
#? tidy-module: Run `go mod` tidy for all modules
tidy-module:
echo "Running 'go mod tidy' for all modules"
scripts/tidy_modules.sh
GOFLAGS= scripts/tidy_modules.sh

#? tidy-module-check: Make sure all modules are up to date
tidy-module-check: tidy-module
Expand Down Expand Up @@ -474,7 +487,7 @@ mobile-rpc:
#? vendor: Create a vendor directory with all dependencies
vendor:
@$(call print, "Re-creating vendor directory.")
rm -r vendor/; $(GOCC) mod vendor
rm -r vendor/; GOFLAGS= $(GOCC) mod vendor

#? apple: Build mobile RPC stubs and project template for iOS and macOS
apple: mobile-rpc
Expand Down
2 changes: 1 addition & 1 deletion aezeed/cipherseed.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"time"

"github.com/Yawning/aez"
"github.com/kkdai/bstream"
"github.com/lightningnetwork/lnd/aezeed/internal/bstream"
"golang.org/x/crypto/scrypt"
)

Expand Down
193 changes: 193 additions & 0 deletions aezeed/internal/bstream/bstream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Package bstream provides a minimal bit-level stream reader/writer used by
// the aezeed cipher seed encoding. It is an internal fork of
// github.com/kkdai/bstream reduced to the call surface aezeed actually uses
// (NewBStreamReader, NewBStreamWriter, ReadBits, WriteBits, Bytes).
//
// IMPORTANT: this package implements the exact bit-packing scheme used to
// serialize aezeed cipher seeds. The wire format is consensus-frozen — any
// change to the bit ordering or padding semantics here will silently
// invalidate every existing 24-word seed mnemonic in the wild. Do not
// modify the algorithm; only refactor.
package bstream

import "io"

// bit is a typed boolean so the API matches the upstream package and the
// WriteBit / ReadBit semantics stay self-documenting.
type bit bool

// BStream is a bit-addressable buffer used to pack and unpack
// non-byte-aligned values (aezeed uses 11-bit indices into the mnemonic
// word list).
type BStream struct {
stream []byte

// rCount is the number of bits still unread from the first byte of
// the stream.
rCount uint8

// wCount is the number of bits still empty in the last byte of the
// stream.
wCount uint8
}

// NewBStreamReader wraps an existing buffer for reading.
func NewBStreamReader(data []byte) *BStream {
return &BStream{stream: data, rCount: 8}
}

// NewBStreamWriter pre-allocates a buffer with the given byte capacity for
// writing.
func NewBStreamWriter(nByte uint8) *BStream {
return &BStream{stream: make([]byte, 0, nByte), rCount: 8}
}

// writeBit appends a single bit to the stream, allocating a new byte when
// the trailing byte is full.
func (b *BStream) writeBit(input bit) {
if b.wCount == 0 {
b.stream = append(b.stream, 0)
b.wCount = 8
}

latestIndex := len(b.stream) - 1
if input {
b.stream[latestIndex] |= 1 << (b.wCount - 1)
}
b.wCount--
}

// writeOneByte appends an 8-bit value, handling the unaligned case where
// the trailing byte still has wCount free bits.
func (b *BStream) writeOneByte(data byte) {
if b.wCount == 0 {
b.stream = append(b.stream, data)
return
}

latestIndex := len(b.stream) - 1

b.stream[latestIndex] |= data >> (8 - b.wCount)
b.stream = append(b.stream, 0)
latestIndex++
b.stream[latestIndex] = data << b.wCount
}

// WriteBits appends the low `count` bits of `data` to the stream, MSB
// first.
func (b *BStream) WriteBits(data uint64, count int) {
data <<= uint(64 - count)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Potential undefined behavior or zero shift if count is 0. While aezeed currently uses fixed bit counts, adding a guard for count > 0 would make this internal utility more robust against future usage or invalid inputs.

Suggested change
data <<= uint(64 - count)
if count <= 0 {
return
}
data <<= uint(64 - count)


// Handle full bytes first so the per-bit loop only runs for the
// trailing remainder.
for count >= 8 {
byt := byte(data >> (64 - 8))
b.writeOneByte(byt)

data <<= 8
count -= 8
}

for count > 0 {
bi := data >> (64 - 1)
b.writeBit(bi == 1)

data <<= 1
count--
}
}

// readBit consumes a single bit from the front of the stream.
func (b *BStream) readBit() (bit, error) {
if len(b.stream) == 0 {
return false, io.EOF
}

// If the first byte is exhausted, advance to the next byte.
if b.rCount == 0 {
b.stream = b.stream[1:]

if len(b.stream) == 0 {
return false, io.EOF
}

b.rCount = 8
}

retBit := b.stream[0] & (1 << (b.rCount - 1))
b.rCount--

return retBit != 0, nil
}

// readByte consumes a full 8-bit value from the stream, handling the
// unaligned case where the byte straddles two stream bytes.
func (b *BStream) readByte() (byte, error) {
if len(b.stream) == 0 {
return 0, io.EOF
}

if b.rCount == 0 {
b.stream = b.stream[1:]

if len(b.stream) == 0 {
return 0, io.EOF
}

b.rCount = 8
}

// Aligned case: the next 8 bits coincide with a byte boundary.
if b.rCount == 8 {
byt := b.stream[0]
b.stream = b.stream[1:]
return byt, nil
}

retByte := b.stream[0] << (8 - b.rCount)
b.stream = b.stream[1:]

if len(b.stream) == 0 {
return 0, io.EOF
}

retByte |= b.stream[0] >> b.rCount
return retByte, nil
}

// ReadBits consumes the next `count` bits from the stream into the low
// bits of the returned uint64, MSB first.
func (b *BStream) ReadBits(count int) (uint64, error) {
var retValue uint64

for count >= 8 {
retValue <<= 8
byt, err := b.readByte()
if err != nil {
return 0, err
}
retValue |= uint64(byt)
count -= 8
}

for count > 0 {
retValue <<= 1
bi, err := b.readBit()
if err != nil {
return 0, err
}
if bi {
retValue |= 1
}

count--
}

return retValue, nil
}

// Bytes returns the backing buffer. For writers this is the packed
// output; for readers it is whatever input remains unread.
func (b *BStream) Bytes() []byte {
return b.stream
}
Loading
Loading