Documentation
¶
Overview ¶
Package alexaverify verifies that an inbound HTTP request really was sent by Alexa.
Amazon signs every request it delivers to a custom skill hosted as a web service. The signature covers the exact bytes of the request body, and the public key needed to check it is published as a PEM certificate chain at a URL carried in the SignatureCertChainUrl header. This package implements the checks Amazon's own SDKs perform, in the order that is cheapest to defend:
- the request timestamp is fresh, and
- the Signature-256 header is a valid RSA PKCS#1 v1.5 SHA-256 signature over the body, made by a certificate that chains to a trusted root and is issued for echo-api.amazon.com.
The timestamp is checked first. It is a local, free computation, whereas the signature check may make an outbound HTTPS request to a URL supplied by the caller. Checking freshness first stops an unauthenticated caller driving egress. Both reference SDKs order these differently; this is a deliberate divergence.
Byte fidelity ¶
Verify takes the request body as a []byte and checks the signature over exactly those bytes. Callers must read the body once, verify that buffer, and then decode the request envelope from the same buffer. Decoding and re-encoding the JSON before verification — even through a middleware that means well — produces different bytes and every signature will fail.
Certificate cache and warming ¶
Validated certificate chains are cached in memory under their normalized URLs. The cache is bounded, is never persisted, and rechecks the leaf's NotBefore and NotAfter bounds on every hit. Warm can seed that same cache at startup. A warm failure should be logged and ignored so certificate egress cannot prevent the caller's service from becoming ready.
There is no way to disable verification ¶
This package provides no switch, environment variable, build tag or option that turns verification off. The Java SDK has such a system property; it is not ported. Tests get the three seams below instead — the trusted root pool, the clock and the HTTP client.
Certificate revocation is not checked ¶
This is deliberate, not an oversight. Neither reference SDK checks revocation either (the Node adapter carries an explicit TODO), so an implementation that did would be rejecting requests that every other Alexa skill on the internet accepts. Chain validity is bounded instead by the NotBefore and NotAfter dates on the leaf, which are re-checked on every verification including cache hits.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrMissingHeader reports that a required verification header — // Signature-256 or SignatureCertChainUrl — was absent or empty. ErrMissingHeader = errors.New("alexaverify: missing verification header") // ErrCertURLInvalid reports that the certificate chain URL failed scheme, // host, path prefix or port validation. ErrCertURLInvalid = errors.New("alexaverify: invalid certificate chain URL") // ErrCertURLUserinfo reports that the certificate chain URL carried // userinfo. Both reference SDKs accept this; we do not. See the residual // risk register in the implementation plan: Amazon emits a fixed literal // URL with no credentials, and accepting one would have Go attach a // pointless Authorization header to the S3 fetch. ErrCertURLUserinfo = errors.New("alexaverify: certificate chain URL contains userinfo") // ErrCertURLTraversal reports that the certificate chain URL's normalized // path escaped the required prefix, including via percent-encoded dot // segments. Both reference SDKs accept the percent-encoded form; we do // not, for the same reason as ErrCertURLUserinfo. ErrCertURLTraversal = errors.New("alexaverify: certificate chain URL escapes the permitted path") // ErrCertFetch reports that the certificate chain could not be retrieved: // a transport failure, a non-200 response, or a body over the size cap. ErrCertFetch = errors.New("alexaverify: fetching the certificate chain") // ErrUntrustedChain reports that the certificate chain did not parse, or // did not validate to a trusted root at the current time. ErrUntrustedChain = errors.New("alexaverify: certificate chain is not trusted") // ErrCertHostname reports that the leaf certificate does not carry the // required DNS subject alternative name. ErrCertHostname = errors.New("alexaverify: certificate is not valid for the Alexa API domain") // ErrUnsupportedKey reports that the leaf certificate's public key is not // RSA, and so cannot have produced an RSA PKCS#1 v1.5 signature. ErrUnsupportedKey = errors.New("alexaverify: unsupported certificate public key type") // ErrBadSignature reports that the signature header was not valid base64, // or did not verify over the supplied body bytes. ErrBadSignature = errors.New("alexaverify: signature verification failed") // ErrStaleTimestamp reports that the request's freshness could not be // established: either the envelope carried no usable timestamp, or the // timestamp differs from now by more than the tolerance in either // direction. A future-dated request fails this check, which the Node // adapter does not catch. ErrStaleTimestamp = errors.New("alexaverify: request timestamp outside tolerance") )
Every error returned by Verifier.Verify wraps exactly one of these sentinels, so a caller can classify a failure with a single errors.Is chain and log it distinctly. They are part of the package's public contract: removing one, or changing which condition maps to which, is a breaking change.
Callers must return the same HTTP status for all of them. The distinction exists for the operator reading logs, not for the caller on the wire — telling a prospective attacker which of the checks they failed is free intelligence.
Functions ¶
This section is empty.
Types ¶
type Option ¶
Option configures a Verifier passed to New.
func WithHTTPClient ¶
WithHTTPClient sets the client used to fetch certificate chains. This is a test seam.
func WithLogger ¶
WithLogger sets the logger used for operational events such as certificate fetches and a clamped tolerance. It does not affect any verification verdict.
func WithRoots ¶
WithRoots sets the trusted root pool. This is a test seam; production leaves it nil so the host's system pool is used.
func WithTolerance ¶
WithTolerance sets the freshness window for standard requests.
type Verifier ¶
type Verifier struct {
// Roots is the pool of trusted certificate authorities the chain must
// validate to. A nil Roots means the host's system pool, which is what
// production always uses; tests inject a pool containing a generated test
// root so the crypto path can be exercised without the network.
Roots *x509.CertPool
// Now supplies the current time. A nil Now means time.Now. Nothing else in
// this package reads the clock directly, so a test can pin it.
Now func() time.Time
// HTTPClient fetches the certificate chain. A nil HTTPClient means a
// bounded default. Tests point this at an httptest server.
HTTPClient *http.Client
// Tolerance is the freshness window for standard requests, applied in both
// directions. Values above the 150s ceiling are clamped; a non-positive
// value means the default. Skill event request types always use their own
// one hour window regardless of this setting.
Tolerance time.Duration
// contains filtered or unexported fields
}
Verifier checks that a request was signed by Alexa.
The zero value is usable but unvalidated; prefer New, which rejects nonsensical configuration up front. The four exported fields are the package's only configuration, and three of them exist solely as test seams. None of them can disable verification: see the package documentation.
A Verifier is safe for concurrent use.
func New ¶
New builds a Verifier, applying opts and validating the result.
A negative tolerance is an error. A tolerance above the 150 second ceiling is clamped to it and logged at warn, matching the Java SDK's constructor behaviour rather than failing an otherwise workable configuration.
func (*Verifier) Verify ¶
Verify reports whether body, as received on the wire, was signed by Alexa.
body must be the exact bytes read from the request: the signature is computed over them, so a decoded-and-re-encoded copy will not verify. hdr is the request's header set; both Signature-256 and SignatureCertChainUrl must be present.
Checks run in the order timestamp then signature. The timestamp check is local; the signature check may reach out to S3 for the certificate chain, so running it second means a stale or replayed request cannot make this process open an outbound connection.
A nil return means the request is authentic. Every non-nil return wraps exactly one of the sentinels in this package.
func (*Verifier) Warm ¶
Warm populates the in-memory certificate cache from seedURL, so the first real request after a restart does not pay for a cold fetch inside Alexa's response budget.
It is best effort by contract. Callers should log a failure and carry on: egress being unavailable at boot is not a reason to refuse to start.