Generating JWK from EC public key x and y coordinates

I'm trying to generate a JWK from an EC public key that I want to send to my server. The issue I'm running into is that the base64 URL Encoded string representation of the x and y coordinates from the external representation of the EC public key seems to be invalid on the server and I get an error saying that the points are not on the curve.

Here's what I'm doing to generate the base64 URL encoded string from the x and y coords:

Code Block objc
+ (NSArray<NSString *> *)getBase64EncodedCoordinatesFromECPublicKey:(SecKeyRef)publicKey error:(out NSError **)error{
  CFErrorRef copyPublicKeyError = NULL;
  NSData* keyData = (NSData*)CFBridgingRelease(
    SecKeyCopyExternalRepresentation(publicKey, &copyPublicKeyError)
  );
  if (!keyData) {
    NSError *err = CFBridgingRelease(copyPublicKeyError);
    NSLog(@"%@", err);
    return nil;
  }
  NSString *xbytes;
  NSString *ybytes;
   
  CFByteOrder byteOrder = CFByteOrderGetCurrent();
  NSLog(@"%ld",(long)byteOrder);
   
  NSData *xData = [keyData subdataWithRange:NSMakeRange(1, 32)];
  NSData *xDataRev = [self reverseData:xData];
  NSString *xEncoded = [OIDTokenUtilities encodeBase64urlNoPadding:xDataRev];
   
  NSData *yData = [keyData subdataWithRange:NSMakeRange(33, 32)];
  NSData *yDataRev = [self reverseData:yData];
  NSString *yEncoded = [OIDTokenUtilities encodeBase64urlNoPadding:yDataRev];
   
  xbytes = [OIDTokenUtilities encodeBase64urlNoPadding:xData];
  ybytes = [OIDTokenUtilities encodeBase64urlNoPadding:yData];
  NSArray *coordinates = @[xbytes, ybytes];
  return coordinates;
}


The byte order on iOS seems to be little-endian (byteOrder above is 1 == CFByteOrderLittleEndian) and the JWK RFC 7517 Appendix A says it expects the big-endian values for x and y. So, I tried swapping the bytes to create a big-endian representation of the data. But, neither works.

I'd appreciate any insight or help with this.

Accepted Reply

So, what we found was the following -

The X and Y values of the EC public key are treated as signed base64 encoded values by our server. So, while extracting the x (or y) byte components from public key, if the most significant byte is greater than 0x7f, the x (or y) value will be treated as negative values. However, our server expects the the X and Y values to be positive for a valid EC key.

So the fix was to check If the x or y component of the public key has a first byte value greater than 0x7f, then add an extra 0x00 byte, to make the value positive before encoding.

The code is:

Code Block objC
+ (NSArray<NSString *> *)getBase64EncodedCoordinatesFromECPublicKey:(SecKeyRef)publicKey error:(out NSError **)error{
CFErrorRef copyPublicKeyError = NULL;
NSData* keyData = (NSData*)CFBridgingRelease(
SecKeyCopyExternalRepresentation(publicKey, &copyPublicKeyError)
);
if (!keyData) {
NSError *err = CFBridgingRelease(copyPublicKeyError);
NSLog(@"%@", err);
return nil;
}
NSString *xCoordinate; NSString *yCoordinate;
NSData *xDataRaw = [keyData subdataWithRange:NSMakeRange(1, keyData.length/2)];
NSData *yDataRaw = [keyData subdataWithRange:NSMakeRange((keyData.length / 2)+1, keyData.length/2)];
uint8_t zeroByte = 0x00;
const unsigned char* xBytes = [xDataRaw bytes];
const unsigned char* yBytes = [yDataRaw bytes];
int mostSignificantByte_x = xBytes[0];
int mostSignificantByte_y = yBytes[0];
if (mostSignificantByte_x > 127) {
NSMutableData *xData = [[NSMutableData alloc] initWithBytes:&zeroByte length:1];
[xData appendData:xDataRaw];
xCoordinate = [self encodeBase64urlWithPadding:xData];
} else {
xCoordinate = [self encodeBase64urlWithPadding:xDataRaw];
}
if (mostSignificantByte_y > 127) {
NSMutableData *yData = [[NSMutableData alloc] initWithBytes:&zeroByte length:1];
[yData appendData:yDataRaw];
yCoordinate = [self encodeBase64urlWithPadding:yData];
} else {
yCoordinate = [self encodeBase64urlWithPadding:yDataRaw];
}
NSArray *coordinates = @[xCoordinate, yCoordinate];
return coordinates;
}


Replies

The endianness here is a irrelevant. While iOS is little endian, the X and Y values in the EC external representation aren’t swapped.

