Search code examples
c++openssljwtbase64url

What are the exact base64 url decoding rules and implementation using openssl and C++ to validate JWT


I am quite new to openssl interface and base64 decoding rules as general and trying to verify JWT signed with private key (RS256 alg) using the solution: Segmentation Fault while verifying JWT token using a public key through openSSL in C++ But my sample JWT is failing to verify.

I only do not have the base64 url decode function, so I manually executed the steps as I understood it - before calling my base64 decode function I replaced '-' -> '+' and '_' -> '/'. To be able to get the proper signature length I also had to manually add the padding ('='). Is there some rule for base64 url decoding that I am missing or is the approach with first replacing the symbols wrong in any way?

I tried first to use RSA_verify, but the error is the same.

The same token and key are validated using python and online JWT validation so they should be ok. I printed the signature after the b64 decoding in hex on the gdb and after the urlsafe_b64decode on the python and the values were the same (except some symbols at the end in the python version) which even more made be believe that the replacing strategy should be working.

   // This is the original token
   static constexpr const char* buffer_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXBpLXJlc291cmNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNTYzNDUwODkzLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMzVhYmVjZDEtNjcxMi00M2M3LWE4MDItZjg3MGYzMTY4MmI0IiwiY2xpZW50X2lkIjoidGVzdCJ9.ZCXtI2nN-d0Cn5dgb3K9JMI41nrEaK_AVSMRG9c5cyZqXpnMQETfGcDEs0jPzmRh-jDc-Kuq53naOtjkItMcR_vYPn72dKZ4Fpp8mvOAZXypkVCLzof3Lsxrtqq9G3V4LNTuOHiXW_q-9mEu51zWg1HDr1-rSt3YXkFFSWp5e4MWS2TNP1MB7lBbZC-kdMZ_GqZ9lrfNo2YqJR7tqcHOrfOmFTzqxVivEB8s-A0iEv_MwdlS6LpJBKU9-d94i1P9Lsqzlg7b_0ekRoYJEG4DXeNp2zxxBxZ1u3FBlIbyJoOGDmX-EU4A5eh2RlDdEvG1YF_zcMARpP1bFV86WTSOuQ";

    // This is token with replaced symbols that I am testing before writing the method for url decode
    static constexpr const char* buffer_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXBpLXJlc291cmNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNTYzNDUwODkzLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMzVhYmVjZDEtNjcxMi00M2M3LWE4MDItZjg3MGYzMTY4MmI0IiwiY2xpZW50X2lkIjoidGVzdCJ9.ZCXtI2nN+d0Cn5dgb3K9JMI41nrEaK/AVSMRG9c5cyZqXpnMQETfGcDEs0jPzmRh+jDc+Kuq53naOtjkItMcR/vYPn72dKZ4Fpp8mvOAZXypkVCLzof3Lsxrtqq9G3V4LNTuOHiXW/q+9mEu51zWg1HDr1+rSt3YXkFFSWp5e4MWS2TNP1MB7lBbZC+kdMZ/GqZ9lrfNo2YqJR7tqcHOrfOmFTzqxVivEB8s+A0iEv/MwdlS6LpJBKU9+d94i1P9Lsqzlg7b/0ekRoYJEG4DXeNp2zxxBxZ1u3FBlIbyJoOGDmX+EU4A5eh2RlDdEvG1YF/zcMARpP1bFV86WTSOuQ==";

    // this is how I create the RSA from a key, hopefully successfully because a key is returned with no error
    RSA* create_public_rsa(const unsigned char* p_key)
    {
       BIO *keybio = BIO_new_mem_buf(p_key, -1); // -1: assume string is null terminated
       if (!keybio)
       {
            return nullptr;
       }

       RSA* l_res = nullptr;

       l_res = PEM_read_bio_RSA_PUBKEY(keybio, NULL, NULL, NULL);

       BIO_free(keybio);

       return l_res;
    }

    bool RSAVerifySignature(RSA* rsa, std::string const& token)
    {
       auto pub_key_handle = std::shared_ptr<EVP_PKEY>(EVP_PKEY_new(), EVP_PKEY_free);
       if (!pub_key_handle)
       {
           return false;
       }

       RSA_up_ref(rsa);
       EVP_PKEY_assign_RSA(pub_key_handle.get(), rsa);


       std::string decoded_header(token, 0, token.find('.'));
       std::string decoded_body;
       decoded_body.append(token.begin()+ token.find('.')+1, token.begin() + token.rfind('.')-1);
       std::string sig;
       sig.append(token.begin() + token.rfind('.') + 1, token.end());
       std::string sig_decoded;
       base64_decode(sig.c_str(), sig.size(), sig_decoded);

       EVP_MD_CTX* l_ctx = EVP_MD_CTX_create();
       EVP_MD_CTX_init(l_ctx);

       EVP_PKEY_CTX *pctx;
       if (1 != EVP_DigestVerifyInit(l_ctx, /*&pctx*/nullptr, 
 EVP_sha256(), nullptr, pub_key_handle.get())) return false;
       //pub_key_handle.reset();
       if (1 != EVP_DigestVerifyUpdate(l_ctx, reinterpret_cast<const unsigned char*>(decoded_header.data()), decoded_header.length())) return false;
       if (1 != EVP_DigestVerifyUpdate(l_ctx, ".", 1)) return false;
       if (1 != EVP_DigestVerifyUpdate(l_ctx, reinterpret_cast<const unsigned char*>(decoded_body.data()), decoded_body.length())) return false;
       if(1 == EVP_DigestVerifyFinal(l_ctx, reinterpret_cast<const unsigned char*>(sig_decoded.data()), sig_decoded.length())) return true;

       // ERR_print_errors_fp(stdout);

       ERR_load_crypto_strings();
       char err[130];
       while(auto e = ERR_get_error())
       {
          ERR_error_string(e, err);
          fprintf(stderr, "Error verifying message: %s\n", err);
       }

       return false;
    }  

