The go-bitcoin-auth
library simplifies authenticating REST APIs using Bitcoin keys by generating and verifying cryptographic signatures in an X-Auth-Token
header.
To install the go-bitcoin-auth
library, use go get
:
go get github.com/b-open-io/go-bitcoin-auth
Import the library and generate a token:
import (
"fmt"
"time" // Required for timestamp in AuthPayload if you construct it manually for verification
auth "github.com/b-open-io/go-bitcoin-auth"
)
func main() {
privateKeyWif := "yourPrivateKeyWif" // Replace with your actual WIF private key
requestPath := "/some/api/path?param1=value1"
bodyString := `{"key":"value"}` // Optional: include if your request has a body
// Generate token with body, default scheme (brc77) and encoding (utf8)
configWithBody := auth.AuthConfig{
PrivateKeyWIF: privateKeyWif,
RequestPath: requestPath,
Body: bodyString,
}
token, err := auth.GetAuthToken(configWithBody)
if err != nil {
fmt.Println("Error generating token:", err)
return
}
fmt.Println("Generated Token:", token)
// Generate token without body, using bsm scheme
configNoBodyBsm := auth.AuthConfig{
PrivateKeyWIF: privateKeyWif,
RequestPath: requestPath,
Scheme: auth.SchemeBSM,
}
tokenNoBody, err := auth.GetAuthToken(configNoBodyBsm)
if err != nil {
fmt.Println("Error generating token (no body, bsm):", err)
return
}
fmt.Println("Generated Token (No Body, BSM):", tokenNoBody)
// Include it in your API request header: 'X-Auth-Token': token
}
- Auth Token Generation & Verification: Simple functions for handling tokens.
- Dual Cryptographic Schemes: Supports legacy 'bsm' (Bitcoin Signed Message) and modern 'brc77' (recommended, based on BRC-77).
- Minimal Dependencies: Designed to be lightweight.
Tokens include:
- Public key derived from the private key (hex-encoded, compressed)
- Request path (including query parameters)
- ISO8601 timestamp (e.g.,
2006-01-02T15:04:05.000Z
) - SHA256 hash of the body (if present, hex-encoded)
- Signing scheme ('bsm' or 'brc77')
Token format:
pubkey|scheme|timestamp|requestPath|signature
Cryptographic schemes:
auth.SchemeBRC77
("brc77"
): Default and recommended.auth.SchemeBSM
("bsm"
): Legacy Bitcoin Signed Message.
import (
"fmt"
auth "github.com/b-open-io/go-bitcoin-auth"
)
func main() {
// Assume 'tokenWithBody' is a previously generated token string
tokenWithBody := "yourGeneratedTokenString" // Replace with an actual token
parsedToken, err := auth.ParseAuthToken(tokenWithBody)
if err != nil {
fmt.Println("Failed to parse bitcoin-auth token:", err)
return
}
fmt.Printf("Parsed Token: Pubkey: %s, Scheme: %s, Timestamp: %s, Path: %s\n",
parsedToken.Pubkey, parsedToken.Scheme, parsedToken.Timestamp, parsedToken.RequestPath)
}
import (
"fmt"
"time"
auth "github.com/b-open-io/go-bitcoin-auth"
)
func main() {
// Assume 'tokenWithBody' and 'tokenNoBodyBsm' are previously generated token strings
// Assume 'privateKeyWif', 'requestPath', and 'bodyString' are defined as in token generation example
privateKeyWif := "yourPrivateKeyWif"
requestPath := "/some/api/path?param1=value1"
bodyString := `{"key":"value"}`
// Example: Generate a token to verify (replace with actual token in practice)
configForVerification := auth.AuthConfig{
PrivateKeyWIF: privateKeyWif,
RequestPath: requestPath,
Body: bodyString,
}
tokenWithBody, _ := auth.GetAuthToken(configForVerification)
configNoBodyForVerification := auth.AuthConfig{
PrivateKeyWIF: privateKeyWif,
RequestPath: requestPath,
Scheme: auth.SchemeBSM,
}
tokenNoBodyBsm, _ := auth.GetAuthToken(configNoBodyForVerification)
// Verification payload for a token with a body
// IMPORTANT: Timestamp should be the server's current time when receiving the request
payloadForVerification := auth.AuthPayload{
RequestPath: requestPath,
Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
Body: bodyString, // Original, unencoded body string
}
isValid, err := auth.VerifyAuthToken(tokenWithBody, payloadForVerification,
auth.WithTimePad(5*time.Minute), // Allow 5-minute clock skew
auth.WithBodyEncoding(auth.EncodingUTF8), // Specify body encoding if not default
)
if err != nil {
fmt.Println("Error verifying token:", err)
} else {
fmt.Println("Token with body is valid:", isValid)
}
// Verification payload for a token without a body
payloadNoBody := auth.AuthPayload{
RequestPath: requestPath,
Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
}
isValidNoBody, err := auth.VerifyAuthToken(tokenNoBodyBsm, payloadNoBody, auth.WithTimePad(5*time.Minute))
if err != nil {
fmt.Println("Error verifying token (no body):", err)
} else {
fmt.Println("Token no body (BSM) is valid:", isValidNoBody)
}
}
Security Note: Always handle your WIF private key securely. Never expose it in client-side code or commit it to version control without appropriate safeguards.
Generates an authentication token.
config
: AnAuthConfig
struct containing all necessary parameters:PrivateKeyWIF
: Private key in WIF (Wallet Import Format). (Required)RequestPath
: The full URL path, including any query parameters. (Required)Body
: The request body as a string. Pass an empty string if no body. (Optional)Scheme
: Signing scheme (auth.SchemeBRC77
orauth.SchemeBSM
). Defaults toauth.SchemeBRC77
if an empty string is provided inconfig.Scheme
.BodyEncoding
: Encoding for theBody
(auth.EncodingUTF8
,auth.EncodingHex
, orauth.EncodingBase64
). Defaults toauth.EncodingUTF8
if an empty string is provided inconfig.BodyEncoding
. Relevant only ifBody
is not empty.
Returns the generated token string and an error if one occurred.
Verifies an authentication token.
tokenString
: The authentication token string to verify.target
: AnAuthPayload
struct containing the expected values forRequestPath
,Timestamp
, andBody
(if applicable). The timestamp intarget
should be the current time on the server (or the time the request was received), which will be compared against the timestamp in the token.opts
: Variadic functional options:auth.WithTimePad(duration time.Duration)
: Sets the allowed time difference (clock skew) between the token's timestamp andtarget.Timestamp
. Defaults to 5 minutes.auth.WithBodyEncoding(encoding string)
: Sets the encoding oftarget.Body
if present (must be one ofauth.EncodingUTF8
,auth.EncodingHex
,auth.EncodingBase64
). Defaults toauth.EncodingUTF8
. This is used to decodetarget.Body
before hashing for comparison if the token includes a body hash.
Returns true
if the token is valid, false
otherwise. An error is returned if parsing fails or an unexpected issue occurs during verification (e.g., invalid encoding specified). A false
result with nil
error means the token is structurally valid but does not match the target payload (e.g., signature mismatch, timestamp out of range, path mismatch, body hash mismatch).
Parses a token string into an AuthToken
struct.
tokenString
: The authentication token string.
Returns a pointer to an AuthToken
struct ({ Pubkey, Scheme, Timestamp, RequestPath, Signature }
) or nil
and an error if parsing fails (e.g., invalid format, unknown scheme).
package auth // Assuming these types are in the 'auth' package
// AuthToken represents the parsed structure of an authentication token.
type AuthToken struct {
Pubkey string
Scheme string
Timestamp string // ISO8601 format (e.g., "2006-01-02T15:04:05.000Z")
RequestPath string
Signature string // Base64 encoded signature
}
Data required for signing (implicitly via AuthConfig
) and verification.
package auth // Assuming these types are in the 'auth' package
// AuthPayload represents the data used for verifying a token against.
type AuthPayload struct {
RequestPath string
Timestamp string // ISO8601 format (e.g., "2006-01-02T15:04:05.000Z"); for verification, this is the server's current/target time
Body string // Optional request body string, matching the encoding specified during verification
}
Example AuthPayload
construction:
import (
"time"
auth "github.com/b-open-io/go-bitcoin-auth" // Assuming your types are accessible via this import
)
// Payload with a body
payloadWithBody := auth.AuthPayload{
RequestPath: "/api/items",
Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
Body: `{"name":"gadget","price":9.99}`,
}
// Payload without a body
payloadNoBody := auth.AuthPayload{
RequestPath: "/api/items/123",
Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
}
Ensure you have Go installed.
To build the library:
go build ./...
To run tests:
go test ./...