Do you have an example of a JWT key X and Y values that your server will accept? If so, can you post them here?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Yes here are the base64 URL encoded x and y coords that did work (this is the ONLY one that worked)

x: LWUQoWBjj4yHpPcOiawHF3745LRk6s8p4pMGJ9ss
y: KWY7lHhDCfzl3C70az9RNxHPty
3TuLqA1FIptcQJ0g


====================================

Here is a key that does NOT work.

(lldb) po publicKey
<SecKeyRef curve type: kSecECCurveSecp256r1, algorithm id: 3, key type: ECPublicKey, version: 4, block size: 256 bits, y: 9FA590CFBB8C90800B80B339C6B88036CE555CACBFD90EF58DEB6823A3F3A239, x: 11403670547D66B189A336E4C187B206B6EB1B7A780A11C142EFEC59E9B6B203, addr: 0x7fa4a680fd60>

(lldb) po keyData
<04114036 70547d66 b189a336 e4c187b2 06b6eb1b 7a780a11 c142efec 59e9b6b2 039fa590 cfbb8c90 800b80b3 39c6b880 36ce555c acbfd90e f58deb68 23a3f3a2 39>

(lldb) po xData
<11403670 547d66b1 89a336e4 c187b206 b6eb1b7a 780a11c1 42efec59 e9b6b203>

(lldb) po yData
<9fa590cf bb8c9080 0b80b339 c6b88036 ce555cac bfd90ef5 8deb6823 a3f3a239>

(lldb) po xbytes
EUA2cFR9ZrGJozbkwYeyBrbrG3p4ChHBQusWem2sgM

(lldb) po ybytes
n6WQz7uMkIALgLM5xriANs5VXKy
2Q71jetoI6Pzojk

I get an error saying the points are not on the curve and when I debug further, the computation shows the LHS does not match the RHS in the equation below.

y^2 = x^3 + ax + b

Let me know if you need more info.

Thanks
Srini
Are you sure you’re doing the Base64 encoding properly? I fed your key into some JWT-style Base64 encoding code I have lying around (1) and it produced different results. Specifically:

Code Block
you: EUA2cFR9ZrGJozbkwYeyBrbrG3p4ChHBQusWem2sgM
me: EUA2cFR9ZrGJozbkwYeyBrbrG3p4ChHBQu_sWem2sgM
you: n6WQz7uMkIALgLM5xriANs5VXKy2Q71jetoI6Pzojk
me: n6WQz7uMkIALgLM5xriANs5VXKy_2Q71jetoI6Pzojk


Note your strings are missing the underscores.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"

(1) The following is modelled on the code in Appendix C of RFC 7515.

Code Block
extension Data {
init?(base64URLEncodedString: String) {
let unpadded = base64URLEncodedString
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let padCount: Int
switch unpadded.count % 4 {
case 0: padCount = 0
case 1: return nil
case 2: padCount = 2
case 3: padCount = 1
default: fatalError()
}
self.init(base64Encoded: String(unpadded + String(repeating: "=", count: padCount)))
}
var base64URLEncodedString: String {
let base64 = self.base64EncodedString()
return String(base64.split(separator: "=").first!)
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
}
}

That's really weird because I have the same thing with underscores in the lldb output and in this box. Looks like it is stripped if it's not inside a code block. Pasting it again here within a code block. I'm using the base64 encoder on NSData and making it URL safe as you've shown.

Code Block
x: EUA2cFR9ZrGJozbkwYeyBrbrG3p4ChHBQu_sWem2sgM
y: n6WQz7uMkIALgLM5xriANs5VXKy_2Q71jetoI6Pzojk


==== DON'T USE THIS (BELOW) SINCE UNDERSCORES ARE STRIPPED =====

(lldb) po xbytes
EUA2cFR9ZrGJozbkwYeyBrbrG3p4ChHBQusWem2sgM

