PolarSSL is now part of ARM Official announcement and rebranded as Mbed TLS.

Support for curve25519


Jun 10, 2014 10:39
Billy OMahony

Hi,

I'm a bit confused as to what support exactly polarSSL has for curve25519.

In the release notes for 1.3.3 it says "Our ECP curves module now includes Curve25519, thus allowing you to perform operations with it."

However at Knowledge Base > Cryptography > Elliptic Curve performance: NIST vs Brainpool, it says: "Curve25519 support... Unfortunately, they [Curve25519 and Ed25519 ] use slightly different data structures/representations than the other curves, so their use with TLS and PKIX is not standardized yet. We do support Curve25519 and will implement its use in TLS / PKIX as soon as a standard is out."

I am interested in using Polar to perform ECDH key exchange using Curve25519. This is in order to implement Apple's Wireless Accessory Configuration protocol which is based on this.

In library/ecp.c I notice that there is no entry for POLARSSL_ECP_DP_M255 in the ecp_supported_curves array.

I can build and run the self tests (make test) and benchmark (benchmark ecdh) runs the ecdh tests fine - but it's not using the M255 curve as it is not in the list of supported curves.

If I try to modify the the benchmark.c to force it to use the M255 curve like so:

ecdh_init( &ecdh );

ecp_use_known_dp (&ecdh.grp, POLARSSL_ECP_DP_M255);

ecdh_make_public( &ecdh, &olen, buf, sizeof( buf),
                  myrand, NULL );


ret |= ecdh_calc_secret( &ecdh, &olen, buf, sizeof( buf ),
                         myrand, NULL ) );

ecdh_free( &ecdh );

ecp_use_known_dp() returns 0/success then I get a seg fault in ecdh_make_public. This occurs way down in bignum - mpi_msb().

Is it the case that PolarSSL only supports some basic curve25519 primitives but not the higher level operations such as ecdh_make_public?

Or is there some #defs or other configuration I am missing?

Thanks,

Billy.

 
Jun 11, 2014 10:01
Manuel Pégourié-Gonnard

Hi Billy,

Short answer: "Is it the case that PolarSSL only supports some basic curve25519 primitives but not the higher level operations such as ecdh_make_public?" Yes, that's exactly that. Support for Curve25519 is limited to the ECP module. More precisely, the supported operations are: - the basic init/free functions as well as ecp_use_known_dp() - ecp_mul() which is the primitive used for the crypto

If you want to perform an ECDH key exchange with Curve25519, you'll need to use the "raw" ecdh interface.

#include "polarssl/entropy.h"
#include "polarssl/ctr_drbg.h"
#include "polarssl/ecdh.h"

/* For testing purposes only */
const unsigned char peer_value[32] = 
{
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
};

int main( void )
{
    int ret;
    entropy_context entropy;
    ctr_drbg_context ctr_drbg;
    ecdh_context ctx;
    mpi z;
    const char pers[] = "Sample ECDH Curve25519 code";

    ecdh_init( &ctx );
    mpi_init( &z );

    entropy_init( &entropy );
    ctr_drbg_init( &ctr_drbg, entropy_func, &entropy,
                   (const unsigned char *) pers, sizeof( pers ) );

    MPI_CHK( ecp_use_known_dp( &ctx.grp, POLARSSL_ECP_DP_M255 ) );

    MPI_CHK( ecdh_gen_public( &ctx.grp, &ctx.d, &ctx.Q,
                              ctr_drbg_random, &ctr_drbg ) );

    /* You might need to reverse the bytes of peer_value here! */
    MPI_CHK( mpi_read_binary( &ctx.Qp.X, peer_value, sizeof( peer_value ) ) );
    MPI_CHK( mpi_lset( &ctx.Qp.Z, 1 ) );

    MPI_CHK( ecdh_compute_shared( &ctx.grp, &z, &ctx.Qp, &ctx.d,
                                  ctr_drbg_random, &ctr_drbg ) );

    /*
     * Now the shared secret is in z.
     * You can export it to bytes using mpi_write_binary().
     * Again, please check byte order for your protocol!
     */

cleanup:
    ecdh_free( &ctx );
    mpi_free( &z );

    return( ret );
}

