Skip to content

Commit 775194d

Browse files
committed
Merge branch 'sm9-keyexchange'
2 parents 77fad50 + 511c7bc commit 775194d

5 files changed

Lines changed: 397 additions & 4 deletions

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package org.bouncycastle.crypto.agreement;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.math.BigInteger;
5+
import java.security.SecureRandom;
6+
7+
import org.bouncycastle.crypto.digests.SM9Sm3;
8+
import org.bouncycastle.crypto.params.SM9EncMasterPrivateKeyParameters;
9+
import org.bouncycastle.crypto.params.SM9EncPrivateKeyParameters;
10+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
11+
import org.bouncycastle.crypto.digests.SM3Digest;
12+
import org.bouncycastle.math.ec.ECConstants;
13+
import org.bouncycastle.math.ec.ECPoint;
14+
import org.bouncycastle.math.ec.sm9.Fp12;
15+
import org.bouncycastle.math.ec.sm9.SM9Curve;
16+
import org.bouncycastle.math.ec.sm9.SM9G2Point;
17+
import org.bouncycastle.math.ec.sm9.SM9Pairing;
18+
import org.bouncycastle.util.BigIntegers;
19+
20+
/**
21+
* The SM9 key exchange protocol (GM/T 0044.3-2016).
22+
* <p>
23+
* Usage per party: construct with your own key-exchange private key (derived with
24+
* hid = 0x02), the peer's identity, and whether you are the initiator (user A) or
25+
* responder (user B). Call {@link #generateEphemeral} to produce your R value,
26+
* exchange R values, then call {@link #calculateKey} with the peer's R to obtain
27+
* the shared key. The optional key-confirmation tags are then available via
28+
* {@link #getResponderConfirmation()} (S_B) and {@link #getInitiatorConfirmation()}
29+
* (S_A).
30+
*/
31+
public class SM9KeyExchange
32+
{
33+
private final SM9EncPrivateKeyParameters key;
34+
private final byte[] peerIdentity;
35+
private final boolean initiator;
36+
37+
private BigInteger ephemeralScalar;
38+
private ECPoint ephemeralPoint;
39+
40+
// retained after calculateKey for the confirmation tags
41+
private Fp12 g1;
42+
private Fp12 g2;
43+
private Fp12 g3;
44+
private byte[] idA;
45+
private byte[] idB;
46+
private byte[] raBytes;
47+
private byte[] rbBytes;
48+
49+
public SM9KeyExchange(SM9EncPrivateKeyParameters key, byte[] peerIdentity, boolean initiator)
50+
{
51+
this.key = key;
52+
this.peerIdentity = peerIdentity;
53+
this.initiator = initiator;
54+
}
55+
56+
/**
57+
* Generate this party's ephemeral value R = [r]Q_peer (a G1 point) and retain
58+
* the ephemeral scalar r. Q_peer = [H1(peerId||0x02, N)]P1 + P_pub-e.
59+
*/
60+
public ECPoint generateEphemeral(SecureRandom random)
61+
{
62+
ECPoint qPeer = key.getMasterPublicKey().recipientPoint(
63+
peerIdentity, SM9EncMasterPrivateKeyParameters.HID_EXCHANGE);
64+
SecureRandom rand = CryptoServicesRegistrar.getSecureRandom(random);
65+
ephemeralScalar = BigIntegers.createRandomInRange(
66+
ECConstants.ONE, SM9Curve.N.subtract(ECConstants.ONE), rand);
67+
ephemeralPoint = SM9Curve.multiplySecure(qPeer, ephemeralScalar).normalize();
68+
return ephemeralPoint;
69+
}
70+
71+
/**
72+
* Compute the shared key of {@code klenBits} bits from the peer's ephemeral
73+
* value {@code peerR}. Must be called after {@link #generateEphemeral}.
74+
*/
75+
public byte[] calculateKey(int klenBits, ECPoint peerR)
76+
{
77+
if (klenBits <= 0)
78+
{
79+
// match SM9KEMGenerator: a non-positive length has no KDF output
80+
throw new IllegalArgumentException("klenBits must be positive");
81+
}
82+
if (ephemeralPoint == null)
83+
{
84+
throw new IllegalStateException("generateEphemeral must be called first");
85+
}
86+
peerR = peerR.normalize();
87+
if (peerR.isInfinity() || !peerR.isValid())
88+
{
89+
throw new IllegalArgumentException("invalid SM9 peer ephemeral point");
90+
}
91+
92+
BigInteger r = ephemeralScalar;
93+
Fp12 gPP = key.getMasterPublicKey().pairingWithP2(); // e(P_pub-e, P2)
94+
SM9G2Point de = key.getPrivatePoint();
95+
96+
if (initiator)
97+
{
98+
g1 = gPP.powSecure(r); // e(P_pub-e,P2)^rA
99+
g2 = SM9Pairing.pairing(peerR, de); // e(RB, deA)
100+
g3 = g2.powSecure(r);
101+
}
102+
else
103+
{
104+
g1 = SM9Pairing.pairing(peerR, de); // e(RA, deB)
105+
g2 = gPP.powSecure(r); // e(P_pub-e,P2)^rB
106+
g3 = g1.powSecure(r);
107+
}
108+
109+
idA = initiator ? key.getIdentity() : peerIdentity;
110+
idB = initiator ? peerIdentity : key.getIdentity();
111+
ECPoint ra = initiator ? ephemeralPoint : peerR;
112+
ECPoint rb = initiator ? peerR : ephemeralPoint;
113+
raBytes = SM9Curve.g1ToBytes(ra);
114+
rbBytes = SM9Curve.g1ToBytes(rb);
115+
116+
ByteArrayOutputStream z = new ByteArrayOutputStream();
117+
write(z, idA);
118+
write(z, idB);
119+
write(z, raBytes);
120+
write(z, rbBytes);
121+
write(z, SM9Pairing.toBytes(g1));
122+
write(z, SM9Pairing.toBytes(g2));
123+
write(z, SM9Pairing.toBytes(g3));
124+
return SM9Sm3.kdf(z.toByteArray(), klenBits);
125+
}
126+
127+
/**
128+
* S_B = Hash(0x82 || g1 || Hash(g2||g3||IDA||IDB||RA||RB)): the confirmation
129+
* the responder sends to (and the initiator checks against) the initiator.
130+
* <p>
131+
* The returned tag is a secret authenticator; a received value must be compared
132+
* against it with {@link org.bouncycastle.util.Arrays#constantTimeAreEqual(byte[], byte[])},
133+
* not {@code Arrays.equals}, to avoid a timing side channel.
134+
*/
135+
public byte[] getResponderConfirmation()
136+
{
137+
return confirmation((byte)0x82);
138+
}
139+
140+
/**
141+
* S_A = Hash(0x83 || g1 || Hash(g2||g3||IDA||IDB||RA||RB)): the confirmation
142+
* the initiator sends to (and the responder checks against) the responder.
143+
* <p>
144+
* The returned tag is a secret authenticator; a received value must be compared
145+
* against it with {@link org.bouncycastle.util.Arrays#constantTimeAreEqual(byte[], byte[])},
146+
* not {@code Arrays.equals}, to avoid a timing side channel.
147+
*/
148+
public byte[] getInitiatorConfirmation()
149+
{
150+
return confirmation((byte)0x83);
151+
}
152+
153+
private byte[] confirmation(byte tag)
154+
{
155+
if (g1 == null)
156+
{
157+
throw new IllegalStateException("calculateKey must be called first");
158+
}
159+
SM3Digest sm3 = new SM3Digest();
160+
update(sm3, SM9Pairing.toBytes(g2));
161+
update(sm3, SM9Pairing.toBytes(g3));
162+
update(sm3, idA);
163+
update(sm3, idB);
164+
update(sm3, raBytes);
165+
update(sm3, rbBytes);
166+
byte[] inner = new byte[32];
167+
sm3.doFinal(inner, 0);
168+
169+
sm3.update(tag);
170+
update(sm3, SM9Pairing.toBytes(g1));
171+
update(sm3, inner);
172+
byte[] out = new byte[32];
173+
sm3.doFinal(out, 0);
174+
return out;
175+
}
176+
177+
private static void write(ByteArrayOutputStream out, byte[] b)
178+
{
179+
out.write(b, 0, b.length);
180+
}
181+
182+
private static void update(SM3Digest sm3, byte[] b)
183+
{
184+
sm3.update(b, 0, b.length);
185+
}
186+
}