(lldb) po ybytes
n6WQz7uMkIALgLM5xriANs5VXKy
2Q71jetoI6Pzojk
I have tried recreating the key from your data in iOS using Swift and in Python using the cryptography library (which I can't link because of the new editor here, sigh...).

Anyways, my iOS code looks like this

Code Block swift
import CryptoKit
import Foundation
let x963 = Data(base64Encoded: "BBFANnBUfWaxiaM25MGHsga26xt6eAoRwULv7FnptrIDn6WQz7uMkIALgLM5xriANs5VXKy/2Q71jetoI6Pzojk=")!
print(x963, x963 as NSData)
let x = x963.prefix(33).suffix(32)
print(x, x as NSData)
let y = x963.suffix(32)
print(y, y as NSData)
print(x.base64EncodedString())
print(y.base64EncodedString())
let pub = try! P256.Signing.PublicKey(x963Representation: x963)
/* Outputs the following
65 bytes {length = 65, bytes = 0x04114036 70547d66 b189a336 e4c187b2 ... 8deb6823 a3f3a239 }
32 bytes {length = 32, bytes = 0x11403670 547d66b1 89a336e4 c187b206 ... 42efec59 e9b6b203 }
32 bytes {length = 32, bytes = 0x9fa590cf bb8c9080 0b80b339 c6b88036 ... 8deb6823 a3f3a239 }
EUA2cFR9ZrGJozbkwYeyBrbrG3p4ChHBQu/sWem2sgM=
n6WQz7uMkIALgLM5xriANs5VXKy/2Q71jetoI6Pzojk=
*/

Which successfully creates the P256 key, meaning, the data provided is correct. Do note that I do not use the URL encoding as JWT suggests, but that shouldn't matter.

Code Block python
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64
a = bytes.fromhex('04\ 
11403670547D66B189A336E4C187B206\
B6EB1B7A780A11C142EFEC59E9B6B203\
9FA590CFBB8C90800B80B339C6B88036\
CE555CACBFD90EF58DEB6823A3F3A239')
ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), a)
x = base64.b64decode('EUA2cFR9ZrGJozbkwYeyBrbrG3p4ChHBQu/sWem2sgM=')
y = base64.b64decode('n6WQz7uMkIALgLM5xriANs5VXKy/2Q71jetoI6Pzojk=')
x963 = bytes.fromhex('04') + x + y
ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), x963)

And this too works perfectly fine. So I guess it's a server error. May I ask what platform you're using on your server to reconstruct the keys? Note that some cryptography libraries are a bit finicky when serialising elliptic curve keys.
Make sure your library supports:
  1. Reconstruction of the public key from given public key format (uncompressed point). Which is different from, for instance, subject public key info (used in certificates) and compressed point.

  2. Reconstruction of the public key from X9.62/X9.63 encoded data is supported. Which is different from DER or PEM.

One last thing to check, make sure your base64 url encoded data is decoded correctly. As seen in the example code eskimo provided, you can see the padding is removed and the + and / are replaced. Make sure this is of course reversed on your server (if that is one of the requirements. base64 URL encoded is not the same as base64 encoded). Hope this can help you some!

So, what we found was the following -

The X and Y values of the EC public key are treated as signed base64 encoded values by our server. So, while extracting the x (or y) byte components from public key, if the most significant byte is greater than 0x7f, the x (or y) value will be treated as negative values. However, our server expects the the X and Y values to be positive for a valid EC key.

So the fix was to check If the x or y component of the public key has a first byte value greater than 0x7f, then add an extra 0x00 byte, to make the value positive before encoding.

The code is:

Code Block objC
+ (NSArray<NSString *> *)getBase64EncodedCoordinatesFromECPublicKey:(SecKeyRef)publicKey error:(out NSError **)error{
CFErrorRef copyPublicKeyError = NULL;
NSData* keyData = (NSData*)CFBridgingRelease(
SecKeyCopyExternalRepresentation(publicKey, &copyPublicKeyError)
);
if (!keyData) {
NSError *err = CFBridgingRelease(copyPublicKeyError);
NSLog(@"%@", err);
return nil;
}
NSString *xCoordinate; NSString *yCoordinate;
NSData *xDataRaw = [keyData subdataWithRange:NSMakeRange(1, keyData.length/2)];
NSData *yDataRaw = [keyData subdataWithRange:NSMakeRange((keyData.length / 2)+1, keyData.length/2)];
uint8_t zeroByte = 0x00;
const unsigned char* xBytes = [xDataRaw bytes];
const unsigned char* yBytes = [yDataRaw bytes];
int mostSignificantByte_x = xBytes[0];
int mostSignificantByte_y = yBytes[0];
if (mostSignificantByte_x > 127) {
NSMutableData *xData = [[NSMutableData alloc] initWithBytes:&zeroByte length:1];
[xData appendData:xDataRaw];
xCoordinate = [self encodeBase64urlWithPadding:xData];
} else {
xCoordinate = [self encodeBase64urlWithPadding:xDataRaw];
}
if (mostSignificantByte_y > 127) {
NSMutableData *yData = [[NSMutableData alloc] initWithBytes:&zeroByte length:1];
[yData appendData:yDataRaw];
yCoordinate = [self encodeBase64urlWithPadding:yData];
} else {
yCoordinate = [self encodeBase64urlWithPadding:yDataRaw];
}
NSArray *coordinates = @[xCoordinate, yCoordinate];
return coordinates;
}