From afd8dfb9b487551ad36a225155fbdf9287ce860a Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Wed, 11 Jul 2018 15:04:10 -0700 Subject: [PATCH 1/5] lnwallet/script_utils: expose CommitScriptUnencumbered --- lnwallet/script_utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnwallet/script_utils.go b/lnwallet/script_utils.go index acdff3b4..5afe2839 100644 --- a/lnwallet/script_utils.go +++ b/lnwallet/script_utils.go @@ -953,10 +953,10 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) return builder.Script() } -// commitScriptUnencumbered constructs the public key script on the commitment +// CommitScriptUnencumbered constructs the public key script on the commitment // transaction paying to the "other" party. The constructed output is a normal // p2wkh output spendable immediately, requiring no contestation period. -func commitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { +func CommitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { // This script goes to the "other" party, and it spendable immediately. builder := txscript.NewScriptBuilder() builder.AddOp(txscript.OP_0) From 1eba460a2ce02278e9a462aad858355bee631c5f Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Wed, 11 Jul 2018 15:04:26 -0700 Subject: [PATCH 2/5] lnwallet/script_utils_test: use exposed CommitScriptUnencumbered --- lnwallet/script_utils_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnwallet/script_utils_test.go b/lnwallet/script_utils_test.go index bc51e4bd..0bfac97a 100644 --- a/lnwallet/script_utils_test.go +++ b/lnwallet/script_utils_test.go @@ -90,7 +90,7 @@ func TestCommitmentSpendValidation(t *testing.T) { // We're testing an uncooperative close, output sweep, so construct a // transaction which sweeps the funds to a random address. - targetOutput, err := commitScriptUnencumbered(aliceKeyPub) + targetOutput, err := CommitScriptUnencumbered(aliceKeyPub) if err != nil { t.Fatalf("unable to create target output: %v", err) } @@ -186,7 +186,7 @@ func TestCommitmentSpendValidation(t *testing.T) { // Finally, we test bob sweeping his output as normal in the case that // Alice broadcasts this commitment transaction. - bobScriptP2WKH, err := commitScriptUnencumbered(bobPayKey) + bobScriptP2WKH, err := CommitScriptUnencumbered(bobPayKey) if err != nil { t.Fatalf("unable to create bob p2wkh script: %v", err) } From b0379dfa5918a0aa48f27f8a21280e9bcd90fc0d Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Wed, 11 Jul 2018 15:04:50 -0700 Subject: [PATCH 3/5] lnwallet/channel: use exposed CommitScriptUnencumbered --- lnwallet/channel.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 4f41166e..1d284a95 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1953,7 +1953,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, if err != nil { return nil, err } - localPkScript, err := commitScriptUnencumbered(keyRing.NoDelayKey) + localPkScript, err := CommitScriptUnencumbered(keyRing.NoDelayKey) if err != nil { return nil, err } @@ -4889,7 +4889,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer Signer, // Before we can generate the proper sign descriptor, we'll need to // locate the output index of our non-delayed output on the commitment // transaction. - selfP2WKH, err := commitScriptUnencumbered(keyRing.NoDelayKey) + selfP2WKH, err := CommitScriptUnencumbered(keyRing.NoDelayKey) if err != nil { return nil, fmt.Errorf("unable to create self commit script: %v", err) } @@ -5906,7 +5906,7 @@ func CreateCommitTx(fundingOutput wire.TxIn, // Next, we create the script paying to them. This is just a regular // P2WPKH output, without any added CSV delay. - theirWitnessKeyHash, err := commitScriptUnencumbered(keyRing.NoDelayKey) + theirWitnessKeyHash, err := CommitScriptUnencumbered(keyRing.NoDelayKey) if err != nil { return nil, err } From 974909640343b3f59090de27e70eca721c0aa416 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 10 Jul 2018 22:41:28 -0700 Subject: [PATCH 4/5] watchtower/blob/justice_kit: adds v0 encoding and encryption --- watchtower/blob/justice_kit.go | 442 +++++++++++++++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 watchtower/blob/justice_kit.go diff --git a/watchtower/blob/justice_kit.go b/watchtower/blob/justice_kit.go new file mode 100644 index 00000000..c61cf50e --- /dev/null +++ b/watchtower/blob/justice_kit.go @@ -0,0 +1,442 @@ +package blob + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + "golang.org/x/crypto/chacha20poly1305" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" +) + +const ( + // MinVersion is the minimum blob version supported by this package. + MinVersion = 0 + + // MaxVersion is the maximumm blob version supported by this package. + MaxVersion = 0 + + // NonceSize is the length of a chacha20poly1305 nonce, 12 bytes. + NonceSize = chacha20poly1305.NonceSize + + // KeySize is the length of a chacha20poly1305 key, 32 bytes. + KeySize = chacha20poly1305.KeySize + + // CiphertextExpansion is the number of bytes padded to a plaintext + // encrypted with chacha20poly1305, which comes from a 16-byte MAC. + CiphertextExpansion = 16 + + // V0PlaintextSize is the plaintext size of a version 0 encoded blob. + // sweep address: 42 bytes + // revocation pubkey: 33 bytes + // local delay pubkey: 33 bytes + // csv delay: 4 bytes + // commit to-local revocation sig: 64 bytes + // commit to-remote pubkey: 33 bytes, maybe blank + // commit to-remote sig: 64 bytes, maybe blank + V0PlaintextSize = 273 +) + +// Size returns the size of the encoded-and-encrypted blob in bytes. +// enciphered plaintext: n bytes +// MAC: 16 bytes +func Size(ver uint16) int { + return PlaintextSize(ver) + CiphertextExpansion +} + +// PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes. +func PlaintextSize(ver uint16) int { + switch ver { + case 0: + return V0PlaintextSize + default: + return 0 + } +} + +var ( + // byteOrder specifies a big-endian encoding of all integer values. + byteOrder = binary.BigEndian + + // ErrUnknownBlobVersion signals that we don't understand the requested + // blob encoding scheme. + ErrUnknownBlobVersion = errors.New("unknown blob version") + + // ErrCiphertextTooSmall is a decryption error signaling that the + // ciphertext is smaller than the ciphertext expansion factor. + ErrCiphertextTooSmall = errors.New( + "ciphertext is too small for chacha20poly1305", + ) + + // ErrNonceSize signals that the provided nonce is improperly sized. + ErrNonceSize = fmt.Errorf( + "chacha20poly1305 nonce must be %d bytes", NonceSize, + ) + + // ErrKeySize signals that the provided key is improperly sized. + ErrKeySize = fmt.Errorf( + "chacha20poly1305 key size must be %d bytes", KeySize, + ) + + // ErrNoCommitToRemoteOutput is returned when trying to retrieve the + // commit to-remote output from the blob, though none exists. + ErrNoCommitToRemoteOutput = errors.New( + "cannot obtain commit to-remote p2wkh output script from blob", + ) +) + +// PubKey is a 33-byte, serialized compressed public key. +type PubKey [33]byte + +// JusticeKit is lé Blob of Justice. The JusticeKit contains information +// required to construct a justice transaction, that sweeps a remote party's +// revoked commitment transaction. It supports encryption and decryption using +// chacha20poly1305, allowing the client to encrypt the contents of the blob, +// and for a watchtower to later decrypt if action must be taken. The encoding +// format is versioned to allow future extensions. +type JusticeKit struct { + // SweepAddress is the witness program of the output where the client's + // fund will be deposited. This value is included in the blobs, as + // opposed to the session info, such that the sweep addresses can't be + // correlated across sessions and/or towers. + // + // NOTE: This is chosen to be the length of a maximally sized witness + // program. + SweepAddress [42]byte + + // RevocationPubKey is the compressed pubkey that guards the revocation + // clause of the remote party's to-local output. + RevocationPubKey PubKey + + // LocalDelayPubKey is the compressed pubkey in the to-local script of + // the remote party, which guards the path where the remote party + // claims their commitment output. + LocalDelayPubKey PubKey + + // CSVDelay is the relative timelock in the remote party's to-local + // output, which the remote party must wait out before sweeping their + // commitment output. + CSVDelay uint32 + + // CommitToLocalSig is a signature under RevocationPubKey using + // SIGHASH_ALL. + CommitToLocalSig lnwire.Sig + + // CommitToRemotePubKey is the public key in the to-remote output of the revoked + // commitment transaction. + // + // NOTE: This value is only used if it contains a valid compressed + // public key. + CommitToRemotePubKey PubKey + + // CommitToRemoteSig is a signature under CommitToRemotePubKey using SIGHASH_ALL. + // + // NOTE: This value is only used if CommitToRemotePubKey contains a valid + // compressed public key. + CommitToRemoteSig lnwire.Sig +} + +// CommitToLocalWitnessScript returns the serialized witness script for the +// commitment to-local output. +func (b *JusticeKit) CommitToLocalWitnessScript() ([]byte, error) { + revocationPubKey, err := btcec.ParsePubKey( + b.RevocationPubKey[:], btcec.S256(), + ) + if err != nil { + return nil, err + } + + localDelayedPubKey, err := btcec.ParsePubKey( + b.LocalDelayPubKey[:], btcec.S256(), + ) + if err != nil { + return nil, err + } + + return lnwallet.CommitScriptToSelf( + b.CSVDelay, localDelayedPubKey, revocationPubKey, + ) +} + +// CommitToLocalRevokeWitnessStack constructs a witness stack spending the +// revocation clause of the commitment to-local output. +// 1 +func (b *JusticeKit) CommitToLocalRevokeWitnessStack() [][]byte { + witnessStack := make([][]byte, 2) + witnessStack[0] = append(b.CommitToLocalSig[:], byte(txscript.SigHashAll)) + witnessStack[1] = []byte{1} + + return witnessStack +} + +// HasCommitToRemoteOutput returns true if the blob contains a to-remote p2wkh +// pubkey. +func (b *JusticeKit) HasCommitToRemoteOutput() bool { + return btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) +} + +// CommitToRemoteWitnessScript returns the witness script for the commitment +// to-remote p2wkh output, which is the pubkey itself. +func (b *JusticeKit) CommitToRemoteWitnessScript() ([]byte, error) { + if !btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) { + return nil, ErrNoCommitToRemoteOutput + } + + return b.CommitToRemotePubKey[:], nil +} + +// CommitToRemoteWitnessStack returns a witness stack spending the commitment +// to-remote output, which is a regular p2wkh. +// +func (b *JusticeKit) CommitToRemoteWitnessStack() [][]byte { + witnessStack := make([][]byte, 1) + witnessStack[0] = append(b.CommitToRemoteSig[:], byte(txscript.SigHashAll)) + + return witnessStack +} + +// Encrypt encodes the blob of justice using encoding version, and then +// creates a ciphertext using chacha20poly1305 under the chosen (nonce, key) +// pair. +// +// NOTE: It is the caller's responsibility to ensure that this method is only +// called once for a given (nonce, key) pair. +func (b *JusticeKit) Encrypt(nonce, key []byte, version uint16) ([]byte, error) { + switch { + + // Fail if the nonce is not 12-bytes. + case len(nonce) != NonceSize: + return nil, ErrNonceSize + + // Fail if the nonce is not 32-bytes. + case len(key) != KeySize: + return nil, ErrKeySize + } + + // Encode the plaintext using the provided version, to obtain the + // plaintext bytes. + var ptxtBuf bytes.Buffer + err := b.encode(&ptxtBuf, version) + if err != nil { + return nil, err + } + + // Create a new chacha20poly1305 cipher, using a 32-byte key. + cipher, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + // Allocate the ciphertext, which will contain the encrypted plaintext + // and MAC. + plaintext := ptxtBuf.Bytes() + ciphertext := make([]byte, len(plaintext)+CiphertextExpansion) + + // Finally, encrypt the plaintext using the given nonce, storing the + // result in the ciphertext buffer. + cipher.Seal(ciphertext[:0], nonce, plaintext, nil) + + return ciphertext, nil +} + +// Decrypt unenciphers a blob of justice by decrypting the ciphertext using +// chacha20poly1305 with the chosen (nonce, key) pair. The internal plaintext is +// then deserialized using the given encoding version. +func Decrypt(nonce, key, ciphertext []byte, version uint16) (*JusticeKit, error) { + switch { + + // Fail if the blob's overall length is less than the expansion factor. + case len(ciphertext) < CiphertextExpansion: + return nil, ErrCiphertextTooSmall + + // Fail if the nonce is not 12-bytes. + case len(nonce) != NonceSize: + return nil, ErrNonceSize + + // Fail if the key is not 32-bytes. + case len(key) != KeySize: + return nil, ErrKeySize + } + + // Create a new chacha20poly1305 cipher, using a 32-byte key. + cipher, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + // Allocate the final buffer that will contain the blob's plaintext + // bytes, which is computed by subtracting the ciphertext expansion + // factor from the blob's length. + plaintext := make([]byte, len(ciphertext)-CiphertextExpansion) + + // Decrypt the ciphertext, placing the resulting plaintext in our + // plaintext buffer. + _, err = cipher.Open(plaintext[:0], nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + // If decryption succeeded, we will then decode the plaintext bytes + // using the specified blob version. + boj := &JusticeKit{} + err = boj.decode(bytes.NewReader(plaintext), version) + if err != nil { + return nil, err + } + + return boj, nil +} + +// encode serializes the JusticeKit according to the version, returning an +// error if the version is unknown. +func (b *JusticeKit) encode(w io.Writer, ver uint16) error { + switch ver { + case 0: + return b.encodeV0(w) + default: + return ErrUnknownBlobVersion + } +} + +// decode deserializes the JusticeKit according to the version, returning an +// error if the version is unknown. +func (b *JusticeKit) decode(r io.Reader, ver uint16) error { + switch ver { + case 0: + return b.decodeV0(r) + default: + return ErrUnknownBlobVersion + } +} + +// encodeV0 encodes the JusticeKit using the version 0 encoding scheme to the +// provided io.Writer. The encoding supports sweeping of the commit to-local +// output, and optionally the commit to-remote output. The encoding produces a +// constant-size plaintext size of 273 bytes. +// +// blob version 0 plaintext encoding: +// sweep address: 42 bytes +// revocation pubkey: 33 bytes +// local delay pubkey: 33 bytes +// csv delay: 4 bytes +// commit to-local revocation sig: 64 bytes +// commit to-remote pubkey: 33 bytes, maybe blank +// commit to-remote sig: 64 bytes, maybe blank +func (b *JusticeKit) encodeV0(w io.Writer) error { + // Write 42-byte sweep address where client funds will be deposited. + _, err := w.Write(b.SweepAddress[:]) + if err != nil { + return err + } + + // Write 33-byte revocation public key. + _, err = w.Write(b.RevocationPubKey[:]) + if err != nil { + return err + } + + // Write 33-byte local delay public key. + _, err = w.Write(b.LocalDelayPubKey[:]) + if err != nil { + return err + } + + // Write 4-byte CSV delay. + err = binary.Write(w, byteOrder, b.CSVDelay) + if err != nil { + return err + } + + // Write 64-byte revocation signature for commit to-local output. + _, err = w.Write(b.CommitToLocalSig[:]) + if err != nil { + return err + } + + // Write 33-byte commit to-remote public key, which may be blank. + _, err = w.Write(b.CommitToRemotePubKey[:]) + if err != nil { + return err + } + + // Write 64-byte commit to-remote signature, which may be blank. + _, err = w.Write(b.CommitToRemoteSig[:]) + return err +} + +// decodeV0 reconstructs a JusticeKit from the io.Reader, using version 0 +// encoding scheme. This will parse a constant size input stream of 273 bytes to +// recover information for the commit to-local output, and possibly the commit +// to-remote output. +// +// blob version 0 plaintext encoding: +// sweep address: 42 bytes +// revocation pubkey: 33 bytes +// local delay pubkey: 33 bytes +// csv delay: 4 bytes +// commit to-local revocation sig: 64 bytes +// commit to-remote pubkey: 33 bytes, maybe blank +// commit to-remote sig: 64 bytes, maybe blank +func (b *JusticeKit) decodeV0(r io.Reader) error { + // Read 42-byte sweep address where client funds will be deposited. + _, err := io.ReadFull(r, b.SweepAddress[:]) + if err != nil { + return err + } + + // Read 33-byte revocation public key. + _, err = io.ReadFull(r, b.RevocationPubKey[:]) + if err != nil { + return err + } + + // Read 33-byte local delay public key. + _, err = io.ReadFull(r, b.LocalDelayPubKey[:]) + if err != nil { + return err + } + + // Read 4-byte CSV delay. + err = binary.Read(r, byteOrder, &b.CSVDelay) + if err != nil { + return err + } + + // Read 64-byte revocation signature for commit to-local output. + _, err = io.ReadFull(r, b.CommitToLocalSig[:]) + if err != nil { + return err + } + + var ( + commitToRemotePubkey PubKey + commitToRemoteSig lnwire.Sig + ) + + // Read 33-byte commit to-remote public key, which may be discarded. + _, err = io.ReadFull(r, commitToRemotePubkey[:]) + if err != nil { + return err + } + + // Read 64-byte commit to-remote signature, which may be discarded. + _, err = io.ReadFull(r, commitToRemoteSig[:]) + if err != nil { + return err + } + + // Only populate the commit to-remote fields in the decoded blob if a + // valid compressed public key was read from the reader. + if btcec.IsCompressedPubKey(commitToRemotePubkey[:]) { + b.CommitToRemotePubKey = commitToRemotePubkey + b.CommitToRemoteSig = commitToRemoteSig + } + + return nil +} From 085e1f9ca8920260dc970eab55d914d6957f6dfc Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 10 Jul 2018 22:42:05 -0700 Subject: [PATCH 5/5] watchtower/blob/justice_kit_test: add encrypt/decrypt test vectors --- watchtower/blob/justice_kit_test.go | 170 ++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 watchtower/blob/justice_kit_test.go diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go new file mode 100644 index 00000000..4d9c7537 --- /dev/null +++ b/watchtower/blob/justice_kit_test.go @@ -0,0 +1,170 @@ +package blob_test + +import ( + "crypto/rand" + "encoding/binary" + "io" + "reflect" + "testing" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/watchtower/blob" +) + +func makePubKey(i uint64) blob.PubKey { + var pk blob.PubKey + pk[0] = 0x02 + if i%2 == 1 { + pk[0] |= 0x01 + } + binary.BigEndian.PutUint64(pk[1:9], i) + return pk +} + +func makeSig(i int) lnwire.Sig { + var sig lnwire.Sig + binary.BigEndian.PutUint64(sig[:8], uint64(i)) + return sig +} + +var descriptorTests = []struct { + name string + encVersion uint16 + decVersion uint16 + revPubKey blob.PubKey + delayPubKey blob.PubKey + csvDelay uint32 + commitToLocalSig lnwire.Sig + hasCommitToRemote bool + commitToRemotePubKey blob.PubKey + commitToRemoteSig lnwire.Sig + encErr error + decErr error +}{ + { + name: "to-local only", + encVersion: 0, + decVersion: 0, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + }, + { + name: "to-local and p2wkh", + encVersion: 0, + decVersion: 0, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + hasCommitToRemote: true, + commitToRemotePubKey: makePubKey(2), + commitToRemoteSig: makeSig(2), + }, + { + name: "unknown encrypt version", + encVersion: 1, + decVersion: 0, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + encErr: blob.ErrUnknownBlobVersion, + }, + { + name: "unknown decrypt version", + encVersion: 0, + decVersion: 1, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + decErr: blob.ErrUnknownBlobVersion, + }, +} + +// TestBlobJusticeKitEncryptDecrypt asserts that encrypting and decrypting a +// plaintext blob produces the original. The tests include negative assertions +// when passed invalid combinations, and that all successfully encrypted blobs +// are of constant size. +func TestBlobJusticeKitEncryptDecrypt(t *testing.T) { + for i, test := range descriptorTests { + boj := &blob.JusticeKit{ + RevocationPubKey: test.revPubKey, + LocalDelayPubKey: test.delayPubKey, + CSVDelay: test.csvDelay, + CommitToLocalSig: test.commitToLocalSig, + CommitToRemotePubKey: test.commitToRemotePubKey, + CommitToRemoteSig: test.commitToRemoteSig, + } + + // Generate a random encryption key for the blob. The key is + // sized at 32 byte, as in practice we will be using the remote + // party's commitment txid as the key. + key := make([]byte, blob.KeySize) + _, err := io.ReadFull(rand.Reader, key) + if err != nil { + t.Fatalf("test #%d %s -- unable to generate blob "+ + "encryption key: %v", i, test.name, err) + } + + nonce := make([]byte, blob.NonceSize) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + t.Fatalf("test #%d %s -- unable to generate nonce "+ + "nonce: %v", i, test.name, err) + } + + // Encrypt the blob plaintext using the generated key and + // target version for this test. + ctxt, err := boj.Encrypt(nonce, key, test.encVersion) + if err != test.encErr { + t.Fatalf("test #%d %s -- unable to encrypt blob: %v", + i, test.name, err) + } else if test.encErr != nil { + // If the test expected an encryption failure, we can + // continue to the next test. + continue + } + + // Ensure that all encrypted blobs are padded out to the same + // size: 282 bytes for version 0. + if len(ctxt) != blob.Size(test.encVersion) { + t.Fatalf("test #%d %s -- expected blob to have "+ + "size %d, got %d instead", i, test.name, + blob.Size(test.encVersion), len(ctxt)) + + } + + // Decrypt the encrypted blob, reconstructing the original + // blob plaintext from the decrypted contents. We use the target + // decryption version specified by this test case. + boj2, err := blob.Decrypt(nonce, key, ctxt, test.decVersion) + if err != test.decErr { + t.Fatalf("test #%d %s -- unable to decrypt blob: %v", + i, test.name, err) + } else if test.decErr != nil { + // If the test expected an decryption failure, we can + // continue to the next test. + continue + } + + // Check that the decrypted blob properly reports whether it has + // a to-remote output or not. + if boj2.HasCommitToRemoteOutput() != test.hasCommitToRemote { + t.Fatalf("test #%d %s -- expected blob has_to_remote "+ + "to be %v, got %v", i, test.name, + test.hasCommitToRemote, + boj2.HasCommitToRemoteOutput()) + } + + // Check that the original blob plaintext matches the + // one reconstructed from the encrypted blob. + if !reflect.DeepEqual(boj, boj2) { + t.Fatalf("test #%d %s -- decrypted plaintext does not "+ + "match original, want: %v, got %v", + i, test.name, boj, boj2) + } + } +}