stir

package
v1.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Index

Constants

View Source
const AlgES256 = "ES256"

AlgES256 is the only RFC 8588 SHAKEN-mandated signing algorithm (ECDSA P-256 with SHA-256). RFC 8225 §6.1 permits others, but every STI-CA in production today issues ES256-only.

View Source
const PptShaken = "shaken"

PptShaken is the RFC 8588 PASSporT extension identifier. Carriers reject anything else on the wire today.

Variables

This section is empty.

Functions

func FormatIdentityHeader

func FormatIdentityHeader(h IdentityHeader) (string, error)

FormatIdentityHeader renders the Identity header value (the part after `Identity: `). Produces the unquoted RFC 8224 form. info and alg are required; ppt is included only when non-empty.

Returns an error rather than emitting a malformed header — every downstream verifier rejects malformed input as a 438 Invalid Identity Header response (RFC 8224 §6.2.4).

func LoadES256Certificate

func LoadES256Certificate(pemBytes []byte) (*x509.Certificate, error)

LoadES256Certificate parses a PEM "CERTIFICATE" block and returns the leaf cert. Validates that the embedded public key is ECDSA P-256 so signing-key/cert mismatches are caught at load time.

func LoadES256PrivateKey

func LoadES256PrivateKey(pemBytes []byte) (*ecdsa.PrivateKey, error)

LoadES256PrivateKey parses a PEM-encoded ECDSA P-256 private key. Accepts both the SEC1 ("EC PRIVATE KEY") and PKCS#8 ("PRIVATE KEY") block types — STI-CA issuance pipelines emit one or the other depending on tooling. Returns an error on:

  • any other PEM block type
  • non-EC keys (RSA / Ed25519 / etc.)
  • EC keys whose curve isn't P-256

Use LoadES256PrivateKeyFile to read straight from disk.

func LoadES256PrivateKeyFile

func LoadES256PrivateKeyFile(path string) (*ecdsa.PrivateKey, error)

LoadES256PrivateKeyFile is LoadES256PrivateKey + os.ReadFile.

func PublicKeyFromCert

func PublicKeyFromCert(cert *x509.Certificate) *ecdsa.PublicKey

PublicKeyFromCert is a tiny convenience: cert → *ecdsa.PublicKey suitable for VerifyPassport. Panics on non-ECDSA certs (caller must have already gone through LoadES256Certificate).

func PublicKeyFromCertOrNil

func PublicKeyFromCertOrNil(c *x509.Certificate) *ecdsa.PublicKey

PublicKeyFromCertOrNil is the non-panicking variant of PublicKeyFromCert. Returns nil when the cert's key isn't ECDSA.

Types

type AttestationLevel

type AttestationLevel string

AttestationLevel is the SHAKEN attestation level (RFC 8588 §6). A=fully attested (carrier-verified caller, carrier-issued number), B=partial (carrier-verified caller, third-party number), C=gateway (no direct relationship — typically inbound from PSTN).

const (
	AttestA AttestationLevel = "A"
	AttestB AttestationLevel = "B"
	AttestC AttestationLevel = "C"
)

func (AttestationLevel) IsValid

func (a AttestationLevel) IsValid() bool

IsValid reports whether a is one of A/B/C.

type HTTPFetcher

type HTTPFetcher struct {
	// HTTPClient is used for all GETs. Default: http.Client with
	// FetchTimeout. Inject a custom one for tests or to pin TLS.
	HTTPClient *http.Client

	// RootCAs is the STI-PA trust pool. nil → system roots (NOT
	// recommended in production; STI certs are issued by industry-
	// specific CAs, not WebPKI).
	RootCAs *x509.CertPool

	// IntermediateCAs is the optional intermediate pool — STI chains
	// typically have one intermediate cert between the leaf and the
	// STI-PA root. The fetcher will also accept intermediates served
	// inline in the x5u response.
	IntermediateCAs *x509.CertPool

	// CacheTTL is how long a successful lookup is cached. Zero →
	// defaultCertCacheTTL.
	CacheTTL time.Duration

	// NegativeCacheTTL is how long a failed lookup is cached. Zero
	// → defaultCertNegativeTTL.
	NegativeCacheTTL time.Duration

	// FetchTimeout caps one HTTP GET. Zero → defaultFetchTimeout.
	FetchTimeout time.Duration

	// Now is the clock function used for cache TTL and cert
	// notBefore/notAfter checks. Default time.Now. Injected for
	// deterministic tests.
	Now func() time.Time

	// SkipChainVerify disables chain validation. **Test-only.** When
	// true, only the PEM parse + key-type check runs. Never set this
	// in production — it accepts any cert as legitimate.
	SkipChainVerify bool
	// contains filtered or unexported fields
}

HTTPFetcher is the default X5UFetcher: HTTP GET + PEM parse + chain validation + in-memory TTL cache.

