panva / paseto

PASETO (Platform-Agnostic SEcurity TOkens) for Node.js with no dependencies
MIT License
428 stars 26 forks source link

question: Nodejs Crypto vs libsodium #8

Closed blelump closed 4 years ago

blelump commented 4 years ago

Hi!

Problem: lack of compatibility(?) between generated private keys via Nodejs Crypto module and various libsodium implementations.

It seems that Nodejs Crypto expects 48 bytes length keys whereas libsodium implementations provide 64 bytes length private keys. Of course due to that fact const token = await sign({ sub: 'johndoe' }, privateKey) fails.

Already stated a question on Nodejs https://github.com/nodejs/help/issues/2543 help repo, but perhaps you could put more light on this problem, maybe I'm just missing something?

A low level example of the problem:

'use strict';

var sodium = require('sodium-native')
const paseto = require('paseto');
const { V2 } = paseto
const { V2: { sign } } = paseto
const crypto = require('crypto')
const { promisify } = require('util')
const generateKeyPair = promisify(crypto.generateKeyPair)
const {
  createPrivateKey,
  KeyObject
} = require('crypto');

let pk = Buffer.alloc(32);
let sk = Buffer.alloc(64);

// Create key pair with libsodium impl:
sodium.crypto_sign_keypair(pk, sk);

console.log(sk.length);

(async () => {
  const { privateKey } = await generateKeyPair('ed25519')
  const exp = privateKey.export({ type: 'pkcs8', format: 'der' });
  console.log(exp.length)

  // Try to instantiate Crypto private key from the one generated via libsodium:
  // Here it will fail with Error: error:0D07207B:asn1 encoding routines:ASN1_get_object:header too long
  createPrivateKey({ key: sk, format: 'der', type: 'pkcs8' }); 

})();
panva commented 4 years ago

I think you’re missing the fact that node does not work with arbitrary raw keys but DER/PEM encoded ones and only needs the 32 byte private one

e.g. PEM

-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEILRPjkQvM1vRgoBFHSGUzSgoUVaultWVGJ7jekxq8hS+
-----END PRIVATE KEY-----

https://lapo.it/asn1js/#MC4CAQAwBQYDK2VwBCIEILRPjkQvM1vRgoBFHSGUzSgoUVaultWVGJ7jekxq8hS-

What you're passing to createPrivateKey({ key: sk, format: 'der', type: 'pkcs8' }) as sk is not a pkcs8 formatted der private key.

blelump commented 4 years ago

@panva do you have any suggestion to overcome this, I mean to get it / encode it in DER format?

panva commented 4 years ago

1) get the 32 byte private key raw value from sodium 2) encode it as per pkcs8 (see the link for parsed DER structure above)

I don't have the code excerpt at hand, but you can trace back this code https://github.com/panva/jose/blob/v1.25.0/lib/help/key_utils.js#L278-L291

blelump commented 4 years ago

@panva , thanks for help!

This one "passes":

'use strict';

const nacl = require("tweetnacl");
const util = require("tweetnacl-util");
const paseto = require('paseto');
const { V2 } = paseto
const { V2: { sign, verify } } = paseto
const crypto = require('crypto')
const { promisify } = require('util')
const generateKeyPair = promisify(crypto.generateKeyPair)
const {
  createPrivateKey,
  createPublicKey
} = require('crypto');
const asn = require('asn1.js')

var OneAsymmetricKey = asn.define('OneAsymmetricKey', function() {
  this.seq().obj(
    this.key('version').int(),
    this.key('algorithm').use(AlgorithmIdentifier),
    this.key('privateKey').use(PrivateKey)
  );
});

var PrivateKey = asn.define('PrivateKey', function() {
  this.octstr().contains().obj(
    this.key('privateKey').octstr()
  );
});

var AlgorithmIdentifier = asn.define('AlgorithmIdentifier', function() {
  this.seq().obj(
    this.key('algorithm').objid(),
  );
});

const a = nacl.sign.keyPair();

let sk = a.secretKey;
let sk_cp = Buffer.from(sk.slice(0, 32));

var output = OneAsymmetricKey.encode({
  version: 0,
  privateKey: { privateKey: sk_cp },
  algorithm: { algorithm: '1.3.101.112'.split('.') }
}, 'der');
output.write('04', 12, 1, 'hex');

console.log(output.toString('base64'))

const priv = createPrivateKey({ key: output, format: 'der', type: 'pkcs8' });

const pub = createPublicKey({ key: priv, format: 'der', type: 'pkcs1' });

(async () => {
  const token = await sign({ sub: 'johndoe' }, priv)
  console.log(token)
  const payload = await verify(token, pub)
  console.log(payload)
})()
panva commented 4 years ago

That's why i said walk back the code, to reimplement it using e.g. asn1.js yourself, your own code. What you're requiring are internals and I do not recommend anyone doing so.

blelump commented 4 years ago

Thanks @panva ! Wouldn't solve this without your help, appreciate that!

First tried to find something out of the box and got into https://github.com/digitalbazaar/forge with asn1 encode/decode, but then following your comment from the above link https://github.com/panva/jose/blob/v1.25.0/lib/help/key_utils.js#L287 ended up with updated example above only with dependency to asn1.js.

somdoron commented 4 years ago

I sign the token with another library (libsodium) in another language (scala) and verify using this library. I have spent the entire day trying to convert libsodium ed25519 public key to something nodejs crypto module can understand, eventually, I end up with the following:

const {createPublicKey} = require("crypto");

const der = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), libsodiumPublicKey]);
const publicKey = createPublicKey({
    key: der,
    format: "der",
    type: "spki"
});

I thought it might help someone else in the future.

panva commented 4 years ago

If libsodium supported actual ASN.1 encoded keys rather than arbitrary binary data as keys you'd be set. Anyway libsodium <> openssl (ASN.1) conversion is not of concern to this lib, ASN.1 is a used standard for a good reason. The code from @somdoron works for ed25519 public keys tho. It won't work for private ones.

somdoron commented 4 years ago

I agree, libsodium supporting ASN.1 would be nice and would save me a day. Yes, I know it only works for public keys (I'm currently not concerned with private keys at the moment).