Skip to content

Commit

Permalink
JWT Version Checks (#364)
Browse files Browse the repository at this point in the history
Fixes #362 

In a follow-up, I will also start logging these incoming requests.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

## Release Notes

- **New Features**
- Added version tracking capabilities to authentication and token
generation processes.
	- Introduced version compatibility validation for JWT claims.

- **Improvements**
- Enhanced token factory and server initialization to support version
information.
- Updated the build process to utilize version identifiers instead of
commit hashes.
- Improved GitHub Actions workflow to provide descriptive commit
references during Docker image builds.

- **Dependencies**
- Added `github.com/Masterminds/semver/v3` package for version
management.

The changes primarily focus on improving version handling and
compatibility across the system's authentication and server components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
mkysel authored Jan 9, 2025
1 parent 3356985 commit 994f91e
Show file tree
Hide file tree
Showing 24 changed files with 319 additions and 51 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/build-xmtpd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Git describe
id: ghd
uses: proudust/gh-describe@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
Expand Down Expand Up @@ -62,7 +64,7 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: "GIT_COMMIT=dev-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}"
build-args: "VERSION=${{ steps.ghd.outputs.describe }}"

- name: Set xmtpd digest output
if: ${{ matrix.image == 'xmtpd' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-from-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: "GIT_COMMIT=${{ github.ref_name }}-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}"
build-args: "VERSION=${{ github.ref_name }}-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}"
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ jobs:
name: Test (Node)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-tags: true
fetch-depth: 0
- uses: actions/setup-go@v3
with:
go-version-file: go.mod
Expand Down
16 changes: 12 additions & 4 deletions cmd/replication/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"github.com/Masterminds/semver/v3"
"log"
"sync"

Expand All @@ -18,7 +19,7 @@ import (
"go.uber.org/zap"
)

var Commit string = "unknown"
var Version string = "unknown"

var options config.ServerOptions

Expand All @@ -33,7 +34,7 @@ func main() {
}

if options.Version {
fmt.Printf("Version: %s\n", Commit)
fmt.Printf("Version: %s\n", Version)
return
}

Expand All @@ -48,10 +49,16 @@ func main() {
}
logger = logger.Named("replication")

logger.Info(fmt.Sprintf("Version: %s", Commit))
logger.Info(fmt.Sprintf("Version: %s", Version))

version, err := semver.NewVersion(Version)
if err != nil {
logger.Error(fmt.Sprintf("Could not parse semver version (%s): %s", Version, err))
}

if options.Tracing.Enable {
logger.Info("starting tracer")
tracing.Start(Commit, logger)
tracing.Start(Version, logger)
defer func() {
logger.Info("stopping tracer")
tracing.Stop()
Expand Down Expand Up @@ -124,6 +131,7 @@ func main() {
dbInstance,
blockchainPublisher,
fmt.Sprintf("0.0.0.0:%d", options.API.Port),
version,
)
if err != nil {
log.Fatal("initializing server", zap.Error(err))
Expand Down
2 changes: 1 addition & 1 deletion dev/cli
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ cd "$TOP_LEVEL_DIR"

export XMTPD_LOG_ENCODING=json

go run -ldflags="-X main.Commit=$(git rev-parse HEAD)" cmd/cli/main.go "$@"
go run -ldflags="-X main.Version=$(git describe HEAD --tags --long)" cmd/cli/main.go "$@"
4 changes: 2 additions & 2 deletions dev/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ WORKDIR /app
COPY . .

# Build the final node binary
ARG GIT_COMMIT=unknown
RUN go build -ldflags="-X 'main.Commit=$GIT_COMMIT'" -o bin/xmtpd cmd/replication/main.go
ARG VERSION=unknown
RUN go build -ldflags="-X 'main.Version=$VERSION'" -o bin/xmtpd cmd/replication/main.go

# ACTUAL IMAGE -------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions dev/docker/Dockerfile-cli
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ WORKDIR /app
COPY . .

# Build the final node binary
ARG GIT_COMMIT=unknown
RUN go build -ldflags="-X 'main.Commit=$GIT_COMMIT'" -o bin/xmtpd-cli cmd/cli/main.go
ARG VERSION=unknown
RUN go build -ldflags="-X 'main.Version=$VERSION'" -o bin/xmtpd-cli cmd/cli/main.go

# ACTUAL IMAGE -------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions dev/docker/build
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ set -e

DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG:-dev}"
DOCKER_IMAGE_NAME="${DOCKER_IMAGE_NAME:-xmtp/xmtpd}"
GIT_COMMIT="$(git rev-parse HEAD)"
VERSION="$(git describe HEAD --tags --long)"

docker buildx build \
--build-arg="GIT_COMMIT=${GIT_COMMIT}" \
--build-arg="VERSION=${VERSION}" \
--tag "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
-f dev/docker/Dockerfile \
.
2 changes: 1 addition & 1 deletion dev/run
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export XMTPD_PAYER_ENABLE=true
export XMTPD_REPLICATION_ENABLE=true
export XMTPD_SYNC_ENABLE=true

go run -ldflags="-X main.Commit=dev-$(git rev-parse HEAD)" cmd/replication/main.go "$@"
go run -ldflags="-X main.Version=$(git describe HEAD --tags --long)" cmd/replication/main.go "$@"
2 changes: 1 addition & 1 deletion dev/run-2
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export XMTPD_PAYER_ENABLE=true
export XMTPD_REPLICATION_ENABLE=true
export XMTPD_SYNC_ENABLE=true

go run -ldflags="-X main.Commit=dev-$(git rev-parse HEAD)" cmd/replication/main.go -p 5051 "$@"
go run -ldflags="-X main.Version=$(git describe HEAD --tags --long)" cmd/replication/main.go -p 5051 "$@"
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
)

require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/valyala/fastjson v1.6.4
Expand Down
41 changes: 41 additions & 0 deletions pkg/authn/claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package authn

import (
"fmt"
"github.com/Masterminds/semver/v3"
"github.com/golang-jwt/jwt/v5"
)

const (
// XMTPD_COMPATIBLE_VERSION_CONSTRAINT major or minor serverVersion bumps indicate backwards incompatible changes
XMTPD_COMPATIBLE_VERSION_CONSTRAINT = "~ 0.1.3"
)

type XmtpdClaims struct {
Version *semver.Version `json:"version,omitempty"`
jwt.RegisteredClaims
}

func ValidateVersionClaimIsCompatible(claims *XmtpdClaims) error {
if claims.Version == nil {
return nil
}

c, err := semver.NewConstraint(XMTPD_COMPATIBLE_VERSION_CONSTRAINT)
if err != nil {
return err
}

// SemVer implementations generally do not consider pre-releases to be valid next releases
// we use SemVer here to allow incoming connections, for which in-development nodes are acceptable
// see discussion in https://github.com/Masterminds/semver/issues/21
sanitizedVersion, err := claims.Version.SetPrerelease("")
if err != nil {
return err
}
if ok := c.Check(&sanitizedVersion); !ok {
return fmt.Errorf("serverVersion %s is not compatible", *claims.Version)
}

return nil
}
130 changes: 130 additions & 0 deletions pkg/authn/claims_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package authn

import (
"bytes"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/require"
"github.com/xmtp/xmtpd/pkg/registry"
"github.com/xmtp/xmtpd/pkg/testutils"
"os/exec"
"strings"
"testing"
)

func getLatestTag(t *testing.T) string {
// Prepare the command
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")

// Capture the output
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out

// Run the command
err := cmd.Run()
require.NoError(t, err, out.String())
return strings.TrimSpace(out.String())
}

func getLatestVersion(t *testing.T) semver.Version {
tag := getLatestTag(t)
v, err := semver.NewVersion(tag)
require.NoError(t, err)

return *v
}

func newVersionNoError(t *testing.T, version string, pre string, meta string) semver.Version {
v, err := semver.NewVersion(version)
require.NoError(t, err)

vextras, err := v.SetPrerelease(pre)
require.NoError(t, err)

vmeta, err := vextras.SetMetadata(meta)
require.NoError(t, err)

return vmeta
}

func TestClaimsVerifierNoVersion(t *testing.T) {
signerPrivateKey := testutils.RandomPrivateKey(t)

tests := []struct {
name string
version *semver.Version
wantErr bool
}{
{"no-version", nil, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tokenFactory := NewTokenFactory(signerPrivateKey, uint32(SIGNER_NODE_ID), tt.version)

verifier, nodeRegistry := buildVerifier(t, uint32(VERIFIER_NODE_ID))
nodeRegistry.EXPECT().GetNode(uint32(SIGNER_NODE_ID)).Return(&registry.Node{
SigningKey: &signerPrivateKey.PublicKey,
NodeID: uint32(SIGNER_NODE_ID),
}, nil)

token, err := tokenFactory.CreateToken(uint32(VERIFIER_NODE_ID))
require.NoError(t, err)
verificationError := verifier.Verify(token.SignedString)
if tt.wantErr {
require.Error(t, verificationError)
} else {
require.NoError(t, verificationError)
}
})
}
}

func TestClaimsVerifier(t *testing.T) {
signerPrivateKey := testutils.RandomPrivateKey(t)

currentVersion := getLatestVersion(t)

tests := []struct {
name string
version semver.Version
wantErr bool
}{
{"current-version", currentVersion, false},
{"next-patch-version", currentVersion.IncPatch(), false},
{"next-minor-version", currentVersion.IncMinor(), true},
{"next-major-version", currentVersion.IncMajor(), true},
{"last-supported-version", newVersionNoError(t, currentVersion.String(), "", ""), false},
{
"with-prerelease-version",
newVersionNoError(t, currentVersion.String(), "17-gdeadbeef", ""),
false,
},
{
"with-metadata-version",
newVersionNoError(t, currentVersion.String(), "", "branch-dev"),
false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tokenFactory := NewTokenFactory(signerPrivateKey, uint32(SIGNER_NODE_ID), &tt.version)

verifier, nodeRegistry := buildVerifier(t, uint32(VERIFIER_NODE_ID))
nodeRegistry.EXPECT().GetNode(uint32(SIGNER_NODE_ID)).Return(&registry.Node{
SigningKey: &signerPrivateKey.PublicKey,
NodeID: uint32(SIGNER_NODE_ID),
}, nil)

token, err := tokenFactory.CreateToken(uint32(VERIFIER_NODE_ID))
require.NoError(t, err)
verificationError := verifier.Verify(token.SignedString)
if tt.wantErr {
require.Error(t, verificationError)
} else {
require.NoError(t, verificationError)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/authn/signingMethod.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var (
/*
*
The JWT signing method for secp256k1. Inspired by https://github.com/ureeves/jwt-go-secp256k1/blob/master/secp256k1.go
but updated to work with the latest version of jwt-go.
but updated to work with the latest serverVersion of jwt-go.
*/
type SigningMethodSecp256k1 struct{}

Expand Down
Loading

0 comments on commit 994f91e

Please sign in to comment.