Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f6d211b
Add Key abstraction class
rmccue Jun 13, 2025
ef42a36
Switch usage to new Key class
rmccue Jun 13, 2025
4098fd9
Tweak some coding standards
rmccue Jun 13, 2025
cd8e5e0
Use new Key class for preparation
rmccue Jun 22, 2025
8b8d00c
Ensure EdDSA implements interface
rmccue Jun 22, 2025
470c03d
Generate keys with correct secret
rmccue Jun 22, 2025
f0bffba
Sort canonical maps using correct keys
rmccue Jun 22, 2025
9e397d0
Add all verification keys to the operation
rmccue Jun 22, 2025
dde9db6
Switch keys to prefixed
rmccue Jun 22, 2025
7807225
Add key rotation UI
rmccue Jun 22, 2025
cb06074
Display key IDs in list
rmccue Jun 22, 2025
f0ac64e
Use exception rather than absolute reference
rmccue Jul 28, 2025
e3678a3
Display diff when resynching
rmccue Jul 28, 2025
75718ba
Use correct prefix for private keys
rmccue Aug 15, 2025
23de43f
Add missing character causing syntax error
rmccue Sep 1, 2025
60ba71a
Add extra handling for legacy keys
rmccue Sep 1, 2025
edddef9
Add warning if key is outdated
rmccue Sep 1, 2025
317a70f
Merge branch 'main' into use-ed25519-for-signing
rmccue Sep 1, 2025
56c9b8c
Use newest key for signing, not oldest
rmccue Sep 1, 2025
8ebc6d8
Indicate current key in UI
rmccue Sep 1, 2025
b395471
Detect legacy-encoded keys when revoking too
rmccue Sep 8, 2025
42d2942
Use proper way to generate legacy encoding
rmccue Sep 8, 2025
a97278a
Ensure verification keys get reindexed
rmccue Sep 8, 2025
392954b
Ensure redirect for new DIDs works
rmccue Sep 12, 2025
9059773
Add ability to export/import DIDs
rmccue Sep 12, 2025
3472c48
Bake a warning into the file itself
rmccue Sep 12, 2025
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
332 changes: 302 additions & 30 deletions inc/admin/namespace.php

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions inc/git-updater/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,21 @@ function get_artifact_metadata( DID $did, $url ) {
* @return array|WP_Error
*/
function generate_artifact_metadata( DID $did, $url ) {
$signing_key = $did->get_verification_keys()[0] ?? null;
if ( ! $signing_key ) {
var_dump( 'No signing key found for DID' );
return;
$keys = $did->get_verification_keys();
if ( empty( $keys ) ) {
return new WP_Error(
'minifair.generate_artifact_metadata.missing_keys',
__( 'No verification keys found for DID', 'minifair' )
);
}

// todo: make active key selectable
$signing_key = end( $keys );
if ( empty( $signing_key ) ) {
return new WP_Error(
'minifair.generate_artifact_metadata.missing_signing_key',
__( 'No signing key found for DID', 'minifair' )
);
}

$artifact_id = sprintf( '%s:%s', $did->id, substr( sha1( $url ), 0, 8 ) );
Expand Down
206 changes: 206 additions & 0 deletions inc/keys/class-eckey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

namespace MiniFAIR\Keys;

use Elliptic\EC;
use Elliptic\EC\KeyPair;
use Elliptic\EC\Signature;
use Elliptic\Utils;
use Exception;
use YOCLIB\Multiformats\Multibase\Multibase;

class ECKey implements Key {
public function __construct(
protected KeyPair $keypair,
protected string $curve
) {
}

/**
* Does this key represent a private key?
*
* @return bool True if the key is a private keypair, false if it is a public key.
*/
public function is_private() : bool {
return $this->keypair->getPrivate() !== null;
}

/**
* Convert a keypair object to a multibase public key string.
*
* @see https://atproto.com/specs/cryptography
*
* @return string The multibase public key string (starts with z).
*/
public function encode_public() : string {
$pub = $this->keypair->getPublic( true, 'hex' );
$prefix = match ( $this->curve ) {
CURVE_K256 => bin2hex( PREFIX_CURVE_K256 ),
CURVE_P256 => bin2hex( PREFIX_CURVE_P256 ),
default => throw new Exception( 'Unsupported curve' ),
};
$encoded = Multibase::encode( Multibase::BASE58BTC, hex2bin( $prefix . $pub ) );
return $encoded;
}

/**
* Convert a keypair object to a multibase private key string.
*
* @see https://atproto.com/specs/cryptography
*
* @return string The multibase private key string (starts with z).
*/
public function encode_private() : string {
if ( ! $this->is_private() ) {
throw new Exception( 'Cannot encode private key for a public key' );
}

$priv = $this->keypair->getPrivate( 'hex' );
$prefix = match ( $this->curve ) {
CURVE_K256 => bin2hex( PREFIX_CURVE_K256_PRIVATE ),
CURVE_P256 => bin2hex( PREFIX_CURVE_P256_PRIVATE ),
default => throw new Exception( 'Unsupported curve' ),
};
$encoded = Multibase::encode( Multibase::BASE58BTC, hex2bin( $prefix . $priv ));
return $encoded;
}

/**
* Convert a key to an incorrectly-encoded key string.
*
* Used only for revocation.
*
* @throws Exception If the curve is not supported.
* @return string The multibase private key string (starts with z).
*/
public function encode_private_legacy_do_not_use_or_you_will_be_fired() : string {
if ( ! $this->is_private() ) {
throw new Exception( 'Cannot encode private key for a public key' );
}

$priv = $this->keypair->getPrivate( 'hex' );
$prefix = match ( $this->curve ) {
CURVE_K256 => bin2hex( PREFIX_CURVE_K256 ),
CURVE_P256 => bin2hex( PREFIX_CURVE_P256 ),
default => throw new Exception( 'Unsupported curve' ),
};
$encoded = Multibase::encode( Multibase::BASE58BTC, hex2bin( $prefix . $priv ));
return $encoded;
}

/**
* Convert a signature to compact (IEEE-P1363) representation.
*
* (Equivalent to secp256k1_ecdsa_sign_compact().)
*
* @internal Elliptic does not support compact signatures, only DER-encoded, so
* we need to do it ourselves. Compact signatures are just the r and
* s bytes concatenated, but must be padded to 32 bytes each.
*
* @param EC $ec The elliptic curve object.
* @param Signature $signature The signature object.
* @return string The compact signature.
*/
protected function signature_to_compact( EC $ec, Signature $signature ) : string {
$byte_length = ceil( $ec->curve->n->bitLength() / 8 );
$compact = Utils::toHex( $signature->r->toArray( 'be', $byte_length ) ) . Utils::toHex( $signature->s->toArray( 'be', $byte_length ) );
return $compact;
}

/**
* Sign data using the private key.
*
* @param string $data The data to sign, as a hex-encoded string.
* @return string The signature encoded as a binary string.
*/
public function sign( string $data ) : string {
if ( ! $this->is_private() ) {
throw new Exception( 'Cannot sign with a public key' );
}

/**
* Hash with SHA-256, then sign, using canonical (low-S) form.
*
* @var \Elliptic\EC\Signature
*/
$signature = $this->keypair->sign( $data, 'hex', [
'canonical' => true
] );

// Convert to compact (IEEE-P1363) form.
// todo: do we need to do this for p256 too?
if ( $this->curve === CURVE_K256 ) {
return $this->signature_to_compact( $this->keypair->ec, $signature );
}
return $signature->toDER( 'hex' );
}

/**
* Generate a new keypair.
*
* We use NIST K-256 as the default to match ATProto.
*
* @see https://atproto.com/specs/cryptography
*
* @throws Exception If the curve is not supported.
* @return static The generated keypair object.
*/
public static function generate( string $curve ) : static {
$ec = new EC( $curve );
return new static( $ec->genKeyPair(), $curve );
}

/**
* Convert a multibase public key string to a keypair object.
*
* @see https://atproto.com/specs/cryptography
*
* @throws Exception If the curve is not supported.
* @param string $key The multibase public key string (starts with z).
* @return static The key object.
*/
public static function from_public( string $key ) : static {
$decoded = Multibase::decode( $key );

$curve = match ( substr( $decoded, 0, 2 ) ) {
PREFIX_CURVE_P256 => CURVE_P256,
PREFIX_CURVE_K256 => CURVE_K256,
default => throw new Exception( 'Unsupported curve' ),
};

$ec = new EC( $curve );

$stripped = bin2hex( substr( $decoded, 2 ) );
$keypair = $ec->keyFromPublic( $stripped, 'hex' );
return new static( $keypair, $curve );
}

/**
* Convert a multibase private key string to a keypair object.
*
* @see https://atproto.com/specs/cryptography
*
* @throws Exception If the curve is not supported.
* @param string $key The multibase public key string (starts with z).
* @return static The key object.
*/
public static function from_private( string $key ) : static {
$decoded = Multibase::decode( $key );

$curve = match ( substr( $decoded, 0, 2 ) ) {
PREFIX_CURVE_P256_PRIVATE => CURVE_P256,
PREFIX_CURVE_K256_PRIVATE => CURVE_K256,

// todo: Legacy, remove this later.
PREFIX_CURVE_P256 => CURVE_P256,
PREFIX_CURVE_K256 => CURVE_K256,
default => throw new Exception( 'Unsupported curve' ),
};

$ec = new EC( $curve );

$stripped = bin2hex( substr( $decoded, 2 ) );
$keypair = $ec->keyFromPrivate( $stripped, 'hex' );
return new static( $keypair, $curve );
}
}
Loading