Sunday, September 8, 2013

OpenSSL is written by monkeys

Source: http://www.peereboom.us/assl/assl/html/openssl.html

I know it sounds harsh but lets investigate the facts.
First lets get out of the way who is complaining. Hi, I am Marco Peereboom and I write open source code for fun. I have been involved in several projects and the google will tell you which ones. I am by no stretch of the imagination a great programmer but I have been around and I have written a couple of things.
Recently I got involved in writing some code that requires secure communications and I figured that there was no better way to get cranking than to use a well known and widely used library. Essentially my problem boils down to this:
  1. Write an application that provides CA (Certificate Authority) services.
  2. Store all certificates in an LDAP tree.
The debate is not about the merit of this idea but this is where the journey started. It sounded simple enough so off to the web looking for documentation and code snippets, this is obviously going to be a walk in the park since all these problems have been solved before.
After messing around with this code for about a month I decided to write this up for the tubes in the hope that I can save some souls. I have come to the conclusion that OpenSSL is equivalent to monkeys throwing feces at the wall. It is, bar none, the worst library I have ever worked with. I can not believe that the internet is running on such a ridiculous complex and gratuitously stupid piece of code. Since circa 1998 the whole world has been trusting their secure communications to this impenetrable morass that calls itself the "OpenSSL" project. I bet that the doctors that work on that shitshow can not prescribe anything useful either!

Day 1:
Hmmm can't find a whole bunch of code at all. What I do find doesn't help me with my CA problem at all. Everything I do find is basically a whole bunch of HOWTOS written by people who don't seem to grasp the underlying problems. They mean well but really hurt the community as a whole by providing recipes for disaster. Alright no problem off to Barnes & Nobles and lets see what we can find.
Crap! Nothing really. I found 2 books and they were mostly about using the command line tool. One had some code examples but nothing advanced at all. Apparently people only seem to care about using openssl "the tool".
I had peeked at the code and it gave me vertigo so I stepped away from it. Anyway at this point I was starting to suspect that this might not be as easy as I had hopped. Time to go home anyway and drink a cold one.

Day 2:
Alright screw it! I'll look at the damn code; I mean all books and examples on the web use it. At least it'll point me to what functions I need to call and I'll be able to find those in the fine documentation. First I obviously need to figure out how to use openssl "the tool".
8 hours later...
Phew I got something that works after reading endless HOWTOS, specs and postings on the tubes. The documentation was, how does one say that? Really really bad! Let me share with you my labor of love. I created 3 scripts to illustrate my problem of having a CA that signs client and server certificates.
  1. create_ca, this script creates the CA
  2. create_server, this script creates a server certificate and keys
  3. create_client, this script creates a client certificate and keys
create_ca
#/bin/sh
mkdir -p ca/private
chmod 700 ca/private
openssl req -x509 -days 3650 -newkey rsa:1024 -keyout ca/private/ca.key -out ca/ca.crt

create_server
#/bin/sh
mkdir -p server/private
chmod 700 server/private
openssl genrsa -out server/private/server.key 1024
openssl req -new -key server/private/server.key -out server/server.csr

create_client
#/bin/sh
mkdir -p client/private
chmod 700 client/private
openssl genrsa -out client/private/client.key 1024
openssl req -new -key client/private/client.key -out client/client.csr
enough for a day. Time to go home.

Day 3: Alright, that openssl "the tool" stuff out of the way lets dig into that code. Hmmm, where is main? How does it call the individual modules? Random camel capitalization mixed with underscores wow! Ugh MAIN macro, great. *WASH_Eyes_outWith_soap*
Ok we are going to need some serious tags to be able to navigate this morass. After a couple of hours of digging through the code I kind of get the hang of it (thanks vim!). Now it is time to start walking this code backwards and see if I can generate a CA without having to use openssl "the tool". A few more hours go by and the incomplete man pages, no man pages, poorly written man pages really start to wear on me. No help from google, bing, yahoo etc. The documentation is simply non-existent and what does exist is outdated and does not match reality. Screw it, I am going home!

