Skip to content

Commit 852e02e

Browse files
authored
Merge pull request #22 from str4d/21-curdle-wg-spec
Implement current encoding of Ed25519 keys Closes #21.
2 parents 8497fa1 + b449602 commit 852e02e

File tree

7 files changed

+493
-88
lines changed

7 files changed

+493
-88
lines changed

src/net/i2p/crypto/eddsa/EdDSAPrivateKey.java

Lines changed: 147 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@
2424
/**
2525
* An EdDSA private key.
2626
*<p>
27-
* Warning: Private key encoding is not fully specified in the
28-
* current IETF draft. This implementation uses PKCS#8 encoding,
27+
* Warning: Private key encoding is based on the current curdle WG draft,
2928
* and is subject to change. See getEncoded().
3029
*</p><p>
31-
* Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
30+
* For compatibility with older releases, decoding supports both the old and new
31+
* draft specifications. See decode().
32+
*</p><p>
33+
* Ref: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
34+
*</p><p>
35+
* Old Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
3236
*</p>
3337
* @author str4d
3438
*
@@ -42,6 +46,12 @@ public class EdDSAPrivateKey implements EdDSAKey, PrivateKey {
4246
private final byte[] Abyte;
4347
private final EdDSAParameterSpec edDsaSpec;
4448

49+
// OID 1.3.101.xxx
50+
private static final int OID_OLD = 100;
51+
private static final int OID_ED25519 = 112;
52+
private static final int OID_BYTE = 11;
53+
private static final int IDLEN_BYTE = 6;
54+
4555
public EdDSAPrivateKey(EdDSAPrivateKeySpec spec) {
4656
this.seed = spec.getSeed();
4757
this.h = spec.getH();
@@ -67,108 +77,197 @@ public String getFormat() {
6777
}
6878

6979
/**
70-
* This follows the docs from
71-
* java.security.spec.PKCS8EncodedKeySpec
72-
* quote:
80+
* Returns the public key in its canonical encoding.
81+
*<p>
82+
* This implements the following specs:
83+
*<ul><li>
84+
* General encoding: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
85+
*</li></li>
86+
* Key encoding: https://tools.ietf.org/html/rfc8032
87+
*</li></ul>
88+
*</p><p>
89+
* This encodes the seed. It will return null if constructed from
90+
* a spec which was directly constructed from H, in which case seed is null.
91+
*</p><p>
92+
* For keys in older formats, decoding and then re-encoding is sufficient to
93+
* migrate them to the canonical encoding.
94+
*</p>
95+
* Relevant spec quotes:
7396
*<pre>
74-
* The PrivateKeyInfo syntax is defined in the PKCS#8 standard as follows:
75-
* PrivateKeyInfo ::= SEQUENCE {
97+
* OneAsymmetricKey ::= SEQUENCE {
7698
* version Version,
7799
* privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
78100
* privateKey PrivateKey,
79-
* attributes [0] IMPLICIT Attributes OPTIONAL }
101+
* attributes [0] Attributes OPTIONAL,
102+
* ...,
103+
* [[2: publicKey [1] PublicKey OPTIONAL ]],
104+
* ...
105+
* }
106+
*
80107
* Version ::= INTEGER
81108
* PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
82109
* PrivateKey ::= OCTET STRING
110+
* PublicKey ::= OCTET STRING
83111
* Attributes ::= SET OF Attribute
84112
*</pre>
85113
*
86114
*<pre>
87-
* AlgorithmIdentifier ::= SEQUENCE
88-
* {
89-
* algorithm OBJECT IDENTIFIER,
90-
* parameters ANY OPTIONAL
91-
* }
115+
* ... when encoding a OneAsymmetricKey object, the private key is wrapped
116+
* in a CurvePrivateKey object and wrapped by the OCTET STRING of the
117+
* 'privateKey' field.
118+
*
119+
* CurvePrivateKey ::= OCTET STRING
92120
*</pre>
93121
*
94-
* Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
122+
*<pre>
123+
* AlgorithmIdentifier ::= SEQUENCE {
124+
* algorithm OBJECT IDENTIFIER,
125+
* parameters ANY DEFINED BY algorithm OPTIONAL
126+
* }
95127
*
96-
* Note that the private key encoding is not fully specified in the Josefsson draft version 04,
97-
* and the example could be wrong, as it's lacking Version and AlgorithmIdentifier.
98-
* This will hopefully be clarified in the next draft.
99-
* But sun.security.pkcs.PKCS8Key expects them so we must include them for keytool to work.
128+
* For all of the OIDs, the parameters MUST be absent.
129+
*</pre>
100130
*
101-
* This encodes the seed. It will return null if constructed from
102-
* a spec which was directly constructed from H, in which case seed is null.
131+
*<pre>
132+
* id-Ed25519 OBJECT IDENTIFIER ::= { 1 3 101 112 }
133+
*</pre>
103134
*
104-
* @return 49 bytes for Ed25519, null for other curves
135+
* @return 48 bytes for Ed25519, null for other curves
105136
*/
106137
@Override
107138
public byte[] getEncoded() {
108139
if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)))
109140
return null;
110-
int totlen = 17 + seed.length;
141+
if (seed == null)
142+
return null;
143+
int totlen = 16 + seed.length;
111144
byte[] rv = new byte[totlen];
112145
int idx = 0;
113146
// sequence
114147
rv[idx++] = 0x30;
115-
rv[idx++] = (byte) (15 + seed.length);
116-
148+
rv[idx++] = (byte) (totlen - 2);
117149
// version
118-
// not in the Josefsson example
119150
rv[idx++] = 0x02;
120151
rv[idx++] = 1;
152+
// v1 - no public key included
121153
rv[idx++] = 0;
122-
123154
// Algorithm Identifier
124155
// sequence
125-
// not in the Josefsson example
126156
rv[idx++] = 0x30;
127-
rv[idx++] = 8;
128-
// OID 1.3.101.100
157+
rv[idx++] = 5;
158+
// OID
129159
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx
130-
// not in the Josefsson example
131160
rv[idx++] = 0x06;
132161
rv[idx++] = 3;
133162
rv[idx++] = (1 * 40) + 3;
134163
rv[idx++] = 101;
135-
rv[idx++] = 100;
136-
// params
137-
rv[idx++] = 0x0a;
138-
rv[idx++] = 1;
139-
rv[idx++] = 1; // Ed25519
140-
// the key
164+
rv[idx++] = (byte) OID_ED25519;
165+
// params - absent
166+
// PrivateKey
167+
rv[idx++] = 0x04; // octet string
168+
rv[idx++] = (byte) (2 + seed.length);
169+
// CurvePrivateKey
141170
rv[idx++] = 0x04; // octet string
142171
rv[idx++] = (byte) seed.length;
172+
// the key
143173
System.arraycopy(seed, 0, rv, idx, seed.length);
144174
return rv;
145175
}
146176