Our functions for converting bytes strings to MPIs use big-endian byte order. This might not be true for your protocol, so that's something you'll want to check.

 
Jun 11, 2014 10:04
Manuel Pégourié-Gonnard

PS: the segmentation fault should not happen. Maybe if was caused by forgetting to call ecdh_init() as I unfortunately did in an example a few days ago. If so, fine. But if you get segfaults with a correctly initialised ecdh_context, even calling a function that does not support Curve25519 yet, we'd appreciate a report about it, so that we can fix it.

 
Jun 12, 2014 11:41
Billy OMahony

Hi Manuel,

thanks for that reply.

Yes that solves my hardfault issue - yay!

I was initialising the ecdh_context ecdh_init(&cxt); correctly (I think!). The problem as far as I could make out was that the .p array in the mpi representing one of the curves points was ending up being set to start at address 0x0. I'm not sure but I think it was cxt.Q.Z.p. I can recreate the issue and give you further information if you like.

I haven't got successful encryption just yet, I'm using CTR mode for which I can't find a complete example.

Thanks, Billy.

 
Jun 16, 2014 15:47
Billy OMahony

Hi Manuel,

I am having a problem getting at the shared secret using mpi_write_buffer.

I am using the exact code that you supplied just with an added call to mpi_write_buffer. All the functions including mpi_write_buffer return 0/success.

static  unsigned char ss[512];
...

