Skip to content

Commit 3d32382

Browse files
committed
Merge branch 'sm9-vector-coverage-v2' into 'main'
SM9: assert the remaining official GM/T 0044.5 values, add the GM/T 0081-2020 message-syntax OIDs See merge request root/bc-java!302
2 parents 3c1632c + 55de0c0 commit 3d32382

8 files changed

Lines changed: 364 additions & 23 deletions

File tree

core/src/main/java/org/bouncycastle/asn1/gm/GMObjectIdentifiers.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,22 @@ public interface GMObjectIdentifiers
106106
ASN1ObjectIdentifier sm2_pkcs7_signedAndEnvelopedData = sm2_pkcs7.branch("4");
107107
ASN1ObjectIdentifier sm2_pkcs7_encryptedData = sm2_pkcs7.branch("5");
108108
ASN1ObjectIdentifier sm2_pkcs7_keyAgreementInfoData = sm2_pkcs7.branch("6");
109+
110+
// GM/T 0081-2020 SM9 encryption and signature message syntax content types, clause 5 table 1.
111+
// Base arc 1.2.156.10197.6.1.4.4 - the SM9 sibling of the GM/T 0010-2012 SM2 arc above, with
112+
// the same six content types in the same order. The structures they identify are modelled on
113+
// PKCS#7 but are not interchangeable with it: GM/T 0081 SignedData keeps the PKCS#7 field
114+
// order and tags while replacing certificates [0] / crls [1] with ibcSysParamsPublishInfos [0]
115+
// / irls [1], so org.bouncycastle.asn1.pkcs.SignedData parses it and silently mislabels those
116+
// two sets. The mismatch only bites one level down, in SignerInfo, which identifies the signer
117+
// by an identity-based Identifier rather than an IssuerAndSerialNumber and carries an
118+
// SM9Signature SEQUENCE where PKCS#7 has an EncryptedDigest OCTET STRING.
119+
ASN1ObjectIdentifier sm9_pkcs7 = new ASN1ObjectIdentifier("1.2.156.10197.6.1.4.4");
120+
ASN1ObjectIdentifier sm9_pkcs7_data = sm9_pkcs7.branch("1");
121+
ASN1ObjectIdentifier sm9_pkcs7_signedData = sm9_pkcs7.branch("2");
122+
ASN1ObjectIdentifier sm9_pkcs7_envelopedData = sm9_pkcs7.branch("3");
123+
ASN1ObjectIdentifier sm9_pkcs7_signedAndEnvelopedData = sm9_pkcs7.branch("4");
124+
ASN1ObjectIdentifier sm9_pkcs7_encryptedData = sm9_pkcs7.branch("5");
125+
// GM/T 0081 names this object keyAgreementInfo; the SM2 constant above carries a Data suffix.
126+
ASN1ObjectIdentifier sm9_pkcs7_keyAgreementInfo = sm9_pkcs7.branch("6");
109127
}

core/src/test/java/org/bouncycastle/crypto/test/SM9KEMTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ public void performTest()
7979
SM9EncMasterPrivateKeyParameters master = new SM9EncMasterPrivateKeyParameters(ke);
8080
SM9EncPublicKeyParameters recipient = master.getPublicKeyParameters().getUserPublicKey(identity);
8181

82+
// the master public key and the KGC-derived user key the standard prints
83+
// alongside the encapsulation (P_pub-e = [ke]P1 in G1, de_B = [t2]P2 in G2)
84+
isTrue("SM9 KEM master public key Ppub-e", Arrays.areEqual(
85+
master.getPublicKeyParameters().getEncoded(),
86+
Arrays.concatenate(new byte[]{0x04}, hex(v, "Ppube_x"), hex(v, "Ppube_y"))));
87+
isTrue("SM9 KEM user key deB", Arrays.areEqual(
88+
master.generateUserKey(identity, SM9EncMasterPrivateKeyParameters.HID)
89+
.getPrivatePoint().getEncoded(),
90+
Arrays.concatenate(
91+
Arrays.concatenate(new byte[]{0x04}, hex(v, "deB_x_hi"), hex(v, "deB_x_lo")),
92+
Arrays.concatenate(hex(v, "deB_y_hi"), hex(v, "deB_y_lo")))));
93+
8294
SM9KEMGenerator gen = new SM9KEMGenerator(klen, new TestRandomBigInteger(256, hex(v, "r")));
8395
SecretWithEncapsulation enc = gen.generateEncapsulated(recipient);
8496
isTrue("SM9 KEM key K", Arrays.areEqual(enc.getSecret(), hex(v, "K")));

