diff --git a/cmd/mtc/log/internal/subtreewitness/client.go b/cmd/mtc/log/internal/subtreewitness/client.go new file mode 100644 index 000000000..106f1fdca --- /dev/null +++ b/cmd/mtc/log/internal/subtreewitness/client.go @@ -0,0 +1,235 @@ +// Copyright 2026 The Tessera authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package subtreewitness provides a gateway for subtree cosigning with witnesses. +package subtreewitness + +import ( + "context" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "log/slog" + "net/http" + "net/url" + "sync" + + f_note "github.com/transparency-dev/formats/note" + "github.com/transparency-dev/tessera" + "github.com/transparency-dev/tessera/cmd/mtc/log/internal/mtcproof" + wc "github.com/transparency-dev/witness/client/http" + "golang.org/x/mod/sumdb/note" +) + +// ErrPolicyNotSatisfied is returned when witness responses do not satisfy the required policy. +var ErrPolicyNotSatisfied = errors.New("witness policy was not satisfied") + +type witnessKey struct { + name string + keyHash uint32 +} + +type witness struct { + client SubtreeWitnessClient + verifier f_note.SubtreeVerifier + cosignerID []byte +} + +// SubtreeWitnessClient defines the interface for calling a witness's sign-subtree endpoint. +type SubtreeWitnessClient interface { + SignSubtree(ctx context.Context, start, end uint64, subRoot []byte, proof [][]byte, rawCp []byte) ([]byte, error) +} + +// Gateway manages concurrent requests to subtree witnesses and evaluates policy satisfaction. +type Gateway struct { + witnesses map[witnessKey]witness + policy tessera.WitnessGroup +} + +// NewGateway creates a new subtree witness Gateway. +func NewGateway(httpClient *http.Client, policy tessera.WitnessGroup) (*Gateway, error) { + if httpClient == nil { + httpClient = http.DefaultClient + } + + endpoints := policy.WitnessEndpoints() + witnesses := make(map[witnessKey]witness) + + for uStr, vs := range endpoints { + u, err := url.Parse(uStr) + if err != nil { + return nil, fmt.Errorf("invalid witness URL %q: %w", uStr, err) + } + if len(vs) == 0 { + return nil, fmt.Errorf("no verifiers for witness %s", uStr) + } + client := wc.NewWitness(u, httpClient) + for _, v := range vs { + if sv, ok := v.(f_note.SubtreeVerifier); ok { + cosignerID, err := mtcproof.ParseCosignerID(sv.Name()) + if err != nil { + return nil, fmt.Errorf("invalid cosigner ID for witness %s: %w", sv.Name(), err) + } + witnesses[witnessKey{name: sv.Name(), keyHash: sv.KeyHash()}] = witness{ + client: client, + verifier: sv, + cosignerID: cosignerID, + } + } + } + } + + return &Gateway{ + witnesses: witnesses, + policy: policy, + }, nil +} + +// CosignSubtree sends concurrent subtree cosigning requests to witnesses and returns verified +// SubtreeSignatures as soon as the gateway's policy is satisfied on the reconstructed checkpoint. +func (gw *Gateway) CosignSubtree(ctx context.Context, origin string, logVerifier note.Verifier, start, end uint64, subRoot []byte, consProof [][]byte, rawCp []byte) ([]mtcproof.SubtreeSignature, error) { + if len(gw.witnesses) == 0 { + if gw.policy.Satisfied(rawCp) { + return nil, nil + } + return nil, ErrPolicyNotSatisfied + } + + var n *note.Note + noteRes, err := note.Open(rawCp, note.VerifierList(logVerifier)) + if err != nil { + var unverified *note.UnverifiedNoteError + if !errors.As(err, &unverified) || len(unverified.Note.Sigs) == 0 { + return nil, fmt.Errorf("checkpoint failed log signature verification: %w", err) + } + n = unverified.Note + } else { + n = noteRes + } + + var logSig *note.Signature + for i, s := range n.Sigs { + if s.Name == logVerifier.Name() && s.Hash == logVerifier.KeyHash() { + logSig = &n.Sigs[i] + break + } + } + if logSig == nil { + return nil, fmt.Errorf("checkpoint missing verified signature from log %q (%08x)", logVerifier.Name(), logVerifier.KeyHash()) + } + + reconstructedCp := fmt.Appendf(nil, "%s\n— %s %s\n", n.Text, logSig.Name, logSig.Base64) + + cpSigs := make(map[witnessKey]string) + for _, s := range n.UnverifiedSigs { + cpSigs[witnessKey{name: s.Name, keyHash: s.Hash}] = s.Base64 + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + var waitGroup sync.WaitGroup + type sigOrErr struct { + sig []byte + err error + } + results := make(chan sigOrErr, len(gw.witnesses)) + + for _, w := range gw.witnesses { + waitGroup.Add(1) + go func(w witness) { + defer waitGroup.Done() + sig, err := w.client.SignSubtree(ctx, start, end, subRoot, consProof, rawCp) + results <- sigOrErr{ + sig: sig, + err: err, + } + }(w) + } + + go func() { + waitGroup.Wait() + close(results) + }() + + var ( + reqErr error + verifiedSubtreeSigs []mtcproof.SubtreeSignature + ) + + for r := range results { + if r.err != nil { + reqErr = errors.Join(reqErr, r.err) + continue + } + + var sigNote *note.UnverifiedNoteError + _, sigErr := note.Open(append([]byte("text\n\n"), r.sig...), nil) + if !errors.As(sigErr, &sigNote) { + slog.WarnContext(ctx, "Failed to parse witness subtree signature response", slog.Any("error", sigErr)) + continue + } + + for _, s := range sigNote.Note.UnverifiedSigs { + raw, bErr := base64.StdEncoding.DecodeString(s.Base64) + if bErr != nil || len(raw) < 4 { + continue + } + keyHash := binary.BigEndian.Uint32(raw[:4]) + sigBytes := raw[4:] + + k := witnessKey{name: s.Name, keyHash: keyHash} + b64, ok := cpSigs[k] + if !ok { + slog.WarnContext(ctx, "Received subtree signature from witness not present on checkpoint", + slog.String("witness", s.Name), + slog.String("key_hash", fmt.Sprintf("%08x", keyHash)), + ) + continue + } + + w, ok := gw.witnesses[k] + if !ok { + slog.ErrorContext(ctx, "Received subtree signature from unknown witness key", + slog.String("witness", s.Name), + slog.String("key_hash", fmt.Sprintf("%08x", keyHash)), + ) + continue + } + + if !w.verifier.VerifySubtree(0, origin, start, end, subRoot, sigBytes) { + slog.ErrorContext(ctx, "Subtree signature verification failed", + slog.String("witness", s.Name), + slog.Uint64("start", start), + slog.Uint64("end", end), + ) + continue + } + + delete(cpSigs, k) + verifiedSubtreeSigs = append(verifiedSubtreeSigs, mtcproof.SubtreeSignature{ + CosignerID: w.cosignerID, + Signature: sigBytes, + }) + reconstructedCp = fmt.Appendf(reconstructedCp, "— %s %s\n", s.Name, b64) + } + + if gw.policy.Satisfied(reconstructedCp) { + return verifiedSubtreeSigs, nil + } + } + + return nil, errors.Join(ErrPolicyNotSatisfied, reqErr) +} diff --git a/cmd/mtc/log/internal/subtreewitness/subtreewitness_test.go b/cmd/mtc/log/internal/subtreewitness/subtreewitness_test.go new file mode 100644 index 000000000..76e3abb05 --- /dev/null +++ b/cmd/mtc/log/internal/subtreewitness/subtreewitness_test.go @@ -0,0 +1,240 @@ +// Copyright 2026 The Tessera authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package subtreewitness + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "net/url" + "testing" + + f_note "github.com/transparency-dev/formats/note" + "github.com/transparency-dev/tessera" + "golang.org/x/mod/sumdb/note" +) + +type mockSubtreeClient struct { + signFunc func(ctx context.Context, start, end uint64, subRoot []byte, proof [][]byte, rawCp []byte) ([]byte, error) +} + +func (m *mockSubtreeClient) SignSubtree(ctx context.Context, start, end uint64, subRoot []byte, proof [][]byte, rawCp []byte) ([]byte, error) { + return m.signFunc(ctx, start, end, subRoot, proof, rawCp) +} + +func TestGateway_CosignSubtree(t *testing.T) { + origin := "example.com/log" + start := uint64(0) + end := uint64(1024) + root := bytes.Repeat([]byte{0xcc}, 32) + cpBody := fmt.Sprintf("%s\n%d\n%s\n", origin, end, base64.StdEncoding.EncodeToString(root)) + + logSKey, logVKey, _ := f_note.GenerateMLDSAKey(origin) + logSigner, _ := f_note.NewMLDSASigner(logSKey) + logVer, _ := f_note.NewMLDSAVerifier(logVKey) + + logCpSig, err := logSigner.Sign([]byte(cpBody)) + if err != nil { + t.Fatalf("Sign checkpoint with log key: %v", err) + } + logCpSigRaw := binary.BigEndian.AppendUint32(nil, logSigner.KeyHash()) + logCpSigRaw = append(logCpSigRaw, logCpSig...) + logCpSigLine := fmt.Sprintf("— %s %s\n", logSigner.Name(), base64.StdEncoding.EncodeToString(logCpSigRaw)) + + skey1, vkey1, _ := f_note.GenerateMLDSAKey("oid/1.3.6.1.4.1.32473.101") + signer1, _ := f_note.NewMLDSASigner(skey1) + ver1, err := f_note.NewMLDSAVerifier(vkey1) + if err != nil { + t.Fatalf("NewMLDSAVerifier: %v", err) + } + + // Checkpoint signature on the checkpoint body for witness 1 + witCpSig, err := signer1.Sign([]byte(cpBody)) + if err != nil { + t.Fatalf("Sign checkpoint with witness key: %v", err) + } + witCpSigRaw := binary.BigEndian.AppendUint32(nil, signer1.KeyHash()) + witCpSigRaw = append(witCpSigRaw, witCpSig...) + witCpSigLine := fmt.Sprintf("— %s %s\n", signer1.Name(), base64.StdEncoding.EncodeToString(witCpSigRaw)) + + rawCp := append(append([]byte(cpBody), '\n'), []byte(logCpSigLine+witCpSigLine)...) + + // Subtree signature for the subtree + rawSubSig, err := signer1.SignSubtree(0, origin, start, end, root) + if err != nil { + t.Fatalf("SignSubtree: %v", err) + } + subSigRaw := binary.BigEndian.AppendUint32(nil, signer1.KeyHash()) + subSigRaw = append(subSigRaw, rawSubSig...) + subSigLine := []byte(fmt.Sprintf("— %s %s\n", signer1.Name(), base64.StdEncoding.EncodeToString(subSigRaw))) + + u1, _ := url.Parse("https://wit1.example.com") + w1, err := tessera.NewWitness(vkey1, u1) + if err != nil { + t.Fatalf("NewWitness: %v", err) + } + policy := tessera.NewWitnessGroup(1, w1) + + gw := &Gateway{ + witnesses: map[witnessKey]witness{ + {name: ver1.Name(), keyHash: ver1.KeyHash()}: { + client: &mockSubtreeClient{ + signFunc: func(ctx context.Context, start, end uint64, subRoot []byte, proof [][]byte, cp []byte) ([]byte, error) { + return subSigLine, nil + }, + }, + verifier: ver1, + cosignerID: []byte{0x01}, + }, + }, + policy: policy, + } + + verified, err := gw.CosignSubtree(context.Background(), origin, logVer, start, end, root, nil, rawCp) + if err != nil { + t.Fatalf("CosignSubtree: %v", err) + } + + if len(verified) != 1 { + t.Fatalf("got %d verified sigs, want 1", len(verified)) + } + if !bytes.Equal(verified[0].Signature, rawSubSig) { + t.Errorf("got signature %x, want %x", verified[0].Signature, rawSubSig) + } +} + +func TestGateway_CosignSubtree_PolicyNotSatisfied(t *testing.T) { + origin := "example.com/log" + start := uint64(0) + end := uint64(1024) + root := bytes.Repeat([]byte{0xcc}, 32) + + logSKey, logVKey, _ := f_note.GenerateMLDSAKey(origin) + logSigner, _ := f_note.NewMLDSASigner(logSKey) + logVer, _ := f_note.NewMLDSAVerifier(logVKey) + + cpBody := fmt.Sprintf("%s\n%d\n%s\n", origin, end, base64.StdEncoding.EncodeToString(root)) + logCpSig, _ := logSigner.Sign([]byte(cpBody)) + logCpSigRaw := binary.BigEndian.AppendUint32(nil, logSigner.KeyHash()) + logCpSigRaw = append(logCpSigRaw, logCpSig...) + logCpSigLine := fmt.Sprintf("— %s %s\n", logSigner.Name(), base64.StdEncoding.EncodeToString(logCpSigRaw)) + + skey1, vkey1, _ := f_note.GenerateMLDSAKey("oid/1.3.6.1.4.1.32473.101") + signer1, _ := f_note.NewMLDSASigner(skey1) + ver1, _ := f_note.NewMLDSAVerifier(vkey1) + + witCpSig, _ := signer1.Sign([]byte(cpBody)) + witCpSigRaw := binary.BigEndian.AppendUint32(nil, signer1.KeyHash()) + witCpSigRaw = append(witCpSigRaw, witCpSig...) + witCpSigLine := fmt.Sprintf("— %s %s\n", signer1.Name(), base64.StdEncoding.EncodeToString(witCpSigRaw)) + + rawCp := append(append([]byte(cpBody), '\n'), []byte(logCpSigLine+witCpSigLine)...) + + u1, _ := url.Parse("https://wit1.example.com") + w1, _ := tessera.NewWitness(vkey1, u1) + policy := tessera.NewWitnessGroup(1, w1) + + gw := &Gateway{ + witnesses: map[witnessKey]witness{ + {name: ver1.Name(), keyHash: ver1.KeyHash()}: { + client: &mockSubtreeClient{ + signFunc: func(ctx context.Context, start, end uint64, subRoot []byte, proof [][]byte, cp []byte) ([]byte, error) { + return nil, errors.New("witness down") + }, + }, + verifier: ver1, + cosignerID: []byte{0x01}, + }, + }, + policy: policy, + } + + _, err := gw.CosignSubtree(context.Background(), origin, logVer, start, end, root, nil, rawCp) + if !errors.Is(err, ErrPolicyNotSatisfied) { + t.Fatalf("got error %v, want ErrPolicyNotSatisfied", err) + } +} + +func TestNewGateway_NonSubtreeVerifierSkipped(t *testing.T) { + skey, vkey, err := note.GenerateKey(nil, "non-subtree-witness") + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + _ = skey + u, _ := url.Parse("https://wit.example.com") + w, err := tessera.NewWitness(vkey, u) + if err != nil { + t.Fatalf("NewWitness: %v", err) + } + policy := tessera.NewWitnessGroup(1, w) + + gw, err := NewGateway(nil, policy) + if err != nil { + t.Fatalf("NewGateway returned unexpected error: %v", err) + } + if len(gw.witnesses) != 0 { + t.Errorf("got %d witnesses in gateway, want 0", len(gw.witnesses)) + } +} + +func TestGateway_CosignSubtree_EmptyGateway(t *testing.T) { + origin := "example.com/log" + start := uint64(0) + end := uint64(1024) + root := bytes.Repeat([]byte{0xcc}, 32) + cpBody := fmt.Sprintf("%s\n%d\n%s\n", origin, end, base64.StdEncoding.EncodeToString(root)) + + logSKey, logVKey, _ := f_note.GenerateMLDSAKey(origin) + logSigner, _ := f_note.NewMLDSASigner(logSKey) + logVer, _ := f_note.NewMLDSAVerifier(logVKey) + + logCpSig, _ := logSigner.Sign([]byte(cpBody)) + logCpSigRaw := binary.BigEndian.AppendUint32(nil, logSigner.KeyHash()) + logCpSigRaw = append(logCpSigRaw, logCpSig...) + logCpSigLine := fmt.Sprintf("— %s %s\n", logSigner.Name(), base64.StdEncoding.EncodeToString(logCpSigRaw)) + + rawCp := append(append([]byte(cpBody), '\n'), []byte(logCpSigLine)...) + + _, vkey1, _ := f_note.GenerateMLDSAKey("oid/1.3.6.1.4.1.32473.101") + u1, _ := url.Parse("https://wit1.example.com") + w1, _ := tessera.NewWitness(vkey1, u1) + + // Policy requires 1 witness, but gateway has 0 witnesses + gw := &Gateway{ + witnesses: map[witnessKey]witness{}, + policy: tessera.NewWitnessGroup(1, w1), + } + + _, err := gw.CosignSubtree(context.Background(), origin, logVer, start, end, root, nil, rawCp) + if !errors.Is(err, ErrPolicyNotSatisfied) { + t.Fatalf("got error %v, want ErrPolicyNotSatisfied", err) + } + + // Policy requires 0 witnesses (empty group) + gwEmptyPolicy := &Gateway{ + witnesses: map[witnessKey]witness{}, + policy: tessera.WitnessGroup{}, + } + sigs, err := gwEmptyPolicy.CosignSubtree(context.Background(), origin, logVer, start, end, root, nil, rawCp) + if err != nil { + t.Fatalf("CosignSubtree with empty policy: %v", err) + } + if len(sigs) != 0 { + t.Errorf("got %d sigs, want 0", len(sigs)) + } +} diff --git a/cmd/mtc/log/mtc.go b/cmd/mtc/log/mtc.go index 4fce6b696..192a6ba45 100644 --- a/cmd/mtc/log/mtc.go +++ b/cmd/mtc/log/mtc.go @@ -35,6 +35,7 @@ import ( "github.com/transparency-dev/tessera/cmd/mtc/log/internal/entry" "github.com/transparency-dev/tessera/cmd/mtc/log/internal/landmark" "github.com/transparency-dev/tessera/cmd/mtc/log/internal/mtcproof" + "github.com/transparency-dev/tessera/cmd/mtc/log/internal/subtreewitness" "github.com/transparency-dev/tessera/internal/parse" "golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte/asn1" @@ -228,7 +229,7 @@ type MTCLog struct { origin string subtreeSigner note.SubtreeSigner logCosignerID []byte - subtreeWitnesses tessera.WitnessGroup + subtreeGateway *subtreewitness.Gateway } // AddTBSRsp contains enough information from the log @@ -435,14 +436,28 @@ func NewMTCLog(ctx context.Context, a *tessera.Appender, opts *Options) (*MTCLog return nil, fmt.Errorf("checkOriginSignerName: %v", err) } + httpClient := opts.httpClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + var gateway *subtreewitness.Gateway + if len(opts.subtreeWitnesses.Components) > 0 { + gw, err := subtreewitness.NewGateway(httpClient, opts.subtreeWitnesses) + if err != nil { + return nil, fmt.Errorf("creating subtree witness gateway: %w", err) + } + gateway = gw + } + l := &MTCLog{ - a: a, - reader: opts.reader, - maxCertLifetime: opts.maxCertLifetime, - origin: opts.origin, - subtreeSigner: opts.subtreeSigner, - logCosignerID: logCosignerID, - subtreeWitnesses: opts.subtreeWitnesses, + a: a, + reader: opts.reader, + maxCertLifetime: opts.maxCertLifetime, + origin: opts.origin, + subtreeSigner: opts.subtreeSigner, + logCosignerID: logCosignerID, + subtreeGateway: gateway, } cpReader, err := checkpoint.NewReader(ctx, opts.reader.ReadCheckpoint, l.getSubtreeSigs) @@ -492,10 +507,24 @@ func (l *MTCLog) getSubtreeSigs(ctx context.Context, start, end uint64, rawCp [] return nil, fmt.Errorf("cannot sign subtree [%d, %d): %v", start, end, err) } - // TODO: fetch signatures from mirrors. - return []mtcproof.SubtreeSignature{ + allSigs := []mtcproof.SubtreeSignature{ {CosignerID: l.logCosignerID, Signature: selfSig}, - }, nil + } + + if l.subtreeGateway != nil { + consProof, err := pb.SubtreeConsistencyProof(ctx, start, end) + if err != nil { + return nil, fmt.Errorf("cannot get subtree consistency proof for [%d, %d): %v", start, end, err) + } + + witnessSigs, err := l.subtreeGateway.CosignSubtree(ctx, l.origin, l.subtreeSigner.Verifier(), start, end, subRoot, consProof, rawCp) + if err != nil { + return nil, fmt.Errorf("failed to fetch subtree cosignatures for [%d, %d): %v", start, end, err) + } + allSigs = append(allSigs, witnessSigs...) + } + + return allSigs, nil } // AddTBS adds a TBSCertificateLogEntry to the log. @@ -662,7 +691,7 @@ func CreateSignerAndOrigin(caID string, logNumber uint64, privKey string) (origi return origin, s, nil } -// checkOriginSigner verifies that an origin and signer match with each other. +// checkOriginSignerName verifies that an origin and signer match with each other. // // SPEC: draft-ietf-plants-merkle-tree-certs section 5.2. // "Each issuance log has a log ID, which is a trust anchor ID constructed by concatenating the following OID components: diff --git a/cmd/mtc/log/mtc_test.go b/cmd/mtc/log/mtc_test.go index 0c33e22fa..9398f714c 100644 --- a/cmd/mtc/log/mtc_test.go +++ b/cmd/mtc/log/mtc_test.go @@ -835,7 +835,7 @@ func setupTestMTCLog(t *testing.T) *MTCLog { opts := tessera.NewAppendOptions(). WithCheckpointSigner(signer). WithBatching(4, 500*time.Millisecond). - WithCheckpointInterval(500*time.Millisecond) + WithCheckpointInterval(500 * time.Millisecond) appender, _, reader, err := tessera.NewAppender(ctx, driver, opts) if err != nil { t.Fatalf("Failed to initialize Tessera appender: %v", err) @@ -984,3 +984,71 @@ func TestMTCLog_AddTBS(t *testing.T) { }) } } + +func TestNewMTCLog_WithSubtreeWitnesses(t *testing.T) { + ctx := t.Context() + + driver, err := tposix.New(ctx, tposix.Config{Path: t.TempDir()}) + if err != nil { + t.Fatalf("tposix.New: %v", err) + } + signer := mustTestSigner() + appender, _, reader, err := tessera.NewAppender(ctx, driver, tessera.NewAppendOptions().WithCheckpointSigner(signer)) + if err != nil { + t.Fatalf("NewAppender: %v", err) + } + + _, vkey1, err := note.GenerateMLDSAKey("oid/1.3.6.1.4.1.32473.101") + if err != nil { + t.Fatalf("GenerateMLDSAKey: %v", err) + } + + tests := []struct { + name string + policy string + hasGateway bool + wantErr bool + }{ + { + name: "single witness policy", + policy: fmt.Sprintf("witness wit1 %s http://wit1.example.com\nquorum wit1\n", vkey1), + hasGateway: true, + }, + { + name: "empty witness group", + policy: "", + hasGateway: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var wg tessera.WitnessGroup + if tc.policy != "" { + var err error + wg, err = tessera.NewWitnessGroupFromPolicy([]byte(tc.policy)) + if err != nil { + t.Fatalf("NewWitnessGroupFromPolicy error: %v", err) + } + } + + opts := NewOptions(). + WithTesseraReader(reader). + WithLandmarksStorage(dummyLandmarksStorage{}). + WithOrigin(testOrigin). + WithSubtreeSigner(signer). + WithSubtreeWitnesses(wg) + + l, err := NewMTCLog(ctx, appender, opts) + if (err != nil) != tc.wantErr { + t.Fatalf("NewMTCLog error = %v, wantErr %v", err, tc.wantErr) + } + if tc.wantErr { + return + } + if (l.subtreeGateway != nil) != tc.hasGateway { + t.Errorf("has subtreeGateway = %v, want %v", l.subtreeGateway != nil, tc.hasGateway) + } + }) + } +} diff --git a/witness.go b/witness.go index a2908228e..b4b9cc9f6 100644 --- a/witness.go +++ b/witness.go @@ -190,9 +190,15 @@ func isBadName(n string) bool { // NewWitness returns a Witness given a verifier key and the root URL for where this // witness can be reached. func NewWitness(vkey string, witnessRoot *url.URL) (Witness, error) { - v, err := f_note.NewVerifierForCosignatureV1(vkey) - if err != nil { - return Witness{}, err + var v note.Verifier + if sv, err := f_note.NewMLDSAVerifier(vkey); err == nil { + v = sv + } else { + var err error + v, err = f_note.NewVerifierForCosignatureV1(vkey) + if err != nil { + return Witness{}, err + } } return Witness{