nanoca

package module
v0.0.0-...-2285290 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 3, 2026 License: MIT Imports: 20 Imported by: 0

README

nanoca

A lightweight enterprise ACME Certificate Authority service with device attestation support. It provides just the HTTP handlers needed to implement ACME, it is intended to be integrated into nanomdm or another service of your choosing. Storage, signing, authorization, and logging are implemented as pluggable interfaces to integrate into a wide variety of environments.

Usage

import (
	"github.com/brandonweeks/nanoca"
	"github.com/brandonweeks/nanoca/authorizers/null"
	"github.com/brandonweeks/nanoca/issuers/inprocess"
	"github.com/brandonweeks/nanoca/signers/file"
	"github.com/brandonweeks/nanoca/storage/badger"
)

signer, _ := file.LoadSigner("rootCA.key")
storage, _ := badger.New(badger.Options{InMemory: true})

ca, _ := nanoca.New(
	inprocess.New(signer),
	null.New(),
	storage,
	"https://localhost:8443",
	nanoca.WithPrefix("/acme"),
)
defer ca.Close()

mux := http.NewServeMux()
mux.Handle("/", ca.Handler())

Documentation

Index

Constants

View Source
const (
	OrderStatusPending    = "pending"
	OrderStatusReady      = "ready"
	OrderStatusProcessing = "processing"
	OrderStatusValid      = "valid"
	OrderStatusInvalid    = "invalid"
)
View Source
const (
	IdentifierTypePermanentIdentifier = "permanent-identifier"
	IdentifierTypeHardwareModule      = "hardware-module"
)
View Source
const (
	AuthzStatusPending = "pending"
	AuthzStatusValid   = "valid"
	AuthzStatusInvalid = "invalid"
	AuthzStatusExpired = "expired"
)
View Source
const (
	ChallengeStatusPending    = "pending"
	ChallengeStatusProcessing = "processing"
	ChallengeStatusValid      = "valid"
	ChallengeStatusInvalid    = "invalid"
)
View Source
const (
	// ACMEProblemTypePrefix is the URN prefix for ACME error types
	ACMEProblemTypePrefix = "urn:ietf:params:acme:error:"
)
View Source
const (
	ChallengeTypeDeviceAttest01 = "device-attest-01"
)

Variables

View Source
var (
	ErrNonceNotFound = errors.New("nonce not found")
	ErrNonceExpired  = errors.New("nonce expired")
)

Functions

func WithAccountID

func WithAccountID(ctx context.Context, id string) context.Context

func WithOrderID

func WithOrderID(ctx context.Context, id string) context.Context

Types

type Account

type Account struct {
	ID                   string           `json:"id"`                 // Include in storage
	Key                  *jose.JSONWebKey `json:"key,omitempty"`      // Include in storage
	KeyBytes             []byte           `json:"keyBytes,omitempty"` // Include in storage
	Status               string           `json:"status"`
	Contact              []string         `json:"contact,omitempty"`
	TermsOfServiceAgreed bool             `json:"termsOfServiceAgreed,omitempty"`
	Orders               string           `json:"orders,omitempty"`
	CreatedAt            time.Time        `json:"createdAt"` // Include in storage
}

type AccountRequest

type AccountRequest struct {
	Contact              []string `json:"contact,omitempty"`
	TermsOfServiceAgreed bool     `json:"termsOfServiceAgreed,omitempty"`
	OnlyReturnExisting   bool     `json:"onlyReturnExisting,omitempty"`
}

type AttestationObject

type AttestationObject struct {
	Format  string         `json:"fmt" cbor:"fmt"`
	AttStmt map[string]any `json:"attStmt" cbor:"attStmt"`
}

AttestationObject represents an ACME Device Attestation object Based on WebAuthn attestation object but simplified for ACME use case Uses CBOR encoding as per WebAuthn specification

type AttestationStatement

type AttestationStatement struct {
	Format   string
	AttStmt  map[string]any
	AuthData []byte // optional, may be omitted per spec
}

type AttestationVerifier

type AttestationVerifier interface {
	Format() string
	Verify(ctx context.Context, stmt AttestationStatement, challenge []byte) (*DeviceInfo, error)
}

type Authorization

type Authorization struct {
	ID         string      `json:"id"`
	Status     string      `json:"status"`
	Expires    *time.Time  `json:"expires,omitempty"`
	Identifier Identifier  `json:"identifier"`
	Challenges []Challenge `json:"challenges"`
	Wildcard   bool        `json:"wildcard,omitempty"`
	AccountID  string      `json:"accountId"`
	OrderID    string      `json:"orderId"`
	CreatedAt  time.Time   `json:"createdAt"`
}

type Authorizer

type Authorizer interface {
	Authorize(ctx context.Context, device *DeviceInfo) (bool, error)
}

type CA

type CA struct {
	// contains filtered or unexported fields
}

func New

func New(logger *slog.Logger, issuer CertificateIssuer, authorizer Authorizer, storage Storage, baseURL string, opts ...Option) (*CA, error)

func (*CA) Close

func (ca *CA) Close() error

func (*CA) Handler

