Skip to content
Merged
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
122 changes: 111 additions & 11 deletions hash/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"strings"
"time"
)

/*
Package hash provides utilities for generating cryptographic hashes.
Package hash provides utilities for generating cryptographic hashes and
signing HTTP requests with HMAC-based authentication headers.

This file contains a function to generate a SHA-256 HMAC (Hash-based Message Authentication Code)
from a secret and one or more input strings.
This file contains:
- Functions to generate a SHA-256 HMAC (Hash-based Message Authentication Code)
from a secret and one or more input strings.
- An interface and implementation for attaching authentication headers to HTTP requests.

# Usage

Basic HMAC Generation:

import (
"fmt"
"yourmodule/hash"
Expand All @@ -30,6 +37,20 @@ from a secret and one or more input strings.
fmt.Println("HMAC:", signature)
}

Signing HTTP Requests:

import (
"net/http"
"yourmodule/hash"
)

func main() {
gen := hash.NewGenerator("api-key-id", "supersecret")
req, _ := http.NewRequest("GET", "https://api.example.com/resource", nil)
signedReq := gen.AddAuthHeaders(req)
// signedReq now contains x-signature, x-api-key-id, and x-timestamp headers
}

# Function Details

- GenerateSHA256HMAC(secret string, v ...string) string
Expand All @@ -38,8 +59,36 @@ from a secret and one or more input strings.
- v: Variadic string arguments to be concatenated and signed.

Returns a hex-encoded string representing the HMAC-SHA256 signature.

- Generator interface

- AddAuthHeaders(r *http.Request) *http.Request
Adds authentication headers to the provided HTTP request.

- NewGenerator(id, secret string) Generator

- id: API key identifier.
- secret: Secret key for HMAC signing.

Returns a Generator instance for signing HTTP requests.
*/

// generateSHA256HMAC computes the raw SHA-256 HMAC for the concatenated input strings using the provided secret key.
// Returns the HMAC as a byte slice (not hex-encoded).
func generateSHA256HMAC(secret string, v ...string) []byte {
// Concatenate all input strings into a single string
raw := strings.Join(v, "")

// Create a new HMAC hasher using SHA-256 and the provided secret key
h := hmac.New(sha256.New, []byte(secret))

// Write the concatenated string to the hasher
h.Write([]byte(raw))

// Return the raw HMAC bytes
return h.Sum(nil)
}

// GenerateSHA256HMAC generates a SHA-256 HMAC signature for the given input strings using the provided secret key.
// The input strings are concatenated in the order provided, and the resulting string is signed.
// Returns the signature as a hex-encoded string.
Expand All @@ -53,15 +102,66 @@ from a secret and one or more input strings.
// sig := GenerateSHA256HMAC("mysecret", "foo", "bar")
// // sig now contains the hex-encoded HMAC of "foobar" using "mysecret" as the key.
func GenerateSHA256HMAC(secret string, v ...string) string {
// Concatenate all input strings into a single string
raw := strings.Join(v, "")
// Compute the HMAC and return it as a hex-encoded string
return hex.EncodeToString(generateSHA256HMAC(secret, v...))
}

// Create a new HMAC hasher using SHA-256 and the provided secret key
h := hmac.New(sha256.New, []byte(secret))
// Generator defines an interface for adding authentication headers to HTTP requests.
// Implementations should add at least a signature, API key ID, and timestamp.
type Generator interface {
// AddAuthHeaders adds authentication headers to the provided HTTP request and returns it.
AddAuthHeaders(r *http.Request) *http.Request
}

// Write the concatenated string to the hasher
h.Write([]byte(raw))
// generator is a concrete implementation of the Generator interface.
// It holds the API key ID and secret used for signing requests.
type generator struct {
id string // API key identifier
secret string // Secret key for HMAC signing
}

// Compute the HMAC and return it as a hex-encoded string
return hex.EncodeToString(h.Sum(nil))
// AddAuthHeaders attaches authentication headers to the given HTTP request.
// The following headers are added:
// - x-signature: HMAC-SHA256 signature of the HTTP method, path, and timestamp
// - x-api-key-id: The API key identifier
// - x-timestamp: The current timestamp in RFC3339 format
//
// The signature is computed as HMAC(secret, method + path + timestamp).
func (g *generator) AddAuthHeaders(r *http.Request) *http.Request {
// use RFC3339 format for the time stamp in the header
timeStamp := time.Now().Format(time.RFC3339)

// Compute the signature using HTTP method, path, and timestamp
sig := GenerateSHA256HMAC(g.secret, r.Method, r.URL.Path, timeStamp)

// Add the computed signature to the request headers
r.Header.Add("x-signature", sig)

// Add the API key ID to the request headers
r.Header.Add("x-api-key-id", g.id)

// add timestamp to header
r.Header.Add("x-timestamp", timeStamp)
return r
}

// NewGenerator creates a new Generator instance for signing HTTP requests.
//
// Parameters:
// - id: API key identifier
// - secret: Secret key for HMAC signing
//
// Returns:
// - Generator: An instance that can add authentication headers to HTTP requests.
//
// Example:
//
// gen := hash.NewGenerator("api-key-id", "supersecret")
// req, _ := http.NewRequest("GET", "https://api.example.com/resource", nil)
// signedReq := gen.AddAuthHeaders(req)
func NewGenerator(id, secret string) Generator {
return &generator{
id: id,
secret: secret,
}
}
148 changes: 148 additions & 0 deletions hash/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved
// Author: Prabhjot Singh Sethi <prabhjot.sethi@gmail.com>
//
// This file is part of the hash package, which provides cryptographic utilities
// for validating HMAC-SHA256 signatures and authenticating HTTP requests.