Day 4:
I start writing some code based on what I find in openssl "the tool". Progress is painfully slow due to the embarrassingly bad style, indentation and don't get me started about impenetrable #ifdef goo. Speaking of #ifdefs I saw several dangling ones that would eat an instruction; e.g.
#ifdef (OMG)
 if (moo) {
  ...
 } else
#endif /* OMG */
  yeah();
This is actually the pretty version. The one I ran into was over a couple hundred lines of code with indentation that makes a grown man cry. Lets also get one other thing out of the way. If you think
 if (moo)
     {
     dome_something_dumb();
     }
     else
     {
     or_not();
     }
or
 if (   moo)
    {
    blah();
    }
 if (bad)
  goto err;
 ...
 if (0) {
err:
  do_something_horrible();
 }
is readable I suggest you visit an optometrist. Possibly even a proctologist. Lets look at a few real examples
  if ((OBJ_obj2nid(obj) == NID_pkcs9_emailAddress) &&
   (str->type != V_ASN1_IA5STRING))
   {
   BIO_printf(bio_err,"\nemailAddress type needs to be of type IA5STRING\n");
   goto err;
   }
  if ((str->type != V_ASN1_BMPSTRING) && (str->type != V_ASN1_UTF8STRING))
   {
   j=ASN1_PRINTABLE_type(str->data,str->length);
   if ( ((j == V_ASN1_T61STRING) &&
     (str->type != V_ASN1_T61STRING)) ||
    ((j == V_ASN1_IA5STRING) &&
     (str->type == V_ASN1_PRINTABLESTRING)))
    {
    BIO_printf(bio_err,"\nThe string contains characters that are illegal for the ASN.1 type\n");
    goto err;
    }
   }
Here is an example of a function stack; vim to the rescue!
 if (!SSL_CTX_use_certificate_file(ctx, "server/server.crt", SSL_FILETYPE_PEM))
ctrl ]
...
 else if (type == SSL_FILETYPE_PEM)
  {
  j=ERR_R_PEM_LIB;
  x=PEM_read_bio_X509(in,NULL,ctx->default_passwd_callback,ctx->default_passwd_callback_userdata);
ctrl ]
...
#define PEM_read_bio_X509(bp,x,cb,u) (X509 *)PEM_ASN1_read_bio( \
 (char *(*)())d2i_X509,PEM_STRING_X509,bp,(char **)x,cb,u)
ctrl ]
...
 if (!PEM_bytes_read_bio(&data, &len, NULL, name, bp, cb, u))
  return NULL;
ctrl ]
...
  if (!PEM_read_bio(bp,&nm,&header,&data,&len)) {
ctrl ]
...
  i=BIO_gets(bp,buf,254);
ctrl ]
...
 i=b->method->bgets(b,in,inl);
That spanned 5 files, 6 indirections and all that to open and fgets the contents of a file. And we still are doing an indirect call. All this work and jumping around when all I wanted is to have a function that can translate a PEM (NOT in a file!!!) cert into a X509 structure. But between the million or so functions nothing handy like that exists; or so I suspect but since there are no docs I really have to guess. I can't rob you guys from this gem either:
#ifndef OPENSSL_NO_STDIO
/*!
 * Load CA certs from a file into a ::STACK. Note that it is somewhat misnamed;
 * it doesn't really have anything to do with clients (except that a common use
 * for a stack of CAs is to send it to the client). Actually, it doesn't have
 * much to do with CAs, either, since it will load any old cert.
 * \param file the file containing one or more certs.
 * \return a ::STACK containing the certs.
 */
