sharplispers / ironclad

A cryptographic toolkit written in Common Lisp
BSD 3-Clause "New" or "Revised" License
166 stars 28 forks source link

ECDSA public/private keys? #33

Closed dnaeon closed 3 years ago

dnaeon commented 3 years ago

Hi,

Would you consider implementing ECDSA public/private keys in ironclad?

Thanks!

glv2 commented 3 years ago

You're thinking about ECDSA on which curve(s)?

dnaeon commented 3 years ago

Sorry, I should have mentioned that :)

I was thinking about NIST P-256, P-384 and P-521.

These are the ones used by OpenSSH when generating new ECDSA keys.

https://tools.ietf.org/html/rfc5656#section-6.1

I'm working on the following Common Lisp system, which allows for decoding and encoding of OpenSSH private and public keys, and also allows to generate new OpenSSH private/public key pairs by using ironclad.

https://github.com/dnaeon/cl-ssh-keys

Currently RSA, DSA and ED25519 are implemented (all of these are being generated by ironclad), and the only missing key type is ECDSA.

Would be great if I could use ironclad to generate ECDSA keys as well, which I can then properly encode in the OpenSSH format.

Thanks!

glv2 commented 3 years ago

I added the curves, they are called secp256r1, secp384r1 and secp521r1. Could you try and tell me if you notice something not working properly?

Thanks.

dnaeon commented 3 years ago

@glv2, thanks a lot!

I'll spend some time in the next days and will let you know how it goes.

Thanks again!

dnaeon commented 3 years ago

I've just gave it a try using secp384r and here's what I have so far.

When parsing an existing ECDSA public key, that was previously generated with ssh-keygen(1) here's what I see.

CL-USER> (defparameter pub
       (ssh-keys:parse-public-key-file #P"~/.ssh/id_ecdsa.pub"))
PUB

These are the bytes contained within the OpenSSH public key after decoding them.

#(4 19 149 248 209 88 203 242 176 145 59 173 96 25 106 126 189 185 124 6 229
  214 116 185 95 233 202 189 178 111 199 3 73 24 45 197 38 39 38 26 110 25 77
  50 127 15 52 54 222 153 180 1 245 2 194 188 192 217 17 128 194 169 6 222 237
  192 100 243 137 107 85 231 160 62 229 6 19 196 86 84 2 94 125 146 140 3 143
  73 202 3 211 7 135 56 31 217 75)

I'm not a crypto expert at all, so please excuse any silly questions asked from my side :)

Above vector looks like as an un-compressed EC point (first byte is 4), if I'm not mistaken, and I can decode it properly using EC-DECODE-POINT.

CL-USER> (defparameter point 
       (ironclad::ec-decode-point :secp384r1 (ironclad:secp384r1-key-y pub)))
POINT

When I compare this to a public key generated from ironclad I get this.

CL-USER> (multiple-value-bind (priv pub) (ironclad:generate-key-pair :secp384r1)
       (ironclad:secp384r1-key-y pub))
#(2 62 232 53 50 104 5 85 12 215 105 109 39 76 154 177 19 56 5 134 117 199 198
  182 46 196 110 172 196 240 27 209 55 192 42 108 248 199 95 74 34 221 21 115
  123 21 182 52 30)

The output is shorter than the ones I would have to put in an OpenSSH buffer, which seems to be an secp384r1-point instance in this case with x and y set.

I've also done the following test and had EC-ENCODE-POINT also encode the y part of the point.

diff --git a/src/public-key/secp384r1.lisp b/src/public-key/secp384r1.lisp
index 40e1868..74c7eb3 100644
--- a/src/public-key/secp384r1.lisp
+++ b/src/public-key/secp384r1.lisp
@@ -160,7 +160,8 @@
            (y-sign (logand y 1)))
       (concatenate '(simple-array (unsigned-byte 8) (*))
                    (vector (+ 2 y-sign))
-                   (ec-encode-scalar :secp384r1 x)))))
+                   (ec-encode-scalar :secp384r1 x)
+                   (ec-encode-scalar :secp384r1 y)))))

 (defmethod ec-decode-point ((kind (eql :secp384r1)) octets)
   (declare (optimize (speed 3) (safety 0) (space 0) (debug 0)))

Then I've tested out an OpenSSH key with the following contents.

CL-USER> (multiple-value-bind (priv pub) (ironclad:generate-key-pair :secp384r1)
       (ironclad:secp384r1-key-y pub))
#(2 42 25 234 117 144 12 58 157 6 66 11 104 50 21 173 125 5 254 2 138 107 186
  140 174 100 158 216 55 140 154 123 179 96 51 141 150 105 139 166 118 62 99
  191 199 93 88 179 160 69 37 175 235 96 241 125 103 13 125 122 221 128 13 137
  143 122 60 6 42 244 185 36 174 174 182 46 151 17 40 21 69 8 134 64 170 224 96
  60 218 146 73 2 193 215 7 151 16)

The only thing that needs to be "patched" in above vector is the first byte to be 4 to make it un-compressed point, after that I was able to use the generated key to perform public-key authentication against a remote system.

I guess my question is how could I get to the representation of an un-compressed point (if that's what it actually is) by using the ironclad:secp384r1-key-y reader?

Thanks!

glv2 commented 3 years ago

Currently the ec-encode-point function returns compressed points (where the first byte is 2 or 3).

Is there a way for OpenSSH to understand compressed points? If not, I can make ec-encode-point return an uncompressed point (where the first byte is 4) as it doesn't make much of a difference for the rest of the code.

dnaeon commented 3 years ago

Looking at the release notes from past releases it seems they have not implemented compressed points.

OpenSSH 5.7/5.7p1 (2011-01-24)

OpenSSH 5.7 was released on 2011-01-24. It is available from the
mirrors listed at https://www.openssh.com/.
OpenSSH is a 100% complete SSH protocol version 1.3, 1.5 and 2.0
implementation and includes sftp client and server support.

Once again, we would like to thank the OpenSSH community for their
continued support of the project, especially those who contributed
code or patches, reported bugs, tested snapshots or donated to the
project. More information on donations may be found at:
http://www.openssh.com/donations.html

Changes since OpenSSH 5.6
=========================

Features:

 * Implement Elliptic Curve Cryptography modes for key exchange (ECDH)
   and host/user keys (ECDSA) as specified by RFC5656. ECDH and ECDSA
   offer better performance than plain DH and DSA at the same equivalent
   symmetric key length, as well as much shorter keys.

   Only the mandatory sections of RFC5656 are implemented, specifically
   the three REQUIRED curves nistp256, nistp384 and nistp521 and only
   ECDH and ECDSA. Point compression (optional in RFC5656) is NOT
   implemented.

   Certificate host and user keys using the new ECDSA key types are
   supported - an ECDSA key may be certified, and an ECDSA key may act
   as a CA to sign certificates.
glv2 commented 3 years ago

I made ec-encode-point return uncompressed points for the secp* curves (commit 4c689d0215e77d799fb6007dba97cbe3262d5f82).

dnaeon commented 3 years ago

Thanks a lot, @glv2 !