package doorkey import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/json" "strings" "testing" "time" ) func rec1(entry Entry) Record { now := time.Now().UTC() return Record{V: 1, Account: "bae", App: "titogo", Revision: 1, Keys: []Entry{entry}, SignedAt: now, ExpiresAt: now.Add(Lifetime)} } func TestTokenRoundTripAndAdmit(t *testing.T) { raw, entry, err := NewToken("bae", "titogo") if err != nil { t.Fatal(err) } if !strings.HasPrefix(raw, "cdk1.bae.titogo.") { t.Fatalf("token %q lacks its prefix", raw) } tok, err := ParseToken(raw) if err != nil || tok.ID != entry.ID { t.Fatalf("parse %q: %+v %v", raw, tok, err) } if !rec1(entry).Admits(tok) { t.Fatal("the record does not admit its own key") } tok.Secret = strings.Repeat("A", 43) if rec1(entry).Admits(tok) { t.Fatal("a wrong secret was admitted") } } func TestRecordAdmitsOnlyItsOwnAccountAndApp(t *testing.T) { raw, entry, _ := NewToken("bae", "titogo") tok, _ := ParseToken(raw) other := rec1(entry) other.App = "other" if other.Admits(tok) { t.Fatal("a record for another app admitted the key") } } func TestParseTokenRefusesMalformed(t *testing.T) { good := "0123456789abcdef." + strings.Repeat("A", 43) for _, raw := range []string{ "", "cdk1", "cdk2.bae.titogo." + good, "cdk1.BAE.titogo." + good, "cdk1.bae.titogo.0123456789ABCDEF." + strings.Repeat("A", 43), "cdk1.bae.titogo.0123456789abcdef.short", "cdk1.bae.titogo." + good + ".extra", } { if _, err := ParseToken(raw); err == nil { t.Errorf("ParseToken(%q) accepted a malformed token", raw) } } } func TestSignVerifyExpiryAndKeys(t *testing.T) { pub, priv, _ := ed25519.GenerateKey(rand.Reader) _, entry, _ := NewToken("bae", "titogo") wire, err := Sign(priv, rec1(entry)) if err != nil { t.Fatal(err) } got, err := Verify(pub, wire, time.Now()) if err != nil || got.Revision != 1 || got.Keys[0].ID != entry.ID { t.Fatalf("Verify = %+v, %v", got, err) } if _, err := Verify(pub, wire, time.Now().Add(Lifetime+time.Minute)); err == nil { t.Fatal("an expired record verified") } otherPub, _, _ := ed25519.GenerateKey(rand.Reader) if _, err := Verify(otherPub, wire, time.Now()); err == nil { t.Fatal("a record signed by another key verified") } } // A record signed more than MaxClockSkew ahead of the verifier's clock is // refused, so a future-dated record cannot outlive Lifetime from now. func TestVerifyRefusesAFutureDatedRecord(t *testing.T) { pub, priv, _ := ed25519.GenerateKey(rand.Reader) _, entry, _ := NewToken("bae", "titogo") r := rec1(entry) r.SignedAt = time.Now().Add(time.Hour) r.ExpiresAt = r.SignedAt.Add(Lifetime) wire, err := Sign(priv, r) if err != nil { t.Fatal(err) } if _, err := Verify(pub, wire, time.Now()); err == nil { t.Fatal("a record signed an hour ahead verified") } r.SignedAt = time.Now().Add(time.Minute) r.ExpiresAt = r.SignedAt.Add(Lifetime) wire, _ = Sign(priv, r) if _, err := Verify(pub, wire, time.Now()); err != nil { t.Fatalf("a record a minute ahead (within skew) was refused: %v", err) } } // VerifyRevision is the publisher's read: signature and shape, not expiry, // so an expired record still yields the revision it carried. func TestVerifyRevisionIgnoresExpiry(t *testing.T) { pub, priv, _ := ed25519.GenerateKey(rand.Reader) _, entry, _ := NewToken("bae", "titogo") r := rec1(entry) r.Revision = 7 r.SignedAt = time.Now().Add(-100 * time.Hour) r.ExpiresAt = r.SignedAt.Add(Lifetime) wire, err := Sign(priv, r) if err != nil { t.Fatal(err) } if rev, err := VerifyRevision(pub, wire); err != nil || rev != 7 { t.Fatalf("VerifyRevision = %d, %v; want 7", rev, err) } if _, err := VerifyRevision(pub, []byte("garbage")); err == nil { t.Fatal("VerifyRevision accepted garbage") } } // The domain prefix: a wrapper signed over the bare payload (every other // root-signed record's shape) must not verify as a door-key record. func TestVerifyRefusesABareSignedWrapper(t *testing.T) { pub, priv, _ := ed25519.GenerateKey(rand.Reader) _, entry, _ := NewToken("bae", "titogo") payload, _ := json.Marshal(rec1(entry)) b64 := base64.StdEncoding.EncodeToString(payload) wire, _ := json.Marshal(map[string]string{"payload": b64, "sig": base64.StdEncoding.EncodeToString(ed25519.Sign(priv, []byte(b64)))}) if _, err := Verify(pub, wire, time.Now()); err == nil { t.Fatal("a bare-signed wrapper verified as a door-key record") } // And the other direction: a door-key record's signature does not // verify over its bare payload. door, _ := Sign(priv, rec1(entry)) var w struct{ Payload, Sig string } if err := json.Unmarshal(door, &w); err != nil { t.Fatal(err) } sig, _ := base64.StdEncoding.DecodeString(w.Sig) if ed25519.Verify(pub, []byte(w.Payload), sig) { t.Fatal("a door-key signature verifies over the bare payload") } } func TestSignRefusesTooManyKeys(t *testing.T) { _, priv, _ := ed25519.GenerateKey(rand.Reader) _, e, _ := NewToken("bae", "titogo") r := rec1(e) for i := 0; i < MaxKeys; i++ { _, e, _ := NewToken("bae", "titogo") r.Keys = append(r.Keys, e) } if _, err := Sign(priv, r); err == nil { t.Fatal("signed a record over MaxKeys") } }