From 60ee72d192e0533bc4200db0c294b87ac530361f Mon Sep 17 00:00:00 2001 From: John Hyde Date: Tue, 9 Sep 2025 10:22:19 -0700 Subject: [PATCH] Fix byte order for Urbit Ed25519 key compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add reverseBytes helper function for key processing - Reverse authentication and encryption key bytes when returned from GraphQL - This resolves signature verification failures in downstream applications - Urbit stores Ed25519 keys in reverse byte order compared to standard implementations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index b992201..d0dd90f 100644 --- a/client.go +++ b/client.go @@ -11,17 +11,28 @@ import ( "github.com/Khan/genqlient/graphql" ) +// reverseBytes reverses the byte order of the input slice +// This is needed because Urbit stores Ed25519 keys in reverse byte order +// compared to the standard Go crypto/ed25519 implementation +func reverseBytes(input []byte) []byte { + result := make([]byte, len(input)) + for i, b := range input { + result[len(input)-1-i] = b + } + return result +} + // Default configuration constants const ( // DefaultEndpoint is the default azimuth-watcher GraphQL endpoint DefaultEndpoint = "https://azimuth.dev.vdb.to/graphql" - + // AzimuthContract is the Azimuth contract address on Ethereum mainnet AzimuthContract = "0x223c067F8CF28ae173EE5CafEa60cA44C335fecB" - + // DefaultCacheTTL is the default cache duration for ship keys DefaultCacheTTL = 1 * time.Hour - + // DefaultTimeout is the default HTTP client timeout DefaultTimeout = 30 * time.Second ) @@ -88,7 +99,7 @@ func NewClientWithOptions(opts ClientOptions) *Client { } httpClient := graphql.NewClient(opts.Endpoint, nil) - + return &Client{ gqlClient: httpClient, cache: NewKeyCache(opts.CacheTTL), @@ -319,7 +330,8 @@ func (c *Client) getShipKeys(ctx context.Context, point uint32) (*ShipKeys, erro if err != nil { return nil, fmt.Errorf("failed to decode encryption key: %w", err) } - keys.EncryptionKey = encKey + // Reverse the authentication key bytes for Urbit Ed25519 compatibility + keys.EncryptionKey = reverseBytes(encKey) } // Parse authentication key (32 bytes hex, remove 0x prefix if present) @@ -334,7 +346,8 @@ func (c *Client) getShipKeys(ctx context.Context, point uint32) (*ShipKeys, erro if len(authKey) != 32 { return nil, fmt.Errorf("invalid authentication key length: expected 32 bytes, got %d", len(authKey)) } - keys.AuthenticationKey = authKey + // Reverse the authentication key bytes for Urbit Ed25519 compatibility + keys.AuthenticationKey = reverseBytes(authKey) // Parse crypto suite version suite, err := resp.AzimuthGetKeys.Value.Value2.ToUint32() @@ -372,7 +385,7 @@ func ValidateStarSponsorship(ctx context.Context, starID uint32, expectedGalaxyI } if sponsor != expectedGalaxyID { - return fmt.Errorf("star %d is not sponsored by galaxy %d (actual sponsor: %d)", + return fmt.Errorf("star %d is not sponsored by galaxy %d (actual sponsor: %d)", starID, expectedGalaxyID, sponsor) } @@ -385,11 +398,11 @@ func ParseStarID(starIDStr string) (uint32, error) { if err != nil { return 0, fmt.Errorf("invalid star ID format: %w", err) } - + // Validate that it's actually a star (256-65535) if starPoint < 256 || starPoint > 65535 { return 0, fmt.Errorf("ID %d is not a valid star (must be 256-65535)", starPoint) } - + return uint32(starPoint), nil -} \ No newline at end of file +}