wallet-storage
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
153 lines (121 loc) • 7.38 kB
text/typescript
import { Certificate as BsvCertificate, CompletedProtoWallet, KeyDeriver, MasterCertificate, PrivateKey, Utils, WalletCertificate } from "@bsv/sdk"
import { sdk } from '../../index.all'
describe('CertificateLifeCycle tests', () => {
jest.setTimeout(99999999)
test('0 encrypt decrypt sign verify', async () => {
const subjectWallet = new CompletedProtoWallet(PrivateKey.fromRandom())
const { cert, certifier, subject } = makeSampleCert(subjectWallet.keyDeriver.rootKey.toString())
const c = new BsvCertificate(cert.type, cert.serialNumber, cert.subject, cert.certifier, cert.revocationOutpoint, cert.fields)
const certifierWallet = new CompletedProtoWallet(certifier)
const imc = await MasterCertificate.issueCertificateForSubject(certifierWallet, c.subject, c.fields, c.type, async () => c.revocationOutpoint)
const imcSignature = imc.signature
await expect(imc.sign(certifierWallet)).rejects.toThrow('Certificate has already been signed')
expect(imcSignature).toBeTruthy()
expect(imcSignature).toBe(imc.signature)
const imcVerified = await imc.verify()
expect(imcVerified).toBe(true)
const dfs = await imc.decryptFields(subjectWallet)
for (const fn of Object.keys(cert.fields)) {
// decrypted fields should be original un-encrypted fields
expect(cert.fields[fn]).toBe(dfs[fn])
// issued certificate fields should encrypted, not the original un-encrypted fields
expect(cert.fields[fn]).not.toBe(imc.fields[fn])
}
await c.sign(certifierWallet)
const verified = await c.verify()
expect(verified).toBe(true)
const co = new sdk.CertOps(certifierWallet, cert)
expect(co.signature).toBe('')
await expect(co.verify()).rejects.toThrow('Signature DER must start with 0x30')
await co.sign(new CompletedProtoWallet(new KeyDeriver(certifier)))
expect(await co.verify()).toBe(true)
{
await co.encryptFields(subject.toPublicKey().toString())
await expect(co.verify()).rejects.toThrow('Signature is not valid')
co.signature = undefined
await co.sign(new CompletedProtoWallet(new KeyDeriver(certifier)))
expect(await co.verify()).toBe(true)
}
await co.decryptFields()
for (const n of Object.keys(co.fields)) {
expect(co.fields[n]).toBe(co._decryptedFields![n])
}
{
await co.encryptFields()
const crypto2 = new CompletedProtoWallet(new KeyDeriver(PrivateKey.fromHex('2'.repeat(64))))
const co2 = new sdk.CertOps(crypto2, co.toWalletCertificate())
// even with the keyring, without the right crypto root key decryption will fail.
co2._keyring = co._keyring
await expect(co2.decryptFields()).rejects.toThrow('Decryption failed!')
}
})
test('1 createKeyringForVerifier', async () => {
const { cert, certifier, subject } = makeSampleCert()
const crypto = new CompletedProtoWallet(subject)
const co = new sdk.CertOps(crypto, cert)
})
test('2 complete flow', async () => {
// Issuer beging with an un-encrypted (decrypted) raw certificate template:
// The public keys of both the certifier (the authority issuing the certificate),
// and the subject (who the certificate pertains to) are included in the certificate.
const { cert, certifier, subject } = makeSampleCert()
// Next the certifier must encrypt the field values for privacy and sign the certificate
// such that the values it contains can be attributed to the certifier through its public key.
// Encryption is done with random symmetric keys and the keys are then encrypted by the certifier
// such that each key can also be decrypted by the subject:
const certifierWallet = new CompletedProtoWallet(certifier)
const co = new sdk.CertOps(certifierWallet, cert)
await co.encryptAndSignNewCertificate()
// Grab a copy of the certificate to send to the subject
const exportForSubject = co.exportForSubject()
// The subject imports their copy of the new certificate:
const subjectWallet = new CompletedProtoWallet(subject)
const cs = await sdk.CertOps.fromCertifier(subjectWallet, exportForSubject)
// The subject's imported certificate should verify
expect(await cs.verify()).toBe(true)
// Confirm subject can decrypt the certifier's copy of the cert:
await co.decryptFields(subject.toPublicKey().toString())
// Confirm subject can decrypt their own copy of the cert:
await cs.decryptFields(cs.certifier, co._keyring)
// Restore the encrypted field values.
cs.fields = cs._encryptedFields!
// Prepare to send certificate to third party veifier of the 'name' and 'email' fields.
// The verifier must be able to confirm the signature on the original certificate's encrypted values.
// And then use a keyRing that their public key will work to reveal decrypted values for 'name' and 'email' only.
const verifier = PrivateKey.fromRandom()
// subject makes a keyring for the verifier
const exportForVerifier = await cs.exportForCounterparty(verifier.toPublicKey().toString(), ['name', 'email'])
// The verifier uses their own wallet to import the certificate, verify it, and decrypt their designated fields.
const verifierWallet = new CompletedProtoWallet(verifier)
const cv = await sdk.CertOps.fromCounterparty(verifierWallet, exportForVerifier)
// verifier must check that the certifier's public key generates a matching signature over all the encrypted
// certificate field values before using their keyring to decrypt the fields they've been authorized to see.
expect(await cv.verify()).toBe(true)
// The wallet's private key is the verifier's, so the counterparty is the certificate subject (who sent the cert to verifier).
// This decrypt is using the keyring provided for verification by the subject.
await cv.decryptFields()
expect(cv.fields['name']).toBe('Alice')
expect(cv.fields['email']).toBe('alice@example.com')
expect(cv.fields['organization']).not.toBe('Example Corp')
})
})
function makeSampleCert(subjectRootKeyHex?: string): { cert: WalletCertificate, subject: PrivateKey, certifier: PrivateKey }
{
const subject = subjectRootKeyHex ? PrivateKey.fromString(subjectRootKeyHex) : PrivateKey.fromRandom()
const certifier = PrivateKey.fromRandom()
const verifier = PrivateKey.fromRandom()
const cert: WalletCertificate = {
type: Utils.toBase64(new Array(32).fill(1)),
serialNumber: Utils.toBase64(new Array(32).fill(2)),
revocationOutpoint: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef.1',
subject: subject.toPublicKey().toString(),
certifier: certifier.toPublicKey().toString(),
fields: {
name: 'Alice',
email: 'alice@example.com',
organization: 'Example Corp'
},
signature: "",
}
return { cert, subject, certifier }
}