The signatures module provides functions for creating and verifying cryptographic signatures using Ed25519. Signatures prove that a message was created by the holder of a specific private key and hasn’t been tampered with.
createDetachedSignatureAsymmetric
Creates a detached signature for a message with context information to prevent signature re-interpretation attacks.
function createDetachedSignatureAsymmetric(
message: string,
signingPrivateKey: string,
context: SignatureContext,
additionalContext?: AdditionalContext
): string
Parameters
Base64-encoded Ed25519 private signing key from generatePublicPrivateKeyPair()
Principal context enum value that prevents signature re-interpretation across different use cases
Optional sub-context for additional signature specificity
Returns
Base64-encoded detached signature (64 bytes)
Example
import {
createDetachedSignatureAsymmetric,
generatePublicPrivateKeyPair,
SignatureContext
} from 'skiff-crypto';
const keys = generatePublicPrivateKeyPair();
const message = 'Important document content';
const signature = createDetachedSignatureAsymmetric(
message,
keys.signingPrivateKey,
SignatureContext.DocumentData
);
console.log(signature); // "base64-encoded-signature"
verifyDetachedSignatureAsymmetric
Verifies a detached signature on a message given the context information.
function verifyDetachedSignatureAsymmetric(
message: string,
signature: string,
signingPublicKey: string,
context: SignatureContext,
additionalContext?: AdditionalContext
): boolean
Parameters
The message that was signed
Base64-encoded signature from createDetachedSignatureAsymmetric()
Base64-encoded Ed25519 public signing key of the signer
The same context value used when creating the signature
The same additional context used when creating the signature (if any)
Returns
true if signature is valid for the given message and context, false otherwise
Example
import {
verifyDetachedSignatureAsymmetric,
SignatureContext
} from 'skiff-crypto';
const isValid = verifyDetachedSignatureAsymmetric(
message,
signature,
keys.signingPublicKey,
SignatureContext.DocumentData
);
if (isValid) {
console.log('Signature is valid!');
} else {
console.log('Signature verification failed!');
}
SignatureContext
Enum defining signature contexts to prevent re-interpretation attacks. When signing data, you must specify what the signature is being used for.
enum SignatureContext {
// Account operations
DeleteAccount = 'DELETE_ACCOUNT',
DisableMfa = 'DISABLE_MFA',
EnrollMfa = 'ENROLL_MFA',
RegenerateMfaBackupCodes = 'REGENERATE_MFA_BACKUP_CODES',
// Document operations
DeleteDoc = 'DELETE_DOC',
DocumentChunk = 'DOCUMENT_CHUNK',
DocumentData = 'DOCUMENT_DATA',
DocumentMetadata = 'DOCUMENT_METADATA',
DocumentParent = 'DOCUMENT_PARENT',
UnshareDoc = 'UNSHARE_DOC',
// Recovery operations
DeleteRecoveryData = 'DELETE_RECOVERY_DATA',
RecoveryData = 'RECOVERY_DATA',
UploadRecoveryEncryptedUserData = 'UPLOAD_RECOVERY_ENCRYPTED_USER_DATA',
UploadRecoveryEncryptionPublicKey = 'UPLOAD_RECOVERY_ENCRYPTION_PUBLIC_KEY',
UploadRecoveryServerShare = 'UPLOAD_RECOVERY_SERVER_SHARE',
UploadRecoverySigningPublicKey = 'UPLOAD_RECOVERY_SIGNING_PUBLIC_KEY',
// Authentication
SessionKey = 'SESSION_KEY',
SrpSalt = 'SRP_SALT',
SrpVerifier = 'SRP_VERIFIER',
MobileLogin = 'MOBILE_LOGIN',
// User data
UpdateUserData = 'UPDATE_USER_DATA',
UserData = 'USER_DATA',
UserPublicKey = 'USER_PUBLIC_KEY',
// Links
LinksLinkKey = 'LINKS_LINK_KEY',
LinksSessionKey = 'LINKS_SESSION_KEY',
// Email
EmailContent = 'EMAIL_CONTENT'
}
AdditionalContext
Enum for optional sub-contexts, primarily used for chunked data.
enum AdditionalContext {
LastChunk = 'LAST_CHUNK',
NotLastChunk = 'NOT_LAST_CHUNK',
NoContext = 'NO_CONTEXT'
}
Example with Additional Context
import {
createDetachedSignatureAsymmetric,
verifyDetachedSignatureAsymmetric,
SignatureContext,
AdditionalContext
} from 'skiff-crypto';
// Sign a document chunk
const chunkSignature = createDetachedSignatureAsymmetric(
chunkData,
signingPrivateKey,
SignatureContext.DocumentChunk,
AdditionalContext.NotLastChunk
);
// Verify with matching context
const isValid = verifyDetachedSignatureAsymmetric(
chunkData,
chunkSignature,
signingPublicKey,
SignatureContext.DocumentChunk,
AdditionalContext.NotLastChunk
);
Signature Re-interpretation Prevention
The context system prevents an attacker from taking a signature meant for one purpose and using it for another.
import {
createDetachedSignatureAsymmetric,
verifyDetachedSignatureAsymmetric,
SignatureContext
} from 'skiff-crypto';
const message = 'delete-user-123';
// Sign for document deletion
const signature = createDetachedSignatureAsymmetric(
message,
signingPrivateKey,
SignatureContext.DeleteDoc
);
// Signature is valid for DeleteDoc context
const validForDoc = verifyDetachedSignatureAsymmetric(
message,
signature,
signingPublicKey,
SignatureContext.DeleteDoc
);
console.log(validForDoc); // true
// Same signature is INVALID for DeleteAccount context
const validForAccount = verifyDetachedSignatureAsymmetric(
message,
signature,
signingPublicKey,
SignatureContext.DeleteAccount
);
console.log(validForAccount); // false - prevents attack!
Complete Signing Workflow
import {
generatePublicPrivateKeyPair,
createDetachedSignatureAsymmetric,
verifyDetachedSignatureAsymmetric,
SignatureContext
} from 'skiff-crypto';
// 1. Generate keys for signer
const signerKeys = generatePublicPrivateKeyPair();
// 2. Create document and sign it
const document = {
title: 'Important Document',
content: 'This is the document content',
timestamp: Date.now()
};
const documentString = JSON.stringify(document);
const signature = createDetachedSignatureAsymmetric(
documentString,
signerKeys.signingPrivateKey,
SignatureContext.DocumentData
);
// 3. Store signature with document and share public key
const signedDocument = {
document: documentString,
signature,
signerPublicKey: signerKeys.signingPublicKey
};
// 4. Later, recipient verifies the signature
const isAuthentic = verifyDetachedSignatureAsymmetric(
signedDocument.document,
signedDocument.signature,
signedDocument.signerPublicKey,
SignatureContext.DocumentData
);
if (isAuthentic) {
const doc = JSON.parse(signedDocument.document);
console.log('Document verified:', doc.title);
} else {
console.error('Document signature invalid - may be tampered!');
}
Multi-Signature Verification
import {
generatePublicPrivateKeyPair,
createDetachedSignatureAsymmetric,
verifyDetachedSignatureAsymmetric,
SignatureContext
} from 'skiff-crypto';
interface SignedMessage {
message: string;
signatures: Array<{
publicKey: string;
signature: string;
}>;
}
function signMessage(
message: string,
signers: Array<{ privateKey: string; publicKey: string }>,
context: SignatureContext
): SignedMessage {
return {
message,
signatures: signers.map(signer => ({
publicKey: signer.publicKey,
signature: createDetachedSignatureAsymmetric(
message,
signer.privateKey,
context
)
}))
};
}
function verifyMultiSignature(
signedMsg: SignedMessage,
context: SignatureContext
): boolean {
return signedMsg.signatures.every(sig =>
verifyDetachedSignatureAsymmetric(
signedMsg.message,
sig.signature,
sig.publicKey,
context
)
);
}
// Usage
const signer1 = generatePublicPrivateKeyPair();
const signer2 = generatePublicPrivateKeyPair();
const multiSigned = signMessage(
'Transfer $1000',
[
{ privateKey: signer1.signingPrivateKey, publicKey: signer1.signingPublicKey },
{ privateKey: signer2.signingPrivateKey, publicKey: signer2.signingPublicKey }
],
SignatureContext.UserData
);
const allValid = verifyMultiSignature(multiSigned, SignatureContext.UserData);
console.log('All signatures valid:', allValid);
Implementation Details
Signature Generation Process
- Combines message, context, and additional context into JSON array:
[context, additionalContext, message]
- JSON stringifies the array
- Converts to UTF-8 bytes
- Signs using Ed25519 with
nacl.sign.detached()
- Returns base64-encoded signature
Verification Process
- Reconstructs the same JSON array with provided context
- Converts to UTF-8 bytes
- Verifies signature using Ed25519 with
nacl.sign.detached.verify()
- Returns boolean result
Ed25519 Properties
- Algorithm: EdDSA (Edwards-curve Digital Signature Algorithm)
- Curve: Curve25519
- Signature Size: 64 bytes
- Security Level: ~128 bits
- Performance: Very fast signing and verification
- Deterministic: Same message + key = same signature
Security Best Practices
- Always use context: Never create signatures without a
SignatureContext
- Match contexts: Use the same context for signing and verification
- Protect private keys: Never expose signing private keys
- Verify before trusting: Always verify signatures before processing signed data
- Use fresh signatures: For time-sensitive operations, include timestamps in the message
- Key rotation: Periodically rotate signing keys for long-term use
Common Patterns
Timestamped Signatures
function createTimestampedSignature(
data: any,
signingPrivateKey: string,
context: SignatureContext
): { signature: string; timestamp: number } {
const timestamp = Date.now();
const message = JSON.stringify({ data, timestamp });
const signature = createDetachedSignatureAsymmetric(
message,
signingPrivateKey,
context
);
return { signature, timestamp };
}
function verifyTimestampedSignature(
data: any,
signature: string,
timestamp: number,
signingPublicKey: string,
context: SignatureContext,
maxAge: number = 3600000 // 1 hour
): boolean {
// Check timestamp freshness
if (Date.now() - timestamp > maxAge) {
return false;
}
const message = JSON.stringify({ data, timestamp });
return verifyDetachedSignatureAsymmetric(
message,
signature,
signingPublicKey,
context
);
}