求助:使用Perl验证Amazon Alexa请求签名的实现问题
Hey there! Let’s work through this Alexa signature verification snag in Perl together— I’ve tackled this exact scenario before, so I can walk you through the common pitfalls and working code to get you up and running.
Before diving into the public key extraction, it’s critical to remember Alexa’s full validation flow— skipping any step can lead to failed checks:
- Validate that the
SignatureCertChainUrlcomes from a trusted Amazon domain (*.amazontrust.com) over HTTPS (port 443) to avoid malicious certificate spoofing - Download and verify the entire certificate chain (not just the primary cert) to ensure it’s rooted in Amazon’s official root CA
- Extract the public key from the primary certificate
- Verify the request body’s signature using this public key (Alexa uses SHA-256 hashing now, so don’t forget that!)
- Validate the request timestamp to prevent replay attacks (it should be within ±15 minutes of current UTC time)
The most common issues with extracting the public key from $primary_cert are related to PEM format handling or using the wrong module. Here are two reliable approaches:
Option 1: Use Crypt::OpenSSL::X509 (requires OpenSSL)
This is the most straightforward option if you have OpenSSL installed on your server. First install the module:
cpanm Crypt::OpenSSL::X509 Crypt::OpenSSL::RSA MIME::Base64
Then use this code to parse the certificate and extract the public key:
use strict; use warnings; use Crypt::OpenSSL::X509; use Crypt::OpenSSL::RSA; use MIME::Base64; # Assume $primary_cert is the full PEM-formatted primary certificate (including BEGIN/END lines) my $x509 = Crypt::OpenSSL::X509->new_from_string($primary_cert, Crypt::OpenSSL::X509::FORMAT_PEM()); # Extract the public key in PEM format my $pub_key_pem = $x509->pubkey(); # Prepare the RSA object for signature verification (use SHA-256 as required by Alexa) my $rsa = Crypt::OpenSSL::RSA->new_public_key($pub_key_pem); $rsa->use_sha256_hash(); # Verify the signature: $signature is base64-encoded from the request header, $request_body is raw unmodified body my $decoded_signature = decode_base64($signature); my $is_valid = $rsa->verify($request_body, $decoded_signature); if ($is_valid) { print "Signature is valid!\n"; } else { print "Signature verification failed— check certificate or request body integrity!\n"; }
Option 2: Use Crypt::X509 (pure Perl, no OpenSSL dependency)
If you can’t install OpenSSL-based modules, use this pure-Perl alternative. Install first:
cpanm Crypt::X509 Crypt::RSA MIME::Base64
Code example:
use strict; use warnings; use Crypt::X509; use Crypt::RSA; use MIME::Base64; my $x509 = Crypt::X509->new(cert => $primary_cert); if ($x509->error) { die "Failed to parse certificate: " . $x509->error; } # Convert the raw DER public key to PEM format my $pub_key_der = $x509->pubkey(); my $pub_key_pem = "-----BEGIN PUBLIC KEY-----\n" . encode_base64($pub_key_der) . "-----END PUBLIC KEY-----"; # Verify the signature with SHA-256 my $rsa = Crypt::RSA->new(); my $pub_key = $rsa->import_key(Key => $pub_key_pem, Format => 'PEM'); my $verification_result = $rsa->verify( Message => $request_body, Signature => decode_base64($signature), Key => $pub_key, Hash => 'SHA256' ); if ($verification_result) { print "Signature validated successfully!\n"; } else { die "Invalid signature: " . $rsa->errstr; }
- Don’t skip certificate chain validation: Just checking the primary cert isn’t enough— you need to ensure it links back to Amazon’s root CA (e.g., Amazon Root CA 1). For
Crypt::OpenSSL::X509, you can use theverify()method with a trusted root CA bundle. - Keep the request body intact: The signature is generated against the raw, unmodified request body. Use
do { local $/; <STDIN> }to read the entire body without altering any characters. - Validate the
SignatureCertChainUrl: Always check that the URL uses HTTPS, port 443, and ends with.amazontrust.com— this prevents attackers from feeding you a malicious certificate. - Check the timestamp: Alexa includes a
Timestampheader— reject any requests where this timestamp is more than 15 minutes off from current UTC time to stop replay attacks.
Here’s a condensed version of the end-to-end process you can adapt for your web service:
use strict; use warnings; use Crypt::OpenSSL::X509; use Crypt::OpenSSL::RSA; use MIME::Base64; use URI; use HTTP::Tiny; use DateTime; use DateTime::Format::ISO8601; # Grab request headers and body my $signature = $ENV{'HTTP_SIGNATURE'}; my $cert_chain_url = $ENV{'HTTP_SIGNATURECERTCHAINURL'}; my $timestamp = $ENV{'HTTP_TIMESTAMP'}; my $request_body = do { local $/; <STDIN> }; # 1. Validate timestamp my $req_time = DateTime::Format::ISO8601->parse_datetime($timestamp); my $now = DateTime->now(time_zone => 'UTC'); if (abs($now->delta_ms($req_time)->in_units('minutes')) > 15) { die "Request timestamp is too old or far in the future"; } # 2. Validate certificate URL my $uri = URI->new($cert_chain_url); unless ($uri->scheme eq 'https' && $uri->port == 443 && $uri->host =~ /\.amazontrust\.com$/) { die "Invalid certificate chain URL"; } # 3. Download certificate chain my $http = HTTP::Tiny->new(); my $cert_response = $http->get($cert_chain_url); die "Failed to download cert chain: " . $cert_response->{reason} unless $cert_response->{success}; my @certs = split /-----END CERTIFICATE-----\s*/, $cert_response->{content}; my $primary_cert = $certs[0] . "-----END CERTIFICATE-----"; # 4. Parse primary cert and verify signature my $x509 = Crypt::OpenSSL::X509->new_from_string($primary_cert, Crypt::OpenSSL::X509::FORMAT_PEM()); my $pub_key_pem = $x509->pubkey(); my $rsa = Crypt::OpenSSL::RSA->new_public_key($pub_key_pem); $rsa->use_sha256_hash(); my $decoded_sig = decode_base64($signature); if ($rsa->verify($request_body, $decoded_sig)) { # Handle valid request here print "Content-Type: application/json\r\n\r\n"; print '{"version":"1.0","response":{"outputSpeech":{"type":"PlainText","text":"Hello from your verified Alexa service!"}}}'; } else { # Reject invalid request print "HTTP/1.1 403 Forbidden\r\n"; print "Content-Type: text/plain\r\n\r\n"; print "Invalid request signature"; }
内容的提问来源于stack exchange,提问作者chris

