Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES256 signature cannot be verified by own public key #1

Closed
shrugs opened this issue Dec 26, 2018 · 5 comments
Closed

ES256 signature cannot be verified by own public key #1

shrugs opened this issue Dec 26, 2018 · 5 comments

Comments

@shrugs
Copy link

shrugs commented Dec 26, 2018

Howdy, and happy holidays!

I'm testing out this library for signing ES256 JWTs for authenticating against a backend, and having a bit of trouble after getting past the basics. I've narrowed the error down to the sign/verify lines that eventually delegate to pointycastle.

This code is how I'm generating my keypairs and JWKs with pointcastle. Assume that this code is copy-pasted above the other examples.

    final Uint8List message = mycrypto.createUint8ListFromString("TEST");
    final ECKeyGenerator generator = KeyGenerator("EC");
    generator.init(
      ParametersWithRandom(
        ECKeyGeneratorParameters(
          ECDomainParameters("secp256r1"),
        ),
        NativeDartRandom(),
      ),
    );

    final AsymmetricKeyPair pair = generator.generateKeyPair();

    final crv = "P-256";

    final d = convert.base64Url.encode(mycrypto.encodeBigInt(
      (pair.privateKey as ECPrivateKey).d,
    ));
    final x = convert.base64Url.encode(mycrypto.encodeBigInt(
      (pair.publicKey as ECPublicKey).Q.x.toBigInteger(),
    ));
    final y = convert.base64Url.encode(mycrypto.encodeBigInt(
      (pair.publicKey as ECPublicKey).Q.y.toBigInteger(),
    ));
    // ^ d,x,y are b64 encoded bigint, and yes, they match the internal decoded BigInts inside of KeyPair

    final privateJWK = JsonWebKey.fromJson({
      "kty": "EC",
      "crv": crv,
      "d": d,
    });

    final publicJWK = JsonWebKey.fromJson({
      "kty": "EC",
      "crv": crv,
      "x": x,
      "y": y,
    });

Using pointycastle directly like this works just fine:

    final signer = Signer("SHA-256/DET-ECDSA");
    signer.init(
      true,
      PrivateKeyParameter(pair.privateKey),
    );
    final ECSignature sig = signer.generateSignature(message);

    final verifier = Signer("SHA-256/DET-ECDSA");
    verifier.init(false, PublicKeyParameter(pair.publicKey));
    // decrypt
    final isValid = verifier.verifySignature(message, sig);
    print("isValid: $isValid");

And here's how I'd use these generated keypairs with jose, but this is failing to validate.

    final claims = JsonWebTokenClaims.fromJson({
      "iat": DateTime.now().millisecondsSinceEpoch,
      "exp": DateTime.now().add(Duration(hours: 4)).millisecondsSinceEpoch,
      "iss": "alice",
    });

    final builder = JsonWebSignatureBuilder()
      ..jsonContent = claims.toJson()
      ..addRecipient(
        privateJWK,
        algorithm: "ES256",
      );

    // build the jws
    final jws = builder.build().toCompactSerialization();

    // output the compact serialization
    print("jwt compact serialization: $jws");

    final keyStore = JsonWebKeyStore()..addKey(publicJWK);

    final jwt = await JsonWebToken.decodeAndVerify(
      jws,
      keyStore,
    );

    print("isVerified: ${jwt.isVerified}");
    // false

As far as I can tell, this line in JsonWebSignature#getPayloadFor is returning false. Reducing the problem even further, based on the code from the crypto_keys test file,

    final privateKeyPair = ck.KeyPair.fromJwk(privateJWK.toJson());
    var signer =
        privateKeyPair.createSigner(ck.algorithms.signing.ecdsa.sha256);
    final publicKeyPair = ck.KeyPair.fromJwk(publicJWK.toJson());
    var verifier =
        publicKeyPair.createVerifier(ck.algorithms.signing.ecdsa.sha256);

    var signature = signer.sign(message);
    final isValid = verifier.verify(message, signature);
    print("expected valid: $isValid");
    // false