STACK_OF(X509_NAME) *SSL_load_client_CA_file(const char *file)
 {
 BIO *in;
 X509 *x=NULL;
 X509_NAME *xn=NULL;
 STACK_OF(X509_NAME) *ret = NULL,*sk;

 sk=sk_X509_NAME_new(xname_cmp);

 in=BIO_new(BIO_s_file_internal());

 if ((sk == NULL) || (in == NULL))
  {
  SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE);
  goto err;
  }
 
 if (!BIO_read_filename(in,file))
  goto err;

 for (;;)
  {
  if (PEM_read_bio_X509(in,&x,NULL,NULL) == NULL)
   break;
  if (ret == NULL)
   {
   ret = sk_X509_NAME_new_null();
   if (ret == NULL)
    {
    SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE);
    goto err;
    }
   }
  if ((xn=X509_get_subject_name(x)) == NULL) goto err;
  /* check for duplicates */
  xn=X509_NAME_dup(xn);
  if (xn == NULL) goto err;
  if (sk_X509_NAME_find(sk,xn) >= 0)
   X509_NAME_free(xn);
  else
   {
   sk_X509_NAME_push(sk,xn);
   sk_X509_NAME_push(ret,xn);
   }
  }

 if (0)
  {
err:
  if (ret != NULL) sk_X509_NAME_pop_free(ret,X509_NAME_free);
  ret=NULL;
  }
 if (sk != NULL) sk_X509_NAME_free(sk);
 if (in != NULL) BIO_free(in);
 if (x != NULL) X509_free(x);
 if (ret != NULL)
  ERR_clear_error();
 return(ret);
 }
#endif
Wow! that one does it all! unreadable, lots of indirection and no clear direction as to what the function is trying to achieve. Got to love the if (0) construct! I mean that obviously wins all the beauty, aesthetics & NIH awards. You have to be overjoyed to know that this type of code runs a lot of our "secure" internet.
Silly me to just want to have a function to read certs from LDAP or memory so that I can write the LDAP code myself. Not much code got written, I need to go home and go into some sort of drunken stooper.

