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 ccd2a5c..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,7 +65,16 @@ Future _importKey(String privateKeyPem) { { 'kty': 'RSA', 'n': _jwkInt(key.modulus!), - 'e': _jwkInt(key.publicExponent!), + // 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), @@ -83,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