Skip to content
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fair-tools did verification-key add Add a verification key
fair-tools did verification-key check Check if a verification key is valid for a DID
fair-tools did verification-key revoke Revoke a verification key
fair-tools did rotation-key add Add a rotation key
fair-tools did rotation-key check Check if a rotation key is valid for a DID
fair-tools did rotation-key revoke Revoke a rotation key
fair-tools did log verify Validate a DID operation log from genesis
fair-tools did aka add Add a URL to the alsoKnownAs field
Expand Down Expand Up @@ -251,7 +252,7 @@ Use `--output-file` to save the new key to a different file instead of the signi

### Check verification key

Checks if a verification key is present in the DID document's verification methods.
Checks if a verification key is valid by checking that it's present in the DID document's verification methods.

```bash
fair-tools did verification-key check \
Expand Down Expand Up @@ -285,6 +286,31 @@ fair-tools did rotation-key add \

Use `--output-file` to save the new key to a different file instead of the signing file.

### Check rotation key

Checks if a rotation key is valid by checking that it's present in the latest operation of the DID log.

```bash
fair-tools did rotation-key check \
--did did:plc:xxx \
--key did:key:zQ3sh...
```

You can also provide the key via file or environment variable:

```bash
# From a file (accepts public key or private keypair)
fair-tools did rotation-key check \
--did did:plc:xxx \
--key-file ./key.pem

# From environment variable
FAIR_ROTATION_KEY=zQ3sh... fair-tools did rotation-key check \
--did did:plc:xxx
```

If neither `--key` nor `--key-file` is provided, uses `FAIR_ROTATION_KEY` environment variable.

### Revoke verification key

Revokes a verification key from a DID.
Expand Down
10 changes: 0 additions & 10 deletions src/Ed25519Keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@ export const ED25519_PUBLIC_PREFIX = new Uint8Array([0xed, 0x01]);
*/
export const ED25519_PUBLIC_MULTIBASE_PREFIX = 'z6Mk';

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

/**
* Multicodec prefix for Ed25519 private keys.
*/
export const ED25519_PRIVATE_PREFIX = new Uint8Array([0x80, 0x26]);

/**
* Ed25519 verification keypair.
*/
Expand Down
146 changes: 146 additions & 0 deletions src/cli/did-rotation-key-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env node

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 { checkRotationKey, DidLogFetchError, DidLogValidationError } from '../verify.js';

const { values } = parseArgs({
options: {
did: {
type: 'string',
},
key: {
type: 'string',
},
'key-file': {
type: 'string',
},
help: {
type: 'boolean',
},
},
});

if (values.help) {
console.log(`Usage: fair-tools did rotation-key check [options]

Check if a rotation key is valid for signing PLC operations.

Required:
--did <did> The DID to check (did:plc:...)

Key input (one required):
--key <key> Public key in did:key format (did:key:zQ3sh...) or multibase format (zQ3sh...).
--key-file <file> Read rotation key from file. Accepts a public key or a private keypair.
Public key should be in did:key format (did:key:zQ3sh...) or multibase format (zQ3sh...).
Private key can be in PEM, multibase, or hex format.

If neither --key nor --key-file is provided, uses FAIR_ROTATION_KEY environment variable.

Optional:
--help Show this help message

