Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion lib/src/dpop/dpop_signer_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,14 @@ library;
import 'package:solid_auth/src/dpop/dpop_signer_common.dart';

/// The base64url (unpadded) RS256 signature of [signingInput].
Future<String> 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<String> signRs256(
String signingInput,
String privateKeyPem,
String publicExponent,
) async =>
signRs256Sync(signingInput, privateKeyPem);
22 changes: 18 additions & 4 deletions lib/src/dpop/dpop_signer_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ String _jwkInt(BigInt value) {
final JSObject _algorithm =
{'name': 'RSASSA-PKCS1-v1_5', 'hash': 'SHA-256'}.jsify()! as JSObject;

Future<web.CryptoKey> _importKey(String privateKeyPem) {
Future<web.CryptoKey> _importKey(String privateKeyPem, String publicExponent) {
final key = parsedPrivateKey(privateKeyPem).key;
final p = key.p!;
final q = key.q!;
Expand All @@ -65,7 +65,16 @@ Future<web.CryptoKey> _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),
Expand All @@ -83,8 +92,13 @@ Future<web.CryptoKey> _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<String> signRs256(String signingInput, String privateKeyPem) async {
final key = await (_keys[privateKeyPem] ??= _importKey(privateKeyPem));
Future<String> 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;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/dpop/dpop_token_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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, `<header>.<payload>` each
Expand Down