core/src/test/java/org/bouncycastle/crypto/test/SM9KeyExchangeTest.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
import org.bouncycastle.crypto.params.SM9EncPrivateKeyParameters;
1212
import org.bouncycastle.crypto.params.SM9EncUserKeyParametersGenerator;
1313
import org.bouncycastle.math.ec.ECPoint;
14+
import org.bouncycastle.math.ec.sm9.SM9Curve;
1415
import org.bouncycastle.test.TestResourceFinder;
1516
import org.bouncycastle.util.Arrays;
16-
import org.bouncycastle.util.BigIntegers;
1717
import org.bouncycastle.util.encoders.Hex;
1818
import org.bouncycastle.util.test.SimpleTest;
1919
import org.bouncycastle.util.test.TestRandomBigInteger;
@@ -24,9 +24,9 @@
2424
* edition's example (hid = 0x02, crypto/sm9/sm9_keyexchange.txt) and the
2525
* official English edition's example (hid = 0x03,
2626
* crypto/sm9/sm9_keyexchange_hid03.txt); the hid is the KGC's published
27-
* choice, taken from the vector file. For each, both parties' ephemeral
28-
* values, the shared key and the key-confirmation tags S_A / S_B are
29-
* reproduced byte-for-byte.
27+
* choice, taken from the vector file. For each, the master public key, both
28+
* KGC-derived user keys, both parties' ephemeral values, the shared key and the
29+
* key-confirmation tags S_A / S_B are reproduced byte-for-byte.
3030
*/
3131
public class SM9KeyExchangeTest
3232
extends SimpleTest
@@ -97,13 +97,24 @@ private void checkVector(String fileName)
9797
isTrue(fileName + " deA records its hid", deA.getHid() == hid);
9898
isTrue(fileName + " deA is an exchange key", deA.isExchangeKey());
9999

100+
// the master public key and both KGC-derived user keys the standard prints
101+
isTrue(fileName + " master public key Ppub-e", Arrays.areEqual(
102+
master.getPublicKeyParameters().getEncoded(),
103+
Arrays.concatenate(new byte[]{0x04}, hex(v, "Ppube_x"), hex(v, "Ppube_y"))));
104+
isTrue(fileName + " user key deA", Arrays.areEqual(
105+
deA.getPrivatePoint().getEncoded(), g2(v, "deA_x_hi", "deA_x_lo", "deA_y_hi", "deA_y_lo")));
106+
isTrue(fileName + " user key deB", Arrays.areEqual(
107+
deB.getPrivatePoint().getEncoded(), g2(v, "deB_x_hi", "deB_x_lo", "deB_y_hi", "deB_y_lo")));
108+
100109
SM9KeyExchange a = new SM9KeyExchange(deA, identityB, true);
101110
SM9KeyExchange b = new SM9KeyExchange(deB, identityA, false);
102111
ECPoint ra = a.generateEphemeral(new TestRandomBigInteger(256, hex(v, "rA")));
103112
ECPoint rb = b.generateEphemeral(new TestRandomBigInteger(256, hex(v, "rB")));
104113

