Skip to content

Commit

Permalink
fix the encoding of ECDSA public keys with leading 00's (#240)
Browse files Browse the repository at this point in the history
The public portion of an ECDSA public key has an 'x' and 'y' component.
For a new account request that uses EAB token, this public key is added
to the protected section of the JWS. When encoding the key, it's
important to preserve all of the bytes in the different parts, even if
they are zero bytes.

The previous encoding would dump the key to hex and split the x and y
component from those, then encode them into an OpenSSL bignum, then dump
that bignum to bytes and urlsafe-base64 encode that. But OpenSSL's
bignum does not track the number of input bytes, only the most compact
representation of the number. This means that leading zero bytes are
dropped when re-encoded into a byte array from the bignum.

Pushing the 'x' and 'y' component through OpenSSL::BN is unecessary and
avoids issues for ECDSA keys that are randomly generated with leading
zeros. This can be demonstrated with the following code:

irb> OpenSSL::BN.new("00FF", 16).to_s(2)
=> "\xFF"

The test generates a key with a leading zero for the encoded 'x'
component, then checks to make sure the JWS contains the corresponding
0x00 bytes in the encoding for 'x'.
  • Loading branch information
benburkert authored Jun 14, 2024
1 parent 5377e34 commit 3c0da04
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 4 deletions.
8 changes: 4 additions & 4 deletions lib/acme/client/jwk/ecdsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ def to_h
{
crv: @curve_params[:jwa_crv],
kty: 'EC',
x: Acme::Client::Util.urlsafe_base64(coordinates[:x].to_s(2)),
y: Acme::Client::Util.urlsafe_base64(coordinates[:y].to_s(2))
x: Acme::Client::Util.urlsafe_base64(coordinates[:x]),
y: Acme::Client::Util.urlsafe_base64(coordinates[:y])
}
end

Expand Down Expand Up @@ -92,8 +92,8 @@ def coordinates
hex_y = hex[2 + data_len / 2, data_len / 2]

{
x: OpenSSL::BN.new([hex_x].pack('H*'), 2),
y: OpenSSL::BN.new([hex_y].pack('H*'), 2)
x: [hex_x].pack('H*'),
y: [hex_y].pack('H*')
}
end
end
Expand Down
19 changes: 19 additions & 0 deletions spec/jwk_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@
expect(payload).to include('some' => 'data')
end
end

describe 'key with leading zero bytes in the encoded public key x' do
let(:private_key) {
loop do
key = OpenSSL::PKey::EC.generate('prime256v1')
break key if key.public_key.to_bn.to_s(16)[2..3] == '00'
end
}

it 'can be encoded correctly' do
jws_s = subject.jws(header: { 'a-header' => 'header-value' }, payload: { 'some' => 'data' })
jws = JSON.parse(jws_s)

b64_point_x = JSON.parse(Base64.urlsafe_decode64(jws['protected']))['jwk']['x']
hex_point_x = Base64.urlsafe_decode64(b64_point_x).unpack('H*').first

expect(hex_point_x[0..1]).to eq('00')
end
end
end

def generate_key(klass)
Expand Down

0 comments on commit 3c0da04

Please sign in to comment.