Exit codes:
0 Key is valid
1 Key is not valid (not found or DID has no rotation keys)
2 Error occurred (invalid input, network error, etc.)`);
process.exit(0);
}

// Validate required options
if (!values.did) {
console.error('Error: Missing required option: --did');
console.error('Run with --help for usage information.');
process.exit(2);
}

if (values.key && values['key-file']) {
console.error('Error: Cannot specify both --key and --key-file');
console.error('Run with --help for usage information.');
process.exit(2);
}

const did = values.did;

// Validate DID format
try {
validatePlcDid(did);
} catch (err) {
if (err instanceof DidValidationError) {
console.error(`Error: ${err.message}`);
process.exit(2);
}
throw err;
}

// Extract the public key multibase
let publicKeyMultibase: string;
try {
if (values['key-file']) {
// --key-file accepts both public and private keys
const keyInput = await readFile(values['key-file'], 'utf-8');
publicKeyMultibase = await getRotationPublicKeyMultibase(keyInput);
} else if (values.key) {
// --key only accepts public keys
publicKeyMultibase = await parseRotationPublicKeyOnly(values.key);
} else if (process.env.FAIR_ROTATION_KEY) {
// FAIR_ROTATION_KEY env var - handles like --key-file
publicKeyMultibase = await getRotationPublicKeyMultibase(process.env.FAIR_ROTATION_KEY);
} else {
console.error('Error: Must provide --key, --key-file, or set FAIR_ROTATION_KEY environment variable');
console.error('Run with --help for usage information.');
process.exit(2);
}
} catch (err) {
if (err instanceof RotationKeyInputError) {
console.error(`Error: ${err.message}`);
process.exit(2);
}
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
console.error(`Error reading key file: ${(err as Error).message}`);
process.exit(2);
}
throw err;
}

// Check if the key is valid for the DID
console.log(`Checking rotation key for ${did}...`);

let result;
try {
result = await checkRotationKey(did, publicKeyMultibase);
} catch (err) {
if (err instanceof DidLogFetchError) {
console.error(`Error: Failed to fetch DID log: ${err.message}`);
process.exit(2);
}
if (err instanceof DidLogValidationError) {
console.error(`Error: DID log validation failed: ${err.message}`);
process.exit(2);
}
throw err;
}

if (result.allKeys.length === 0) {
console.log(`\n❌ No rotation keys found in DID log`);
console.log(`The DID ${did} has no rotation keys.`);
process.exit(1);
}

if (result.valid) {
console.log(`\n✓ Rotation key is valid`);
console.log(`Public key: did:key:${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(`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}`);
}
process.exit(1);
}
4 changes: 4 additions & 0 deletions src/cli/fair-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ const commands: { [key: string]: CommandTree } = {
description: 'Revoke a rotation key',
load: () => import('./did-rotation-key-revoke.js'),
},
check: {
description: 'Check if a rotation key is valid',
load: () => import('./did-rotation-key-check.js'),
},
},
keys: {
migrate: {
Expand Down
34 changes: 30 additions & 4 deletions src/did-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,55 @@ export class PublicKeyValidationError extends Error {}
*/
const DID_PLC_LENGTH = 32;

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

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

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

/**
* Expected length of a did:key Ed25519 public key.
* Format: did:key: (8 chars) + multibase 'z' (1 char) + base58btc encoded (multicodec prefix + 32-byte key) = 56 characters total.
*/
const ED25519_DID_KEY_LENGTH = 56;
export const ED25519_DID_KEY_LENGTH = 56;

/**
* Expected length of a did:key Secp256k1 compressed public key.
* Format: did:key: (8 chars) + multibase 'z' (1 char) + base58btc encoded (multicodec prefix + 33-byte compressed key) = 57 characters total.
*/
const SECP256K1_DID_KEY_LENGTH = 57;
export const SECP256K1_DID_KEY_LENGTH = 57;

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

/**
* Multicodec prefix for secp256k1 compressed public keys (rotation keys).
*/
export const SECP256K1_PUBLIC_MULTICODEC_PREFIX = new Uint8Array([0xe7, 0x01]);

/**
* Multicodec prefix for secp256k1 private keys (rotation keys).
*/
export const SECP256K1_PRIVATE_MULTICODEC_PREFIX = new Uint8Array([0x81, 0x26]);

/**
* Multicodec prefix for Ed25519 private keys (verification keys).
*/
export const ED25519_PRIVATE_MULTICODEC_PREFIX = new Uint8Array([0x80, 0x26]);

/**
* Validates that a DID has the required did:plc: prefix and correct length.
Expand Down
Loading