147177
/**
148-
* This is really dumb for now.
149-
* See getEncoded().
178+
* Extracts the private key bytes from the provided encoding.
179+
*<p>
180+
* This will decode data conforming to the current spec at
181+
* https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
182+
* or as inferred from the old spec at
183+
* https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04.
184+
*</p><p>
185+
* Contrary to draft-ietf-curdle-pkix-04, it WILL accept a parameter value
186+
* of NULL, as it is required for interoperability with the default Java
187+
* keystore. Other implementations MUST NOT copy this behaviour from here
188+
* unless they also need to read keys from the default Java keystore.
189+
*</p><p>
190+
* This is really dumb for now. It does not use a general-purpose ASN.1 decoder.
191+
* See also getEncoded().
150192
*
151-
* @return 32 bytes for Ed25519, throws for other curves
193+
* @return 32 bytes for Ed25519, throws for other curves
152194
*/
153195
private static byte[] decode(byte[] d) throws InvalidKeySpecException {
154196
try {
197+
//
198+
// Setup and OID check
199+
//
200+
int totlen = 48;
201+
int idlen = 5;
202+
int doid = d[OID_BYTE];
203+
if (doid == OID_OLD) {
204+
totlen = 49;
205+
idlen = 8;
206+
} else if (doid == OID_ED25519) {
207+
// Detect parameter value of NULL
208+
if (d[IDLEN_BYTE] == 7) {
209+
totlen = 50;
210+
idlen = 7;
211+
}
212+
} else {
213+
throw new InvalidKeySpecException("unsupported key spec");
214+
}
215+
216+
//
217+
// Pre-decoding check
218+
//
219+
if (d.length != totlen) {
220+
throw new InvalidKeySpecException("invalid key spec length");
221+
}
222+
223+
//
224+
// Decoding
225+
//
155226
int idx = 0;
156227
if (d[idx++] != 0x30 ||
157-
d[idx++] != 47 ||
228+
d[idx++] != (totlen - 2) ||
158229
d[idx++] != 0x02 ||
159230
d[idx++] != 1 ||
160231
d[idx++] != 0 ||
161232
d[idx++] != 0x30 ||
162-
d[idx++] != 8 ||
233+
d[idx++] != idlen ||
163234
d[idx++] != 0x06 ||
164235
d[idx++] != 3 ||
165236
d[idx++] != (1 * 40) + 3 ||
166-
d[idx++] != 101 ||
167-
d[idx++] != 100 ||
168-
d[idx++] != 0x0a ||
169-
d[idx++] != 1 ||
170-
d[idx++] != 1 ||
171-
d[idx++] != 0x04 ||
237+
d[idx++] != 101) {
238+
throw new InvalidKeySpecException("unsupported key spec");
239+
}
240+
idx++; // OID, checked above
241+
// parameters only with old OID
242+
if (doid == OID_OLD) {
243+
if (d[idx++] != 0x0a ||
244+
d[idx++] != 1 ||
245+
d[idx++] != 1) {
246+
throw new InvalidKeySpecException("unsupported key spec");
247+
}
248+
} else {
249+
// Handle parameter value of NULL
250+
//
251+
// Quote https://tools.ietf.org/html/draft-ietf-curdle-pkix-04 :
252+
// For all of the OIDs, the parameters MUST be absent.
253+
// Regardless of the defect in the original 1997 syntax,
254+
// implementations MUST NOT accept a parameters value of NULL.
255+
//
256+
// But Java's default keystore puts it in (when decoding as
257+
// PKCS8 and then re-encoding to pass on), so we must accept it.
258+
if (idlen == 7) {
259+
if (d[idx++] != 0x05 ||
260+
d[idx++] != 0) {
261+
throw new InvalidKeySpecException("unsupported key spec");
262+
}
263+
}
264+
// PrivateKey wrapping the CurvePrivateKey
265+
if (d[idx++] != 0x04 ||
266+
d[idx++] != 34) {
267+
throw new InvalidKeySpecException("unsupported key spec");
268+
}
269+
}
270+
if (d[idx++] != 0x04 ||
172271
d[idx++] != 32) {
173272
throw new InvalidKeySpecException("unsupported key spec");
174273
}

0 commit comments

Comments
 (0)