Day 5:
This is starting to piss me off! Armed with a fresh headache I get with the coding part. After a couple of hours of reading and rereading some openssl "the tool" code I came up with this:
int
create_ca(char *retstr, size_t retlen)
{
 int   rv = 1;
 int   days = 365 * 10;
 char   *password = NULL;
 EVP_PKEY  pkey, *tmppkey = NULL;
 BIGNUM   bn;
 RSA   *rsa = NULL;
 X509_REQ  *req = NULL;
 X509_NAME  *subj;
 X509   *x509 = NULL;
 BIO   *out = NULL;

 /* generate private key */
 if ((rsa = RSA_new()) == NULL)
  ERROR_OUT(ERR_SSL, done);
 bzero(&bn, sizeof bn);
 if (BN_set_word(&bn, 0x10001) == 0)
  ERROR_OUT(ERR_SSL, done);
 if (RSA_generate_key_ex(rsa, 1024, &bn, NULL) == 0)
  ERROR_OUT(ERR_SSL, done);
 bzero(&pkey, sizeof pkey);
 if (EVP_PKEY_assign_RSA(&pkey, rsa) == 0)
  ERROR_OUT(ERR_SSL, done);

 /* setup req for certificate */
 if ((req = X509_REQ_new()) == NULL)
  ERROR_OUT(ERR_SSL, done);
 if (X509_REQ_set_version(req, 0) == 0)
  ERROR_OUT(ERR_SSL, done);
 subj = X509_REQ_get_subject_name(req);
 if (validate_canew(subj, &password)) {
  snprintf(last_error, sizeof last_error,
      "validate_canew failed");
  ERROR_OUT(ERR_OWN, done);
 }
 /* set public key to req */
 if (X509_REQ_set_pubkey(req, &pkey) == 0)
  ERROR_OUT(ERR_SSL, done);

 /* generate 509 cert */
 if ((x509 = X509_new()) == NULL)
  ERROR_OUT(ERR_SSL, done);
 bzero(&bn, sizeof bn);
 if (BN_pseudo_rand(&bn, 64 /* bits */, 0, 0) == 0)
  ERROR_OUT(ERR_SSL, done);
 if (BN_to_ASN1_INTEGER(&bn, X509_get_serialNumber(x509)) == 0)
  ERROR_OUT(ERR_SSL, done);
 if (X509_set_issuer_name(x509, X509_REQ_get_subject_name(req)) == 0)
  ERROR_OUT(ERR_SSL, done);
 if (X509_gmtime_adj(X509_get_notBefore(x509), 0) == 0)
  ERROR_OUT(ERR_SSL, done);
 if (days == 0) {
  snprintf(last_error, sizeof last_error,
      "not enough days for certificate");
  ERROR_OUT(ERR_OWN, done);
 }
 days *= 60 * 60 * 24;
 if (X509_gmtime_adj(X509_get_notAfter(x509), days) == 0)
  ERROR_OUT(ERR_SSL, done);
 if (X509_set_subject_name(x509, X509_REQ_get_subject_name(req)) == 0)
  ERROR_OUT(ERR_SSL, done);
 if ((tmppkey = X509_REQ_get_pubkey(req)) == NULL)
  ERROR_OUT(ERR_SSL, done);
 if (X509_set_pubkey(x509, tmppkey) == 0)
  ERROR_OUT(ERR_SSL, done);
 if (X509_sign(x509, &pkey, EVP_sha1()) == 0)
  ERROR_OUT(ERR_SSL, done);

 /* write private key */
 out = BIO_new(BIO_s_file());
 if (BIO_write_filename(out, CA_PKEY) <= 0)
  ERROR_OUT(ERR_SSL, done);
 if (chmod(CA_PKEY, S_IRWXU))
  ERROR_OUT(ERR_LIBC, done);
 if (PEM_write_bio_PrivateKey(out, &pkey, EVP_des_ede3_cbc(), NULL, 0,
     NULL, password) == 0)
  ERROR_OUT(ERR_SSL, done);
 BIO_free_all(out);

 /* write cert */
 out = BIO_new(BIO_s_file());
 if (BIO_write_filename(out, CA_CERT) <= 0)
  ERROR_OUT(ERR_SSL, done);
 if (PEM_write_bio_X509(out, x509) == 0)
  ERROR_OUT(ERR_SSL, done);
 BIO_free_all(out);

 rv = 0;
done:
 if (tmppkey)
  EVP_PKEY_free(tmppkey);
 if (x509)
  X509_free(x509);
 if (req)
  X509_REQ_free(req);
 if (rsa)
  RSA_free(rsa);

 return (rv);
}
As you can see I create an awful mechanism to at least get some sort of usable fault stack to trace errors. Here is the macro in its full horror:
/* errors */
#define ERR_LIBC (0)
#define ERR_SSL  (1)
#define ERR_OWN  (2)

#define ERROR_OUT(e, g) do { push_error(__FILE__, __FUNCTION__, __LINE__, e); goto g; } while(0)
Dear $DEITY I beg your forgiveness for I have sinned.
Let me show the other bits to make this work just for completions sake and to hope I can help another lost soul that has to put up with this poo. The functions that play with this garbage:
char *
geterror(int et)
{
 char   *es;

 switch (et) {
 case ERR_LIBC:
  strlcpy(last_error, strerror(errno), sizeof last_error);
  break;
 case ERR_SSL:
  es = (char *)ERR_lib_error_string(ERR_get_error());
  if (es)
   strlcpy(last_error, es, sizeof last_error);
  else
   strlcpy(last_error, "unknown SSL error",
       sizeof last_error);
  break;
 default:
  strlcpy(last_error, "unknown error", sizeof last_error);
  /* FALLTHROUGH */
 case ERR_OWN:
  break;
 }

 return (last_error);
}

