How to use x509 public key to verify a signed/hashed Alexa request body?



I’m trying to work through the steps listed here for an Alexa Skill I’m developing in Java.

I’m getting a request from Alexa which is a POST. Two of the headers are the signing certificate chain url and the signature.

Amazon SHA1 hashes then signs the entire body of the Alexa request with an X509 key, and then base64 encodes the signed body. This is the “signature.” The signing certificate chain is the url where I can GET the X509 certificate chain that contains their public key.

What I need to do is base64 decode the signature, then use the X509 public key to decrypt the signature. This leaves me with a SHA1 hashed request body. Then I need to SHA1 hash the body of the request myself and compare the two.

I validate the certificate chain. I extract the public key. I hash the body of the POST and produce a derived hash value (its SHA1withRSA). I base64 decode the “signature” then decrypt it with the public key to get the asserted hash value.

I’ve not been able to produce a derived hash value that matches the asserted hash value. This is where I’m stuck and I can’t understand what I’m doing wrong. I don’t really understand this encryption stuff very well so perhaps I’m missing something super simple.

Step 8 from the link above is where I’m stuck.

First, I borrowed the code from the alexa SDK here. The problem is this code doesn’t seem to work:

    Signature signature = Signature.getInstance(ServletConstants.SIGNATURE_ALGORITHM);
    signature.initVerify(signingCertificate.getPublicKey());
    signature.update(body);
        
    if (!signature.verify(Base64.decodeBase64(baseEncoded64Signature.getBytes(ServletConstants.CHARACTER_ENCODING)))) {
        throw new SecurityException("Failed to verify the signature/certificate for the provided skill request");
    }

SIGNATURE_ALGORITHM = SHA1withRSA
CHARACTER_ENCODING = UTF-8.
signingCertificate is the X509Certificate.

This code wasn’t giving me matching derived and asserted hash values. So I followed this tutorial which used the Cipher Class.

I created a scratch file and hard coded the values in. I extracted the body from the request. I used postman to get the body of the x509 certificate chain. I also took the signature header from the request.

    byte[] decodedSignature = Base64.decodeBase64(encodedSignature);
    Cipher cipher = Cipher.getInstance("RSA");
    PublicKey key = signingCertificate.getPublicKey();
    cipher.init(Cipher.DECRYPT_MODE, key);
    
    byte[] decryptedSig = cipher.doFinal(decodedSignature);
    byte[] hashedBody = DigestUtils.sha1(body);

body = converted the body of the alexa request to byte.
encodedSignature = the signature header provided in the alexa request.

Obviously the decryptedSig and hashedBody are not matching. See below.

decryptedSig: 48,33,48,9,6,5,43,14,3,2,26,5,0,4,20,-68,25,70,-54,-63,91,-37,73,34,82,-63,62,45,48,-117,112,42,18,-24,-113

hashedBody: -107,76,55,24,9,79,77,-21,101,57,-103,25,42,-54,-28,5,34,-26,117,38

The problem is: I have no idea what the problem is. Like I said, I don’t understand this stuff. I’m just trying to build an alexa skill and this is part of what Amazon requires.

This is the base64 encoded signature:
fmBSIwM+GIN977W9ztbagtnMalXPJBWat8KwoWBauAIrXHaKvjVlY8hqA/vXEdzPYy7rL0B6Tw9uUeHYah6LU7xISIiUpZjm1Ls2t1Nt2LXbyTgLGdNU4RQJiSxoq+87BEmOOBUNTGiDOveZs/9+KTgQgLgyelG6wHwk34p6w/TgqardQ39vjpzqui63s5/2om1KgJs5e1gt24Cemapr6f+Slz0xmmdLmLZ1Hn7nNgnIB3UjQzcVxU6KYJ1rNfnzZNHFSPcnrZ9ArvUT+M7OM10NfkPp53M6Oy3/5pibV13iQKibFijTCZQEFGLl6fXBgoWpBr1iWyYZbUGTk2+yow==

