Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions src/Ed25519Keypair.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { bytesToMultibase, multibaseToBytes, Keypair } from '@atproto/crypto';
import { DID_KEY_PREFIX } from './did-validation.js';
import { ed25519 } from '@noble/curves/ed25519';
import * as uint8arrays from 'uint8arrays';

Expand All @@ -8,10 +9,9 @@ import * as uint8arrays from 'uint8arrays';
export const ED25519_PUBLIC_PREFIX = new Uint8Array([0xed, 0x01]);

/**
* Multibase base58btc prefix for Ed25519 public keys (z6Mk).
* This is the base58btc encoding of ED25519_PUBLIC_PREFIX.
* Length of an Ed25519 public key in bytes.
*/
export const ED25519_PUBLIC_MULTIBASE_PREFIX = 'z6Mk';
const ED25519_PUBLIC_KEY_SIZE = 32;

/**
* Ed25519 verification keypair.
Expand Down Expand Up @@ -56,7 +56,7 @@ export class Ed25519Keypair implements Keypair {
*/
static async fromPublicKeyMultibase(publicKeyMultibase: string): Promise<Ed25519Keypair> {
const decoded = multibaseToBytes(publicKeyMultibase);
const expectedLength = ED25519_PUBLIC_PREFIX.length + 32;
const expectedLength = ED25519_PUBLIC_PREFIX.length + ED25519_PUBLIC_KEY_SIZE;

// Validate minimum length before accessing array indices
if (decoded.length < ED25519_PUBLIC_PREFIX.length) {
Expand Down Expand Up @@ -106,7 +106,7 @@ export class Ed25519Keypair implements Keypair {
* Get the public key as a did:key string.
*/
did(): string {
return `did:key:${this.publicKeyStr()}`;
return `${DID_KEY_PREFIX}${this.publicKeyStr()}`;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/cli/did-rotation-key-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { parseArgs } from 'node:util';
import { readFile } from 'node:fs/promises';
import { getRotationPublicKeyMultibase, parseRotationPublicKeyOnly, RotationKeyInputError } from '../keys.js';
import { validatePlcDid, DidValidationError } from '../did-validation.js';
import { validatePlcDid, DidValidationError, DID_KEY_PREFIX } from '../did-validation.js';
import { checkRotationKey, DidLogFetchError, DidLogValidationError } from '../verify.js';

const { values } = parseArgs({
Expand Down Expand Up @@ -131,16 +131,16 @@ if (result.allKeys.length === 0) {

if (result.valid) {
console.log(`\n✓ Rotation key is valid`);
console.log(`Public key: did:key:${result.publicKeyMultibase}`);
console.log(`Public key: ${DID_KEY_PREFIX}${result.publicKeyMultibase}`);
console.log(`This key can be used to sign PLC operations for ${did}`);
process.exit(0);
} else {
console.log(`\n❌ Rotation key is not valid`);
console.log(`Public key: did:key:${result.publicKeyMultibase}`);
console.log(`Public key: ${DID_KEY_PREFIX}${result.publicKeyMultibase}`);
console.log(`This key is not present in the latest operation of the DID log for ${did}`);
console.log(`\nValid rotation keys for this DID:`);
for (const key of result.allKeys) {
console.log(` did:key:${key}`);
console.log(` ${DID_KEY_PREFIX}${key}`);
}
process.exit(1);
}
46 changes: 32 additions & 14 deletions src/did-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,45 @@ export class DidValidationError extends Error {}
*/
export class PublicKeyValidationError extends Error {}

/**
* Prefix for did:plc URIs.
*/
export const DID_PLC_PREFIX = 'did:plc:';

/**
* Expected length of a valid did:plc: identifier.
* Format: did:plc: (8 chars) + 24 character base32 hash = 32 characters total.
*/
const DID_PLC_LENGTH = 32;
export const DID_PLC_LENGTH = 32;

/**
* Prefix for did:key URIs.
*/
export const DID_KEY_PREFIX = 'did:key:';

/**
* Multibase prefix for Ed25519 public keys (verification keys).
* Format: z6Mk...
*/
export const ED25519_PUBLIC_MULTIBASE_PREFIX = 'z6Mk';

/**
* Multibase prefix for Secp256k1 public keys (rotation keys).
* Format: zQ3sh...
*/
export const SECP256K1_PUBLIC_MULTIBASE_PREFIX = 'zQ3sh';

/**
* Prefix for Ed25519 public keys in did:key format (verification keys).
* Format: did:key:z6Mk...
*/
export const ED25519_DID_KEY_PREFIX = 'did:key:z6Mk';
export const ED25519_DID_KEY_PREFIX = DID_KEY_PREFIX + ED25519_PUBLIC_MULTIBASE_PREFIX;

/**
* Prefix for Secp256k1 public keys in did:key format (rotation keys).
* Format: did:key:zQ3sh...
*/
export const SECP256K1_DID_KEY_PREFIX = 'did:key:zQ3sh';
export const SECP256K1_DID_KEY_PREFIX = DID_KEY_PREFIX + SECP256K1_PUBLIC_MULTIBASE_PREFIX;

/**
* Expected length of a did:key Ed25519 public key.
Expand All @@ -44,15 +61,14 @@ export const ED25519_DID_KEY_LENGTH = 56;
export const SECP256K1_DID_KEY_LENGTH = 57;

/**
* Multibase prefix for Secp256k1 public keys (rotation keys).
* Format: zQ3sh...
* Multicodec prefix for secp256k1 compressed public keys (rotation keys).
*/
export const SECP256K1_PUBLIC_MULTIBASE_PREFIX = 'zQ3sh';
export const SECP256K1_PUBLIC_MULTICODEC_PREFIX = new Uint8Array([0xe7, 0x01]);

/**
* Multicodec prefix for secp256k1 compressed public keys (rotation keys).
* Length of a secp256k1 compressed public key in bytes.
*/
export const SECP256K1_PUBLIC_MULTICODEC_PREFIX = new Uint8Array([0xe7, 0x01]);
export const SECP256K1_COMPRESSED_PUBLIC_KEY_SIZE = 33;

/**
* Multicodec prefix for secp256k1 private keys (rotation keys).
Expand All @@ -71,8 +87,8 @@ export const ED25519_PRIVATE_MULTICODEC_PREFIX = new Uint8Array([0x80, 0x26]);
* @throws {DidValidationError} If the DID doesn't start with 'did:plc:' or has incorrect length
*/
export function validatePlcDid(did: string): void {
if (!did.startsWith('did:plc:')) {
throw new DidValidationError(`Invalid DID format. DID must have the prefix 'did:plc:'.`);
if (!did.startsWith(DID_PLC_PREFIX)) {
throw new DidValidationError(`Invalid DID format. DID must have the prefix '${DID_PLC_PREFIX}'.`);
}
if (did.length !== DID_PLC_LENGTH) {
throw new DidValidationError(`Invalid DID format. DID must be ${DID_PLC_LENGTH} characters in length.`);
Expand All @@ -86,8 +102,10 @@ export function validatePlcDid(did: string): void {
* @throws {PublicKeyValidationError} If the key format is invalid
*/
export function validateVerificationKey(key: string): void {
if (!key.startsWith('did:key:')) {
throw new PublicKeyValidationError(`Invalid verification key format. Key must start with 'did:key:' prefix.`);
if (!key.startsWith(DID_KEY_PREFIX)) {
throw new PublicKeyValidationError(
`Invalid verification key format. Key must start with '${DID_KEY_PREFIX}' prefix.`,
);
}

if (key.startsWith(SECP256K1_DID_KEY_PREFIX)) {
Expand Down Expand Up @@ -116,8 +134,8 @@ export function validateVerificationKey(key: string): void {
* @throws {PublicKeyValidationError} If the key format is invalid
*/
export function validateRotationKey(key: string): void {
if (!key.startsWith('did:key:')) {
throw new PublicKeyValidationError(`Invalid rotation key format. Key must start with 'did:key:' prefix.`);
if (!key.startsWith(DID_KEY_PREFIX)) {
throw new PublicKeyValidationError(`Invalid rotation key format. Key must start with '${DID_KEY_PREFIX}' prefix.`);
}

if (key.startsWith(ED25519_DID_KEY_PREFIX)) {
Expand Down
9 changes: 7 additions & 2 deletions src/domain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { resolveTxt } from 'node:dns/promises';

/**
* Maximum length of a domain name per DNS specification.
*/
const MAX_DOMAIN_LENGTH = 255;

const DOMAIN_REGEX = /^[a-z0-9][a-z0-9-]{0,62}(\.[a-z0-9][a-z0-9-]{0,62})+$/i;
const DID_RECORD_REGEX = /^did="?([^"]+)"?$/;

Expand Down Expand Up @@ -29,8 +34,8 @@ export function validateDomain(domain: string): void {
throw new InvalidDomainError('Domain is required');
}

if (domain.length > 255) {
throw new InvalidDomainError('Domain must not exceed 255 characters');
if (domain.length > MAX_DOMAIN_LENGTH) {
throw new InvalidDomainError(`Domain must not exceed ${MAX_DOMAIN_LENGTH} characters`);
}

if (!DOMAIN_REGEX.test(domain)) {
Expand Down
18 changes: 16 additions & 2 deletions src/keyfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ interface FormatKeyFileContentOptions {
*/
export class SaveKeyError extends Error {}

/**
* Size of the uncompressed public key prefix byte (0x04).
*/
const UNCOMPRESSED_KEY_PREFIX_SIZE = 1;

/**
* Size of a secp256k1 coordinate (X or Y) in bytes.
*/
const SECP256K1_COORDINATE_SIZE = 32;

/**
* Encodes a rotation key (secp256k1) as a PEM string in SEC1 format.
*
Expand All @@ -51,8 +61,12 @@ export class SaveKeyError extends Error {}
*/
export function encodeRotationKey(privateKey: Uint8Array): string {
const uncompressedPublicKey = secp256k1.getPublicKey(privateKey, false);
const publicKeyX = uncompressedPublicKey.slice(1, 33);
const publicKeyY = uncompressedPublicKey.slice(33, 65);
// Uncompressed format: 0x04 prefix + 32-byte X + 32-byte Y
const xStart = UNCOMPRESSED_KEY_PREFIX_SIZE;
const xEnd = xStart + SECP256K1_COORDINATE_SIZE;
const yEnd = xEnd + SECP256K1_COORDINATE_SIZE;
const publicKeyX = uncompressedPublicKey.slice(xStart, xEnd);
const publicKeyY = uncompressedPublicKey.slice(xEnd, yEnd);

const keyObject = crypto.createPrivateKey({
key: {
Expand Down
12 changes: 8 additions & 4 deletions src/keys.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Keypair, Secp256k1Keypair, verifySignature, multibaseToBytes } from '@atproto/crypto';
import { ed25519 } from '@noble/curves/ed25519';
import { Ed25519Keypair, ED25519_PUBLIC_MULTIBASE_PREFIX } from './Ed25519Keypair.js';
import { Ed25519Keypair } from './Ed25519Keypair.js';
import {
DID_KEY_PREFIX,
ED25519_PUBLIC_MULTIBASE_PREFIX,
ED25519_DID_KEY_PREFIX,
SECP256K1_PUBLIC_MULTIBASE_PREFIX,
SECP256K1_DID_KEY_PREFIX,
SECP256K1_PUBLIC_MULTICODEC_PREFIX,
SECP256K1_COMPRESSED_PUBLIC_KEY_SIZE,
} from './did-validation.js';

export interface KeyPairBundle<T extends Keypair = Keypair> {
Expand Down Expand Up @@ -203,7 +207,7 @@ export async function getVerificationPublicKeyMultibase(keyInput: string): Promi
}

throw new VerificationKeyInputError(
'Unrecognized key format. Expected a public key (did:key:z6Mk...) or private key (PEM, multibase, or hex)',
`Unrecognized key format. Expected a public key (${ED25519_DID_KEY_PREFIX}...) or private key (PEM, multibase, or hex)`,
);
}

Expand Down Expand Up @@ -237,7 +241,7 @@ function validateSecp256k1PublicKeyMultibase(multibase: string): void {
}

// Validate total length (2-byte prefix + 33-byte compressed public key = 35 bytes)
const expectedLength = SECP256K1_PUBLIC_MULTICODEC_PREFIX.length + 33;
const expectedLength = SECP256K1_PUBLIC_MULTICODEC_PREFIX.length + SECP256K1_COMPRESSED_PUBLIC_KEY_SIZE;
if (decoded.length !== expectedLength) {
throw new RotationKeyInputError(
`Invalid key length: expected ${expectedLength} bytes (2-byte prefix + 33-byte key), ` +
Expand Down Expand Up @@ -323,6 +327,6 @@ export async function getRotationPublicKeyMultibase(keyInput: string): Promise<s
}

throw new RotationKeyInputError(
'Unrecognized key format. Expected a public key (did:key:zQ3sh...) or private key (PEM, multibase, or hex)',
`Unrecognized key format. Expected a public key (${SECP256K1_DID_KEY_PREFIX}...) or private key (PEM, multibase, or hex)`,
);
}
45 changes: 32 additions & 13 deletions src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,25 @@ interface AssetPattern {
height: number | null;
}

/**
* WordPress plugin banner dimensions.
*/
const BANNER_SMALL_WIDTH = 772;
const BANNER_SMALL_HEIGHT = 250;
const BANNER_LARGE_WIDTH = 1544;
const BANNER_LARGE_HEIGHT = 500;

/**
* WordPress plugin icon dimensions.
*/
const ICON_SMALL_SIZE = 128;
const ICON_LARGE_SIZE = 256;

/**
* Maximum number of keywords allowed in metadata.
*/
const MAX_KEYWORDS = 5;

/**
* Asset file patterns for WordPress plugins.
*
Expand All @@ -375,16 +394,16 @@ interface AssetPattern {
const ASSET_PATTERNS: AssetPattern[] = [
// Banners
{
pattern: /^banner-772x250\.(png|jpe?g|gif)$/i,
pattern: new RegExp(`^banner-${BANNER_SMALL_WIDTH}x${BANNER_SMALL_HEIGHT}\\.(png|jpe?g|gif)$`, 'i'),
type: 'banner',
width: 772,
height: 250,
width: BANNER_SMALL_WIDTH,
height: BANNER_SMALL_HEIGHT,
},
{
pattern: /^banner-1544x500\.(png|jpe?g|gif)$/i,
pattern: new RegExp(`^banner-${BANNER_LARGE_WIDTH}x${BANNER_LARGE_HEIGHT}\\.(png|jpe?g|gif)$`, 'i'),
type: 'banner',
width: 1544,
height: 500,
width: BANNER_LARGE_WIDTH,
height: BANNER_LARGE_HEIGHT,
},
// Icons
{
Expand All @@ -394,16 +413,16 @@ const ASSET_PATTERNS: AssetPattern[] = [
height: null,
},
{
pattern: /^icon-128x128\.(png|jpe?g|gif)$/i,
pattern: new RegExp(`^icon-${ICON_SMALL_SIZE}x${ICON_SMALL_SIZE}\\.(png|jpe?g|gif)$`, 'i'),
type: 'icon',
width: 128,
height: 128,
width: ICON_SMALL_SIZE,
height: ICON_SMALL_SIZE,
},
{
pattern: /^icon-256x256\.(png|jpe?g|gif)$/i,
pattern: new RegExp(`^icon-${ICON_LARGE_SIZE}x${ICON_LARGE_SIZE}\\.(png|jpe?g|gif)$`, 'i'),
type: 'icon',
width: 256,
height: 256,
width: ICON_LARGE_SIZE,
height: ICON_LARGE_SIZE,
},
];

Expand Down Expand Up @@ -691,7 +710,7 @@ export async function buildMetadataFromContent(
authors,
license,
security,
keywords: (keywords || []).slice(0, 5),
keywords: (keywords || []).slice(0, MAX_KEYWORDS),
sections,
releases: [release, ...filteredReleases],
});
Expand Down
Loading