105-
isTrue(fileName + " RA", Arrays.areEqual(xCoord(ra), hex(v, "RA_x")));
106-
isTrue(fileName + " RB", Arrays.areEqual(xCoord(rb), hex(v, "RB_x")));
114+
isTrue(fileName + " RA", Arrays.areEqual(SM9Curve.g1ToBytes(ra),
115+
Arrays.concatenate(hex(v, "RA_x"), hex(v, "RA_y"))));
116+
isTrue(fileName + " RB", Arrays.areEqual(SM9Curve.g1ToBytes(rb),
117+
Arrays.concatenate(hex(v, "RB_x"), hex(v, "RB_y"))));
107118

108119
byte[] skA = a.calculateKey(klen, rb);
109120
byte[] skB = b.calculateKey(klen, ra);
@@ -177,9 +188,11 @@ private void checkHidValidation()
177188
}
178189
}
179190

180-
private static byte[] xCoord(ECPoint p)
191+
private byte[] g2(Map v, String xHi, String xLo, String yHi, String yLo)
181192
{
182-
return BigIntegers.asUnsignedByteArray(32, p.normalize().getAffineXCoord().toBigInteger());
193+
return Arrays.concatenate(
194+
Arrays.concatenate(new byte[]{0x04}, hex(v, xHi), hex(v, xLo)),
195+
Arrays.concatenate(hex(v, yHi), hex(v, yLo)));
183196
}
184197

185198
public static void main(String[] args)

core/src/test/java/org/bouncycastle/crypto/test/SM9SignerTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@
1313
import org.bouncycastle.crypto.params.SM9SigPrivateKeyParameters;
1414
import org.bouncycastle.crypto.params.SM9SigUserKeyParametersGenerator;
1515
import org.bouncycastle.crypto.signers.SM9Signer;
16+
import org.bouncycastle.math.ec.ECPoint;
17+
import org.bouncycastle.math.ec.sm9.Fp12;
18+
import org.bouncycastle.math.ec.sm9.SM9Curve;
19+
import org.bouncycastle.math.ec.sm9.SM9Pairing;
1620
import org.bouncycastle.test.TestResourceFinder;
1721
import org.bouncycastle.util.Arrays;
22+
import org.bouncycastle.util.BigIntegers;
1823
import org.bouncycastle.util.encoders.Hex;
1924
import org.bouncycastle.util.test.SimpleTest;
2025
import org.bouncycastle.util.test.TestRandomBigInteger;
@@ -83,6 +88,12 @@ public void performTest()
8388
SM9SigUserKeyParametersGenerator kgc = master;
8489
SM9SigPrivateKeyParameters userKey = kgc.generateUserKey(identity);
8590

91+
// the domain parameters, generators, derived keys and pairing values the
92+
// standard prints alongside the signature itself (GM/T 0044.5-2016 Annex A)
93+
checkDomainParameters(v);
94+
checkKeyDerivation(v, master, userKey);
95+
checkPairingValues(v, master);
96+
8697
SM9Signer signer = new SM9Signer();
8798
signer.init(true, new ParametersWithRandom(userKey, new TestRandomBigInteger(256, hex(v, "r"))));
8899
signer.update(msg, 0, msg.length);
@@ -123,6 +134,70 @@ public void performTest()
123134
!emptyWrong.verifySignature(emptySig));
124135
}
125136