int test_ecdh( void )
{
...

    MPI_CHK( ecdh_compute_shared( &ctx.grp, &z, &ctx.Qp, &ctx.d,
                                  ctr_drbg_random, &ctr_drbg ) );

    ret = mpi_write_binary(&z, ss, sizeof(ss));

...

After the call to compute_shared the value of z is: z.s = 1, z.n = 8, z.p = [0x57, 0xbb, 0x38, 0x49, 0x17, 0x85, 0x1e, 0x21, ...]

These values remain the same across invocations so I guess my entropy is configured to be fully deterministic.

Regards, Billy.

 
Jun 23, 2014 18:02
Manuel Pégourié-Gonnard

Which platform are you using for your tests? Here on Linux/x86_64 I can't reproduce the issue, I get different values for z with each run of the following program.

#include "polarssl/entropy.h"
#include "polarssl/ctr_drbg.h"
#include "polarssl/ecdh.h"

/* For testing purposes only */
const unsigned char peer_value[32] =
{
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
    0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
};


int main( void )
{
    int ret;
    entropy_context entropy;
    ctr_drbg_context ctr_drbg;
    ecdh_context ctx;
    mpi z;
    const char pers[] = "Sample ECDH Curve25519 code";
    unsigned char ss[32] = { 0 };

    ecdh_init( &ctx );
    mpi_init( &z );

    entropy_init( &entropy );
    ctr_drbg_init( &ctr_drbg, entropy_func, &entropy,
                   (const unsigned char *) pers, sizeof( pers ) );

    MPI_CHK( ecp_use_known_dp( &ctx.grp, POLARSSL_ECP_DP_M255 ) );

    MPI_CHK( ecdh_gen_public( &ctx.grp, &ctx.d, &ctx.Q,
                              ctr_drbg_random, &ctr_drbg ) );

    /* You might need to reverse the bytes of peer_value here! */
    MPI_CHK( mpi_read_binary( &ctx.Qp.X, peer_value, sizeof( peer_value ) ) );
    MPI_CHK( mpi_lset( &ctx.Qp.Z, 1 ) );

    MPI_CHK( ecdh_compute_shared( &ctx.grp, &z, &ctx.Qp, &ctx.d,
                                  ctr_drbg_random, &ctr_drbg ) );

    /*
     * Now the shared secret is in z.
     * You can export it to bytes using mpi_write_binary().
     * Again, please check byte order for your protocol!
     */
    MPI_CHK( mpi_write_binary( &z, ss, sizeof( ss ) ) );
    for( size_t i = 0; i < sizeof( ss ); i++ )
        printf( "%02X%s", ss[i], i % 16 == 15 ? "\n" : " " );
    printf( "\n" );

cleanup:
    ecdh_free( &ctx );
    mpi_free( &z );

    return( ret );
}
 
Jun 24, 2014 09:55
Billy OMahony

Hi Manuel,

thanks for your help.

The problem with my call to mpi_write_buffer was actually related to the size of the buffer I was using! I was using a buffer of size 512 to receive a 32 byte value. It's not clear from the mpi_write/read documentation but this writes the values to the elements buf[480]...buf[511] and not buf[0]...buf[31] as I was expecting.

I'm assuming this is because my cortexM0+ mcu is little endian?

You had alluded to this in the comment regarding reversing buffer values. And so it is on my cortexM0+ I must reverse the buffer before calls to mpi_read_buffer and after calls to mpi_write_buffer.

So this is basically the code I used - this has been edited a little so probably will not compile. But it does show:

  • when to use reverse
  • how to get the public key, which is vital and who location is only hinted at in the doxygen comments.
  • reminds you to un-reverse your client's public key after mpi_read, if you are going to use it again later - for instance to get a sha1 of it.
void ecdh_example(void) {   
#define ECDH_KEY_LEN (32)   
    uint8_t             clientECDHPublicKey[ECDH_KEY_LEN]; //supplied by remote side
    uint8_t             ourPublicKey[ECDH_KEY_LEN];
    uint8_t             sharedSecret[ECDH_KEY_LEN];     

    // Generate a random ECDH key pair
    memory_set_own(pvPortMalloc, vPortFree);     //setup heap callbacks for Polar
    int ret;                                     //used by MPI_CHK macro
    entropy_context entropy;
    ctr_drbg_context ctr_drbg;
    ecdh_context ctx;
    mpi z;
    const char pers[] = "Sample ECDH Curve25519 code";

    ecdh_init( &ctx );
    mpi_init( &z );

    entropy_init( &entropy );
    ctr_drbg_init( &ctr_drbg, entropy_func, &entropy,
                   (const unsigned char *) pers, sizeof( pers ) );

    MPI_CHK( ecp_use_known_dp( &ctx.grp, POLARSSL_ECP_DP_M255 ) );

    MPI_CHK( ecdh_gen_public( &ctx.grp, &ctx.d, &ctx.Q,
                              ctr_drbg_random, &ctr_drbg));

    //it should be obvious to even the most dim-witted individual who holds a degree in 
    //advanced cryptography that the ECDH public key is written to ctx.Q.X  ;)
    MPI_CHK( mpi_write_binary(&ctx.Q.X, ourPublicKey, sizeof(ourPublicKey)));
    reverse (ourPublicKey, sizeof (ourPublicKey));

    //it's left as an exercise to the reader to retrieve the private key :) but as it is 
    //ephemeral you probably don't need it either.

    //On some platforms Polar requires the buffer to be reversed before calls to mpi_read_binary
    //and after call to mpi_write_binary
    reverse(clientECDHPublicKey, ECDH_KEY_LEN); 
    MPI_CHK( mpi_read_binary( &ctx.Qp.X, clientECDHPublicKey, ECDH_KEY_LEN ) );
    //Return client public key to previous order as it must be used in it's original order later
    //as input to sha1_update (not shown in this example).
    reverse(clientECDHPublicKey, ECDH_KEY_LEN); 

    MPI_CHK( mpi_lset( &ctx.Qp.Z, 1 ) );

    MPI_CHK( ecdh_compute_shared( &ctx.grp, &z, &ctx.Qp, &ctx.d,
                                  tr_drbg_random, &ctr_drbg));

    ret = mpi_write_binary(&z, sharedSecret, sizeof(sharedSecret));
    reverse (sharedSecret, sizeof (sharedSecret));

cleanup:    
    ecdh_free(&ctx);
    mpi_free(&z);
}



/**
 * Reverse the order of bytes in memory.
 */
static void reverse (uint8_t* buf, size_t sz)
{
    uint8_t* last = buf + sz -1;
    uint8_t tmp;
    while (last > buf) {   //when last & buf meet in the middle we're done
        tmp = *buf;
        *buf = *last;
        *last = tmp;
        buf++;
        last--;
    }
}
 
Jun 24, 2014 10:33
Manuel Pégourié-Gonnard

I'm glad your problem is solved!

To further clarify things: mpi_write_binary() always writes the data in big endian byte order, regardless of the underlying platform, as the documentation says, and always fills the whole buffer. Since the order is big endian, the only right thing to do is to fill with 0s on the left, so that when reading back the whole buffer we get the same value again (obviously, the value 1 in big-endian with four digits is 0001, not 1000).

Now, why is filling the whole buffer the right thing to do? Well, imagine your z values just happens to be less than 2^248, which statically happens one time out of 128 approximately. If mpi_write_binary didn't fill the whole buffer, only 31 bytes would be written in sharedSecret and the last one would have retain the value it had before, which could be anything but certainly not the right thing.

The alternative would be to decide that the shared secret has variable length: most of the time it would be 32 bytes, some times 31, more rarely 30, etc. This is the choice made for Diffie-Hellman in TLS, for example (and is handled using mpi_write_binary( &z, ss, mpi_size( &z ) ) after manually checking that mpi_size( &z ) is not greater than the size of the output buffer), but in the Curve25519 paper, public keys and shared secrets are defined to always be 32 bytes exactly. My humble opinion is it's a sane choice that makes implementations easier.

 
Jun 24, 2014 10:46
Manuel Pégourié-Gonnard

A few more remarks about the comments:

  1. Just to be sure it's perfectly clear, the result of the various mpi_{write,read} functions is totally platform-independent. The only thing in the code that is platform-dependent is the entropy collection, and that's the only reason why I asked about the platform, since you were suspecting an entropy failure.

  2. Yes, you're right about the public key location, and no, it's not supposed to be obvious (you basically need to check the Curve25519 paper to know that). We plan to expand support for Curve25519 in the future and then we'll make it more clear. As i said above, currently we only support the arithmetic, not the higher-level operations, because there is currently no specification for protocols using Curve25519 besides the original paper, which is hardly a protocol spec.

  3. The private key is ctx->d in case you need it. (It's called the "secret exponent" in our doc. it's unfortunate that there are so many denominations for the same thing.)

 
Mar 5, 2015 16:05
Mina Shamana

Hi.. I just registered to say thank you very much Manuel for that help to the person asking for the tecnicalities. I am really looking forward to momentum not being hampered by certain state's military and political efforts to undermine or impair such important things as security. And curve25519 as expected is being bla bla bla'ed over at the various relevant places. I know it is taking time for the x509 things to come together but I am sure as hell can't wait for it.

I also started using polarSSL as I came across an openssl 'chief' mention they had signed 200 NDA's and just read it in 2012 or 2013, knowing something would come up.. well since then we can count on one hand the very crucial vulnerabilities in openssl.

libressl is an interesting project and polarssl being based elsewhere than US I think is also great.

I am looking at your source code to get more into hacking away on some shortcomings for my servers yes.. hence, found your helpful feedbacks and help for the OP very kind, gratutious and nice.

Kisses.

Malina

 
Oct 28, 2015 04:52
Malu Jayachandran

I am sorry if I got this wrong, but does it mean that if I use mbed TLS for a full on implementation of TLS (with ECDHE key exchange), without getting into the internals, I can't use Curve25519?

 
Oct 28, 2015 09:17
Nicholas Wilson

The curve25519 support in mbedTLS does work, we're using it in a shipping product. The RFB protocol has been standardised with a curve25519-based ciphersuite (the trade name for RFB is VNC).

Here's our technical paper announcing the new cipher suite: https://developer.realvnc.com/blog/rfb-5-security-underpinning-vnc-developer/

So there are some protocols using curve25519, but we're still waiting for it in TLS, that's all.

 
Oct 28, 2015 10:50
Manuel Pégourié-Gonnard

In addition to what Nicholas said, let me mention that there is an on-going effort to get Curve25519 standardised in TLS at the IETF. Currently it is in the following draft: https://tools.ietf.org/html/draft-ietf-tls-rfc4492bis-04 but as you'll see in section 5.1.1, Curve25519 it doesn't have an identifier yet, which is the main reason why it can't be used in TLS yet.

 
Oct 29, 2015 10:34
Malu Jayachandran

Okay... Thank you so much for replying.. :)