pointbreak 0.6.0

Durable terminal code review for changes humans and coding agents collaborate on together
Documentation
use serde::{Deserialize, Serialize};

use crate::error::{Result, ShoreError};

const DID_KEY_PREFIX: &str = "did:key:";
const BASE58_BTC_PREFIX: char = 'z';
const ED25519_MULTICODEC_PREFIX: [u8; 2] = [0xed, 0x01];
const ED25519_PUBLIC_KEY_LEN: usize = 32;
const ED25519_DID_KEY_BYTES_LEN: usize = ED25519_MULTICODEC_PREFIX.len() + ED25519_PUBLIC_KEY_LEN;

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SignerId(String);

impl SignerId {
    pub fn parse(value: impl Into<String>) -> Result<Self> {
        let value = value.into();
        decode_ed25519_public_key(&value)?;
        Ok(Self(value))
    }

    pub fn from_ed25519_public_key(bytes: [u8; ED25519_PUBLIC_KEY_LEN]) -> Self {
        let mut payload = Vec::with_capacity(ED25519_DID_KEY_BYTES_LEN);
        payload.extend_from_slice(&ED25519_MULTICODEC_PREFIX);
        payload.extend_from_slice(&bytes);
        Self(format!(
            "{DID_KEY_PREFIX}{BASE58_BTC_PREFIX}{}",
            bs58::encode(payload).into_string()
        ))
    }

    pub fn ed25519_public_key(&self) -> Result<[u8; ED25519_PUBLIC_KEY_LEN]> {
        decode_ed25519_public_key(&self.0)
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

fn decode_ed25519_public_key(value: &str) -> Result<[u8; ED25519_PUBLIC_KEY_LEN]> {
    let encoded = value
        .strip_prefix(DID_KEY_PREFIX)
        .ok_or_else(|| invalid_did_key("unsupported DID method"))?;
    let multibase = encoded
        .strip_prefix(BASE58_BTC_PREFIX)
        .ok_or_else(|| invalid_did_key("missing base58btc multibase prefix"))?;
    if multibase.is_empty() {
        return Err(invalid_did_key("empty base58btc payload"));
    }

    let decoded = bs58::decode(multibase)
        .into_vec()
        .map_err(|_| invalid_did_key("malformed base58btc payload"))?;
    if decoded.len() != ED25519_DID_KEY_BYTES_LEN {
        return Err(invalid_did_key("unexpected Ed25519 did:key byte length"));
    }
    if !decoded.starts_with(&ED25519_MULTICODEC_PREFIX) {
        return Err(invalid_did_key("unsupported did:key multicodec"));
    }

    let key = decoded[ED25519_MULTICODEC_PREFIX.len()..]
        .try_into()
        .expect("length checked above");
    Ok(key)
}

fn invalid_did_key(message: impl Into<String>) -> ShoreError {
    ShoreError::WorkflowInputInvalid {
        reason: format!("invalid Ed25519 did:key: {}", message.into()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn did_key_ed25519_round_trips_public_key_bytes() {
        let public_key = [1u8; 32];
        let signer = SignerId::from_ed25519_public_key(public_key);

        assert!(signer.as_str().starts_with("did:key:z6Mk"));
        assert_eq!(signer.ed25519_public_key().unwrap(), public_key);
    }

    #[test]
    fn did_key_rejects_unsupported_or_malformed_values() {
        let unsupported_multicodec = format!(
            "did:key:z{}",
            bs58::encode([[0x00, 0x01].as_slice(), &[1u8; 32]].concat()).into_string()
        );
        let wrong_key_length = format!(
            "did:key:z{}",
            bs58::encode([[0xed, 0x01].as_slice(), &[1u8; 31]].concat()).into_string()
        );

        for bad in [
            "did:web:example.com".to_owned(),
            "did:key:".to_owned(),
            "did:key:not-multibase".to_owned(),
            "did:key:zbad".to_owned(),
            unsupported_multicodec,
            wrong_key_length,
        ] {
            assert!(SignerId::parse(&bad).is_err(), "{bad:?} must reject");
        }
    }

    #[test]
    fn did_key_fixture_decodes_to_expected_public_key_bytes() {
        let signer = SignerId::parse("did:key:z6MkehRgf7yJbgaGfYsdoAsKdBPE3dj2CYhowQdcjqSJgvVd")
            .expect("fixture did:key parses");

        assert_eq!(
            signer.ed25519_public_key().unwrap(),
            [
                0x03, 0xa1, 0x07, 0xbf, 0xf3, 0xce, 0x10, 0xbe, 0x1d, 0x70, 0xdd, 0x18, 0xe7, 0x4b,
                0xc0, 0x99, 0x67, 0xe4, 0xd6, 0x30, 0x9b, 0xa5, 0x0d, 0x5f, 0x1d, 0xdc, 0x86, 0x64,
                0x12, 0x55, 0x31, 0xb8,
            ]
        );
    }
}