lit_sdk/
signature.rs

1//! Methods for combining signatures from LIT
2
3use crate::{SdkError, SdkResult};
4use ecdsa::{
5    EncodedPoint, RecoveryId,
6    hazmat::{DigestPrimitive, VerifyPrimitive},
7    signature::hazmat::PrehashVerifier,
8};
9use elliptic_curve_tools::{group, prime_field};
10use lit_node_core::{
11    CompressedBytes, CompressedHex, CurveType, EcdsaSignedMessageShare, KeyFormatPreference,
12    PeerId, SignableOutput, SigningAlgorithm, SigningScheme,
13    hd_keys_curves_wasm::{HDDerivable, HDDeriver},
14    lit_rust_crypto::{
15        blsful::{self, Bls12381G2Impl, PublicKey, Signature},
16        decaf377, ed448_goldilocks,
17        elliptic_curve::{
18            self, Curve, CurveArithmetic, Field, FieldBytesSize, PrimeCurve, ScalarPrimitive,
19            generic_array::ArrayLength,
20            ops::Reduce,
21            pkcs8::AssociatedOid,
22            point::{AffineCoordinates, DecompressPoint, PointCompression},
23            sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint},
24        },
25        group::GroupEncoding,
26        jubjub, k256, p256, p384, pallas, vsss_rs,
27    },
28};
29
30use lit_node_core::ethers::utils::keccak256;
31use serde::{Deserialize, Serialize, de::DeserializeOwned};
32use std::ops::Add;
33
34/// An ecdsa signature share
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct EcdsaSignatureShare<C>
37where
38    C: PrimeCurve + CurveArithmetic + DigestPrimitive,
39    C::ProjectivePoint: GroupEncoding,
40    C::Scalar: HDDeriver,
41    <FieldBytesSize<C> as Add>::Output: ArrayLength<u8>,
42{
43    /// The signature `r` component
44    #[serde(with = "group")]
45    pub r: C::ProjectivePoint,
46    /// The signature `s` component
47    #[serde(with = "prime_field")]
48    pub s: C::Scalar,
49}
50
51impl<C> EcdsaSignatureShare<C>
52where
53    C: PrimeCurve + CurveArithmetic + DigestPrimitive,
54    C::ProjectivePoint: GroupEncoding,
55    C::Scalar: HDDeriver,
56    <FieldBytesSize<C> as Add>::Output: ArrayLength<u8>,
57{
58    /// Combine the signature shares into a signature
59    /// Verify should be called after wards to check everything
60    pub fn combine_into_signature(
61        shares: &[EcdsaSignatureShare<C>],
62    ) -> SdkResult<EcdsaFullSignature<C>> {
63        // Ensure non-empty shares
64        if shares.is_empty() {
65            return Err(SdkError::SignatureCombine(
66                "No shares were supplied".to_string(),
67            ));
68        }
69        // Check that all signature shares have the same r
70        if shares[1..].iter().any(|s| s.r != shares[0].r) {
71            return Err(SdkError::SignatureCombine(
72                "Incompatible signature shares".to_string(),
73            ));
74        }
75        let sig_s = shares.iter().fold(C::Scalar::ZERO, |acc, s| acc + s.s);
76
77        Ok(EcdsaFullSignature {
78            r: shares[0].r,
79            s: sig_s,
80        })
81    }
82}
83
84/// A full ecdsa signature
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct EcdsaFullSignature<C>
87where
88    C: PrimeCurve + CurveArithmetic + DigestPrimitive,
89    C::ProjectivePoint: GroupEncoding,
90    C::Scalar: HDDeriver,
91    <FieldBytesSize<C> as Add>::Output: ArrayLength<u8>,
92{
93    /// The signature `r` component
94    #[serde(with = "group")]
95    pub r: C::ProjectivePoint,
96    /// The signature `s` component
97    #[serde(with = "prime_field")]
98    pub s: C::Scalar,
99}
100
101impl<C> TryFrom<EcdsaFullSignature<C>> for ecdsa::Signature<C>
102where
103    C: PrimeCurve + CurveArithmetic + DigestPrimitive,
104    C::ProjectivePoint: GroupEncoding,
105    C::Scalar: HDDeriver,
106    <FieldBytesSize<C> as Add>::Output: ArrayLength<u8>,
107{
108    type Error = SdkError;
109
110    fn try_from(value: EcdsaFullSignature<C>) -> SdkResult<Self> {
111        let r = x_coordinate::<C>(&value.r);
112        let r = <C::Scalar as Into<ScalarPrimitive<C>>>::into(r);
113        let s = <C::Scalar as Into<ScalarPrimitive<C>>>::into(value.s);
114        // from_scalars checks that both r and s are not zero
115        let signature = ecdsa::Signature::<C>::from_scalars(r.to_bytes(), s.to_bytes())?;
116        match signature.normalize_s() {
117            Some(normalized) => Ok(normalized),
118            None => Ok(signature),
119        }
120    }
121}
122
123/// The resulting combined signature output
124#[derive(Clone, Debug, Serialize, Deserialize)]
125pub struct SignedDataOutput {
126    /// The serialized signature
127    pub signature: String,
128    /// The public key
129    pub verifying_key: String,
130    /// The signed data
131    pub signed_data: String,
132    /// The recovery id if ECDSA
133    pub recovery_id: Option<u8>,
134}
135
136/// Attempts to combine the signature shares.
137/// If the resulting combined signature is valid, returns the combined signature.
138/// Otherwise, returns an error.
139///
140/// It does not distinguish between different types of signatures (e.g., ECDSA, BLS, etc.)
141/// and will return the first valid signature it finds in the following order
142///
143/// 1. Frost
144/// 2. BLS
145/// 3. ECDSA
146pub fn combine_and_verify_signature_shares(
147    signature_shares: &[SignableOutput],
148) -> SdkResult<SignedDataOutput> {
149    let mut bls_signing_package = Vec::with_capacity(signature_shares.len());
150    let mut frost_signing_package = Vec::with_capacity(signature_shares.len());
151    let mut ecdsa_signing_package =
152        Vec::<EcdsaSignedMessageShare>::with_capacity(signature_shares.len());
153
154    for signature_share in signature_shares {
155        match signature_share {
156            SignableOutput::EcdsaSignedMessageShare(ecdsa_msg_share) => {
157                if ecdsa_msg_share.result == "success" {
158                    ecdsa_signing_package.push(ecdsa_msg_share.clone());
159                }
160            }
161            SignableOutput::BlsSignedMessageShare(bls_msg_share) => {
162                if bls_msg_share.result == "success" {
163                    let identifier: blsful::inner_types::Scalar =
164                        serde_json::from_str(&bls_msg_share.share_id)?;
165                    let signature_share: blsful::SignatureShare<Bls12381G2Impl> =
166                        serde_json::from_str(&bls_msg_share.signature_share)?;
167                    let verifying_share: blsful::PublicKeyShare<Bls12381G2Impl> =
168                        serde_json::from_str(&bls_msg_share.verifying_share)?;
169                    let public_key: PublicKey<Bls12381G2Impl> =
170                        serde_json::from_str(&bls_msg_share.public_key)?;
171                    let message = hex::decode(&bls_msg_share.message)?;
172                    bls_signing_package.push((
173                        identifier,
174                        signature_share,
175                        verifying_share,
176                        public_key,
177                        message,
178                        bls_msg_share.peer_id.clone(),
179                    ));
180                }
181            }
182            SignableOutput::FrostSignedMessageShare(frost_msg_share) => {
183                if frost_msg_share.result == "success" {
184                    let identifier: lit_frost::Identifier =
185                        serde_json::from_str(&frost_msg_share.share_id)?;
186                    let signature_share: lit_frost::SignatureShare =
187                        serde_json::from_str(&frost_msg_share.signature_share)?;
188                    let verifying_share: lit_frost::VerifyingShare =
189                        serde_json::from_str(&frost_msg_share.verifying_share)?;
190                    let public_key: lit_frost::VerifyingKey =
191                        serde_json::from_str(&frost_msg_share.public_key)?;
192                    let signing_commitments: lit_frost::SigningCommitments =
193                        serde_json::from_str(&frost_msg_share.signing_commitments)?;
194                    let signing_scheme = frost_msg_share.sig_type.parse::<SigningScheme>()?;
195                    let scheme = signing_scheme_to_frost_scheme(signing_scheme)?;
196                    let message = hex::decode(&frost_msg_share.message)?;
197                    frost_signing_package.push((
198                        identifier,
199                        signature_share,
200                        verifying_share,
201                        public_key,
202                        signing_commitments,
203                        scheme,
204                        message,
205                        frost_msg_share.peer_id.clone(),
206                    ));
207                }
208            }
209        }
210    }
211
212    if frost_signing_package.len() > 1 {
213        let first_entry = &frost_signing_package[0];
214        let mut signature_shares = Vec::with_capacity(frost_signing_package.len());
215        let mut verifying_shares = Vec::with_capacity(frost_signing_package.len());
216        let mut signing_commitments = Vec::with_capacity(frost_signing_package.len());
217
218        signature_shares.push((first_entry.0.clone(), first_entry.1.clone()));
219        verifying_shares.push((first_entry.0.clone(), first_entry.2.clone()));
220        signing_commitments.push((first_entry.0.clone(), first_entry.4.clone()));
221
222        for entry in &frost_signing_package[1..] {
223            debug_assert_eq!(
224                first_entry.3, entry.3,
225                "frost public keys do not match: {}, {}",
226                first_entry.2, entry.2
227            );
228            debug_assert_eq!(
229                first_entry.5, entry.5,
230                "frost signing schemes do not match: {}, {}",
231                first_entry.4, entry.4
232            );
233            debug_assert_eq!(
234                first_entry.6,
235                entry.6,
236                "frost messages do not match: {}, {}",
237                hex::encode(&first_entry.6),
238                hex::encode(&entry.6)
239            );
240            signature_shares.push((entry.0.clone(), entry.1.clone()));
241            verifying_shares.push((entry.0.clone(), entry.2.clone()));
242            signing_commitments.push((entry.0.clone(), entry.4.clone()));
243        }
244        let res = first_entry.5.aggregate(
245            &first_entry.6,
246            &signing_commitments,
247            &signature_shares,
248            &verifying_shares,
249            &first_entry.3,
250        );
251        return if res.is_err() {
252            let e = res.expect_err("frost signature from shares is invalid");
253            match e {
254                lit_frost::Error::Cheaters(cheaters) => {
255                    let mut cheater_peer_ids = Vec::with_capacity(cheaters.len());
256                    for cheater in cheaters {
257                        let found = frost_signing_package
258                            .iter()
259                            .find(|p| p.0 == cheater)
260                            .map(|cheater| cheater.7.clone());
261                        if let Some(peer_id) = found {
262                            cheater_peer_ids.push(peer_id);
263                        }
264                    }
265                    Err(SdkError::SignatureCombine(format!(
266                        "frost signature from shares is invalid. Invalid share peer ids: {}",
267                        cheater_peer_ids.join(", ")
268                    )))
269                }
270                _ => Err(SdkError::SignatureCombine(e.to_string())),
271            }
272        } else {
273            Ok(SignedDataOutput {
274                signature: serde_json::to_string(
275                    &res.expect("frost signature from shares is valid"),
276                )?,
277                verifying_key: serde_json::to_string(&first_entry.3)?,
278                signed_data: hex::encode(&first_entry.6),
279                recovery_id: None,
280            })
281        };
282    }
283    if bls_signing_package.len() > 1 {
284        let first_entry = &bls_signing_package[0];
285        let mut signature_shares = Vec::with_capacity(bls_signing_package.len());
286        let mut verifying_shares = Vec::with_capacity(bls_signing_package.len());
287
288        signature_shares.push(first_entry.1);
289        verifying_shares.push((first_entry.0, first_entry.5.clone(), first_entry.2));
290        for entry in &bls_signing_package[1..] {
291            debug_assert_eq!(
292                first_entry.3, entry.3,
293                "bls public keys do not match: {}, {}",
294                first_entry.2, entry.2
295            );
296            debug_assert_eq!(
297                first_entry.4,
298                entry.4,
299                "bls messages do not match: {}, {}",
300                hex::encode(&first_entry.4),
301                hex::encode(&entry.4)
302            );
303            signature_shares.push(entry.1);
304            verifying_shares.push((entry.0, entry.5.clone(), entry.2));
305        }
306        let public_key = first_entry.3;
307        let signature = Signature::<Bls12381G2Impl>::from_shares(&signature_shares)
308            .expect("bls signature from shares");
309        if signature.verify(&public_key, &first_entry.4).is_err() {
310            // Identify which shares are invalid
311            let mut invalid_shares = Vec::with_capacity(signature_shares.len());
312            for (share, (_identifier, peer_id, verifier)) in
313                signature_shares.iter().zip(verifying_shares.iter())
314            {
315                if share.verify(verifier, &first_entry.4).is_err() {
316                    invalid_shares.push(peer_id.clone());
317                }
318            }
319            return Err(SdkError::SignatureCombine(format!(
320                "bls signature from shares is invalid. Invalid share peer ids: {}",
321                invalid_shares.join(", ")
322            )));
323        }
324        return Ok(SignedDataOutput {
325            signature: serde_json::to_string(&signature)?,
326            verifying_key: public_key.0.to_compressed_hex(),
327            signed_data: hex::encode(&first_entry.4),
328            recovery_id: None,
329        });
330    }
331    if ecdsa_signing_package.len() > 1 {
332        let signing_scheme = ecdsa_signing_package[0].sig_type.parse::<SigningScheme>()?;
333        match signing_scheme {
334            SigningScheme::EcdsaK256Sha256 => {
335                return verify_ecdsa_signing_package::<k256::Secp256k1>(&ecdsa_signing_package);
336            }
337            SigningScheme::EcdsaP256Sha256 => {
338                return verify_ecdsa_signing_package::<p256::NistP256>(&ecdsa_signing_package);
339            }
340            SigningScheme::EcdsaP384Sha384 => {
341                return verify_ecdsa_signing_package::<p384::NistP384>(&ecdsa_signing_package);
342            }
343            _ => {}
344        }
345    }
346
347    Err(SdkError::SignatureCombine(
348        "no valid signature shares found".to_string(),
349    ))
350}
351
352/// Verify ECDSA signature shares
353pub fn verify_ecdsa_signing_package<C>(
354    shares: &[EcdsaSignedMessageShare],
355) -> SdkResult<SignedDataOutput>
356where
357    C: PrimeCurve + CurveArithmetic + DigestPrimitive + AssociatedOid + PointCompression,
358    C::ProjectivePoint: GroupEncoding + HDDerivable,
359    C::AffinePoint: DeserializeOwned
360        + FromEncodedPoint<C>
361        + ToEncodedPoint<C>
362        + VerifyPrimitive<C>
363        + DecompressPoint<C>,
364    C::Scalar: HDDeriver + From<PeerId> + DeserializeOwned,
365    <FieldBytesSize<C> as Add>::Output: ArrayLength<u8>,
366    <C as Curve>::FieldBytesSize: ModulusSize,
367{
368    let mut sig_shares = Vec::<EcdsaSignatureShare<C>>::with_capacity(shares.len());
369    let first_share = &shares[0];
370    sig_shares.push(EcdsaSignatureShare {
371        r: C::ProjectivePoint::from(serde_json::from_str::<C::AffinePoint>(&first_share.big_r)?),
372        s: serde_json::from_str(&first_share.signature_share)?,
373    });
374    for share in &shares[1..] {
375        debug_assert_eq!(first_share.public_key, share.public_key);
376        debug_assert_eq!(first_share.digest, share.digest);
377        debug_assert_eq!(first_share.big_r, share.big_r);
378        debug_assert_eq!(first_share.sig_type, share.sig_type);
379
380        sig_shares.push(EcdsaSignatureShare {
381            r: C::ProjectivePoint::from(serde_json::from_str::<C::AffinePoint>(&share.big_r)?),
382            s: serde_json::from_str(&share.signature_share)?,
383        });
384    }
385    let initial_public_key: String =
386        serde_json::from_str(&first_share.public_key).expect("public key");
387    let public_key = hex::decode(&initial_public_key)?;
388    let public_key = EncodedPoint::<C>::from_bytes(&public_key)
389        .map_err(|_| SdkError::SignatureCombine("invalid public key".to_string()))?;
390    let public_key_affine = Option::from(C::AffinePoint::from_encoded_point(&public_key))
391        .ok_or_else(|| SdkError::SignatureCombine("invalid public key".to_string()))?;
392    let signature =
393        EcdsaSignatureShare::<C>::combine_into_signature(&sig_shares).expect("signature");
394
395    let message = hex::decode(&first_share.digest)?;
396    let vk = ecdsa::VerifyingKey::<C>::from_affine(public_key_affine).expect("verifying key");
397    let signature: ecdsa::Signature<C> = signature.try_into().expect("signature");
398    <ecdsa::VerifyingKey<C> as PrehashVerifier<ecdsa::Signature<C>>>::verify_prehash(
399        &vk, &message, &signature,
400    )?;
401
402    let rid = RecoveryId::trial_recovery_from_prehash(&vk, &message, &signature)?;
403
404    Ok(SignedDataOutput {
405        signature: serde_json::to_string(&signature)?,
406        verifying_key: initial_public_key,
407        signed_data: shares[0].digest.clone(),
408        recovery_id: Some(rid.to_byte()),
409    })
410}
411
412/// Verify a signature returned from lit-node
413pub fn verify_signature(
414    signing_scheme: SigningScheme,
415    package: &SignedDataOutput,
416) -> SdkResult<()> {
417    match signing_scheme {
418        SigningScheme::EcdsaK256Sha256 => verify_ecdsa_signature::<k256::Secp256k1>(package),
419        SigningScheme::EcdsaP256Sha256 => verify_ecdsa_signature::<p256::NistP256>(package),
420        SigningScheme::EcdsaP384Sha384 => verify_ecdsa_signature::<p384::NistP384>(package),
421        SigningScheme::SchnorrEd25519Sha512
422        | SigningScheme::SchnorrRistretto25519Sha512
423        | SigningScheme::SchnorrK256Sha256
424        | SigningScheme::SchnorrP256Sha256
425        | SigningScheme::SchnorrP384Sha384
426        | SigningScheme::SchnorrK256Taproot
427        | SigningScheme::SchnorrEd448Shake256
428        | SigningScheme::SchnorrRedJubjubBlake2b512
429        | SigningScheme::SchnorrRedPallasBlake2b512
430        | SigningScheme::SchnorrRedDecaf377Blake2b512
431        | SigningScheme::SchnorrkelSubstrate => {
432            let scheme = signing_scheme_to_frost_scheme(signing_scheme)?;
433            let signature = serde_json::from_str::<lit_frost::Signature>(&package.signature)?;
434            let public_key =
435                serde_json::from_str::<lit_frost::VerifyingKey>(&package.verifying_key)?;
436            let message = hex::decode(&package.signed_data)?;
437            scheme
438                .verify(&message, &public_key, &signature)
439                .map_err(|_| SdkError::SignatureVerify)
440        }
441        SigningScheme::Bls12381G1ProofOfPossession | SigningScheme::Bls12381 => {
442            let public_key: PublicKey<Bls12381G2Impl> =
443                serde_json::from_str(&format!("\"{}\"", &package.verifying_key))?;
444            let signature: Signature<Bls12381G2Impl> = serde_json::from_str(&package.signature)?;
445            let message = hex::decode(&package.signed_data)?;
446            signature.verify(&public_key, &message)?;
447            Ok(())
448        }
449    }
450}
451
452/// Convert the signing_scheme to a frost scheme
453pub fn signing_scheme_to_frost_scheme(value: SigningScheme) -> SdkResult<lit_frost::Scheme> {
454    match value {
455        SigningScheme::Bls12381 | SigningScheme::Bls12381G1ProofOfPossession => Err(
456            SdkError::Parse("BLS signatures are not supported by FROST".to_string()),
457        ),
458        SigningScheme::EcdsaK256Sha256
459        | SigningScheme::EcdsaP256Sha256
460        | SigningScheme::EcdsaP384Sha384 => Err(SdkError::Parse(
461            "ECDSA signatures are not supported by FROST".to_string(),
462        )),
463        SigningScheme::SchnorrEd25519Sha512 => Ok(lit_frost::Scheme::Ed25519Sha512),
464        SigningScheme::SchnorrK256Sha256 => Ok(lit_frost::Scheme::K256Sha256),
465        SigningScheme::SchnorrP256Sha256 => Ok(lit_frost::Scheme::P256Sha256),
466        SigningScheme::SchnorrP384Sha384 => Ok(lit_frost::Scheme::P384Sha384),
467        SigningScheme::SchnorrRistretto25519Sha512 => Ok(lit_frost::Scheme::Ristretto25519Sha512),
468        SigningScheme::SchnorrEd448Shake256 => Ok(lit_frost::Scheme::Ed448Shake256),
469        SigningScheme::SchnorrRedJubjubBlake2b512 => Ok(lit_frost::Scheme::RedJubjubBlake2b512),
470        SigningScheme::SchnorrRedPallasBlake2b512 => Ok(lit_frost::Scheme::RedPallasBlake2b512),
471        SigningScheme::SchnorrK256Taproot => Ok(lit_frost::Scheme::K256Taproot),
472        SigningScheme::SchnorrRedDecaf377Blake2b512 => Ok(lit_frost::Scheme::RedDecaf377Blake2b512),
473        SigningScheme::SchnorrkelSubstrate => Ok(lit_frost::Scheme::SchnorrkelSubstrate),
474    }
475}
476
477/// Verify an ecdsa signature
478pub fn verify_ecdsa_signature<C>(package: &SignedDataOutput) -> SdkResult<()>
479where
480    C: PrimeCurve + CurveArithmetic + DigestPrimitive + AssociatedOid + PointCompression,
481    C::ProjectivePoint: GroupEncoding + HDDerivable,
482    C::AffinePoint: DeserializeOwned
483        + FromEncodedPoint<C>
484        + ToEncodedPoint<C>
485        + VerifyPrimitive<C>
486        + DecompressPoint<C>,
487    C::Scalar: HDDeriver + From<PeerId> + DeserializeOwned,
488    <FieldBytesSize<C> as Add>::Output: ArrayLength<u8>,
489    <C as Curve>::FieldBytesSize: ModulusSize,
490{
491    let message = hex::decode(&package.signed_data)?;
492    let public_key = hex::decode(&package.verifying_key)?;
493    let public_key = EncodedPoint::<C>::from_bytes(&public_key)
494        .map_err(|_| SdkError::SignatureCombine("invalid public key".to_string()))?;
495    let public_key_affine = Option::from(C::AffinePoint::from_encoded_point(&public_key))
496        .ok_or_else(|| SdkError::SignatureCombine("invalid public key".to_string()))?;
497    let vk = ecdsa::VerifyingKey::<C>::from_affine(public_key_affine).expect("verifying key");
498    let signature = serde_json::from_str::<ecdsa::Signature<C>>(&package.signature)?;
499    <ecdsa::VerifyingKey<C> as PrehashVerifier<ecdsa::Signature<C>>>::verify_prehash(
500        &vk, &message, &signature,
501    )?;
502    Ok(())
503}
504
505pub(crate) fn x_coordinate<C>(point: &C::ProjectivePoint) -> C::Scalar
506where
507    C: PrimeCurve + CurveArithmetic,
508{
509    use elliptic_curve::group::Curve as _;
510
511    let pt = point.to_affine();
512    <C::Scalar as Reduce<<C as Curve>::Uint>>::reduce_bytes(&pt.x())
513}
514
515/// Derive the public key given the signing scheme, tweak key id and root keys
516pub fn get_derived_public_key(
517    signing_scheme: SigningScheme,
518    key_id: &[u8],
519    root_keys: &[String],
520) -> SdkResult<String> {
521    match signing_scheme.curve_type() {
522        CurveType::BLS | CurveType::BLS12381G1 => derive_public_key::<
523            blsful::inner_types::G1Projective,
524        >(signing_scheme, key_id, root_keys),
525        CurveType::K256 => {
526            derive_public_key::<k256::ProjectivePoint>(signing_scheme, key_id, root_keys)
527        }
528        CurveType::P256 => {
529            derive_public_key::<p256::ProjectivePoint>(signing_scheme, key_id, root_keys)
530        }
531        CurveType::P384 => {
532            derive_public_key::<p384::ProjectivePoint>(signing_scheme, key_id, root_keys)
533        }
534        CurveType::Ed25519 => derive_public_key::<vsss_rs::curve25519::WrappedEdwards>(
535            signing_scheme,
536            key_id,
537            root_keys,
538        ),
539        CurveType::Ristretto25519 => derive_public_key::<vsss_rs::curve25519::WrappedRistretto>(
540            signing_scheme,
541            key_id,
542            root_keys,
543        ),
544        CurveType::Ed448 => {
545            derive_public_key::<ed448_goldilocks::EdwardsPoint>(signing_scheme, key_id, root_keys)
546        }
547        CurveType::RedJubjub => {
548            derive_public_key::<jubjub::SubgroupPoint>(signing_scheme, key_id, root_keys)
549        }
550        CurveType::RedPallas => {
551            derive_public_key::<pallas::Point>(signing_scheme, key_id, root_keys)
552        }
553        CurveType::RedDecaf377 => {
554            derive_public_key::<decaf377::Element>(signing_scheme, key_id, root_keys)
555        }
556    }
557}
558
559fn derive_public_key<G>(
560    signing_scheme: SigningScheme,
561    key_id: &[u8],
562    root_keys: &[String],
563) -> SdkResult<String>
564where
565    G: HDDerivable + GroupEncoding + Default + CompressedBytes,
566    G::Scalar: HDDeriver,
567{
568    let deriver = G::Scalar::create(key_id, signing_scheme.id_sign_ctx());
569    let mut keys = Vec::with_capacity(root_keys.len());
570    for rk in root_keys {
571        let rk = G::from_compressed_hex(rk)
572            .ok_or_else(|| SdkError::Parse("Invalid root key".to_string()))?;
573        keys.push(rk);
574    }
575    let pk = deriver.hd_derive_public_key(&keys);
576
577    if signing_scheme.supports_algorithm(SigningAlgorithm::Schnorr) {
578        let pk = lit_frost::VerifyingKey {
579            scheme: signing_scheme_to_frost_scheme(signing_scheme)?,
580            value: pk.to_compressed(),
581        };
582        let pk = serde_json::to_string(&pk)?;
583        Ok(pk)
584    } else {
585        Ok(match signing_scheme.preferred_format() {
586            KeyFormatPreference::Compressed => pk.to_compressed_hex(),
587            KeyFormatPreference::Uncompressed => pk.to_uncompressed_hex(),
588        })
589    }
590}
591
592/// Derive the lit action public key
593pub fn get_lit_action_public_key(
594    signing_scheme: SigningScheme,
595    action_ipfs_id: &str,
596    root_keys: &[String],
597) -> SdkResult<String> {
598    let key_id = keccak256(format!("lit_action_{}", action_ipfs_id));
599    get_derived_public_key(signing_scheme, &key_id, root_keys)
600}