‎core/src/test/java/org/bouncycastle/crypto/test/RegressionTest.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ public class RegressionTest
158158
new SM4Test(),
159159
new SM9KEMTest(),
160160
new SM9SignerTest(),
161+
new SM9KeyExchangeTest(),
161162
new DSTU7624Test(),
162163
new DSTU7564Test(),
163164
new IsoTrailerTest(),
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package org.bouncycastle.crypto.test;
2+
3+
import java.io.BufferedReader;
4+
import java.io.InputStreamReader;
5+
import java.math.BigInteger;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
import org.bouncycastle.crypto.agreement.SM9KeyExchange;
10+
import org.bouncycastle.crypto.params.SM9EncMasterPrivateKeyParameters;
11+
import org.bouncycastle.crypto.params.SM9EncPrivateKeyParameters;
12+
import org.bouncycastle.math.ec.ECPoint;
13+
import org.bouncycastle.test.TestResourceFinder;
14+
import org.bouncycastle.util.Arrays;
15+
import org.bouncycastle.util.BigIntegers;
16+
import org.bouncycastle.util.encoders.Hex;
17+
import org.bouncycastle.util.test.SimpleTest;
18+
import org.bouncycastle.util.test.TestRandomBigInteger;
19+
20+
/**
21+
* Known-answer test for the SM9 key exchange protocol (GM/T 0044.3-2016)
22+
* against the GM/T 0044.5-2016 Annex B vector (crypto/sm9/sm9_keyexchange.txt):
23+
* both parties' ephemeral values, the shared key and the key-confirmation tags
24+
* S_A / S_B are reproduced byte-for-byte.
25+
*/
26+
public class SM9KeyExchangeTest
27+
extends SimpleTest
28+
{
29+
public String getName()
30+
{
31+
return "SM9KeyExchange";
32+
}
33+
34+
private Map loadVectors(String fileName)
35+
throws Exception
36+
{
37+
Map m = new HashMap();
38+
BufferedReader br = new BufferedReader(
39+
new InputStreamReader(TestResourceFinder.findTestResource("crypto/sm9", fileName)));
40+
try
41+
{
42+
String line;
43+
while ((line = br.readLine()) != null)
44+
{
45+
line = line.trim();
46+
if (line.length() == 0 || line.startsWith("#"))
47+
{
48+
continue;
49+
}
50+
int eq = line.indexOf('=');
51+
if (eq > 0)
52+
{
53+
m.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim());
54+
}
55+
}
56+
}
57+
finally
58+
{
59+
br.close();
60+
}
61+
return m;
62+
}
63+
64+
private byte[] hex(Map v, String key)
65+
{
66+
return Hex.decode((String)v.get(key));
67+
}
68+
69+
public void performTest()
70+
throws Exception
71+
{
72+
Map v = loadVectors("sm9_keyexchange.txt");
73+
BigInteger ke = new BigInteger((String)v.get("ke"), 16);
74+
byte[] idA = hex(v, "IDA");
75+
byte[] idB = hex(v, "IDB");
76+
int klen = Integer.parseInt((String)v.get("klen_bits"));
77+
byte hid = SM9EncMasterPrivateKeyParameters.HID_EXCHANGE;
78+
79+
SM9EncMasterPrivateKeyParameters master = new SM9EncMasterPrivateKeyParameters(ke);
80+
SM9EncPrivateKeyParameters deA = master.generateUserKey(idA, hid);
81+
SM9EncPrivateKeyParameters deB = master.generateUserKey(idB, hid);
82+
83+
SM9KeyExchange a = new SM9KeyExchange(deA, idB, true);
84+
SM9KeyExchange b = new SM9KeyExchange(deB, idA, false);
85+
ECPoint ra = a.generateEphemeral(new TestRandomBigInteger(256, hex(v, "rA")));
86+
ECPoint rb = b.generateEphemeral(new TestRandomBigInteger(256, hex(v, "rB")));
87+
88+
isTrue("SM9 key exchange RA", Arrays.areEqual(xCoord(ra), hex(v, "RA_x")));
89+
isTrue("SM9 key exchange RB", Arrays.areEqual(xCoord(rb), hex(v, "RB_x")));
90+
91+
byte[] skA = a.calculateKey(klen, rb);
92+
byte[] skB = b.calculateKey(klen, ra);
93+
isTrue("SM9 key exchange SKA", Arrays.areEqual(skA, hex(v, "SK")));
94+
isTrue("SM9 key exchange SKB", Arrays.areEqual(skB, hex(v, "SK")));
95+
96+
isTrue("SM9 key exchange S_B", Arrays.areEqual(b.getResponderConfirmation(), hex(v, "S_B")));
97+
isTrue("SM9 key exchange S_B (initiator agrees)", Arrays.areEqual(a.getResponderConfirmation(), hex(v, "S_B")));
98+
isTrue("SM9 key exchange S_A", Arrays.areEqual(a.getInitiatorConfirmation(), hex(v, "S_A")));
99+
isTrue("SM9 key exchange S_A (responder agrees)", Arrays.areEqual(b.getInitiatorConfirmation(), hex(v, "S_A")));
100+
101+
// a non-positive key length must be rejected: the KDF would produce no
102+
// output (matching the SM9KEMGenerator / SM9KEMExtractor guards)
103+
try
104+
{
105+
a.calculateKey(0, rb);
106+
fail("SM9KeyExchange accepted klenBits = 0");
107+
}
108+
catch (IllegalArgumentException e)
109+
{
110+
isTrue("klenBits must be positive".equals(e.getMessage()));
111+
}
112+
}
113+
114+
private static byte[] xCoord(ECPoint p)
115+
{
116+
return BigIntegers.asUnsignedByteArray(32, p.normalize().getAffineXCoord().toBigInteger());
117+
}
118+
119+
public static void main(String[] args)
120+
{
121+
runTest(new SM9KeyExchangeTest());
122+
}
123+
}

0 commit comments

Comments
 (0)