If it would be helpful (i.e, I'm not doing something incredibly silly just be reading the code) I can make a separate dart project and try and reproduce the code in isolation.

@shrugs
Copy link
Author

shrugs commented Dec 26, 2018

Will leave the above for context, but here's the simplified code. For clarification, the ES256 test passes on my machine with the hard-coded x,y,d parameters. My issue seems to be isolated to the generation of my keypair via pointycastle:

    final ECKeyGenerator generator = KeyGenerator("EC");
    generator.init(
      ParametersWithRandom(
        ECKeyGeneratorParameters(
          ECDomainParameters("secp256r1"),
        ),
        NativeDartRandom(),
      ),
    );

    final AsymmetricKeyPair pair = generator.generateKeyPair();

    final d = convert.base64Encode(
      _bigIntToBytes(
        (pair.privateKey as ECPrivateKey).d,
        32,
      ).toList(),
    );
    final x = convert.base64Encode(
      _bigIntToBytes(
        (pair.publicKey as ECPublicKey).Q.x.toBigInteger(),
        32,
      ).toList(),
    );
    final y = convert.base64Encode(
      _bigIntToBytes(
        (pair.publicKey as ECPublicKey).Q.y.toBigInteger(),
        32,
      ).toList(),
    );
    print("x: $x");
    print("y: $y");
    print("d: $d");

    final allKeyPair = JsonWebKey.fromJson({
      "kty": "EC",
      "crv": "P-256",
      "x": x,
      "y": y,
      "d": d,
    });

    var keyPair = ck.KeyPair.fromJwk(allKeyPair.toJson());
    var signer = keyPair.createSigner(ck.algorithms.signing.ecdsa.sha256);
    var verifier = keyPair.createVerifier(ck.algorithms.signing.ecdsa.sha256);

    final hash = Uint8List.fromList("TEST".codeUnits);
    var sig = signer.sign(hash);
    final valid = verifier.verify(hash, sig);

    print("valid? $valid");
    // false

here's an example of some of the x,y,d values I'm generating:

flutter: x: 1pZTd8n+juKnHBeyLNxsR1R3l6PSIajV8ntfScNOl18=
flutter: y: y23IH4B/A/JlJ880CgXt3RX2OiIAg1jhKocMUyzuivQ=
flutter: d: 4ASGoPRn9N+VxuWDuPEkTUkN+XQko98bHrtOMWJ3skg=
flutter: valid? false

(so I guess this technically belongs in crypto_keys now?)

@ricksondpenha
Copy link

hey there,

I have a similar problem in verifying the ES256 signed JWT. I am generating the JWT to authenticate with my google iot core server. the format of the JWT is:
{ "alg": "ES256", "typ": "JWT"}.{ "aud": "flutteriot", "exp": 1552117829, "iat": 1552031429 }.{signaturebytes}

this is the JWT token that gets generated:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJmbHV0dGVyaW90IiwiZXhwIjoxNTUyMTE3ODI5LCJpYXQiOjE1NTIwMzE0Mjl9.nVOUgDQI2XpslD67PWbeqguO3bwxUvL15tasSg1_22-5BoZPbekmQx7ILft3LwYzVP7dY6N0CLOMBhit9aGM0g

This is the function used to generate the JWT token. I'm using this on flutter framework. I load my ec_private.pem file from assets as a string. encode it to utf8 and then base64url.encode it.

  Future<String> loadKey() async {
    return await rootBundle.loadString('keys/ec_private.pem');
  }

  Future<String> getJose() async {
    String privatekey= await loadKey();
    final int iat =
        ((new DateTime.now().millisecondsSinceEpoch) / 1000).round();
    final int exp = ((new DateTime.now()
                .add(new Duration(hours: 24))
                .millisecondsSinceEpoch) / 1000)
        .round();

    var claims = new JsonWebTokenClaims.fromJson(
        {"aud": projectId, "exp": exp, "iat": iat});

    var privateKeyBytes = utf8.encode(privatekey);
    var privatKeyBase64= base64Url.encode(privateKeyBytes );

    var builder = new JsonWebSignatureBuilder();
    builder.setProtectedHeader("typ", "JWT");
    builder.jsonContent = claims.toJson();
    builder.addRecipient(
        new JsonWebKey.fromJson({"kty": "EC", "d": privatKeyBase64, "crv": "P-256"}),
      algorithm: "ES256");

    var jws = builder.build();
    print("content of JWT: ${jws.toCompactSerialization}");
    return jws.toCompactSerialization();
  }

Also when i generate a JWT token from jwt.io using the same ec_private.pem key, and hardcode the key in my client, it gets authenticated. Its just this jwt doesnt get authenticated and i get a NotAuthorised return code from the server.

I tried digging in, but without any luck.

@shrugs
Copy link
Author

shrugs commented Mar 8, 2019

I was actually able to solve this with a similar library, but forgot to update: thanks for the reminder! I totally forgot all of the context of this, though, so I won't be of much help for jose specifically.

First, we create a SecureRandom implementation in native dart:

import 'dart:math';
import 'dart:typed_data';
import 'package:pointycastle/api.dart';

class NativeDartRandom implements SecureRandom {
  final Random random = Random.secure();

  @override
  String get algorithmName => "dart.math.Random.secure()";

  @override
  BigInt nextBigInteger(int bitLength) {
    // produces a string of length [bitLength] and then parses it with BigInt.parse
    return BigInt.parse(
      Iterable.generate(
        bitLength,
        (_) => random.nextBool() ? "1" : "0",
      ).join(""),
      radix: 2,
    );
  }

  @override
  Uint8List nextBytes(int count) =>
      Uint8List.fromList(List.generate(count, (_) => nextUint8()));

  @override
  int nextUint8() => random.nextInt(256);

  @override
  int nextUint16() => random.nextInt(256 * 256);

  @override
  int nextUint32() => random.nextInt(256 * 256 * 256 * 256);

  @override
  void seed(CipherParameters params) {
    throw UnsupportedError("Seed not supported for this SecureRandom");
  }
}

and use that as part of the key generation code:

import 'package:pointycastle/pointycastle.dart';
import 'package:pointycastle/key_generators/ec_key_generator.dart';
import 'package:pointycastle/ecc/api.dart';

import './native_dart_random.dart';
import './helpers.dart';
import './types.dart';

/// Generate and serialize an EC keypair.
/// Must be run in [compute] to avoid jank (hence null argument).
/// await compute(generateKeyPair, null)
SerializedKeyPair generateKeyPair(_) {
  final ECKeyGenerator generator = KeyGenerator("EC");
  generator.init(
    ParametersWithRandom(
      ECKeyGeneratorParameters(kDomain),
      NativeDartRandom(),
    ),
  );

  final pair = generator.generateKeyPair();

  return SerializedKeyPair(
    privateKey: serializePrivateKey(pair.privateKey as ECPrivateKey),
    publicKey: serializePublicKey(pair.publicKey as ECPublicKey),
  );
}

and then create our own ES256Signer:

import 'dart:typed_data';

import 'package:pointycastle/pointycastle.dart';
import 'package:corsac_jwt/corsac_jwt.dart';

import './helpers.dart';

const kAlgorithmName = "SHA-256/DET-ECDSA";

const _kBitLength = 32;

class ES256Signer implements JWTSigner {
  String get algorithm => 'ES256';

  final AsymmetricKeyPair<PublicKey, PrivateKey> pair;

  ES256Signer({
    this.pair,
  });

  @override
  List<int> sign(List<int> data) {
    final pcSigner = Signer(kAlgorithmName);
    pcSigner.init(
      true,
      PrivateKeyParameter(pair.privateKey),
    );
    final ECSignature sig = pcSigner.generateSignature(data);
    var bytes = Uint8List(_kBitLength * 2);
    bytes.setRange(
      0,
      _kBitLength,
      bigIntToBytes(sig.r, _kBitLength).toList().reversed,
    );
    bytes.setRange(
      _kBitLength,
      _kBitLength * 2,
      bigIntToBytes(sig.s, _kBitLength).toList().reversed,
    );
    return bytes;
  }

  @override
  bool verify(List<int> data, List<int> signature) {
    final verifier = Signer(kAlgorithmName);
    verifier.init(false, PublicKeyParameter(pair.publicKey));
    final sig = ECSignature(
      bigIntFromBytes(signature.take(_kBitLength)),
      bigIntFromBytes(signature.skip(_kBitLength)),
    );
    return verifier.verifySignature(data, sig);
  }
}

then we can finally use all of that to create the JWT

import 'package:meta/meta.dart';
import 'package:corsac_jwt/corsac_jwt.dart';
import 'package:dot_app/constants.dart';

import './crypto/es256_signer.dart';
import './crypto/helpers.dart';
import './crypto/types.dart';

import './did.dart';

// play well with serialized output of generator
String build({
  @required SerializedKeyPair serializedKeyPair,
  @required String id,
}) {
  final builder = JWTBuilder()
    ..issuer = serializedKeyPair.publicKey
    ..audience = DID.dot
    ..issuedAt = DateTime.now()
    ..expiresAt = DateTime.now().add(kJWTValidityDuration)
    ..notBefore = DateTime.now()
    ..setClaim('did', DID.build(id));

  final signer = ES256Signer(pair: deserializeKeyPair(serializedKeyPair));
  final signedToken = builder.getSignedToken(signer);

  return signedToken.toString();
}

@rbellens
Copy link
Contributor

I guess the problem lies with the encodeBigInt method you use. It uses big endian instead of little endian or the other way around.

In version 0.2.0, I implemented a method to generate non-symmetric keys. See example8 in example/jose_example.dart

@shrugs
Copy link
Author

shrugs commented Jun 25, 2020

i'm super far removed from this codebase at this point, but agreed, that was almost definitely the issue 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants