Documentation
¶
Index ¶
- Constants
- func FormatIdentityHeader(h IdentityHeader) (string, error)
- func LoadES256Certificate(pemBytes []byte) (*x509.Certificate, error)
- func LoadES256PrivateKey(pemBytes []byte) (*ecdsa.PrivateKey, error)
- func LoadES256PrivateKeyFile(path string) (*ecdsa.PrivateKey, error)
- func PublicKeyFromCert(cert *x509.Certificate) *ecdsa.PublicKey
- func PublicKeyFromCertOrNil(c *x509.Certificate) *ecdsa.PublicKey
- type AttestationLevel
- type HTTPFetcher
- type IdentityHeader
- type PassportClaims
- type PassportDestSet
- type PassportHeader
- type PassportParty
- type SignedPassport
- type Verdict
- type VerdictCode
- type Verifier
- type VerifyOptions
- type X5UFetcher
Constants ¶
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.
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 ¶
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.
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.