void
push_error(char *file, char *func, int line, int et)
{
 struct error *ce;

 if ((ce = calloc(1, sizeof *ce)) == NULL)
  fatal("push_error ce");
 if ((ce->file = strdup(file)) == NULL)
  fatal("push_error ce->file");
 if ((ce->func = strdup(func)) == NULL)
  fatal("push_error ce->func");
 if ((ce->errstr = strdup(geterror(et))) == NULL)
  fatal("push_error ce->errstr");
 ce->line = line;

 SLIST_INSERT_HEAD(&ces, ce, dlink);
}

Here are the remaining utility functions to make it all "work":
int
cert_find_put(char *entry, X509_NAME *subj, ssize_t min, ssize_t max)
{
 struct valnode  *v;
 int   rv = 1;

 v = find_valtree(entry);
 if (v && v->length > 0) {
  if (min != -1 && v->length < min) {
   snprintf(last_error, sizeof last_error,
       "%s minimum constraint not met %lu < %lu",
       entry, v->length, min);
   ERROR_OUT(ERR_OWN, done);
  }
  if (max != -1 && v->length > max) {
   snprintf(last_error, sizeof last_error,
       "%s maximum constraint not met %lu > %lu",
       entry, v->length, max);
   ERROR_OUT(ERR_OWN, done);
  }
  if (X509_NAME_add_entry_by_txt(subj, entry, MBSTRING_ASC,
      v->value, -1, -1, 0) == 0)
   ERROR_OUT(ERR_SSL, done);
 } else {
  log_debug("cert_find_put: %s not found", entry);
  goto done;
 }

 rv = 0;
done:
 return (rv);
}

int
validate_canew(X509_NAME *subj, char **pwd)
{
 struct valnode  *password, *password2;
 int   rv = 1;

 password = find_valtree("password");
 password2 = find_valtree("password2");

 if (password && password2) {
  if (strcmp(password->value, password2->value) ||
      password->length == 0) {
   snprintf(last_error, sizeof last_error,
       "invalid password");
   ERROR_OUT(ERR_OWN, done);
  }
  *pwd = password->value;
 }
 if (password == NULL && password2 == NULL) {
  snprintf(last_error, sizeof last_error,
      "password can't be NULL");
  ERROR_OUT(ERR_OWN, done);
 }

 if (cert_find_put("C", subj, 2, 2)) {
  snprintf(last_error, sizeof last_error,
      "invalid country");
  ERROR_OUT(ERR_OWN, done);
 }
 cert_find_put("ST", subj, -1, -1);
 cert_find_put("L", subj, -1, -1);
 cert_find_put("O", subj, -1, -1);
 cert_find_put("OU", subj, -1, -1);
 cert_find_put("CN", subj, -1, -1);
 cert_find_put("emailAddress", subj, -1, -1);

 rv = 0;
done:
 return (rv);
}
Jubilation! We have a CA OMG!!!oneONE!!!111~ Time to go home!
At home I sit in the shower and cry a little, WAIT, is that blood???

Day 6:
I'll get to writing stuff into LDAP later. I need to work on something else for a while. So next up, a client/server app that negotiates SSL/TLS. First I try various examples on the net.

Big bag of FAIL
Well, then we'll look at the man pages, I mean they totally come with an example!

Second big bag of FAIL
Farting around with crap I found on the net + example + a lot of time.

Third big bag of FAIL
Enough, I am going home.

Day 7:
Alright, time to go back into my by now favorite piece of code! openssl "the tool" has s_server and s_client and if you push the buttons it sort of seems to work. These are the magic commands I came up with:
openssl s_server -CAfile ca/ca.crt -cert server/server.crt -key server/private/server.key -Verify 1
openssl s_client -CAfile ca/ca.crt -cert client/client.crt -key client/private/client.key
That connects over SSL/TLS according to it and it seemed ok in tcpdump as well. So lets start coding! I'll present you here with my test code, again I am hoping to do other folks a favor and hope that a few murders can be averted. First up the server:
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

#include <sys/types.h>
#include <sys/socket.h>

#include <netinet/in.h>

#include "openssl/bio.h"
#include "openssl/ssl.h"
#include "openssl/err.h"

