cap

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 16 Imported by: 2

README

cap

OASIS Common Alerting Protocol 1.2 in pure Go: the alert model, reading and validation, XML signatures, area geometry and alert feeds.

Go Reference test Go

It reads CAP 1.2 and also takes CAP 1.1 and 1.0, lists every validation problem instead of stopping at the first, signs and verifies enveloped XML signatures against pinned certificates, converts areas to GeoJSON, and reads and writes the Atom and RSS feeds that carry or link alerts. It was written for SigmaM2M, a oneM2M platform, as the protocol half of its CAP feed ingest, publishing and public warning integrations.

Features

Feature Status
CAP 1.2 read and write yes
CAP 1.1, 1.0, no namespace read as 1.2; Alert.Version records the original
Validation required elements, enumerations, date-time form (strict or lenient), references, identifier characters, scope conditions, polygon, circle, altitude and ceiling; Problem.Essential tells which problems leave an alert unusable
XML signatures enveloped, exclusive C14N, SHA-256, RSA or ECDSA; verification pins the KeyInfo certificate (no chain building)
Feeds Atom (RFC 4287) with embedded or linked alerts, RSS 2.0, a bare alert in; Atom out
GeoJSON (RFC 7946) areas as a FeatureCollection; circles as 32-segment polygons
CAP-CP and other national profiles no

Choosing a Go CAP library

Compared from each library's source at the versions named, in October 2026. Check the projects themselves before relying on a row.

this module tannerryan/cap v1.1.1 IBM/cap (2019 commit) alerting/go-cap (2018 commit)
Maintained yes yes (tagged 2026-10-03) archived archived
Licence Apache-2.0 BSD-2-Clause Apache-2.0 MIT
Reads CAP 1.2 yes yes yes yes, without namespace checks
Reads CAP 1.1 and 1.0 yes, as 1.2 no, refuses other namespaces 1.1 as a separate type yes, without namespace checks
Validation every problem listed, each marked essential or not; strict or lenient date-times errors and warnings, each with its clause no no
Canadian profile (CAP-CP) no yes no no
Verifies XML signatures yes, against pinned certificates yes, building a chain to trusted roots no no
Signs alerts yes (RSA, ECDSA) no no no
Atom and RSS feeds reads Atom and RSS, writes Atom no reads the US NWS Atom feed no
Areas as GeoJSON yes polygon and circle parsing only no polygon and circle types

Both maintained libraries sign or verify through github.com/russellhaering/goxmldsig and github.com/beevik/etree. Our interop tests cross-check the two (see Testing).

Pick this module when you publish alerts as well as read them, take alerts from older CAP 1.1 or 1.0 sources, or need the feeds and GeoJSON areas. Pick tannerryan/cap when you receive Canadian alerts and need CAP-CP validation, or want signature certificates checked against a CA chain rather than pinned.

Install

go get github.com/cB-Sigma/cap@latest

Requires Go 1.27.1 or later.

Quick start

import "github.com/cB-Sigma/cap"

a, err := cap.Parse(doc) // a CAP 1.1 alert is read as 1.2
if err != nil {
	return err
}
fmt.Println(a.Version, a.Identifier, a.MostSevere())
if err := a.Validate(cap.Options{}); err != nil {
	fmt.Println(err, "- essential:", err.(cap.ValidationError).Essential())
}
fmt.Println(a.Validate(cap.Options{LenientTime: true}) == nil)
// 1.1 DEMO-1 Severe
// the CAP alert is not valid: sent: "2026-09-24T10:00:00Z" is not in the form 2026-09-24T10:00:00+05:30 - essential: true
// true

This is the runnable Example in example_test.go, which holds the CAP 1.1 document it reads.

Documentation

API reference: https://pkg.go.dev/github.com/cB-Sigma/cap

Package What it does
cap (module root) Alert and its parts, Parse (CAP 1.2, 1.1, 1.0, no namespace), Marshal, Validate with Options.LenientTime and Problem.Essential, references, date-times, MostSevere, Expires; ParsePolygon, ParseCircle, CirclePolygon, Alert.GeoJSON; ParseFeed and WriteAtom; Signer and Verifier
interop (nested module, tests only) cross-checks against github.com/tannerryan/cap

