UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

312 lines (271 loc) 10.9 kB
import { Base64String, PubKeyHex, HexString, OutpointString, CertificateFieldNameUnder50Bytes, WalletProtocol } from '../../wallet/Wallet.interfaces.js' import * as Utils from '../../primitives/utils.js' import ProtoWallet from '../../wallet/ProtoWallet.js' import Signature from '../../primitives/Signature.js' /** * Represents an Identity Certificate as per the Wallet interface specifications. * * This class provides methods to serialize and deserialize certificates, as well as signing and verifying the certificate's signature. */ export default class Certificate { /** * Type identifier for the certificate, base64 encoded string, 32 bytes. */ type: Base64String /** * Unique serial number of the certificate, base64 encoded string, 32 bytes. */ serialNumber: Base64String /** * The public key belonging to the certificate's subject, compressed public key hex string. */ subject: PubKeyHex /** * Public key of the certifier who issued the certificate, compressed public key hex string. */ certifier: PubKeyHex /** * The outpoint used to confirm that the certificate has not been revoked (TXID.OutputIndex), as a string. */ revocationOutpoint: OutpointString /** * All the fields present in the certificate, with field names as keys and encrypted field values as Base64 strings. */ fields: Record<CertificateFieldNameUnder50Bytes, Base64String> /** * Certificate signature by the certifier's private key, DER encoded hex string. */ signature?: HexString /** * Constructs a new Certificate. * * @param {Base64String} type - Type identifier for the certificate, base64 encoded string, 32 bytes. * @param {Base64String} serialNumber - Unique serial number of the certificate, base64 encoded string, 32 bytes. * @param {PubKeyHex} subject - The public key belonging to the certificate's subject, compressed public key hex string. * @param {PubKeyHex} certifier - Public key of the certifier who issued the certificate, compressed public key hex string. * @param {OutpointString} revocationOutpoint - The outpoint used to confirm that the certificate has not been revoked (TXID.OutputIndex), as a string. * @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - All the fields present in the certificate. * @param {HexString} signature - Certificate signature by the certifier's private key, DER encoded hex string. */ constructor( type: Base64String, serialNumber: Base64String, subject: PubKeyHex, certifier: PubKeyHex, revocationOutpoint: OutpointString, fields: Record<CertificateFieldNameUnder50Bytes, string>, signature?: HexString ) { this.type = type this.serialNumber = serialNumber this.subject = subject this.certifier = certifier this.revocationOutpoint = revocationOutpoint this.fields = fields this.signature = signature } /** * Serializes the certificate into binary format, with or without a signature. * * @param {boolean} [includeSignature=true] - Whether to include the signature in the serialization. * @returns {number[]} - The serialized certificate in binary format. */ toBinary(includeSignature: boolean = true): number[] { const writer = new Utils.Writer() // Write type (Base64String, 32 bytes) const typeBytes = Utils.toArray(this.type, 'base64') writer.write(typeBytes) // Write serialNumber (Base64String, 32 bytes) const serialNumberBytes = Utils.toArray(this.serialNumber, 'base64') writer.write(serialNumberBytes) // Write subject (33 bytes compressed PubKeyHex) const subjectBytes = Utils.toArray(this.subject, 'hex') writer.write(subjectBytes) // Write certifier (33 bytes compressed PubKeyHex) const certifierBytes = Utils.toArray(this.certifier, 'hex') writer.write(certifierBytes) // Write revocationOutpoint (TXID + OutputIndex) const [txid, outputIndex] = this.revocationOutpoint.split('.') const txidBytes = Utils.toArray(txid, 'hex') writer.write(txidBytes) writer.writeVarIntNum(Number(outputIndex)) // Write fields // Sort field names lexicographically const fieldNames = Object.keys(this.fields).sort() writer.writeVarIntNum(fieldNames.length) for (const fieldName of fieldNames) { const fieldValue = this.fields[fieldName] // Field name const fieldNameBytes = Utils.toArray(fieldName, 'utf8') writer.writeVarIntNum(fieldNameBytes.length) writer.write(fieldNameBytes) // Field value const fieldValueBytes = Utils.toArray(fieldValue, 'utf8') writer.writeVarIntNum(fieldValueBytes.length) writer.write(fieldValueBytes) } // Write signature if included if (includeSignature && (this.signature ?? '').length > 0) { // ✅ Explicitly handle nullish signature const signatureBytes = Utils.toArray(this.signature as string, 'hex') // ✅ Type assertion ensures it's a string writer.write(signatureBytes) } return writer.toArray() } /** * Deserializes a certificate from binary format. * * @param {number[]} bin - The binary data representing the certificate. * @returns {Certificate} - The deserialized Certificate object. */ static fromBinary(bin: number[]): Certificate { const reader = new Utils.Reader(bin) // Read type const typeBytes = reader.read(32) const type = Utils.toBase64(typeBytes) // Read serialNumber const serialNumberBytes = reader.read(32) const serialNumber = Utils.toBase64(serialNumberBytes) // Read subject (33 bytes) const subjectBytes = reader.read(33) const subject = Utils.toHex(subjectBytes) // Read certifier (33 bytes) const certifierBytes = reader.read(33) const certifier = Utils.toHex(certifierBytes) // Read revocationOutpoint const txidBytes = reader.read(32) const txid = Utils.toHex(txidBytes) const outputIndex = reader.readVarIntNum() const revocationOutpoint = `${txid}.${outputIndex}` // Read fields const numFields = reader.readVarIntNum() const fields: Record<CertificateFieldNameUnder50Bytes, string> = {} for (let i = 0; i < numFields; i++) { // Field name const fieldNameLength = reader.readVarIntNum() const fieldNameBytes = reader.read(fieldNameLength) const fieldName = Utils.toUTF8(fieldNameBytes) // Field value const fieldValueLength = reader.readVarIntNum() const fieldValueBytes = reader.read(fieldValueLength) const fieldValue = Utils.toUTF8(fieldValueBytes) fields[fieldName] = fieldValue } // Read signature if present let signature: string | undefined if (!reader.eof()) { const signatureBytes = reader.read() const sig = Signature.fromDER(signatureBytes) signature = sig.toString('hex') as string } return new Certificate( type, serialNumber, subject, certifier, revocationOutpoint, fields, signature ) } /** * Verifies the certificate's signature. * * @returns {Promise<boolean>} - A promise that resolves to true if the signature is valid. */ async verify(): Promise<boolean> { // A verifier can be any wallet capable of verifying signatures const verifier = new ProtoWallet('anyone') const verificationData = this.toBinary(false) // Exclude the signature from the verification data const signatureHex = this.signature ?? '' // Provide a fallback value (empty string) const { valid } = await verifier.verifySignature({ signature: Utils.toArray(signatureHex, 'hex'), // Now it is always a string data: verificationData, protocolID: [2, 'certificate signature'], keyID: `${this.type} ${this.serialNumber}`, counterparty: this.certifier // The certifier is the one who signed the certificate }) return valid } /** * Signs the certificate using the provided certifier wallet. * * @param {Wallet} certifierWallet - The wallet representing the certifier. * @returns {Promise<void>} */ async sign(certifierWallet: ProtoWallet): Promise<void> { if (this.signature != null && this.signature.length > 0) { // ✅ Explicitly checking for null/undefined throw new Error( `Certificate has already been signed! Signature present: ${this.signature}` ) } // Ensure the certifier declared is the one actually signing this.certifier = ( await certifierWallet.getPublicKey({ identityKey: true }) ).publicKey const preimage = this.toBinary(false) // Exclude the signature when signing const { signature } = await certifierWallet.createSignature({ data: preimage, protocolID: [2, 'certificate signature'], keyID: `${this.type} ${this.serialNumber}` }) this.signature = Utils.toHex(signature) } /** * Helper function which retrieves the protocol ID and key ID for certificate field encryption. * * For master certificate creation, no serial number is provided because entropy is required * from both the client and the certifier. In this case, the `keyID` is simply the `fieldName`. * * For VerifiableCertificates verifier keyring creation, both the serial number and field name are available, * so the `keyID` is formed by concatenating the `serialNumber` and `fieldName`. * * @param fieldName - The name of the field within the certificate to be encrypted. * @param serialNumber - (Optional) The serial number of the certificate. * @returns An object containing: * - `protocolID` (WalletProtocol): The protocol ID for certificate field encryption. * - `keyID` (string): A unique key identifier. It is the `fieldName` if `serialNumber` is undefined, * otherwise it is a combination of `serialNumber` and `fieldName`. */ static getCertificateFieldEncryptionDetails( fieldName: string, serialNumber?: string ): { protocolID: WalletProtocol, keyID: string } { return { protocolID: [2, 'certificate field encryption'], keyID: serialNumber ? `${serialNumber} ${fieldName}` : fieldName } } /** * Creates a Certificate instance from a plain object representation. * * @param obj - The object containing certificate data. * @returns A new Certificate instance. */ static fromObject(obj: { type: Base64String, serialNumber: Base64String, subject: PubKeyHex, certifier: PubKeyHex, revocationOutpoint: OutpointString, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, signature?: HexString }): Certificate { const cert = new Certificate( obj.type, obj.serialNumber, obj.subject, obj.certifier, obj.revocationOutpoint, obj.fields, obj.signature ); return cert; } }