intermediate sdkgogolangapi-reference

Go SDK Reference

Complete API reference for the Sigil Auth Go SDK

Go SDK Reference

The sigilauth package provides an idiomatic Go client for integrating Sigil Auth into your applications.

[!NOTE] Pre-release: The SDK is not yet published as a standalone Go module. For local development, use a replace directive in your go.mod:

replace github.com/sigilauth/server/sdk-go => /path/to/sigilauth/server/sdk-go

Installation

Once published:

go get github.com/sigilauth/server/sdk-go

Requires Go 1.21 or later.

Quick Example

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/sigilauth/server/sdk-go"
)

func main() {
	client, err := sigilauth.New(sigilauth.Config{
		ServiceURL:  "https://sigil.example.com",
		APIKey:      os.Getenv("SIGIL_API_KEY"),
		HTTPTimeout: 30 * time.Second,
	})
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	challenge, err := client.Auth.CreateChallenge(ctx, &sigilauth.ChallengeRequest{
		Fingerprint:     "a1b2c3...",
		DevicePublicKey: "Ag8xYz...",
		Action: sigilauth.Action{
			Type:        "step_up",
			Description: "Transfer $5000 to external account",
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	result, err := client.Auth.AwaitResult(ctx, challenge.ChallengeID, nil)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Status:", result.Status)
}

Configuration

sigilauth.New(config)

Creates a new Sigil Auth client.

Type: func New(config Config) (*Client, error)

Config struct:

type Config struct {
	ServiceURL      string
	APIKey          string
	HTTPTimeout     time.Duration
	TLSCertPinning  []string  // Optional PEM-encoded certs
}
Field Type Required Description
ServiceURL string Yes HTTPS URL of your Sigil service
APIKey string Yes Load from env (os.Getenv("SIGIL_API_KEY"))
HTTPTimeout time.Duration Yes Request timeout (recommend 30 * time.Second)
TLSCertPinning []string No Optional PEM-encoded certificates for pinning

Returns:

  • *Client — Configured client with Auth, MPA, and Webhooks services
  • error — Config validation error

Example:

client, err := sigilauth.New(sigilauth.Config{
	ServiceURL:  "https://sigil.example.com",
	APIKey:      os.Getenv("SIGIL_API_KEY"),
	HTTPTimeout: 30 * time.Second,
})
if err != nil {
	log.Fatal(err)
}

Security:

The SDK enforces TLS 1.2+ and validates certificates by default. You cannot disable TLS verification.

Expected API key format:

sgk_test_<64 hex characters>
sgk_live_<64 hex characters>

Authentication API

client.Auth.CreateChallenge(ctx, request)

Creates a new authentication challenge and sends a push notification to the device.

Type: func (s *AuthService) CreateChallenge(ctx context.Context, req *ChallengeRequest) (*ChallengeResult, error)

Request struct:

type ChallengeRequest struct {
	Fingerprint     string `json:"fingerprint"`
	DevicePublicKey string `json:"device_public_key"`
	Action          Action `json:"action,omitempty"`
}

type Action struct {
	Type        string                 `json:"type,omitempty"`
	Description string                 `json:"description,omitempty"`
	Params      map[string]interface{} `json:"params,omitempty"`
}
Field Type Required Description
Fingerprint string Yes SHA-256 hash of device public key (64 hex chars)
DevicePublicKey string Yes Device’s P-256 public key (base64, 33 bytes compressed)
Action Action No Context shown to user on approval screen
Action.Type string No Action type (e.g., "step_up", "delete_account")
Action.Description string No Human-readable description
Action.Params map[string]interface{} No Additional structured context

Returns: *ChallengeResult

type ChallengeResult struct {
	ChallengeID         string   `json:"challenge_id"`
	Fingerprint         string   `json:"fingerprint"`
	Pictogram           []string `json:"pictogram"`
	PictogramSpeakable  string   `json:"pictogram_speakable"`
	ExpiresAt           string   `json:"expires_at"`
}
Field Type Description
ChallengeID string UUID for this challenge
Fingerprint string Device fingerprint (echoed back)
Pictogram []string Five emoji identifying this challenge
PictogramSpeakable string Space-separated words (e.g., "apple banana plane car dog")
ExpiresAt string ISO 8601 timestamp when challenge expires (5 minutes)

Errors:

Returns error for HTTP errors:

  • Invalid fingerprint format or mismatch
  • Missing/invalid API key
  • Device not registered
  • Rate limit exceeded

Example:

challenge, err := client.Auth.CreateChallenge(ctx, &sigilauth.ChallengeRequest{
	Fingerprint:     "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
	DevicePublicKey: "Ag8xYzI3ZWRkNDUzYmNlYzVmMTJjNmI5MzA4OGY0ZjIxYWI5NzM4NTJhOWU2ZGUzMg==",
	Action: sigilauth.Action{
		Type:        "step_up",
		Description: "Transfer $5000 to external account",
		Params: map[string]interface{}{
			"recipient":  "sarah@example.com",
			"amount_usd": 5000,
		},
	},
})
if err != nil {
	log.Fatal(err)
}

fmt.Println("Challenge ID:", challenge.ChallengeID)
fmt.Println("Pictogram:", challenge.Pictogram)
fmt.Println("Speakable:", challenge.PictogramSpeakable)

client.Auth.GetStatus(ctx, challengeID)

Checks the current status of a challenge.

Type: func (s *AuthService) GetStatus(ctx context.Context, challengeID string) (*ChallengeStatus, error)

Parameters:

Name Type Required Description
challengeID string Yes UUID from CreateChallenge

Returns: *ChallengeStatus

type ChallengeStatus struct {
	Status      string `json:"status"`
	ChallengeID string `json:"challenge_id"`
	Fingerprint string `json:"fingerprint"`
	CreatedAt   string `json:"created_at"`
	ExpiresAt   string `json:"expires_at"`
	RespondedAt string `json:"responded_at,omitempty"`
}
Field Type Description
Status string "pending", "verified", "rejected", "expired"
ChallengeID string UUID
Fingerprint string Device fingerprint
CreatedAt string ISO 8601 timestamp
ExpiresAt string ISO 8601 timestamp
RespondedAt string When device responded (empty if pending)

Example:

status, err := client.Auth.GetStatus(ctx, "550e8400-e29b-41d4-a716-446655440000")
if err != nil {
	log.Fatal(err)
}

if status.Status == "verified" {
	fmt.Println("User approved at", status.RespondedAt)
}

client.Auth.AwaitResult(ctx, challengeID, options)

Polls for challenge completion. Blocks until the user approves, denies, or the challenge expires.

Type: func (s *AuthService) AwaitResult(ctx context.Context, challengeID string, opts *AwaitOptions) (*ChallengeStatus, error)

Parameters:

type AwaitOptions struct {
	PollInterval int  // Milliseconds between status checks
	MaxAttempts  int  // Maximum number of poll attempts
}
Field Type Required Default Description
PollInterval int No 1000 Milliseconds between checks
MaxAttempts int No 30 Maximum poll attempts

Pass nil for opts to use defaults (30 seconds total: 30 attempts × 1 second).

Returns: *ChallengeStatus

Returns when status becomes verified, rejected, or expired.

Returns ErrMaxAttemptsReached if polling exhausts max attempts before reaching a terminal state.

Respects ctx.Done() for early cancellation.

Example:

result, err := client.Auth.AwaitResult(ctx, challenge.ChallengeID, &sigilauth.AwaitOptions{
	PollInterval: 1000, // 1 second
	MaxAttempts:  60,   // 60 seconds total
})
if err != nil {
	if errors.Is(err, sigilauth.ErrMaxAttemptsReached) {
		fmt.Println("Polling timeout")
	} else {
		log.Fatal(err)
	}
	return
}

switch result.Status {
case "verified":
	fmt.Println("User approved!")
case "rejected":
	fmt.Println("User denied the request.")
case "expired":
	fmt.Println("Challenge expired.")
}

Using context timeout:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()

result, err := client.Auth.AwaitResult(ctx, challenge.ChallengeID, nil)
if err != nil {
	if errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("Context timeout")
	} else {
		log.Fatal(err)
	}
}

Multi-Party Approval (MPA)

client.MPA.Request(ctx, request)

Creates a multi-party approval request. Challenges are sent to multiple devices. The request is approved when M out of N groups approve.

Type: func (s *MPAService) Request(ctx context.Context, req *MPARequest) (*MPAResult, error)

Request struct:

type MPARequest struct {
	Required          int             `json:"required"`
	Groups            []ApprovalGroup `json:"groups"`
	Action            Action          `json:"action"`
	ExpiresInSeconds  int             `json:"expires_in_seconds,omitempty"`
}

type ApprovalGroup struct {
	Name      string     `json:"name"`
	Approvers []Approver `json:"approvers"`
}

type Approver struct {
	Fingerprint     string `json:"fingerprint"`
	DevicePublicKey string `json:"device_public_key"`
}
Field Type Required Description
Required int Yes Number of groups that must approve (M)
Groups []ApprovalGroup Yes Array of approval groups (N total)
Action Action Yes Action context shown on all devices
ExpiresInSeconds int No Time to approve (default: 300, min: 60, max: 900)

Returns: *MPAResult

type MPAResult struct {
	RequestID      string `json:"request_id"`
	GroupsRequired int    `json:"groups_required"`
	GroupsTotal    int    `json:"groups_total"`
	ExpiresAt      string `json:"expires_at"`
}

Example:

mpaRequest, err := client.MPA.Request(ctx, &sigilauth.MPARequest{
	Required: 2,
	Groups: []sigilauth.ApprovalGroup{
		{
			Name: "Engineering Leads",
			Approvers: []sigilauth.Approver{
				{
					Fingerprint:     "abc123...",
					DevicePublicKey: "Ag8xYz...",
				},
			},
		},
		{
			Name: "Security Team",
			Approvers: []sigilauth.Approver{
				{
					Fingerprint:     "def456...",
					DevicePublicKey: "AhMxNj...",
				},
			},
		},
		{
			Name: "Operations",
			Approvers: []sigilauth.Approver{
				{
					Fingerprint:     "ghi789...",
					DevicePublicKey: "AjQyOT...",
				},
				{
					Fingerprint:     "jkl012...",
					DevicePublicKey: "AkU3MT...",
				},
			},
		},
	},
	Action: sigilauth.Action{
		Type:        "cold_boot",
		Description: "Cold boot engine ENG-001",
		Params: map[string]interface{}{
			"engine_id": "ENG-001",
			"reason":    "Scheduled maintenance",
		},
	},
	ExpiresInSeconds: 600, // 10 minutes
})
if err != nil {
	log.Fatal(err)
}

fmt.Println("Request ID:", mpaRequest.RequestID)

client.MPA.GetStatus(ctx, requestID)

Checks the current status of an MPA request.

Type: func (s *MPAService) GetStatus(ctx context.Context, requestID string) (*MPAStatus, error)

Returns: *MPAStatus

type MPAStatus struct {
	Status           string   `json:"status"`
	RequestID        string   `json:"request_id"`
	GroupsRequired   int      `json:"groups_required"`
	GroupsSatisfied  []string `json:"groups_satisfied,omitempty"`
	CreatedAt        string   `json:"created_at"`
	ExpiresAt        string   `json:"expires_at"`
	CompletedAt      string   `json:"completed_at,omitempty"`
}
Field Type Description
Status string "pending", "approved", "rejected", "timeout"
RequestID string UUID
GroupsRequired int M
GroupsSatisfied []string Names of groups that have approved
CreatedAt string ISO 8601 timestamp
ExpiresAt string ISO 8601 timestamp
CompletedAt string When request reached terminal state

Example:

status, err := client.MPA.GetStatus(ctx, "7c3e5f9a-2b4d-4a1e-8f6c-9d2a1b3c4e5f")
if err != nil {
	log.Fatal(err)
}

fmt.Printf("%d of %d groups approved\n", len(status.GroupsSatisfied), status.GroupsRequired)

client.MPA.AwaitResult(ctx, requestID, options)

Polls for MPA completion.

Type: func (s *MPAService) AwaitResult(ctx context.Context, requestID string, opts *AwaitOptions) (*MPAStatus, error)

Parameters:

Same AwaitOptions as Auth. Default: PollInterval: 1000ms, MaxAttempts: 30.

Returns: *MPAStatus

Example:

result, err := client.MPA.AwaitResult(ctx, mpaRequest.RequestID, &sigilauth.AwaitOptions{
	PollInterval: 3000, // 3 seconds (MPA is slower)
	MaxAttempts:  60,   // 3 minutes total
})
if err != nil {
	log.Fatal(err)
}

if result.Status == "approved" {
	fmt.Println("MPA approved by", result.GroupsSatisfied)
	// Proceed with the action
} else if result.Status == "rejected" {
	fmt.Println("At least one group denied.")
} else {
	fmt.Println("Request timed out.")
}

Webhooks

client.Webhooks.Verify(headers, body, secret)

Verifies the HMAC signature on a webhook delivery from Sigil.

Type: func (s *WebhookService) Verify(headers http.Header, body []byte, secret string) error

Parameters:

Name Type Required Description
headers http.Header Yes HTTP headers from the webhook request
body []byte Yes Raw request body (not parsed JSON)
secret string Yes Webhook secret from Sigil config

Returns: error

  • nil if signature is valid
  • ErrMissingSignature, ErrMissingTimestamp, ErrInvalidSignature on failure

Example:

func handleWebhook(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read body", http.StatusBadRequest)
		return
	}

	if err := client.Webhooks.Verify(r.Header, body, os.Getenv("SIGIL_WEBHOOK_SECRET")); err != nil {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}

	var event WebhookEvent
	if err := json.Unmarshal(body, &event); err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	log.Printf("Webhook event: %s", event.Type)
	w.WriteHeader(http.StatusOK)
}

client.Webhooks.VerifyTimestamp(timestamp, maxAge)

Checks if the webhook timestamp is within acceptable range.

Type: func (s *WebhookService) VerifyTimestamp(timestamp string, maxAge int) error

Parameters:

Name Type Required Description
timestamp string Yes Value of X-Sigil-Timestamp header (Unix timestamp)
maxAge int Yes Maximum age in seconds (recommend: 300)

Returns: error

  • nil if timestamp is valid
  • ErrInvalidTimestamp, ErrTimestampTooOld, ErrTimestampInFuture on failure

Example:

timestamp := r.Header.Get("X-Sigil-Timestamp")
if err := client.Webhooks.VerifyTimestamp(timestamp, 300); err != nil {
	http.Error(w, "Timestamp validation failed", http.StatusUnauthorized)
	return
}

Error Handling

All SDK methods return standard Go errors. Check for sentinel errors where appropriate:

import "errors"

result, err := client.Auth.AwaitResult(ctx, challengeID, nil)
if err != nil {
	if errors.Is(err, sigilauth.ErrMaxAttemptsReached) {
		// Handle polling timeout
	} else if errors.Is(err, context.DeadlineExceeded) {
		// Handle context timeout
	} else {
		// Handle other errors
		log.Fatal(err)
	}
}

Sentinel errors:

  • sigilauth.ErrMaxAttemptsReached — Polling exhausted max attempts
  • sigilauth.ErrMissingSignature — Webhook missing signature header
  • sigilauth.ErrMissingTimestamp — Webhook missing timestamp header
  • sigilauth.ErrInvalidSignature — Webhook signature verification failed
  • sigilauth.ErrInvalidTimestamp — Webhook timestamp is malformed
  • sigilauth.ErrTimestampTooOld — Webhook timestamp exceeds max age
  • sigilauth.ErrTimestampInFuture — Webhook timestamp is in the future

Next Steps