@@ -426,6 +426,146 @@ impl BitGoPsbt {
426426 . map_err ( |e| e. to_string ( ) )
427427 }
428428
429+ /// Sign a single input with a raw private key
430+ ///
431+ /// This method signs a specific input using the provided private key. It automatically
432+ /// detects the input type and uses the appropriate signing method:
433+ /// - Replay protection inputs (P2SH-P2PK): Signs with legacy P2SH sighash
434+ /// - Regular inputs: Uses standard PSBT signing
435+ /// - MuSig2 inputs: Returns error (requires FirstRound state, use sign_with_first_round)
436+ ///
437+ /// # Arguments
438+ /// - `input_index`: The index of the input to sign
439+ /// - `privkey`: The private key to sign with
440+ ///
441+ /// # Returns
442+ /// - `Ok(())` if signing was successful
443+ /// - `Err(String)` if signing fails or input type is not supported
444+ pub fn sign_with_privkey (
445+ & mut self ,
446+ input_index : usize ,
447+ privkey : & secp256k1:: SecretKey ,
448+ ) -> Result < ( ) , String > {
449+ use miniscript:: bitcoin:: {
450+ ecdsa:: Signature as EcdsaSignature , hashes:: Hash , sighash:: SighashCache , PublicKey ,
451+ } ;
452+
453+ // Get network before mutable borrow
454+ let network = self . network ( ) ;
455+ let is_testnet = network. is_testnet ( ) ;
456+
457+ let psbt = self . psbt_mut ( ) ;
458+
459+ // Check bounds
460+ if input_index >= psbt. inputs . len ( ) {
461+ return Err ( format ! (
462+ "Input index {} out of bounds (total inputs: {})" ,
463+ input_index,
464+ psbt. inputs. len( )
465+ ) ) ;
466+ }
467+
468+ // Check if this is a MuSig2 input
469+ if p2tr_musig2_input:: Musig2Input :: is_musig2_input ( & psbt. inputs [ input_index] ) {
470+ return Err (
471+ "MuSig2 inputs cannot be signed with raw privkey. Use sign_with_first_round instead."
472+ . to_string ( ) ,
473+ ) ;
474+ }
475+
476+ let secp = secp256k1:: Secp256k1 :: new ( ) ;
477+
478+ // Derive public key from private key
479+ let public_key = PublicKey :: from_slice (
480+ & secp256k1:: PublicKey :: from_secret_key ( & secp, privkey) . serialize ( ) ,
481+ )
482+ . map_err ( |e| format ! ( "Failed to derive public key: {}" , e) ) ?;
483+
484+ // Check if this is a replay protection input (P2SH-P2PK)
485+ if let Some ( redeem_script) = & psbt. inputs [ input_index] . redeem_script . clone ( ) {
486+ // Try to extract pubkey from redeem script
487+ if let Ok ( redeem_pubkey) = Self :: extract_pubkey_from_p2pk_redeem_script ( redeem_script) {
488+ // This is a replay protection input - verify the derived pubkey matches
489+ if public_key != redeem_pubkey {
490+ return Err (
491+ "Public key mismatch: derived pubkey does not match redeem_script pubkey"
492+ . to_string ( ) ,
493+ ) ;
494+ }
495+
496+ // Sign the replay protection input with legacy P2SH sighash
497+ let sighash_type = miniscript:: bitcoin:: sighash:: EcdsaSighashType :: All ;
498+ let cache = SighashCache :: new ( & psbt. unsigned_tx ) ;
499+ let sighash = cache
500+ . legacy_signature_hash ( input_index, redeem_script, sighash_type. to_u32 ( ) )
501+ . map_err ( |e| format ! ( "Failed to compute sighash: {}" , e) ) ?;
502+
503+ // Create ECDSA signature
504+ let message = secp256k1:: Message :: from_digest ( sighash. to_byte_array ( ) ) ;
505+ let signature = secp. sign_ecdsa ( & message, privkey) ;
506+ let ecdsa_sig = EcdsaSignature {
507+ signature,
508+ sighash_type,
509+ } ;
510+
511+ // Add signature to partial_sigs
512+ psbt. inputs [ input_index]
513+ . partial_sigs
514+ . insert ( public_key, ecdsa_sig) ;
515+
516+ return Ok ( ( ) ) ;
517+ }
518+ }
519+
520+ // For regular inputs (non-RP, non-MuSig2), use standard signing via miniscript
521+ // This will handle legacy, SegWit, and Taproot script path inputs
522+ match self {
523+ BitGoPsbt :: BitcoinLike ( ref mut psbt, _network) => {
524+ // Create a key provider that returns our single key
525+ // Convert SecretKey to PrivateKey for the GetKey trait
526+ // Note: The network parameter is only used for WIF serialization, not for signing
527+ let bitcoin_network = if is_testnet {
528+ miniscript:: bitcoin:: Network :: Testnet
529+ } else {
530+ miniscript:: bitcoin:: Network :: Bitcoin
531+ } ;
532+ let private_key = miniscript:: bitcoin:: PrivateKey :: new ( * privkey, bitcoin_network) ;
533+ let key_map = std:: collections:: BTreeMap :: from_iter ( [ ( public_key, private_key) ] ) ;
534+
535+ // Sign the PSBT
536+ let result = psbt. sign ( & key_map, & secp) ;
537+
538+ // Check if our specific input was signed
539+ match result {
540+ Ok ( signing_keys) => {
541+ if signing_keys. contains_key ( & input_index) {
542+ Ok ( ( ) )
543+ } else {
544+ Err ( format ! (
545+ "Input {} was not signed (no key found or already signed)" ,
546+ input_index
547+ ) )
548+ }
549+ }
550+ Err ( ( partial_success, errors) ) => {
551+ // Check if there's an error for our specific input
552+ if let Some ( error) = errors. get ( & input_index) {
553+ Err ( format ! ( "Failed to sign input {}: {:?}" , input_index, error) )
554+ } else if partial_success. contains_key ( & input_index) {
555+ // Input was signed successfully despite other errors
556+ Ok ( ( ) )
557+ } else {
558+ Err ( format ! ( "Input {} was not signed" , input_index) )
559+ }
560+ }
561+ }
562+ }
563+ BitGoPsbt :: Zcash ( _zcash_psbt, _network) => {
564+ Err ( "Zcash signing not yet implemented" . to_string ( ) )
565+ }
566+ }
567+ }
568+
429569 /// Sign the PSBT with the provided key.
430570 /// Wraps the underlying PSBT's sign method from miniscript::psbt::PsbtExt.
431571 ///
0 commit comments