void
fatalx(char *s)
{
 ERR_print_errors_fp(stderr);
 errx(1, s);
}

int
main(int argc, char *argv[])
{
 SSL_CTX   *ctx;
 BIO   *sbio;
 SSL   *ssl;
 int   sock, s, r, val = -1;
 struct sockaddr_in sin;

 SSL_load_error_strings();
 OpenSSL_add_ssl_algorithms();

 ctx = SSL_CTX_new(SSLv23_server_method());
 if (ctx == NULL)
  fatalx("ctx");

 if (!SSL_CTX_load_verify_locations(ctx, "ca/ca.crt", NULL))
  fatalx("verify");
 SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file("ca/ca.crt"));

 if (!SSL_CTX_use_certificate_file(ctx, "server/server.crt", SSL_FILETYPE_PEM))
  fatalx("cert");
 if (!SSL_CTX_use_PrivateKey_file(ctx, "server/private/server.key", SSL_FILETYPE_PEM))
  fatalx("key");
 if (!SSL_CTX_check_private_key(ctx))
  fatalx("cert/key");

 SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
 SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
 SSL_CTX_set_verify_depth(ctx, 1);

 /* setup socket */
 if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  err(1, "socket");

 bzero(&sin, sizeof sin);
 sin.sin_addr.s_addr = INADDR_ANY;
 sin.sin_family = AF_INET;
 sin.sin_port = htons(4433);
 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof val);

 if (bind(sock, (struct sockaddr *)&sin, sizeof sin) == -1)
  err(1, "bind");
 listen(sock, 0);

 for (;;) {
  if ((s = accept(sock, 0, 0)) == -1)
   err(1, "accept");

  sbio = BIO_new_socket(s, BIO_NOCLOSE);
  ssl = SSL_new(ctx);
  SSL_set_bio(ssl, sbio, sbio);

  if ((r = SSL_accept(ssl)) == -1)
   fatalx("SSL_accept");

  printf("handle it!\n");
 }

 return (0);
}
and the client:
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

#include <sys/types.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <netdb.h>

#include "openssl/bio.h"
#include "openssl/ssl.h"
#include "openssl/err.h"

void
fatalx(char *s)
{
 ERR_print_errors_fp(stderr);
 errx(1, s);
}

int
main(int argc, char *argv[])
{
 SSL_CTX   *ctx;
 BIO   *sbio;
 SSL   *ssl;
 struct sockaddr_in addr;
 struct hostent  *hp;
 int   sock;


 SSL_load_error_strings();
 OpenSSL_add_ssl_algorithms();

 ctx = SSL_CTX_new(SSLv23_client_method());
 if (ctx == NULL)
  fatalx("ctx");

 if (!SSL_CTX_load_verify_locations(ctx, "ca/ca.crt", NULL))
  fatalx("verify");

 if (!SSL_CTX_use_certificate_file(ctx, "client/client.crt", SSL_FILETYPE_PEM))
  fatalx("cert");
 if (!SSL_CTX_use_PrivateKey_file(ctx, "client/private/client.key", SSL_FILETYPE_PEM))
  fatalx("key");
 if (!SSL_CTX_check_private_key(ctx))
  fatalx("cert/key");

 SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
 SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
 SSL_CTX_set_verify_depth(ctx, 1);

 /* setup connection */
 if ((hp = gethostbyname("localhost")) == NULL)
  err(1, "gethostbyname");

 bzero(&addr, sizeof addr);
 addr.sin_addr = *(struct in_addr *)hp->h_addr_list[0];
 addr.sin_family = AF_INET;
 addr.sin_port = htons(4433);

 if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  err(1, "socket");
 if (connect(sock, (struct sockaddr *)&addr, sizeof addr) == -1)
  err(1, "connect");

 /* go do ssl magic */
 ssl = SSL_new(ctx);
 sbio = BIO_new_socket(sock, BIO_NOCLOSE);
 SSL_set_bio(ssl, sbio, sbio);

 if (SSL_connect(ssl) <= 0)
  fatalx("SSL_connect");

 if (SSL_get_verify_result(ssl) != X509_V_OK)
  fatalx("cert");

 return (0);
}
Not my best work but it works and someone might be helped by this. Going home!