This is the body of the request:
{“version”:”1.0″,”session”:{“new”:false,”sessionId”:”amzn1.echo-api.session.4ebd9d8c-d76c-403f-b82b-952492fffa74″,”application”:{“applicationId”:”amzn1.ask.skill.1ac44f3a-696a-4cc0-9944-6b7d9440b394″},”user”:{“userId”:”amzn1.ask.account.AGO62C2OKQUIGVD4J6SWHKOERZDPPMYLKHP5GAA67TO6Y6KNOGDGGKFHJE6LEYSQTQQ6GJNGSCDIQUYLMYFQXJPV53YEZPLW4AJPOLH7TCMYDKUMZM2QXBSEDEJ43VRLKFF6WUBB47AW7MRKVDE427DQMYX3KIFKO7ZCDPJKQGANEMSNWLWZRICRGVPM6YBOHPV3BB47PZKGSHI”}},”context”:{“Viewports”:[{“type”:”APL”,”id”:”main”,”shape”:”RECTANGLE”,”dpi”:160,”presentationType”:”STANDARD”,”canRotate”:false,”configuration”:{“current”:{“mode”:”HUB”,”video”:{“codecs”:[“H_264_42″,”H_264_41″]},”size”:{“type”:”DISCRETE”,”pixelWidth”:1024,”pixelHeight”:600}}}}],”Viewport”:{“experiences”:[{“arcMinuteWidth”:246,”arcMinuteHeight”:144,”canRotate”:false,”canResize”:false}],”mode”:”HUB”,”shape”:”RECTANGLE”,”pixelWidth”:1024,”pixelHeight”:600,”dpi”:160,”currentPixelWidth”:1024,”currentPixelHeight”:600,”touch”:[“SINGLE”],”video”:{“codecs”:[“H_264_42″,”H_264_41″]}},”System”:{“application”:{“applicationId”:”amzn1.ask.skill.1ac44f3a-696a-4cc0-9944-6b7d9440b394″},”user”:{“userId”:”amzn1.ask.account.AGO62C2OKQUIGVD4J6SWHKOERZDPPMYLKHP5GAA67TO6Y6KNOGDGGKFHJE6LEYSQTQQ6GJNGSCDIQUYLMYFQXJPV53YEZPLW4AJPOLH7TCMYDKUMZM2QXBSEDEJ43VRLKFF6WUBB47AW7MRKVDE427DQMYX3KIFKO7ZCDPJKQGANEMSNWLWZRICRGVPM6YBOHPV3BB47PZKGSHI”},”device”:{“deviceId”:”amzn1.ask.device.AGLWBJS53GJU5GT755HMYMOH7MCGVSVQAICMZGBZSUNVY2OE6DNQFG4K4UMM3R5NPJR6XSHAABZ44VV6BOUR7SVPZF5DJUXXCTEUAQTCCRZSXKHWWS7N4CAEHGK4VGBHJM57ARCABSPZ4C4LACWJX65ZBKZ5N6LGHZVXIPHJMGQBPCYGGWZIE”,”supportedInterfaces”:{}},”apiEndpoint”:”https://api.amazonalexa.com”,”apiAccessToken”:”eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJodHRwczovL2FwaS5hbWF6b25hbGV4YS5jb20iLCJpc3MiOiJBbGV4YVNraWxsS2l0Iiwic3ViIjoiYW16bjEuYXNrLnNraWxsLjFhYzQ0ZjNhLTY5NmEtNGNjMC05OTQ0LTZiN2Q5NDQwYjM5NCIsImV4cCI6MTYxMDE1Mjk2MiwiaWF0IjoxNjEwMTUyNjYyLCJuYmYiOjE2MTAxNTI2NjIsInByaXZhdGVDbGFpbXMiOnsiY29udGV4dCI6IkFBQUFBQUFBQUFBb0FiNFRMdW84M2t4c0FmUEsrYjBpS3dFQUFBQUFBQUM0ekx5ZGFPeGlTdms4UlppLzIrWjduMjFvNEdGYmEveHdqcE5pREo5MzNyb2FyQU5WSnYwUHBBbGwvcWlyK0JhUWQxTjhoY2pkVnhXeTJxQUhrOXNBQmNIRnVJcE5hbTRHYVBKVk9QaTdVZXhic1ZrSEtQSm5Vc0lqSmdtL3JCRm80eGtpWEhNZi84UC9wd0VxMVZIK2xKTHo4ZWRoMGZHUUU3cGQ4TDVYcjcwUlZ0UDFkajEvbld5SGJINVR5RHhHVHU0ZUtGVkJ6ODNKTkRXeG5pa0NHeS9lQjcwNWZBMWxxSDd0QUVRN2lPZUhDR09ZeHh1NE9xcFBvTWgzNHlvUk5wbVZidGd4MGJDQ0dzL3crZHIxRVlIRG50RTdyWnRVM29zei9FOE01UThLV2NCcy8zNERqZlAvaUxvTXVHaWNnOWtRbGNZdm1qWEhpRkNJMDczUEo5NlZweWE3cURVbzc1YmxSZWZuM00wQnJNVVl1VGYzTnN4aElNbFFWQXB2aUhCRndEZVh6eWVtdnc9PSIsImNvbnNlbnRUb2tlbiI6bnVsbCwiZGV2aWNlSWQiOiJhbXpuMS5hc2suZGV2aWNlLkFHTFdCSlM1M0dKVTVHVDc1NUhNWU1PSDdNQ0dWU1ZRQUlDTVpHQlpTVU5WWTJPRTZETlFGRzRLNFVNTTNSNU5QSlI2WFNIQUFCWjQ0VlY2Qk9VUjdTVlBaRjVESlVYWENURVVBUVRDQ1JaU1hLSFdXUzdONENBRUhHSzRWR0JISk01N0FSQ0FCU1BaNEM0TEFDV0pYNjVaQktaNU42TEdIWlZYSVBISk1HUUJQQ1lHR1daSUUiLCJ1c2VySWQiOiJhbXpuMS5hc2suYWNjb3VudC5BR082MkMyT0tRVUlHVkQ0SjZTV0hLT0VSWkRQUE1ZTEtIUDVHQUE2N1RPNlk2S05PR0RHR0tGSEpFNkxFWVNRVFFRNkdKTkdTQ0RJUVVZTE1ZRlFYSlBWNTNZRVpQTFc0QUpQT0xIN1RDTVlES1VNWk0yUVhCU0VERUo0M1ZSTEtGRjZXVUJCNDdBVzdNUktWREU0MjdEUU1ZWDNLSUZLTzdaQ0RQSktRR0FORU1TTldMV1pSSUNSR1ZQTTZZQk9IUFYzQkI0N1BaS0dTSEkifX0.FAaSO9NwDL_lTSST16Fs0Cs-VlYLDpBfD02-m5zYwYvxKDNXcDooRN5SjsLetsNnXT0tyCq20QboCBCqESaDaq9K5RBzkhEQc2BWYp31P9gyEpGIn23YQbm_2JpEDzGwIcZ6CwtXlGyJee7IdZCqDcD9uC7Ytnjf2k-mUAjrTtx4t5XCoy67HhSACh14ySgooW6PRYXKiNrdrOz1VW1dmQKy1obHcAX2fHU7SIEdrQU1Q11-6J2dUH6S2RuMncshhg17GWuzGXGIJW7n-JY5VPEoPSnxXOHnAXZeaCxabVBR9ryaeZUwUxGMF6ZQTBR13L8ea3575os8eBcM6ALtUQ”}},”request”:{“type”:”SessionEndedRequest”,”requestId”:”amzn1.echo-api.request.d5bd94a6-2011-4f72-b39d-5fcc3c276536″,”timestamp”:”2021-01-09T00:37:42Z”,”locale”:”en-US”,”reason”:”USER_INITIATED”}}

I’m really hoping its something stupid and simple. I appreciate anyone who takes the time to read through this to help me out.

Answer

First, digital signature is NOT encryption with the privatekey; Amazon is deceiving you there, see https://security.stackexchange.com/questions/159282/can-openssl-decrypt-the-encrypted-signature-in-an-amazon-alexa-request-to-a-web which was basically the same question except without Java. And Java crypto exacerbates this because it was designed in the 1990s when this mistake was still fairly common, and as a result the Cipher object which is intended to be for encryption and decryption accepts the ‘backwards’ use of RSA keys and internally changes them to the operations used in the Signature scheme ‘NoneWithRSA’ (which might be considered a pseudo-scheme since it doesn’t really match PKCS1).

Expanding on that point, the difference between your ‘decrypted’ (more properly, recovered) value and a simple hash is that the PKCS1v1 signature scheme used here, now retronymed RSASSA-PKCS1-v1_5 in PKCS1v2, actually has four steps:

#1 hash the data

#2 encode the hash value and algorithm in a DigestInfo ASN.1 structure encoded in DER, which amounts to adding a fixed prefix per algorithm

#3-5 prepend padding of the form 00 01 FF…(at least 8) 00

(8.2.1#2) treating the result as a number m, apply RSASP1 which does m ^ d mod n (or for verify 8.2.2#2 apply RSAVP1 which does s ^ e mod n; this is stated as before the three padding steps above but actually can just as well be after)

The backwards-Cipher operation performs, or reverses, only the third and fourth steps above; you have added the first step, but not the second, so your ‘decrypted’ value is actually a DigestInfo structure that contains some metadata, the OID for the SHA1 algorithm, and the hash value that should correspond to the data.

This failure to create or remove the DigestInfo structure is also a very common mistake and problem; see my list at https://crypto.stackexchange.com/questions/87006/why-is-data-signed-with-sha256-rsa-pkcs-and-digest-signed-with-rsa-pkcs-differen/#87022 .

But it doesn’t match. The hash value embedded in the recovered DigestInfo is not the same as the hash value you computed on your data (and I also get). This strongly suggests some change between your data and the data the Amazon signed, but I have no idea what; certainly your data looks superficially like an Alexa request should. Sorry 🙂



Source: stackoverflow