package hash

import (
"crypto/hmac"
"encoding/hex"
"fmt"
"net/http"
"time"
)

/*
Package hash provides utilities for validating cryptographic signatures
on HTTP requests using HMAC-SHA256.

This file contains:
- An interface and implementation for validating authentication headers on HTTP requests.
- Logic to check the signature, timestamp, and expiration of requests.

# Usage

import (
"net/http"
"yourmodule/hash"
)

func main() {
// validity is the allowed time window (in seconds) for a request to be considered valid
validator := hash.NewValidator(60) // 60 seconds validity

// Assume req is an *http.Request with authentication headers
ok, err := validator.Validate(req, "supersecret")
if !ok {
fmt.Println("Validation failed:", err)
return
}
fmt.Println("Request is valid!")
}

# Function Details

- Validator interface

- Validate(r *http.Request, secret string) (bool, error)
Validates the authentication headers on the provided HTTP request.

- NewValidator(validity int64) Validator

- validity: Allowed time window (in seconds) for the request to be valid.

Returns a Validator instance for validating HTTP requests.
*/

// Validator defines an interface for validating authentication headers on HTTP requests.
type Validator interface {
// Validate checks the HMAC signature, timestamp, and expiration of the request.
// Returns true if valid, false and an error otherwise.
Validate(r *http.Request, secret string) (bool, error)
}

// validator is a concrete implementation of the Validator interface.
// It holds the allowed validity window (in seconds) for request timestamps.
type validator struct {
validity int64 // Allowed time window (in seconds) for request validity
}

// Validate checks the HMAC signature, timestamp, and expiration of the HTTP request.
//
// Steps performed:
// 1. Ensures required headers are present: x-signature and x-timestamp.
// 2. Decodes the hex-encoded signature from the x-signature header.
// 3. Parses the timestamp from the x-timestamp header (RFC3339 format).
// 4. Checks if the request is within the allowed validity window.
// 5. Recomputes the expected HMAC signature and compares it to the provided signature.
//
// Parameters:
// - r: The HTTP request to validate.
// - secret: The secret key used for HMAC validation.
//
// Returns:
// - bool: true if the request is valid, false otherwise.
// - error: Reason for validation failure, if any.
func (v *validator) Validate(r *http.Request, secret string) (bool, error) {
// Ensure headers are present
if len(r.Header) == 0 {
return false, fmt.Errorf("missing required headers")
}

// Retrieve the signature from the header
sigStr := r.Header.Get("x-signature")
if sigStr == "" {
return false, fmt.Errorf("missing signature header")
}

// Decode the hex-encoded signature
sig, err := hex.DecodeString(sigStr)
if err != nil {
return false, fmt.Errorf("invalid signature format")
}

// Retrieve the timestamp from the header
timeStr := r.Header.Get("x-timestamp")
if timeStr == "" {
return false, fmt.Errorf("missing timestamp header")
}

// Parse the timestamp (RFC3339 format)
timeStamp, err := time.Parse(time.RFC3339, timeStr)
if err != nil {
return false, fmt.Errorf("error parsing timestamp: %s", err)
}

// Check if the request is within the allowed validity window
now := time.Now().Unix()
if now >= (timeStamp.Unix() + v.validity) {
return false, fmt.Errorf("expired access")
}

// Recompute the expected HMAC signature using the method, path, and timestamp
if !hmac.Equal(sig, generateSHA256HMAC(secret, r.Method, r.URL.Path, timeStr)) {
return false, fmt.Errorf("invalid hmac signature")
}

return true, nil
}

// NewValidator creates a new Validator instance for validating HTTP requests.
//
// Parameters:
// - validity: Allowed time window (in seconds) for the request to be valid.
//
// Returns:
// - Validator: An instance that can validate authentication headers on HTTP requests.
//
// Example:
//
// validator := hash.NewValidator(60) // 60 seconds validity
// ok, err := validator.Validate(req, "supersecret")
func NewValidator(validity int64) Validator {
return &validator{
validity: validity,
}
}
49 changes: 49 additions & 0 deletions hash/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved
// Author: Prabhjot Singh Sethi <prabhjot.sethi@gmail.com>

package hash

import (
"net/http/httptest"
"testing"
"time"
)

// TestGeneratorAndValidator demonstrates signing an HTTP request with Generator
// and validating it with Validator.
func TestGeneratorAndValidator(t *testing.T) {
// Setup
apiKeyID := "test-key"
secret := "supersecret"
validity := int64(60) // 60 seconds validity window

// Create a new HTTP request
req := httptest.NewRequest("GET", "https://api.example.com/resource", nil)

// Sign the request using the Generator
gen := NewGenerator(apiKeyID, secret)
signedReq := gen.AddAuthHeaders(req)

// Validate the signed request using the Validator
validator := NewValidator(validity)
ok, err := validator.Validate(signedReq, secret)
if !ok {
t.Fatalf("Validation failed: %v", err)
}

// Tamper with the signature to ensure validation fails
signedReq.Header.Set("x-signature", "deadbeef")
ok, err = validator.Validate(signedReq, secret)
if ok || err == nil {
t.Fatalf("Expected validation to fail for tampered signature")
}

// Tamper with the timestamp to simulate expiration
signedReq = gen.AddAuthHeaders(req) // re-sign to get a valid signature
oldTime := time.Now().Add(-2 * time.Minute).Format(time.RFC3339)
signedReq.Header.Set("x-timestamp", oldTime)
ok, err = validator.Validate(signedReq, secret)
if ok || err == nil || err.Error() != "expired access" {
t.Fatalf("Expected expired access error, got: %v", err)
}
}