Search code examples
delphiindy

Can Indy load SSL certificates from memory?


I have a client app that needs to authorize itself to a server using certificates. The communication is using SSL over TCP (not HTTP/HTTPS related).

I have this working when I use the private key file and set this on the IOHandler, etc.

I would like to avoid having to put the private key file on disk on the client side. Instead, I would like to provide the private key to Indy via memory in some way.

I have not been able to find a way to do this, so now I am wondering if this is at all possible?

Clarification: My plan is to include the private key in encrypted form as a resource in the .exe file, decrypt it in memory, and then feed it to Indy via a stream or buffer. So the question is, does Indy support this in some way?


Solution

  • At this time, Indy does not expose functionality to load certificates from memory. There is an open ticket for that feature request:

    #150: Support loading OpenSSL certificate/key data from user-defined storage

    However, OpenSSL does support it, and under D2009+ on Windows, Indy resorts to that functionality in order to load certificate files using UTF-16 filenames, which OpenSSL does not support (on *Nix systems, it does support UTF-8 filenames, though). Indy loads the files into memory and uses the same parsing functions that OpenSSL itself uses. So what you are asking for is possible, just not straight forward.

    Here is an example. These are Indy's IndySSL_CTX_use_PrivateKey_file_PKCS12() and (UTF-16 based) IndySSL_CTX_use_PrivateKey_file() wrapper functions, which TIdSSLContext.LoadKey() calls to load the private key file specified in the TIdSSLOptions.KeyFile property:

    function TIdSSLContext.LoadKey: Boolean;
    begin
      if PosInStrArray(ExtractFileExt(KeyFile), ['.p12', '.pfx'], False) <> -1 then begin
        Result := IndySSL_CTX_use_PrivateKey_file_PKCS12(fContext, KeyFile) > 0;
      end else begin
        Result := IndySSL_CTX_use_PrivateKey_file(fContext, KeyFile, SSL_FILETYPE_PEM) > 0;
      end;
      if Result then begin
        Result := SSL_CTX_check_private_key(fContext) > 0;
      end;
    end;
    
    function IndySSL_CTX_use_PrivateKey_file_PKCS12(ctx: PSSL_CTX; const AFileName: String): TIdC_INT;
    var
      LM: TMemoryStream;
      B: PBIO;
      LKey: PEVP_PKEY;
      LCert: PX509;
      P12: PPKCS12;
      CertChain: PSTACK_OF_X509;
      LPassword: array of TIdAnsiChar;
      LPasswordPtr: PIdAnsiChar;
    begin
      Result := 0;
    
      LM := nil;
      try
        LM := TMemoryStream.Create;
        LM.LoadFromFile(AFileName);
      except
        // Surpress exception here since it's going to be called by the OpenSSL .DLL
        // Follow the OpenSSL .DLL Error conventions.
        SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_SYS_LIB);
        LM.Free;
        Exit;
      end;
    
      try
        B := BIO_new_mem_buf(LM.Memory, LM.Size);
        if not Assigned(B) then begin
          SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB);
          Exit;
        end;
        try
          SetLength(LPassword, MAX_SSL_PASSWORD_LENGTH+1);
          LPassword[MAX_SSL_PASSWORD_LENGTH] := TIdAnsiChar(0);
          LPasswordPtr := PIdAnsiChar(LPassword);
          if Assigned(ctx^.default_passwd_callback) then begin
            ctx^.default_passwd_callback(LPasswordPtr, MAX_SSL_PASSWORD_LENGTH, 0, ctx^.default_passwd_callback_userdata);
            // TODO: check return value for failure
          end else begin
            // TODO: call PEM_def_callback(), like PEM_read_bio_X509() does
            // when default_passwd_callback is nil
          end;
          P12 := d2i_PKCS12_bio(B, nil);
          if not Assigned(P12) then begin
            SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_PKCS12_LIB);
            Exit;
          end;
          try
            CertChain := nil;
            if PKCS12_parse(P12, LPasswordPtr, LKey, LCert, @CertChain) <> 1 then begin
              SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_FILE, ERR_R_PKCS12_LIB);
              Exit;
            end;
            try
              Result := SSL_CTX_use_PrivateKey(ctx, LKey);
            finally
              sk_pop_free(CertChain, @X509_free);
              X509_free(LCert);
              EVP_PKEY_free(LKey);
            end;
          finally
            PKCS12_free(P12);
          end;
        finally
          BIO_free(B);
        end;
      finally
        FreeAndNil(LM);
      end;
    end;
    
    function IndySSL_CTX_use_PrivateKey_file(ctx: PSSL_CTX; const AFileName: String;
      AType: Integer): TIdC_INT;
    var
      LM: TMemoryStream;
      B: PBIO;
      LKey: PEVP_PKEY;
      j: TIdC_INT;
    begin
      Result := 0;
    
      LM := nil;
      try
        LM := TMemoryStream.Create;
        LM.LoadFromFile(AFileName);
      except
        // Surpress exception here since it's going to be called by the OpenSSL .DLL
        // Follow the OpenSSL .DLL Error conventions.
        SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_SYS_LIB);
        LM.Free;
        Exit;
      end;
    
      try
        B := BIO_new_mem_buf(LM.Memory, LM.Size);
        if not Assigned(B) then begin
          SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB);
          Exit;
        end;
        try
          case AType of
            SSL_FILETYPE_PEM:
              begin
                j := ERR_R_PEM_LIB;
                LKey := PEM_read_bio_PrivateKey(B, nil,
                  ctx^.default_passwd_callback,
                  ctx^.default_passwd_callback_userdata);
              end;
            SSL_FILETYPE_ASN1:
              begin
                j := ERR_R_ASN1_LIB;
                LKey := d2i_PrivateKey_bio(B, nil);
              end;
          else
            SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, SSL_R_BAD_SSL_FILETYPE);
            Exit;
          end;
          if not Assigned(LKey) then begin
            SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, j);
            Exit;
          end;
          Result := SSL_CTX_use_PrivateKey(ctx, LKey);
          EVP_PKEY_free(LKey);
        finally
          BIO_free(B);
        end;
      finally
        FreeAndNil(LM);
      end;
    end;
    

    As you can see, Indy loads the certificate file into a BIO using BIO_new_mem_buf(), extracts an EVP_PKEY from it, and passes it to OpenSSL's SSL_CTX_use_PrivateKey() function to load the key into the session's SSL_CTX object (Indy does not support RSA/ASN1 private keys at this time).

    So, you would have to adopt similar logic into your own code. Of course, you would have to make your code get invoked at the correct time, and for that you would need to modify Indy's source code to add your code to TIdSSLContext.LoadKey() and then recompile Indy. At least until Indy exposes native access in a future release.