func NewHTTPFetcher

func NewHTTPFetcher() *HTTPFetcher

NewHTTPFetcher returns a fetcher with sensible defaults.

func (*HTTPFetcher) Fetch

func (f *HTTPFetcher) Fetch(ctx context.Context, x5uURL string) (*x509.Certificate, error)

Fetch implements X5UFetcher.

type IdentityHeader

type IdentityHeader struct {
	// Passport is the compact-form JWS (b64.b64.b64). Caller passes
	// this to VerifyPassport after fetching the cert.
	Passport string
	// Info is the x5u URL (cert fetch). Required per RFC 8224 §4.1.
	Info string
	// Alg is the JWS algorithm hint duplicated from the JOSE header.
	// "ES256" is the only value SHAKEN supports.
	Alg string
	// Ppt is the PASSporT extension type ("shaken", "div", "rph").
	// Empty when the PASSporT is a base RFC 8225 token.
	Ppt string
}

IdentityHeader is the parsed Identity header (RFC 8224 §4).

func ParseIdentityHeader

func ParseIdentityHeader(raw string) (IdentityHeader, error)

ParseIdentityHeader parses an Identity header value (the part after `Identity: `, with surrounding whitespace trimmed). We accept:

  • RFC 8224 unquoted form (current): JWT;info=<url>;alg=ES256;ppt=shaken
  • RFC 4474 quoted form (legacy): "JWT";info=<url>;alg=ES256
  • Mixed-case parameter names (RFC 3261 §7.3.1 says they're case-insensitive).

Returns a wrapped error when required params are missing so the caller can decide between 438 / 437 / 436 (RFC 8224 §6).

type PassportClaims

type PassportClaims struct {
	IAT    int64           `json:"iat"`
	Orig   PassportParty   `json:"orig"`
	Dest   PassportDestSet `json:"dest"`
	Attest string          `json:"attest,omitempty"` // SHAKEN only
	OrigID string          `json:"origid,omitempty"` // SHAKEN only; UUID for traceback
}

PassportClaims is the body of a PASSporT (RFC 8225 §5.2 + RFC 8588 §6 SHAKEN extras). Numbers in `orig.tn` / `dest.tn` MUST be E.164 normalised (leading +, only digits) — carriers reject unparseable TNs.

type PassportDestSet

type PassportDestSet struct {
	TN  []string `json:"tn,omitempty"`
	URI []string `json:"uri,omitempty"`
}

PassportDestSet is the `dest` claim — singleton lists of TNs or URIs. Even when a call has one destination, RFC 8225 mandates the list form. Empty lists are invalid.

type PassportHeader

type PassportHeader struct {
	Alg string `json:"alg"`
	Ppt string `json:"ppt,omitempty"`
	Typ string `json:"typ"`
	X5u string `json:"x5u"`
}

PassportHeader is the JOSE header of a PASSporT JWT. RFC 8225 §5 pins `typ`="passport"; `ppt` is set when an extension applies (RFC 8588 SHAKEN uses "shaken"). `x5u` is the public cert URL (mandatory for SHAKEN — RFC 8588 §6).

type PassportParty

type PassportParty struct {
	TN  string `json:"tn,omitempty"`
	URI string `json:"uri,omitempty"`
}

PassportParty identifies one endpoint in a PASSporT. Exactly one of TN / URI should be populated per RFC 8225 §5.2.1 — TN takes precedence when both are set (we mirror what RFC test vectors do).

type SignedPassport

type SignedPassport struct {
	Compact string
	Header  PassportHeader
	Claims  PassportClaims
}

SignedPassport is the wire form: the JWS compact serialization `b64(header).b64(claims).b64(signature)` plus the raw signing inputs (header / claims) so callers can re-render the Identity header without re-encoding.

func SignPassport

func SignPassport(hdr PassportHeader, claims PassportClaims, key *ecdsa.PrivateKey) (*SignedPassport, error)

SignPassport builds a JWS-compact PASSporT signed with key.

SHAKEN-specific validation (RFC 8588 §6):

  • hdr.Alg must be ES256
  • hdr.Ppt must be "shaken" (we don't sign other extensions yet)
  • hdr.X5u must be a non-empty https:// URL
  • claims.Attest must be A/B/C
  • claims.OrigID must be a non-empty UUID (we don't enforce RFC 4122 form here; carriers accept any unique string)
  • claims.Orig.TN required (E.164 normalised externally)
  • claims.Dest.TN must have ≥1 entry

IAT is auto-populated to time.Now().Unix() when zero.

func VerifyPassport

func VerifyPassport(compact string, pub *ecdsa.PublicKey) (*SignedPassport, error)

VerifyPassport validates a JWS-compact PASSporT. Returns parsed header + claims when the signature checks against pub.

This function does NOT fetch the x5u or validate the cert against any STI-CA root pool — the caller does that (it requires network + policy decisions out of scope here). VerifyPassport is the part every implementation gets wrong; isolating it makes audit easier.

type Verdict

type Verdict struct {
	Code     VerdictCode
	Reason   string          // human-readable detail
	Passport *SignedPassport // populated on pass + most failure modes
	Header   *IdentityHeader // populated when the header parsed OK
}

Verdict is the outcome of a single Verifier.Verify call.

func (Verdict) Pass

func (v Verdict) Pass() bool

Pass reports whether the verdict is a clean pass.

type VerdictCode

type VerdictCode int

VerdictCode identifies the outcome of a verification attempt. Distinct from SIP response codes — call SIPResponseCode() for the RFC 8224 §6.2.4 status to emit when rejecting. Keeping the verdict and SIP code separate lets multiple verdicts (e.g. BadIdentity and Mismatch) share the same on-wire response (438) without ambiguity in logs and metrics.

const (
	// VerdictPass: signature valid, cert chain trusted, params match.
	VerdictPass VerdictCode = iota
	// VerdictBadIdentity: header failed to parse, the JWS was
	// malformed, or the signature didn't verify. Maps to SIP 438.
	VerdictBadIdentity
	// VerdictBadCert: the x5u cert wasn't fetchable, didn't validate
	// against the trust pool, or had an unsupported algorithm. Maps
	// to SIP 437 "Unsupported Credential".
	VerdictBadCert
	// VerdictStale: the iat claim is too old (RFC 8224 §6.3.1 replay
	// protection). Maps to SIP 403 "Stale Date".
	VerdictStale
	// VerdictMismatch: info ≠ x5u, attest level filtered out, or
	// claimed orig.tn doesn't match the SIP From header. Maps to
	// SIP 438 (same wire code as BadIdentity but logged distinctly).
	VerdictMismatch
)

func (VerdictCode) SIPResponseCode

func (v VerdictCode) SIPResponseCode() int

SIPResponseCode is the RFC 8224 §6.2.4 SIP response a caller should emit when configured to reject this verdict.

func (VerdictCode) String

func (v VerdictCode) String() string

String renders the verdict for log lines and metrics labels.

type Verifier

type Verifier struct {
	Fetcher X5UFetcher

	// MaxAge is how old the JWS `iat` claim is allowed to be before
	// the verifier returns VerdictStale (RFC 8224 §6.3.1 anti-replay).
	// Zero → 60s, matching the RFC SHOULD recommendation.
	MaxAge time.Duration

	// Now is the clock used for staleness checks. Default time.Now;
	// inject for deterministic tests.
	Now func() time.Time
}

Verifier holds the dependencies and policy for inbound Identity header verification. Construct via NewVerifier; zero value is not usable (Fetcher is required).

func NewVerifier

func NewVerifier() *Verifier

NewVerifier returns a Verifier that uses the default HTTPFetcher and a 60s staleness window. Callers should swap Fetcher with one configured for the deployment's STI-PA trust pool before use.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, identityHeader string, opts VerifyOptions) (Verdict, error)

Verify runs the end-to-end pipeline on one Identity header value (the part after `Identity: `). Returns a Verdict + non-nil error only on programming faults; verification failures are encoded in the Verdict.Code so callers can switch on the SIP response code directly.

type VerifyOptions

type VerifyOptions struct {
	// FromTN is the SIP From header's TN (E.164, leading +). When
	// non-empty, the verifier checks that PASSporT.orig.tn matches.
	// Empty disables the check (useful for URI-only origins).
	FromTN string

	// FromURI is the SIP From header's URI. When non-empty AND the
	// PASSporT's orig is URI-form, the verifier checks for equality.
	FromURI string

	// RequiredPpt: when non-empty, the JWS header's `ppt` MUST equal
	// this value. Typical: "shaken" for North American inbound.
	RequiredPpt string

	// RequiredAttests: when non-empty, the PASSporT's `attest` MUST
	// be one of these (e.g. []{"A","B"} to reject C-attest gateway
	// calls). Empty allows any value.
	RequiredAttests []string
}

VerifyOptions narrows what a single Verify call enforces. None of these are required for a basic signature check; populate them as the SIP server gains confidence about what to reject.

type X5UFetcher

type X5UFetcher interface {
	// Fetch returns the leaf cert for x5uURL. Implementations are
	// expected to cache successful + failed lookups appropriately.
	// The returned cert's PublicKey is the *ecdsa.PublicKey used to
	// verify the PASSporT signature.
	Fetch(ctx context.Context, x5uURL string) (*x509.Certificate, error)
}

X5UFetcher retrieves and validates the certificate chain that signed a PASSporT. Implementations MUST validate the HTTPS server cert and the chain against the STI-PA trust pool before returning.

Jump to

Keyboard shortcuts

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