SpruceID
Search…
Implementing New Signers
To implement a new Signer, the first step is to implement a SignerType. In most cases, SignerType will refer to a public key and Signer<SignerType> will refer to a private key corresponding to the SignerType.
To implement a SignerType you must implement the following trait:
// src/signer/signer
#[async_trait(?Send)]
pub trait SignerType
where
Self: Sized,
{
fn name(&self) -> String;
​
async fn valid_signature(&self, statement: &str, signature: &str) -> Result<(), SignerError>;
​
fn did_id(&self) -> Result<String, SignerError>;
​
fn new(t: &DID) -> Result<Self, SignerError>;
​
fn did(&self) -> DID;
}
The implementation for ed25519 looks like:
// src/signer/ed25519
#[derive(Clone)]
pub enum Ed25519 {
// TODO: Change name?
DIDWebJWK(Option<String>),
}
​
#[async_trait(?Send)]
impl SignerType for Ed25519 {
fn new(t: &SignerDID) -> Result<Self, SignerError> {
match t {
SignerDID::Web(o) => Ok(Ed25519::DIDWebJWK(o.clone())),
_ => Err(SignerError::InvalidSignerOpts {
signer_type: t.to_string(),
reason: "expected ed25519 signer type".to_string(),
}),
}
}
​
fn did(&self) -> SignerDID {
match self {
Ed25519::DIDWebJWK(o) => SignerDID::Web(o.clone()),
}
}
​
fn name(&self) -> String {
match self {
Ed25519::DIDWebJWK(_) => "Ed25519 Web Key".to_string(),
}
}
​
fn did_id(&self) -> Result<String, SignerError> {
match self {
Ed25519::DIDWebJWK(Some(s)) => Ok(s.to_owned()),
_ => Err(SignerError::InvalidId {
signer_type: self.name(),
reason: "no id set or incorrect id type".to_string(),
}),
}
}
​
async fn valid_signature(&self, statement: &str, signature: &str) -> Result<(), SignerError> {
let sig = Signature::from_bytes(&hex::decode(signature).map_err(|e| {
SignerError::InvalidSignature {
signer_type: self.name(),
reason: e.to_string(),
}
})?)
.map_err(|e| SignerError::InvalidSignature {
signer_type: self.name(),
reason: e.to_string(),
})?;
​
let stmt = statement.as_bytes();
let pubkey = self.pubkey().await?;
​
pubkey
.verify(&stmt, &sig)
.map_err(|e| SignerError::InvalidSignature {
signer_type: self.name(),
reason: e.to_string(),
})
}
}
Once this has been implemented, the next step is to add it's did representation to src/signer/signer's DID enum, which as of time of writing looks like:
// src/signer/signer
#[derive(Clone, Deserialize, Serialize)]
pub struct EIP155 {
pub address: String,
pub chain_id: String,
}
​
#[derive(Clone, Deserialize, Serialize)]
pub enum PKH {
#[serde(rename = "eip155")]
EIP155(Option<EIP155>),
}
​
#[derive(Clone, Deserialize, Serialize)]
pub enum DID {
#[serde(rename = "pkh")]
PKH(PKH),
// NOTE: Currently only supports Ed25519 keys for signing
// Could change did::web to an enum if desired.
#[serde(rename = "web")]
Web(Option<String>),
}
Additional slots can be added at any level of the enum safely. Once the DID representation is complete, to use the new SignerType in witness flows, you will need to add it to src/witness/signer_type, both in the SignerTypes enum:
// src/witness/signer_type
pub enum SignerTypes {
Ed25519(Ed25519),
Ethereum(Ethereum),
}
In the impl SignerType for SignerTypes, and the statement_id function for SignerTypes. The statement_id function is used for putting the identifier in public claims, and often the did_id is not desired, so it usually parses the did_id into something simpler. This should be made part of SignerType trait, and may be moved there in the future.
At that point a new SignerType is implemented, and implementing a Signer is going to be a bit easier. The Signer for ed25519 is implemented like so:
// src/signer/ed25519
pub struct Ed25519DidWebJwk {
pub id: String,
pub key: JWK,
pub key_name: String,
signer_type: Ed25519,
}
​
// ...
​
#[async_trait(?Send)]
impl Signer<Ed25519> for Ed25519DidWebJwk {
async fn sign(&self, plain_text: &str) -> Result<String, SignerError> {
match &self.key.params {
Params::OKP(o) => match &o.private_key {
Some(key) => {
let keypair = Keypair {
secret: SecretKey::from_bytes(&key.0).map_err(|e| {
SignerError::Sign(format!(
"could not generate secret key: {}",
e.to_string()
))
})?,
public: PublicKey::from_bytes(&o.public_key.0).map_err(|e| {
SignerError::Sign(format!(
"could not generate public key: {}",
e.to_string()
))
})?,
};
​
let sig = keypair.sign(&plain_text.as_bytes());
​
Ok(hex::encode(sig.to_bytes()))
}
_ => Err(SignerError::Sign(
"could not recover private key from jwk".to_string(),
)),
},
_ => Err(SignerError::Sign(
"could not recover private key from jwk".to_string(),
)),
}
}
​
async fn sign_vc(&self, vc: &mut Credential) -> Result<(), SignerError> {
vc.proof = self.proof(vc).await?;
Ok(())
}
​
async fn generate_jwt(&self, vc: &Credential) -> Result<String, SignerError> {
Ok(vc
.generate_jwt(
Some(&self.key),
&LinkedDataProofOptions {
checks: None,
created: None,
eip712_domain: None,
type_: None,
verification_method: Some(URI::String(format!(
"{}#{}",
self.signer_type.did_id()?,
self.key_name
))),
..Default::default()
},
&DIDWeb,
)
.await?)
}
​
async fn proof(&self, vc: &Credential) -> Result<Option<OneOrMany<Proof>>, SignerError> {
let lpdo = match self.signer_type {
Ed25519::DIDWebJWK(_) => LinkedDataProofOptions {
verification_method: Some(URI::String(format!(
"{}#{}",
self.signer_type.did_id()?,
self.key_name
))),
..Default::default()
},
};
​
Ok(Some(OneOrMany::One(
vc.generate_proof(&self.key, &lpdo, &DIDWeb).await?,
)))
}
​
fn id(&self) -> String {
self.id.clone()
}
​
fn signer_type(&self) -> Ed25519 {
self.signer_type.clone()
}
}
The SignerType for a given Signer is often going to be concrete at the impl Signer<...> level. The key here is to be able to provide a proof entry for the VC and to be able to sign bytes and sign_vc for VCs. If a Signer implements sign, it can be used to sign claims as a client, if it implements sign_vc, it can be used to author VCs as a witness.
It is not necessary to implement Signer if the expectation is that a particular SignerType will only be used by the client. As of writing, ethereum only implements SignerType and cannot be used to issue VCs, only to sign claims that a witness can validate.
Copy link