The error is:

header: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
body: eyJhdWQiOlsiYXBpLXJlc291cmNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNTYzNDUwODkzLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMzVhYmVjZDEtNjcxMi00M2M3LWE4MDItZjg3MGYzMTY4MmI0IiwiY2xpZW50X2lkIjoidGVzdCJ
sig: ZCXtI2nN+d0Cn5dgb3K9JMI41nrEaK/AVSMRG9c5cyZqXpnMQETfGcDEs0jPzmRh+jDc+Kuq53naOtjkItMcR/vYPn72dKZ4Fpp8mvOAZXypkVCLzof3Lsxrtqq9G3V4LNTuOHiXW/q+9mEu51zWg1HDr1+rSt3YXkFFSWp5e4MWS2TNP1MB7lBbZC+kdMZ/GqZ9lrfNo2YqJR7tqcHOrfOmFTzqxVivEB8s+A0iEv/MwdlS6LpJBKU9+d94i1P9Lsqzlg7b/0ekRoYJEG4DXeNp2zxxBxZ1u3FBlIbyJoOGDmX+EU4A5eh2RlDdEvG1YF/zcMARpP1bFV86WTSOuQ==
Error verifying message: error:04091068:rsa routines:INT_RSA_VERIFY:bad signature
error:04091068:rsa routines:INT_RSA_VERIFY:bad signature

Solution

  • This is the base64_url_decode that I used. I didn't write it but I can't find where I got it from.

    With the below code, do you still have a problem?

    /*
    Base64 translates 24 bits into 4 ASCII characters at a time. First,
    3 8-bit bytes are treated as 4 6-bit groups. Those 4 groups are
    translated into ASCII characters. That is, each 6-bit number is treated
    as an index into the ASCII character array.
    
    If the final set of bits is less 8 or 16 instead of 24, traditional base64
    would add a padding character. However, if the length of the data is
    known, then padding can be eliminated.
    
    One difference between the "standard" Base64 is two characters are different.
    See RFC 4648 for details.
    This is how we end up with the Base64 URL encoding.
    */
    
    const char base64_url_alphabet[] = {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
    };
    
    std::string base64_url_encode(const std::string & in) {
      std::string out;
      int val =0, valb=-6;
      size_t len = in.length();
      unsigned int i = 0;
      for (i = 0; i < len; i++) {
        unsigned char c = in[i];
        val = (val<<8) + c;
        valb += 8;
        while (valb >= 0) {
          out.push_back(base64_url_alphabet[(val>>valb)&0x3F]);
          valb -= 6;
        }
      }
      if (valb > -6) {
        out.push_back(base64_url_alphabet[((val<<8)>>(valb+8))&0x3F]);
      }
      return out;
    }
    
    std::string base64_url_decode(const std::string & in) {
      std::string out;
      std::vector<int> T(256, -1);
      unsigned int i;
      for (i =0; i < 64; i++) T[base64_url_alphabet[i]] = i;
    
      int val = 0, valb = -8;
      for (i = 0; i < in.length(); i++) {
        unsigned char c = in[i];
        if (T[c] == -1) break;
        val = (val<<6) + T[c];
        valb += 6;
        if (valb >= 0) {
          out.push_back(char((val>>valb)&0xFF));
          valb -= 8;
        }
      }
      return out;
    }
    

    Update:

    Your problem isn't the base64 decode, it's this line:

    decoded_body.append(token.begin()+ token.find('.')+1, token.begin() + token.rfind('.')-1);
    

    You are out by one. The decoded_body is missing the last character. If you change it to the following it will work ok:

    decoded_body.append(token.begin()+ token.find('.')+1, token.begin() + token.rfind('.'));