From 250a38a91fb345fb7c7d75055e9e7db105f34d57 Mon Sep 17 00:00:00 2001 From: Miduo666 Date: Fri, 9 Oct 2026 17:29:46 +1100 Subject: [PATCH 1/2] Fix web login DataError: use real RSA public exponent (65537) in DPoP JWK --- lib/src/dpop/dpop_signer_web.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/dpop/dpop_signer_web.dart b/lib/src/dpop/dpop_signer_web.dart index ccd2a5c..af721cc 100644 --- a/lib/src/dpop/dpop_signer_web.dart +++ b/lib/src/dpop/dpop_signer_web.dart @@ -65,7 +65,17 @@ Future _importKey(String privateKeyPem) { { 'kty': 'RSA', 'n': _jwkInt(key.modulus!), - 'e': _jwkInt(key.publicExponent!), + // 20261009 Miduo666 fast_rsa always uses the public exponent + // 65537, but [parsedPrivateKey] parses the PEM with + // dart_jsonwebtoken, which drops the real e. pointycastle then + // recovers key.publicExponent as d^-1 mod phi(n) — a valid but + // ~2048-bit exponent, not 65537. BoringSSL caps RSA public + // exponents at ~33 bits, so Chrome's importKey rejects such a JWK + // with DataError and web login fails on ~2/3 of freshly generated + // keys. Pin e to the real 65537 (AQAB) rather than the recovered + // value. + + 'e': _jwkInt(BigInt.from(65537)), 'd': _jwkInt(d), 'p': _jwkInt(p), 'q': _jwkInt(q), From 2ef59d76ba57f05cbe71e6bbf9b75c467f3a632a Mon Sep 17 00:00:00 2001 From: Miduo666 Date: Fri, 9 Oct 2026 17:42:54 +1100 Subject: [PATCH 2/2] Fix web login DataError: use real RSA public exponent (65537) in DPoP JWK --- lib/src/dpop/dpop_signer_native.dart | 11 ++++++++- lib/src/dpop/dpop_signer_web.dart | 32 +++++++++++++++----------- lib/src/dpop/dpop_token_generator.dart | 2 +- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/lib/src/dpop/dpop_signer_native.dart b/lib/src/dpop/dpop_signer_native.dart index 5c38b97..975e404 100644 --- a/lib/src/dpop/dpop_signer_native.dart +++ b/lib/src/dpop/dpop_signer_native.dart @@ -31,5 +31,14 @@ library; import 'package:solid_auth/src/dpop/dpop_signer_common.dart'; /// The base64url (unpadded) RS256 signature of [signingInput]. -Future signRs256(String signingInput, String privateKeyPem) async => +/// +/// [publicExponent] is only needed by the web signer (it builds a JWK for +/// Web Crypto); native signing is `m^d mod n` and never reads it, but the +/// parameter is kept so both signers share one signature behind the +/// conditional export in `dpop_signer.dart`. +Future signRs256( + String signingInput, + String privateKeyPem, + String publicExponent, +) async => signRs256Sync(signingInput, privateKeyPem); diff --git a/lib/src/dpop/dpop_signer_web.dart b/lib/src/dpop/dpop_signer_web.dart index af721cc..7166d43 100644 --- a/lib/src/dpop/dpop_signer_web.dart +++ b/lib/src/dpop/dpop_signer_web.dart @@ -56,7 +56,7 @@ String _jwkInt(BigInt value) { final JSObject _algorithm = {'name': 'RSASSA-PKCS1-v1_5', 'hash': 'SHA-256'}.jsify()! as JSObject; -Future _importKey(String privateKeyPem) { +Future _importKey(String privateKeyPem, String publicExponent) { final key = parsedPrivateKey(privateKeyPem).key; final p = key.p!; final q = key.q!; @@ -65,17 +65,16 @@ Future _importKey(String privateKeyPem) { { 'kty': 'RSA', 'n': _jwkInt(key.modulus!), - // 20261009 Miduo666 fast_rsa always uses the public exponent - // 65537, but [parsedPrivateKey] parses the PEM with - // dart_jsonwebtoken, which drops the real e. pointycastle then - // recovers key.publicExponent as d^-1 mod phi(n) — a valid but - // ~2048-bit exponent, not 65537. BoringSSL caps RSA public - // exponents at ~33 bits, so Chrome's importKey rejects such a JWK - // with DataError and web login fails on ~2/3 of freshly generated - // keys. Pin e to the real 65537 (AQAB) rather than the recovered - // value. - - 'e': _jwkInt(BigInt.from(65537)), + // 20261009 Miduo666 [parsedPrivateKey] parses the PEM with + // dart_jsonwebtoken, which drops the real public exponent, so + // pointycastle's key.publicExponent is a reconstructed d^-1 mod + // phi(n) — a valid but ~2048-bit value. BoringSSL caps RSA public + // exponents at ~33 bits, so Chrome's importKey rejected that JWK + // with DataError and web login failed on most freshly generated + // keys. Use [publicExponent], the real e from the key manager's + // public JWK (already base64url), which is also the e embedded in + // the DPoP proof header, so the two can never diverge. + 'e': publicExponent, 'd': _jwkInt(d), 'p': _jwkInt(p), 'q': _jwkInt(q), @@ -93,8 +92,13 @@ Future _importKey(String privateKeyPem) { // unused-code check doesn't follow (it only sees the native branch). // ignore: unused-code /// The base64url (unpadded) RS256 signature of [signingInput]. -Future signRs256(String signingInput, String privateKeyPem) async { - final key = await (_keys[privateKeyPem] ??= _importKey(privateKeyPem)); +Future signRs256( + String signingInput, + String privateKeyPem, + String publicExponent, +) async { + final key = + await (_keys[privateKeyPem] ??= _importKey(privateKeyPem, publicExponent)); final signature = await web.window.crypto.subtle .sign(_algorithm, key, Uint8List.fromList(utf8.encode(signingInput)).toJS) .toDart; diff --git a/lib/src/dpop/dpop_token_generator.dart b/lib/src/dpop/dpop_token_generator.dart index 791f679..1ea5367 100644 --- a/lib/src/dpop/dpop_token_generator.dart +++ b/lib/src/dpop/dpop_token_generator.dart @@ -177,7 +177,7 @@ abstract class DpopTokenGenerator { httpMethod: httpMethod, accessToken: accessToken, ); - return '$input.${await signRs256(input, keyPair.privateKey)}'; + return '$input.${await signRs256(input, keyPair.privateKey, publicKeyJwk['e'] as String)}'; } /// The JWS signing input of a DPoP proof, `
.` each