Day 8:
Looking some more on how to make those damned files work within LDAP. Between meetings and other lame things I gave up and wrote this rant instead. I'll continue to update this as I make more progress. I shall overcome the excrement flinging ape that is OpenSSL.
The opinions of the author expressed herein do not necessarily state or reflect those of anyone else.
Opinion and code © 2009 Marco Peereboom.
Code snippets from the OpenSSL project are © 1998-2009 The OpenSSL Project.
$assl: openssl.html,v 1.3 2009/08/24 18:45:53 marco Exp $

3 comments:

Fire Dragon said...

Làm sao để gửi hàng đi miền tây? Nếu đây là điều bạn đang thắc mắc thì hãy đến với chúng tôi. Chúng tôi là công ty chuyên nhận vận chuyển hàng. Các dịch vụ của chúng tôi hiện đang được rất nhiều ủng hộ. Và đây là những dịch vụ tiêu biểu được nhiều sử dụng của chúng tôi: giao hàng nhanh, giao hàng nhanh tphcm, nhận ký gửi hàng hóa, dịch vụ giao hàng thu tiền cod, ship hàng nội thành, gửi hàng về miền tây, chuyển hàng về đà nẵng, dịch vụ chuyển hàng. Nếu bạn đang cần vận chuyển hay sử dụng dịch vụ giao hàng nội thành hãy liên hệ với chúng tôi nhé.

Anonymous said...

Mình cũng đồng tình với suy nghĩ này, cảm ơn bạn đã chia sẽ.

Bạn đang làm bên lĩnh vực gì, mình có thể hợp tác chứ?

Bên mình chuyên về lĩnh vực van tai hang hoa, cung cấp dịch vụ gui hang nhanh ra Ha Noi, gui hang di Quang Ninh, xe gui hang di Bac Giang, gửi hàng nhanh đi Lạng Sơn, gửi hàng nhanh đi Thái Nguyên, gửi hàng đi Bắc Cạn, gui hang nhanh ra Cao Bang, gui hang di Tuyen Quang.

Nếu bên bạn chuyên về lĩnh vực thang may gia dinh Mitsubishi, thang máy tải khách mitsubishi thang máy trong bệnh viện thì bên mình sẽ chiết khấu tốt cho bạn nhé.

Kisk Joko said...

Giám đốc đảm nhiệm Nghiên cứu CBRE tại Singapore và khu vực Đông Nam Á, Desmond Sim nhận định: “Hiện vẫn còn rộng rãi rào cản và tránh dich vu van chuyen di Bac Kan mà những nước trong khối ASEAN cần phải vượt qua”. trước nhất là khả năng điều hành yếu kém của chanh xe van chuyen di Thanh Hoa, nguồn cung nhộn nhịp sở hữu thể dẫn tới biến động giá thuê mặt bằng chanh xe di Bac Giang.

Điều này sẽ gây trì trệ hoặc thậm chí ngăn cản việc mở mang của những nhà bán buôn. Thứ hai là việc thiếu nguồn cần lao với tay nghề cao cũng là thách thức, với thể gây chướng ngại trong việc vận chuyển hàng hoá đi Hà Nội, vận chuyển hàng hoá Bắc Ninhvan chuyen hang ra Cao Bangdich vu van tai hang hoa Dien Bien Phu và mở mang của những dịch vụ công nghiệp mang giá trị cao.

Thứ ba là sự chênh lệch to về chuyên môn van chuyen hang hoa giữa các nước thành viên cũng giảm thiểu những tác động tích cực của bắt buộc tự do hóa lao động ASEAN.

Cảm ơn bạn phổ thông nhé.