nanoca

package module
v0.1.1 Latest Latest
Warning

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

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

README

The Freeish Project: nanoca

A lightweight enterprise ACME Certificate Authority service with device attestation support.

This is the Freeish Project fork of brandonweeks/nanoca, maintained on the freeish-core branch. The upstream library is the work of Brandon Weeks -- we are grateful for it. This fork exists to carry features specific to the Freeish stack that are pending or out of scope for upstream.

What we added

Full certificate chain serving (RFC 8555 §7.4.2)

When nanoca is deployed as an intermediate CA, ACME clients need the full certificate chain to build a trust path. The in-process issuer now accepts a variadic chain ...*x509.Certificate argument:

// intermCert is the issuing intermediate, rootCert is the root CA.
// Order: issuer of the leaf first, up toward the root.
issuer := inprocess.New(intermCert, intermSigner, intermCert, rootCert)

The ACME certificate endpoint returns the leaf followed by each chain certificate as a PEM-encoded application/pem-certificate-chain response. Callers that don't pass a chain get the previous behavior (leaf only).

Remote signing oracle (signers/remote)

A crypto.Signer implementation that delegates all signing operations to an external HTTP service. This allows the CA private key to live in an HSM, cloud KMS, Cloudflare Secrets Store, or any custom signing service -- the key never enters the CA process.

Oracle protocol:

  • POST /sign with Authorization: Bearer <token> and JSON body {"digest": "<base64>", "hash": "SHA-256"}
  • Response: {"signature": "<base64-DER>"} (DER-encoded ASN.1 ECDSA)

The signer verifies every signature returned by the oracle against the known public key before passing it to the caller. HTTPS required; plain HTTP only permitted for loopback (development/testing).

Note: The CA key must be ECDSA (P-256 or P-384). The remote signer hard-rejects non-ECDSA keys.

Usage

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

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

ca, _ := nanoca.New(
	logger,
	inprocess.New(caCert, signer),
	null.New(),
	storage,
	"https://ca.example.com",
	nanoca.WithPrefix("/acme"),
)
defer ca.Close()

mux := http.NewServeMux()
mux.Handle("/acme/", ca.Handler())
Remote signer
import "github.com/freeish-project/nanoca/signers/remote"

signer, err := remote.New(
	"https://signer.internal:8443",  // oracle URL (must be HTTPS)
	"bearer-token",                   // Authorization header value
	publicKeyPEM,                     // PEM-encoded ECDSA public key
)

Module path

github.com/freeish-project/nanoca

Branch: freeish-core

If you are consuming this fork from another Go module, import from the above path. The nanoca-go-standalone binary uses this fork via a replace directive in its go.mod.

Upstream

Upstream library: brandonweeks/nanoca

The chain-serving and remote-signer features have an open upstream PR. When merged, this fork's replace directive in nanoca-go-standalone can be dropped and imports updated back to github.com/brandonweeks/nanoca.

License

MIT License. Original copyright Brandon Weeks. See LICENSE.

Modifications copyright The Freeish Project contributors.

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:"-"`
	Raw               []byte              `json:"raw"`
	SerialNumber      string              `json:"serialNumber"`
	Chain             []*x509.Certificate `json:"-"`
	ChainRaw          [][]byte            `json:"chainRaw,omitempty"`
}

Certificate holds an issued certificate and its optional chain.

ChainRaw is the authoritative representation of the chain and is persisted to storage. Chain is a convenience field populated at issuance time but is NOT reconstituted after a storage round-trip (JSON tags exclude it). Code that consumes certificates retrieved from storage must use ChainRaw.

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
remote
Package remote provides a crypto.Signer implementation that delegates signing operations to an authenticated HTTP signing oracle.
Package remote provides a crypto.Signer implementation that delegates signing operations to an authenticated HTTP signing oracle.
storage
verifiers

Jump to

Keyboard shortcuts

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