Testing

go test -race ./...

Tests carry // Covers: SIG-... comments: these are requirement IDs of the SigmaM2M project's compliance report and have no meaning outside it.

interop/ is a nested module with its own go.mod, so its dependency stays out of this one. It runs with go test ./... inside it:

cd interop && go test ./...
Interop tested against
  • github.com/tannerryan/cap v1.1.1 (interop/): our alerts parse and validate there without errors, its output parses and validates here, and our RSA and ECDSA signatures verify there.
  • No national alerting authority's live feed has been read by these tests.
  • cap-sim: a simulated CAP feed server and receiver, as a Go package and a command.

Status

v0.x: the API may change before v1.

Contributing

See CONTRIBUTING.md.

Security

See SECURITY.md to report a vulnerability privately.

Licence

Apache-2.0 (see LICENSE and NOTICE). Copyright 2026 Abhinav Gautam. This module is not affiliated with or endorsed by OASIS; see NOTICE.

Documentation

Overview

Package cap reads, checks, writes and signs OASIS Common Alerting Protocol 1.2 alert messages, and reads and writes the Atom and RSS feeds that carry them.

CAP 1.1 and 1.0 alerts (and alerts without a namespace) are read as CAP 1.2, and Validate lists every problem rather than stopping at the first, so an application can store an alert that strays from the schema with its problems noted (Problem.Essential tells which problems leave an alert unusable). Signatures are enveloped XML signatures over the whole alert, verified against certificates the application pins (sign.go). Areas convert to GeoJSON (geo.go).

The message model follows the CAP 1.2 schema; no schema or specification text is copied here.

Example

A CAP 1.1 alert is read as 1.2; Validate lists every problem, and Essential tells whether the alert can still be kept.

package main

import (
	"fmt"

	"github.com/cB-Sigma/cap"
)

