From 8886328ebd6f50d3e68578cc9f50d9f18c62c252 Mon Sep 17 00:00:00 2001 From: Bas Westerbaan Date: Thu, 24 Apr 2025 13:56:16 +0200 Subject: [PATCH 1/3] Add mtc verify Cf #57 --- README.md | 20 ++++++- cmd/mtc/main.go | 155 ++++++++++++++++++++++++++++++++++++------------ mtc.go | 26 +++++--- 3 files changed, 154 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 0343692..6ab1a34 100644 --- a/README.md +++ b/README.md @@ -419,7 +419,7 @@ dns [example.com] ip4 [198.51.100.60] proof_type merkle_tree_sha256 -CA OID 62253.12.15 +CA TAI 62253.12.15 Batch number 0 index 1 recomputed tree head 043bc6b0e49a085f2370b2e0f0876d154c2e8d8fe049077dbad118a363580345 @@ -429,6 +429,22 @@ authentication path This is indeed the root of batch `0`, and so this certificate is valid. +### Verify certificate + +To automate this, there is the `mtc verify` command that takes +a certificate, the CA parameters, and a signed validity window. + +``` +$ mtc verify -ca-params www/mtc/v04b/ca-params -validity-window www/mtc/v04b/batches/1/validity-window my-cert +$ echo $? +0 +``` + +Status code 0 means verification succeeded. + +For transparency, you should not get the signed validity window directly +from the CA, but rather from one or more mirrors (see below). + ### Run CA as server An Merkle Tree CA can be run just from the commandline, but it's often @@ -476,7 +492,7 @@ dns [example.com] ip4 [198.51.100.60] proof_type merkle_tree_sha256 -CA OID 62253.12.15 +CA TAI 62253.12.15 Batch number 0 index 1 recomputed tree head 043bc6b0e49a085f2370b2e0f0876d154c2e8d8fe049077dbad118a363580345 diff --git a/cmd/mtc/main.go b/cmd/mtc/main.go index 2d4d781..b8b33b1 100644 --- a/cmd/mtc/main.go +++ b/cmd/mtc/main.go @@ -883,7 +883,20 @@ func writeEvidenceList(w *tabwriter.Writer, el mtc.EvidenceList) error { return nil } +func handleVerify(cc *cli.Context) error { + return handleCert(cc, false) +} + func handleInspectCert(cc *cli.Context) error { + return handleCert(cc, true) +} + +// Handles `mtc verify' and `mtc inspect cert' +func handleCert(cc *cli.Context, inspect bool) error { + if !inspect && !cc.IsSet("validity-window") { + return errors.New("-validity-window must be set") + } + buf, err := inspectGetBuf(cc) if err != nil { return err @@ -902,56 +915,99 @@ func handleInspectCert(cc *cli.Context) error { return err } - w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) - writeAssertion(w, c.Assertion) - fmt.Fprintf(w, "\n") tai := c.Proof.TrustAnchorIdentifier() - fmt.Fprintf(w, "proof_type\t%v\n", tai.ProofType(&caStore)) + if !tai.Issuer.Equal(¶ms.Issuer) { + return fmt.Errorf( + "Issuer in certificate (%s) does not match provided CA (%s)", + tai.Issuer, + params.Issuer, + ) + } - fmt.Fprintf(w, "CA OID\t%s\n", tai.Issuer) - fmt.Fprintf(w, "Batch number\t%d\n", tai.BatchNumber) + var ( + vw *mtc.SignedValidityWindow + verifyResult error + ) + if cc.IsSet("validity-window") { + vwPath := cc.String("validity-window") + vwBuf, err := os.ReadFile(vwPath) + if err != nil { + return fmt.Errorf("Reading %s: %w", vwPath, err) + } - switch proof := c.Proof.(type) { - case *mtc.MerkleTreeProof: - fmt.Fprintf(w, "index\t%d\n", proof.Index()) + vw = new(mtc.SignedValidityWindow) + if err := vw.UnmarshalBinary(vwBuf, params); err != nil { + return fmt.Errorf("Parsing %s: %w", vwPath, err) + } + + verifyResult = c.Verify(mtc.VerifyOptions{ + ValidityWindow: &vw.ValidityWindow, + CA: params, + }) } - switch proof := c.Proof.(type) { - case *mtc.MerkleTreeProof: - path := proof.Path() - batch := &mtc.Batch{ - CA: params, - Number: tai.BatchNumber, - } + if inspect { + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + writeAssertion(w, c.Assertion) + fmt.Fprintf(w, "\n") - if !tai.Issuer.Equal(¶ms.Issuer) { - return fmt.Errorf( - "IssuerId doesn't match: %s ≠ %s", - params.Issuer, - tai.Issuer, - ) + fmt.Fprintf(w, "proof_type\t%v\n", params.ProofType) + fmt.Fprintf(w, "CA TAI\t%s\n", tai.Issuer) + fmt.Fprintf(w, "Batch number\t%d\n", tai.BatchNumber) + + if vw != nil { + vrs := "✅" + if verifyResult != nil { + vrs = verifyResult.Error() + } + + fmt.Fprintf(w, "Verification result\t%s\n", vrs) } - be := mtc.NewBatchEntry(c.Assertion, proof.NotAfter()) - head, err := batch.ComputeTreeHeadFromAuthenticationPath( - proof.Index(), - path, - &be, - ) - if err != nil { - return fmt.Errorf("computing tree head: %w", err) + + switch proof := c.Proof.(type) { + case *mtc.MerkleTreeProof: + fmt.Fprintf(w, "index\t%d\n", proof.Index()) } - fmt.Fprintf(w, "recomputed tree head\t%x\n", head) + switch proof := c.Proof.(type) { + case *mtc.MerkleTreeProof: + path := proof.Path() + batch := &mtc.Batch{ + CA: params, + Number: tai.BatchNumber, + } - w.Flush() - fmt.Printf("authentication path\n") - for i := 0; i < len(path)/mtc.HashLen; i++ { - fmt.Printf(" %x\n", path[i*mtc.HashLen:(i+1)*mtc.HashLen]) + if !tai.Issuer.Equal(¶ms.Issuer) { + return fmt.Errorf( + "IssuerId doesn't match: %s ≠ %s", + params.Issuer, + tai.Issuer, + ) + } + be := mtc.NewBatchEntry(c.Assertion, proof.NotAfter()) + head, err := batch.ComputeTreeHeadFromAuthenticationPath( + proof.Index(), + path, + &be, + ) + if err != nil { + return fmt.Errorf("computing tree head: %w", err) + } + + fmt.Fprintf(w, "recomputed tree head\t%x\n", head) + + w.Flush() + fmt.Printf("authentication path\n") + for i := 0; i < len(path)/mtc.HashLen; i++ { + fmt.Printf(" %x\n", path[i*mtc.HashLen:(i+1)*mtc.HashLen]) + } } + + w.Flush() + return nil } - w.Flush() - return nil + return verifyResult } func handleInspectAssertionRequest(cc *cli.Context) error { @@ -1319,6 +1375,13 @@ func main() { Usage: "parses a certificate", Action: handleInspectCert, ArgsUsage: "[path]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "validity-window", + Usage: "path to signed validity window to verify against", + Aliases: []string{"w"}, + }, + }, }, { Name: "umbilical-certificates", @@ -1355,6 +1418,24 @@ func main() { }, ), }, + { + Name: "verify", + Usage: "verifies a merkle tree certificate", + Action: handleVerify, + ArgsUsage: "[path]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "ca-params", + Usage: "path to CA parameters", + Aliases: []string{"p"}, + }, + &cli.StringFlag{ + Name: "validity-window", + Usage: "path to trusted signed validity window", + Aliases: []string{"w"}, + }, + }, + }, }, Before: func(cc *cli.Context) error { if path := cc.String("cpuprofile"); path != "" { diff --git a/mtc.go b/mtc.go index 6b4270f..a2def98 100644 --- a/mtc.go +++ b/mtc.go @@ -429,7 +429,13 @@ func (c *BikeshedCertificate) UnmarshalBinary(data []byte, caStore CAStore) erro if notAfter >= 1<<63 { return errors.New("timestamp too large") } - switch tai.ProofType(caStore) { + + params := caStore.Lookup(tai.Issuer) + if params == nil { + return fmt.Errorf("unknown CA with TAI %s", tai) + } + + switch params.ProofType { case MerkleTreeProofType: proof := &MerkleTreeProof{ notAfter: time.Unix(int64(notAfter), 0), @@ -1965,15 +1971,19 @@ func NewMerkleTreeProof(batch *Batch, index uint64, notAfter time.Time, } type CAStore interface { - Lookup(oid RelativeOID) CAParams + Lookup(oid RelativeOID) *CAParams } type LocalCAStore struct { store map[string]CAParams } -func (s *LocalCAStore) Lookup(oid RelativeOID) CAParams { - return s.store[oid.String()] +func (s *LocalCAStore) Lookup(oid RelativeOID) *CAParams { + ret, ok := s.store[oid.String()] + if !ok { + return nil + } + return &ret } func (s *LocalCAStore) Add(params CAParams) { @@ -1994,10 +2004,6 @@ type TrustAnchorIdentifier struct { type RelativeOID []byte -func (tai *TrustAnchorIdentifier) ProofType(store CAStore) ProofType { - return store.Lookup(tai.Issuer).ProofType -} - func (oid RelativeOID) segments() []uint32 { var res []uint32 cur := uint32(0) @@ -2127,6 +2133,10 @@ func (tai TrustAnchorIdentifier) MarshalBinary() ([]byte, error) { return b.Bytes() } +func (tai TrustAnchorIdentifier) String() string { + return fmt.Sprintf("%s.%d", tai.Issuer, tai.BatchNumber) +} + func (tai *TrustAnchorIdentifier) unmarshal(s *cryptobyte.String) error { var oidBytes []byte if !copyUint8LengthPrefixed(s, &oidBytes) || len(oidBytes) == 0 { From 87c5950345b95d40ad4e84bcf8efa09e74999373 Mon Sep 17 00:00:00 2001 From: Bas Westerbaan Date: Thu, 24 Apr 2025 15:28:49 +0200 Subject: [PATCH 2/3] Luke's suggestion on #70 Co-authored-by: Luke Valenta --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ab1a34..7d55078 100644 --- a/README.md +++ b/README.md @@ -443,7 +443,7 @@ $ echo $? Status code 0 means verification succeeded. For transparency, you should not get the signed validity window directly -from the CA, but rather from one or more mirrors (see below). +from the CA, but rather from one or more mirrors (see below). ### Run CA as server From 29fc7fe75f87f74e5e31538726ddd777228794d7 Mon Sep 17 00:00:00 2001 From: Bas Westerbaan Date: Thu, 24 Apr 2025 15:31:43 +0200 Subject: [PATCH 3/3] Merge similar switch statements --- cmd/mtc/main.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/mtc/main.go b/cmd/mtc/main.go index b8b33b1..abc23c0 100644 --- a/cmd/mtc/main.go +++ b/cmd/mtc/main.go @@ -967,10 +967,6 @@ func handleCert(cc *cli.Context, inspect bool) error { switch proof := c.Proof.(type) { case *mtc.MerkleTreeProof: fmt.Fprintf(w, "index\t%d\n", proof.Index()) - } - - switch proof := c.Proof.(type) { - case *mtc.MerkleTreeProof: path := proof.Path() batch := &mtc.Batch{ CA: params,