func (ca *CA) Handler() http.Handler

type Certificate

type Certificate struct {
	*x509.Certificate `json:"-"` // Exclude from JSON serialization
	Raw               []byte     `json:"raw"`
	SerialNumber      string     `json:"serialNumber"`
}

type CertificateIssuer

type CertificateIssuer interface {
	// The deviceInfos slice contains attestation-derived device information.
	IssueCertificate(csr *x509.CertificateRequest, deviceInfos []*DeviceInfo) (*Certificate, error)
}

type Challenge

type Challenge struct {
	Type      string     `json:"type"`
	URL       string     `json:"url"`
	Status    string     `json:"status"`
	Validated *time.Time `json:"validated,omitempty"`
	Error     *Problem   `json:"error,omitempty"`
	Token     string     `json:"token"`
	KeyAuth   string     `json:"keyAuthorization,omitempty"`
	ID        string     `json:"id"`
	AuthzID   string     `json:"authzId"`
	CreatedAt time.Time  `json:"createdAt"`
	// Device attestation specific fields
	Attestation map[string]any `json:"attestation,omitempty"`
}

type ChallengeRequest

type ChallengeRequest struct {
	// AttObj contains the base64url-encoded WebAuthn attestation object
	// as specified in draft-ietf-acme-device-attest-01
	AttObj string `json:"attObj"`
}

type ContextHandler

type ContextHandler struct {
	// contains filtered or unexported fields
}

func NewContextHandler

func NewContextHandler(inner slog.Handler) *ContextHandler

func (*ContextHandler) Enabled

func (h *ContextHandler) Enabled(ctx context.Context, level slog.Level) bool

func (*ContextHandler) Handle

func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error

func (*ContextHandler) WithAttrs

func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*ContextHandler) WithGroup

func (h *ContextHandler) WithGroup(name string) slog.Handler

type DeviceInfo

type DeviceInfo struct {
	// ACME draft specification identifiers - these map to ACME identifier types
	PermanentIdentifier *PermanentIdentifier
	HardwareModule      *HardwareModule
}

DeviceInfo contains extracted device information from attestation

This structure follows the ACME Device Attestation draft specification. PermanentIdentifier contains device serial numbers or similar persistent identifiers. HardwareModule contains hardware-specific identifiers like UDIDs or TPM data.

type Directory

type Directory struct {
	NewNonce   string `json:"newNonce"`
	NewAccount string `json:"newAccount"`
	NewOrder   string `json:"newOrder"`
	RevokeCert string `json:"revokeCert,omitempty"`
	KeyChange  string `json:"keyChange,omitempty"`
	Meta       *Meta  `json:"meta,omitempty"`
}

type FinalizeRequest

type FinalizeRequest struct {
	CSR string `json:"csr"`
}

type HardwareModule

type HardwareModule struct {
	Type  asn1.ObjectIdentifier
	Value []byte
}

HardwareModule represents a hardware-module name as defined in RFC 4108.

HardwareModuleName ::= SEQUENCE {
    hwType       OBJECT IDENTIFIER,
    hwSerialNum  OCTET STRING
}

type Identifier

type Identifier struct {
	Type  string `json:"type"`
	Value string `json:"value"`
}

type IssuanceEvent

type IssuanceEvent struct {
	Timestamp   time.Time
	DeviceInfo  *DeviceInfo
	Attestation *AttestationStatement
	Certificate *Certificate
	AccountID   string
	OrderID     string
	Metadata    map[string]any
}

IssuanceEvent represents a certificate issuance event

type IssuanceObserver

type IssuanceObserver interface {
	OnIssuance(ctx context.Context, event *IssuanceEvent) error
}

IssuanceObserver handles actions after certificate issuance (logging, inventory updates, etc.)

type Meta

type Meta struct {
	TermsOfService          string   `json:"termsOfService,omitempty"`
	Website                 string   `json:"website,omitempty"`
	CAAIdentities           []string `json:"caaIdentities,omitempty"`
	ExternalAccountRequired bool     `json:"externalAccountRequired,omitempty"`
}

type Nonce

type Nonce struct {
	Value     string    `json:"value"`
	CreatedAt time.Time `json:"createdAt"`
}

type NonceValidationError

type NonceValidationError struct {
	Err error
}

func (*NonceValidationError) Error

func (e *NonceValidationError) Error() string

func (*NonceValidationError) Is

func (e *NonceValidationError) Is(target error) bool

func (*NonceValidationError) Unwrap

func (e *NonceValidationError) Unwrap() error

type Option

type Option func(*CA)

func WithObserver

func WithObserver(obs IssuanceObserver) Option

func WithPrefix

func WithPrefix(prefix string) Option

func WithVerifier

func WithVerifier(v AttestationVerifier) Option

type Order