func main() {
	doc := []byte(`<alert xmlns="urn:oasis:names:tc:emergency:cap:1.1">
  <identifier>DEMO-1</identifier><sender>demo@example.test</sender>
  <sent>2026-09-24T10:00:00Z</sent><status>Actual</status><msgType>Alert</msgType><scope>Public</scope>
  <info><category>Met</category><event>Heavy Rainfall</event><urgency>Expected</urgency>
    <severity>Severe</severity><certainty>Likely</certainty>
    <area><areaDesc>Demo district</areaDesc><circle>18.52,73.85 5</circle></area></info>
</alert>`)
	a, err := cap.Parse(doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(a.Version, a.Identifier, a.MostSevere())
	if err := a.Validate(cap.Options{}); err != nil {
		fmt.Println(err, "- essential:", err.(cap.ValidationError).Essential())
	}
	fmt.Println(a.Validate(cap.Options{LenientTime: true}) == nil)
}
Output:
1.1 DEMO-1 Severe
the CAP alert is not valid: sent: "2026-09-24T10:00:00Z" is not in the form 2026-09-24T10:00:00+05:30 - essential: true
true

Index

Examples

Constants

View Source
const (
	Namespace11 = "urn:oasis:names:tc:emergency:cap:1.1"
	Namespace10 = "http://www.incident.com/cap/1.0"
)

Namespaces of the earlier CAP versions, read like CAP 1.2 (Parse).

View Source
const (
	SignatureValid = "valid"
	SignatureNone  = "none"
)

Signature states.

View Source
const AtomNamespace = "http://www.w3.org/2005/Atom"

AtomNamespace is the Atom (RFC 4287) namespace.

View Source
const MediaType = "application/cap+xml"

MediaType is the media type of a CAP document.

View Source
const Namespace = "urn:oasis:names:tc:emergency:cap:1.2"

Namespace is the CAP 1.2 XML namespace.

View Source
const XMLDSigNamespace = "http://www.w3.org/2000/09/xmldsig#"

XMLDSigNamespace is the XML Signature namespace.

Variables

View Source
var (
	Statuses     = []string{"Actual", "Exercise", "System", "Test", "Draft"}
	MsgTypes     = []string{"Alert", "Update", "Cancel", "Ack", "Error"}
	Scopes       = []string{"Public", "Restricted", "Private"}
	Categories   = []string{"Geo", "Met", "Safety", "Security", "Rescue", "Fire", "Health", "Env", "Transport", "Infra", "CBRNE", "Other"}
	Responses    = []string{"Shelter", "Evacuate", "Prepare", "Execute", "Avoid", "Monitor", "Assess", "AllClear", "None"}
	Urgencies    = []string{"Immediate", "Expected", "Future", "Past", "Unknown"}
	SeverityList = []string{"Extreme", "Severe", "Moderate", "Minor", "Unknown"}
	Certainties  = []string{"Observed", "Likely", "Possible", "Unlikely", "Unknown"}
)

Enumerations of the CAP 1.2 schema.

View Source
var ErrUnsigned = errors.New("the alert is not signed and signatures are required")

ErrUnsigned is an unsigned alert where signatures are required.

Functions

func CirclePolygon

func CirclePolygon(center []float64, radiusKm float64) [][]float64

CirclePolygon approximates a circle as a closed GeoJSON ring (spherical Earth, 6371 km).

func FormatTime

func FormatTime(t time.Time) string

FormatTime writes t in CAP's date-time form: seconds precision with an explicit offset ("2026-09-24T10:00:00+05:30"); UTC is written "-00:00".

func ParseCircle

func ParseCircle(s string) (center []float64, radiusKm float64, err error)

ParseCircle reads a CAP circle "lat,lon radius" (radius in km).

func ParsePolygon

func ParsePolygon(s string) ([][]float64, error)

ParsePolygon reads a CAP polygon: at least four space-separated "lat,lon" pairs whose first and last are the same point. It returns the ring as GeoJSON positions.

func ParseTime

func ParseTime(s string) (time.Time, error)

ParseTime reads a CAP date-time. Strict CAP has seconds precision and a numeric offset; feeds in the field also use "Z" and fractions, which are accepted too.

func Version

func Version(space string) (string, bool)

Version is the CAP version of an alert element's namespace, and whether it is one.

func WriteAtom

func WriteAtom(m FeedMeta, alerts []*Alert, now time.Time) ([]byte, error)

WriteAtom renders alerts (newest first) as an Atom feed whose entries embed each CAP message and link to its own document.

Types

type Alert

type Alert struct {
	XMLName     xml.Name `xml:"urn:oasis:names:tc:emergency:cap:1.2 alert"`
	Identifier  string   `xml:"identifier"`
	Sender      string   `xml:"sender"`
	Sent        string   `xml:"sent"`
	Status      string   `xml:"status"`
	MsgType     string   `xml:"msgType"`
	Source      string   `xml:"source,omitempty"`
	Scope       string   `xml:"scope"`
	Restriction string   `xml:"restriction,omitempty"`
	Addresses   string   `xml:"addresses,omitempty"`
	Code        []string `xml:"code,omitempty"`
	Note        string   `xml:"note,omitempty"`
	References  string   `xml:"references,omitempty"`
	Incidents   string   `xml:"incidents,omitempty"`
	Info        []Info   `xml:"info,omitempty"`
	// Version is the CAP version the document was written in ("1.2",
	// "1.1", "1.0", or "none" without a namespace); it is always
	// marshalled as 1.2.
	Version string `xml:"-"`
}

Alert is a CAP 1.2 alert message.

func Parse

func Parse(b []byte) (*Alert, error)

Parse decodes a CAP document: CAP 1.2, and also CAP 1.1, 1.0 or an alert without a namespace, read as 1.2 (Alert.Version says which). It checks the root element but not the content; call Validate for that.

func (*Alert) Expires

func (a *Alert) Expires() time.Time

Expires is the latest expiry of the alert's info blocks, or zero when one of them has none (it stays in effect).

func (*Alert) GeoJSON

func (a *Alert) GeoJSON() map[string]any

GeoJSON renders every area of the alert as a GeoJSON FeatureCollection: one Feature per area, with a Polygon, a MultiPolygon, or no geometry when the area is described by geocodes only. Circles become 32-sided polygons and keep their centre and radius in the properties.

func (*Alert) Key

func (a *Alert) Key() string

Key identifies a message: sender, identifier and sent together are unique (the same triple a references entry uses).

func (*Alert) Marshal

func (a *Alert) Marshal() ([]byte, error)

Marshal encodes the alert as a CAP 1.2 document with an XML declaration.

func (*Alert) MostSevere

func (a *Alert) MostSevere() string

MostSevere is the highest severity among the info blocks ("Unknown" when none says).

func (*Alert) Validate

func (a *Alert) Validate(o Options) error

Validate checks the alert against the CAP 1.2 rules: required elements, enumerations, date-time form, references, identifier characters, the restriction and addresses conditions of the scope, and area geometry.

type Area

type Area struct {
	AreaDesc string       `xml:"areaDesc" json:"areaDesc"`
	Polygon  []string     `xml:"polygon,omitempty" json:"polygon,omitempty"`
	Circle   []string     `xml:"circle,omitempty" json:"circle,omitempty"`
	Geocode  []NamedValue `xml:"geocode,omitempty" json:"geocode,omitempty"`
	Altitude string       `xml:"altitude,omitempty" json:"altitude,omitempty"`
	Ceiling  string       `xml:"ceiling,omitempty" json:"ceiling,omitempty"`
}

Area is an affected area. The JSON names are the CAP element names, so an area can be given in a JSON configuration.

type Entry

type Entry struct {
	ID      string
	Updated string
	Title   string
	Link    string // the CAP document's URL when it is not embedded
	Alert   *Alert // embedded alert, if any
	// Raw is the embedded alert element's text (for signature checks); a
	// bare CAP document's Raw is the whole document.
	Raw []byte
}

Entry is one feed entry: an embedded CAP alert, or a link to one.

func ParseFeed

func ParseFeed(b []byte) ([]Entry, error)

ParseFeed reads an Atom feed, an RSS 2.0 feed or a bare CAP document (one entry). Atom entries may embed the alert in their content or link to it; RSS items link to it.

func (Entry) Key

func (e Entry) Key() string

Key identifies an entry version: a changed entry is fetched again.

type FeedMeta

type FeedMeta struct {
	ID, Title, Author, Self string
	// AlertURL gives the URL a CAP message is also served at.
	AlertURL func(identifier string) string
}

FeedMeta describes a published Atom feed.

type Info

type Info struct {
	Language     string       `xml:"language,omitempty"`
	Category     []string     `xml:"category"`
	Event        string       `xml:"event"`
	ResponseType []string     `xml:"responseType,omitempty"`
	Urgency      string       `xml:"urgency"`
	Severity     string       `xml:"severity"`
	Certainty    string       `xml:"certainty"`
	Audience     string       `xml:"audience,omitempty"`
	EventCode    []NamedValue `xml:"eventCode,omitempty"`
	Effective    string       `xml:"effective,omitempty"`
	Onset        string       `xml:"onset,omitempty"`
	Expires      string       `xml:"expires,omitempty"`
	SenderName   string       `xml:"senderName,omitempty"`
	Headline     string       `xml:"headline,omitempty"`
	Description  string       `xml:"description,omitempty"`
	Instruction  string       `xml:"instruction,omitempty"`
	Web          string       `xml:"web,omitempty"`
	Contact      string       `xml:"contact,omitempty"`
	Parameter    []NamedValue `xml:"parameter,omitempty"`
	Resource     []Resource   `xml:"resource,omitempty"`
	Area         []Area       `xml:"area,omitempty"`
}

Info is one info block (one language, one event).

type NamedValue

type NamedValue struct {
	ValueName string `xml:"valueName" json:"valueName"`
	Value     string `xml:"value" json:"value"`
}

NamedValue is an eventCode, parameter or geocode.

type Options

type Options struct {
	// LenientTime accepts "Z" and fractional seconds in date-times.
	LenientTime bool
}

Options relax validation for feeds that stray from the schema.

type Problem

type Problem struct {
	Field string `json:"field"`
	Issue string `json:"issue"`
}

Problem is one validation finding.

func (Problem) Essential

func (p Problem) Essential() bool

Essential tells whether a validation problem leaves the alert unusable: without its identifier, sender or a readable sent time it cannot be told apart from others or referenced.

func (Problem) String

func (p Problem) String() string

type Reference

type Reference struct {
	Sender, Identifier, Sent string
}

Reference is one entry of an alert's references.

func ParseReferences

func ParseReferences(s string) ([]Reference, error)

ParseReferences splits a references value into its entries.

func (Reference) Key

func (r Reference) Key() string

Key is the reference in the "sender,identifier,sent" form.

type Resource

type Resource struct {
	ResourceDesc string `xml:"resourceDesc"`
	MimeType     string `xml:"mimeType"`
	Size         string `xml:"size,omitempty"`
	URI          string `xml:"uri,omitempty"`
	DerefURI     string `xml:"derefUri,omitempty"`
	Digest       string `xml:"digest,omitempty"`
}

Resource is a file attached to an info block.

type SignSettings

type SignSettings struct {
	// Key is the PEM private key (PKCS #8, PKCS #1 RSA or SEC 1 EC).
	Key string `json:"key"`
	// Certificate is the PEM certificate of the key, followed by any
	// intermediates; the first goes in each signature's KeyInfo.
	Certificate string `json:"certificate"`
}

SignSettings configures signing of published messages.

type Signer

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

Signer signs CAP documents.

func NewSigner

func NewSigner(s SignSettings) (*Signer, error)

NewSigner reads a key and certificate chain.

func (*Signer) Certificates

func (s *Signer) Certificates() []*x509.Certificate

Certificates is the signing certificate followed by its intermediates.

func (*Signer) Sign

func (s *Signer) Sign(doc []byte) ([]byte, error)

Sign returns the document with an enveloped signature appended to its alert element.

type ValidationError

type ValidationError []Problem

ValidationError lists every problem of an alert.

func (ValidationError) Error

func (v ValidationError) Error() string

func (ValidationError) Essential

func (v ValidationError) Essential() bool

Essential tells whether any of the problems is essential.

type Verifier

type Verifier struct {

	// Now is the clock for certificate validity (tests); nil is time.Now.
	Now func() time.Time
	// contains filtered or unexported fields
}

Verifier checks CAP signatures against trusted certificates.

func NewVerifier

func NewVerifier(s VerifySettings) (*Verifier, error)

NewVerifier reads the trusted certificates.

func NewVerifierSkipping

func NewVerifierSkipping(s VerifySettings, skip func(i int, err error)) *Verifier

NewVerifierSkipping reads the trusted certificates, passing each entry that cannot be used to skip and leaving it out. With none left, every signed alert fails verification (it stays refused).

func (*Verifier) Required

func (v *Verifier) Required() bool

Required tells whether unsigned alerts are refused.

func (*Verifier) Trusted

func (v *Verifier) Trusted() []*x509.Certificate

Trusted is the certificates whose signatures are accepted.

func (*Verifier) Verify

func (v *Verifier) Verify(doc []byte) (verified []byte, state string, err error)

Verify checks the document's signature. It returns the verified alert element as a document of its own (signature removed): callers parse that, never the original bytes, so content outside the signed element cannot be slipped in. An unsigned document is returned as it is with state "none" unless signatures are required.

type VerifySettings

type VerifySettings struct {
	// Trusted are PEM certificates whose signatures are accepted; a
	// signature is valid only when its KeyInfo certificate is one of them
	// (and within its validity period).
	Trusted []string `json:"trusted"`
	// Require refuses alerts without a valid signature. Without it an
	// unsigned alert is accepted and marked "signature": "none"; a
	// signature that does not verify is refused unless acceptInvalid.
	Require bool `json:"require,omitempty"`
	// AcceptInvalid stores an alert whose signature does not verify,
	// marked "signature": "invalid", instead of refusing it (default
	// false). Risk: a forged or tampered alert is stored and can raise
	// alarms. Every such alert is also reported as a warning.
	AcceptInvalid bool `json:"acceptInvalid,omitempty"`
}

VerifySettings configures checking of received signatures.

Jump to

Keyboard shortcuts

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