137+
/**
138+
* The curve order and the two group generators printed by the standard.
139+
*/
140+
private void checkDomainParameters(Map v)
141+
{
142+
isTrue("SM9 curve order N", Arrays.areEqual(f32(SM9Curve.N), hex(v, "N")));
143+
144+
ECPoint p1 = SM9Curve.P1.normalize();
145+
isTrue("SM9 generator P1.x",
146+
Arrays.areEqual(f32(p1.getAffineXCoord().toBigInteger()), hex(v, "P1x")));
147+
isTrue("SM9 generator P1.y",
148+
Arrays.areEqual(f32(p1.getAffineYCoord().toBigInteger()), hex(v, "P1y")));
149+
150+
// G2 points serialize as 0x04 || x_hi || x_lo || y_hi || y_lo, each F_p2
151+
// coordinate high-dimension (u-coefficient) first
152+
isTrue("SM9 generator P2", Arrays.areEqual(SM9Curve.P2.getEncoded(),
153+
g2(v, "P2x_hi", "P2x_lo", "P2y_hi", "P2y_lo")));
154+
}
155+
156+
/**
157+
* The KGC derivation chain: the signature master public key P_pub-s = [ks]P2
158+
* and the user's signing key ds_A = [t2]P1.
159+
*/
160+
private void checkKeyDerivation(Map v, SM9SigMasterPrivateKeyParameters master,
161+
SM9SigPrivateKeyParameters userKey)
162+
{
163+
isTrue("SM9 master public key Ppub-s", Arrays.areEqual(
164+
master.getPublicKeyParameters().getEncoded(),
165+
g2(v, "Ppubsx_hi", "Ppubsx_lo", "Ppubsy_hi", "Ppubsy_lo")));
166+
167+
ECPoint ds = userKey.getPrivatePoint().normalize();
168+
isTrue("SM9 user signing key dsA.x",
169+
Arrays.areEqual(f32(ds.getAffineXCoord().toBigInteger()), hex(v, "dsAx")));
170+
isTrue("SM9 user signing key dsA.y",
171+
Arrays.areEqual(f32(ds.getAffineYCoord().toBigInteger()), hex(v, "dsAy")));
172+
}
173+
174+
/**
175+
* The R-ate pairing itself, against the two G_T values the standard prints:
176+
* g = e(P1, P_pub-s) and w = g^r. Without these the pairing is only checked
177+
* indirectly, through the signature components.
178+
*/
179+
private void checkPairingValues(Map v, SM9SigMasterPrivateKeyParameters master)
180+
{
181+
Fp12 g = SM9Pairing.pairing(SM9Curve.P1, master.getPublicKeyParameters().getPointG2());
182+
isTrue("SM9 pairing g = e(P1, Ppub-s)",
183+
Arrays.areEqual(SM9Pairing.toBytes(g), hex(v, "g_GT")));
184+
185+
Fp12 w = g.pow(new BigInteger((String)v.get("r"), 16));
186+
isTrue("SM9 pairing w = g^r", Arrays.areEqual(SM9Pairing.toBytes(w), hex(v, "w_GT")));
187+
}
188+
189+
private byte[] g2(Map v, String xHi, String xLo, String yHi, String yLo)
190+
{
191+
return Arrays.concatenate(
192+
Arrays.concatenate(new byte[]{0x04}, hex(v, xHi), hex(v, xLo)),
193+
Arrays.concatenate(hex(v, yHi), hex(v, yLo)));
194+
}
195+
196+
private static byte[] f32(BigInteger v)
197+
{
198+
return BigIntegers.asUnsignedByteArray(32, v);
199+
}
200+
126201
public static void main(String[] args)
127202
{
128203
runTest(new SM9SignerTest());

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ <h3>2.1.3 Additional Features and Functionality</h3>
9494
<li>SM2Engine.decrypt and the GOST28147, DSTU7624, DESede and RC2 key-wrap engines sized their output as new byte[inLen - overhead] without first checking the ciphertext was at least that overhead, so a short attacker-supplied ciphertext or wrapped key threw NegativeArraySizeException / ArrayIndexOutOfBoundsException instead of the declared InvalidCipherTextException. The engines now reject an under-length input with InvalidCipherTextException, matching the guard the IES, RFC 3394 and RFC 5649 engines already have.</li>
9595
<li>Four more length-validation guards across the lightweight crypto API reject wrong-length or truncated input up front rather than leaking an unchecked exception; valid-length input is unaffected. The AIMer (org.bouncycastle.pqc.crypto.aimer) private- and public-key parameter constructors reject keyData whose length is not the parameter set's secret / public key size, with IllegalArgumentException. LMSSignature.getInstance rejects trailing data after an LMS signature, making the parse non-malleable. ISO9796d2Signer guards its verify against an RSA-recovered block shorter than the header it must contain, rather than indexing past it. DANEEntry.isValidCertificate (org.bouncycastle.cert.dane) guards against a short or null DNS record rather than throwing ArrayIndexOutOfBoundsException.</li>
9696
<li>Added support for the SM9 identity-based cryptographic algorithms (GM/T 0044-2016), built on an R-ate pairing over a 256-bit Barreto-Naehrig curve: the digital signature algorithm, key encapsulation mechanism, public-key encryption and the key exchange protocol. SM9 is identity-based - a trusted Key Generation Centre (KGC) holds a master key pair per scheme and derives each user's key pair deterministically from the user's identity, so there are no certificates: a user's key pair comes from the master private key via the org.bouncycastle.jcajce.interfaces SM9SigUserKeyGenerator generateUserKeyPair(id) / SM9EncUserKeyGenerator generateUserKeyPair(id, hid) capability interfaces (implemented by SM9SigMasterPrivateKey and SM9EncMasterPrivateKey - the encryption-side hid is the KGC's published private-key generation function identifier, 0x03 for KEM/encryption and 0x02 for key exchange), and a verifier or sender forms the counterparty's public key from the published master public key and the identity alone via SM9SigMasterPublicKey / SM9EncMasterPublicKey getUserPublicKey(id) - so no AlgorithmParameterSpec is needed, the identity travelling in the keys (the model carries inherent key escrow, as the KGC can derive every user's key). The lightweight implementations follow the standard BouncyCastle package layout: SM9Signer in org.bouncycastle.crypto.signers, SM9KEMGenerator / SM9KEMExtractor in org.bouncycastle.crypto.kems, the SM9Engine public-key cipher in org.bouncycastle.crypto.engines, SM9KeyExchange in org.bouncycastle.crypto.agreement, the master key-pair generators in org.bouncycastle.crypto.generators and the key parameter classes in org.bouncycastle.crypto.params, with the BN curve, extension-field tower and R-ate pairing arithmetic in org.bouncycastle.math.ec.sm9. The BouncyCastle provider exposes the algorithms through the GM family as Signature.SM9, Cipher.SM9 and KeyGenerator.SM9-KEM (and, on JDK 21+, KEM.SM9-KEM through the javax.crypto.KEM API), with master key pairs from KeyPairGenerator.SM9-SIGN / SM9-ENC and both schemes' master keys round-tripping through KeyFactory.SM9. Public-key encryption offers the two GM/T 0044.4 data-encapsulation modes - the SM4 block cipher (Cipher.SM9) and a KDF stream cipher (SM9/XOR/NoPadding) - and emits the self-describing GM/T 0080-2020 SM9Cipher structure; the KEM produces the GM/T 0044.4 KDF output at the requested size as the shared secret (the interoperable form, with an optional KTSParameterSpec KDF layer for generic use). The key exchange is a stateful two-party protocol with roles and key confirmation, so it is provided through the lightweight API only. The SM9 private keys honour the JCA javax.security.auth.Destroyable contract, key material is encoded per GM/T 0080-2020, and all algorithms are verified against the official GM/T 0044.5-2016 worked examples.</li>
97+
<li>The GM/T 0081-2020 SM9 encryption and signature message syntax content types are now available as OID constants on org.bouncycastle.asn1.gm.GMObjectIdentifiers - sm9_pkcs7 (arc 1.2.156.10197.6.1.4.4) and its data, signedData, envelopedData, signedAndEnvelopedData, encryptedData and keyAgreementInfo branches - the SM9 counterpart of the GM/T 0010-2012 sm2_pkcs7 constants already present. Note that, unlike the SM2 arc, the structures these OIDs name are modelled on PKCS#7 but are not interchangeable with it: a GM/T 0081 SignedData keeps the PKCS#7 field order and tags while replacing certificates [0] / crls [1] with ibcSysParamsPublishInfos [0] / irls [1] - so org.bouncycastle.asn1.pkcs.SignedData parses it and silently mislabels those two sets - and its SignerInfo identifies the signer by an identity-based Identifier rather than an IssuerAndSerialNumber and carries an SM9Signature where PKCS#7 has an EncryptedDigest OCTET STRING.</li>
9798
<li>Added support for RFC 9850, the SSLKEYLOGFILE format, so that a capture of a test TLS connection can be decrypted by an analyser such as Wireshark. BouncyCastle reports the secrets and does not store them: an application implements the new org.bouncycastle.tls.keylog.TlsKeyLog interface, whose single log(label, clientRandom, secret) method receives one RFC 9850 sec. 2 record at a time, and decides for itself on encoding, destination and access control (the RFC's labels are available as constants in org.bouncycastle.tls.keylog.TlsKeyLogLabel). The implementation is named by the org.bouncycastle.tls.keylog.class property in the JVM's java.security file, read as a security property rather than a system property because the security properties are an administrator's configuration channel rather than the ambient namespace any library writes to. That property is not itself a privilege boundary - Security.setProperty is only permission-checked under a SecurityManager, disabled by default from JDK 17 and unavailable from JDK 24 - so the boundary that holds is the artifact, not the setting. The named class must be public, implement TlsKeyLog, have a public no-argument constructor and be on the application's class path; its type is checked before it is initialised or constructed, so naming some other class is not a way to have arbitrary code run. It is resolved once, on the first secret of the first handshake, and if the property is unset nothing is loaded and no secret leaves the library. Reported are CLIENT_RANDOM (the master secret) for (D)TLS 1.2 and earlier, on full and resumed handshakes alike, and CLIENT_HANDSHAKE_TRAFFIC_SECRET, SERVER_HANDSHAKE_TRAFFIC_SECRET, CLIENT_TRAFFIC_SECRET_0, SERVER_TRAFFIC_SECRET_0 and EXPORTER_SECRET for TLS 1.3; the early-data labels CLIENT_EARLY_TRAFFIC_SECRET and EARLY_EXPORTER_SECRET are wired to the key schedule but cannot yet be reached, and the ECH labels of RFC 9850 sec. 2.3 do not apply as ECH is not implemented. Because the reporting sits in the key schedule itself, it covers the low-level (D)TLS API and the BCJSSE provider equally. This capability ships only in a new artifact, bctls-klog, which is the TLS API built with key logging present; the standard bctls jar contains none of it and no property will give it any. RFC 9850 sec. 1.1 asks that a deployed binary not be able to disclose its own keys at all, and recommends conditional compilation to that end - a separate artifact is the equivalent for Java, so obtaining connection secrets requires deliberately replacing bctls with bctls-klog, which must not be done in production. bctls-klog keeps the module name and packages of bctls (adding only the exported org.bouncycastle.tls.keylog) so that it drops straight in, and for the same reason it is deliberately absent from the BOM: the two are alternatives, not companions.</li>
9899
<li>KeyPurposeId constants for the three Extended Key Usage KeyPurposeIds defined in RFC 9509 sec. 3 for 5G Network Functions: id_kp_jwt (id-kp 37, signing the JWT Claims Set of a Client Credentials Assertion using JWS), id_kp_httpContentEncrypt (id-kp 38, encrypting JSON objects in HTTP messages between Security Edge Protection Proxies using JWE) and id_kp_oauthAccessTokenSigning (id-kp 39, signing OAuth 2.0 access tokens for service authorization using JWS, as issued by a Network Repository Function). The matching human-readable names are also registered in X509CertificateFormatter so the new EKUs print symbolically.</li>
99100
</ul>

prov/src/test/java/org/bouncycastle/jce/provider/test/RegressionTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public class RegressionTest
102102
new SM4Test(),
103103
new SM9KEMTest(),
104104
new SM9CipherTest(),
105+
new SM9EncodingTest(),
105106
new SM9SignatureTest(),
106107
new ThreefishTest(),
107108
new TLSKDFTest(),

0 commit comments

Comments
 (0)