type Order struct {
	ID             string       `json:"id"`
	Status         string       `json:"status"`
	Expires        *time.Time   `json:"expires,omitempty"`
	Identifiers    []Identifier `json:"identifiers"`
	NotBefore      *time.Time   `json:"notBefore,omitempty"`
	NotAfter       *time.Time   `json:"notAfter,omitempty"`
	Error          *Problem     `json:"error,omitempty"`
	Authorizations []string     `json:"authorizations"`
	Finalize       string       `json:"finalize"`
	Certificate    string       `json:"certificate,omitempty"`
	AccountID      string       `json:"accountId"`
	CreatedAt      time.Time    `json:"createdAt"`
}

type OrderRequest

type OrderRequest struct {
	Identifiers []Identifier `json:"identifiers"`
	NotBefore   *time.Time   `json:"notBefore,omitempty"`
	NotAfter    *time.Time   `json:"notAfter,omitempty"`
}

type PermanentIdentifier

type PermanentIdentifier struct {
	Identifier string
	Assigner   asn1.ObjectIdentifier
}

PermanentIdentifier represents a permanent-identifier as defined in RFC 4043.

PermanentIdentifier ::= SEQUENCE {
    identifierValue  UTF8String        OPTIONAL,
    assigner         OBJECT IDENTIFIER OPTIONAL
}

type Problem

type Problem struct {
	// Type contains a URI reference that identifies the problem type
	Type string `json:"type,omitempty"`

	// Title is a short, human-readable summary of the problem type
	Title string `json:"title,omitempty"`

	// Status is the HTTP status code
	Status int `json:"status,omitempty"`

	// Detail is a human-readable explanation specific to this occurrence
	Detail string `json:"detail,omitempty"`

	// Instance is a URI reference that identifies the specific occurrence
	Instance string `json:"instance,omitempty"`

	// Identifier is the ACME identifier this problem relates to (for subproblems)
	Identifier *Identifier `json:"identifier,omitempty"`

	// Subproblems contains an array of sub-problems for compound errors
	Subproblems []Problem `json:"subproblems,omitempty"`

	// RFC 8555 Section 6.2: "The problem document returned with the error MUST include an
	// 'algorithms' field with an array of supported 'alg' values."
	Algorithms []string `json:"algorithms,omitempty"`
}

Problem represents an RFC 7807/9457 compliant problem details object It implements the error interface

func AccountDoesNotExist

func AccountDoesNotExist(detail string) *Problem

func BadCSR

func BadCSR(detail string) *Problem

func BadNonce

func BadNonce(detail string) *Problem

func BadSignatureAlgorithm

func BadSignatureAlgorithm(detail string, supportedAlgorithms []string) *Problem

func InternalServerError

func InternalServerError(detail string) *Problem

func InvalidContact

func InvalidContact(detail string) *Problem

func Malformed

func Malformed(detail string) *Problem

func MethodNotAllowed

func MethodNotAllowed(detail string) *Problem

func RequestTooLarge

func RequestTooLarge(detail string) *Problem

func Unauthorized

func Unauthorized(detail string) *Problem

func UnsupportedMediaTypeProblem

func UnsupportedMediaTypeProblem(detail string) *Problem

func (*Problem) Error

func (p *Problem) Error() string

type Storage

type Storage interface {
	CreateNonce(ctx context.Context, nonce *Nonce) error
	ConsumeNonce(ctx context.Context, value string, expiry time.Duration) (*Nonce, error)

	CreateAccount(ctx context.Context, account *Account) error
	GetAccount(ctx context.Context, id string) (*Account, error)
	GetAccountByKey(ctx context.Context, keyThumbprint string) (*Account, error)
	UpdateAccount(ctx context.Context, account *Account) error

	CreateOrder(ctx context.Context, order *Order) error
	GetOrder(ctx context.Context, id string) (*Order, error)
	UpdateOrder(ctx context.Context, order *Order) error
	GetOrdersByAccount(ctx context.Context, accountID string) ([]*Order, error)

	CreateAuthorization(ctx context.Context, authz *Authorization) error
	GetAuthorization(ctx context.Context, id string) (*Authorization, error)
	UpdateAuthorization(ctx context.Context, authz *Authorization) error

	CreateChallenge(ctx context.Context, challenge *Challenge) error
	GetChallenge(ctx context.Context, id string) (*Challenge, error)
	SetChallengeProcessing(ctx context.Context, id string) error
	SetChallengeValid(ctx context.Context, id string, validated time.Time, attestation map[string]any) error
	SetChallengeInvalid(ctx context.Context, id string, validated time.Time, problem *Problem) error

	CreateCertificate(ctx context.Context, cert *Certificate) error
	GetCertificate(ctx context.Context, id string) (*Certificate, error)

	Close() error
}

Directories

Path Synopsis
authorizers
abm
Package certutil provides ASN.1/X.509 certificate extension utilities for building SubjectAltName extensions with PermanentIdentifier (RFC 4043) and HardwareModuleName (RFC 4108) otherName entries.
Package certutil provides ASN.1/X.509 certificate extension utilities for building SubjectAltName extensions with PermanentIdentifier (RFC 4043) and HardwareModuleName (RFC 4108) otherName entries.
issuers
observers
stderr
Package stderr provides an issuance observer that logs certificate issuance events to stderr.
Package stderr provides an issuance observer that logs certificate issuance events to stderr.
signers
storage
verifiers

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL