aboutsummaryrefslogtreecommitdiff
path: root/pkgs/yokey/crypto_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/yokey/crypto_test.go')
-rw-r--r--pkgs/yokey/crypto_test.go183
1 files changed, 183 insertions, 0 deletions
diff --git a/pkgs/yokey/crypto_test.go b/pkgs/yokey/crypto_test.go
new file mode 100644
index 0000000..6bedb72
--- /dev/null
+++ b/pkgs/yokey/crypto_test.go
@@ -0,0 +1,183 @@
+package main
+
+import (
+ "bytes"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/hex"
+ "strings"
+ "testing"
+
+ "golang.org/x/crypto/curve25519"
+ "golang.org/x/crypto/ssh"
+)
+
+const testMasterHex = "95609277cb88af80a624df3cb8bb130d49ac62707d16e925762dadbec32a1d45"
+
+func testMasterKey(t *testing.T) []byte {
+ t.Helper()
+
+ master, err := hex.DecodeString(testMasterHex)
+ if err != nil {
+ t.Fatalf("decode master key: %v", err)
+ }
+
+ return master
+}
+
+func parseEd25519PrivateKey(t *testing.T, pemBytes []byte) ed25519.PrivateKey {
+ t.Helper()
+
+ rawPriv, err := ssh.ParseRawPrivateKey(pemBytes)
+ if err != nil {
+ t.Fatalf("parse private key: %v", err)
+ }
+
+ switch key := rawPriv.(type) {
+ case ed25519.PrivateKey:
+ return key
+ case *ed25519.PrivateKey:
+ return *key
+ default:
+ t.Fatalf("unexpected private key type: %T", rawPriv)
+ return nil
+ }
+}
+
+func TestBuildInfo(t *testing.T) {
+ if got := buildInfo("ed25519-key-v1", ""); got != "ed25519-key-v1" {
+ t.Fatalf("empty context mismatch: %q", got)
+ }
+
+ if got := buildInfo("ed25519-key-v1", " "); got != "ed25519-key-v1" {
+ t.Fatalf("whitespace context mismatch: %q", got)
+ }
+
+ if got := buildInfo("ed25519-key-v1", "host:ganymede"); got != "ed25519-key-v1:host:ganymede" {
+ t.Fatalf("context append mismatch: %q", got)
+ }
+}
+
+func TestDeriveEd25519DeterministicAndScoped(t *testing.T) {
+ master := testMasterKey(t)
+
+ pubA1, privA1 := deriveEd25519(master, "host:ganymede")
+ pubA2, privA2 := deriveEd25519(master, "host:ganymede")
+ if pubA1 != pubA2 {
+ t.Fatalf("public key should be deterministic")
+ }
+
+ edPrivA1 := parseEd25519PrivateKey(t, privA1)
+ edPrivA2 := parseEd25519PrivateKey(t, privA2)
+
+ if !bytes.Equal(edPrivA1, edPrivA2) {
+ t.Fatalf("private key material should be deterministic")
+ }
+
+ pubB, privB := deriveEd25519(master, "host:mercury")
+ if pubA1 == pubB {
+ t.Fatalf("public keys should differ across contexts")
+ }
+
+ edPrivB := parseEd25519PrivateKey(t, privB)
+
+ if bytes.Equal(edPrivA1, edPrivB) {
+ t.Fatalf("private key material should differ across contexts")
+ }
+
+ if !strings.HasPrefix(pubA1, "ssh-ed25519 ") {
+ t.Fatalf("unexpected public key format: %q", pubA1)
+ }
+
+ if len(edPrivA1) != ed25519.PrivateKeySize {
+ t.Fatalf("unexpected private key size: %d", len(edPrivA1))
+ }
+}
+
+func TestDeriveWireGuardDeterministicAndScoped(t *testing.T) {
+ master := testMasterKey(t)
+
+ pubA1, privA1 := deriveWireGuard(master, "vpn:home")
+ pubA2, privA2 := deriveWireGuard(master, "vpn:home")
+ if pubA1 != pubA2 || privA1 != privA2 {
+ t.Fatalf("wireguard keys should be deterministic")
+ }
+
+ pubB, privB := deriveWireGuard(master, "vpn:office")
+ if pubA1 == pubB || privA1 == privB {
+ t.Fatalf("wireguard keys should differ across contexts")
+ }
+
+ privBytes, err := base64.StdEncoding.DecodeString(privA1)
+ if err != nil {
+ t.Fatalf("decode private key: %v", err)
+ }
+ if len(privBytes) != 32 {
+ t.Fatalf("private key should decode to 32 bytes, got %d", len(privBytes))
+ }
+
+ pubBytes, err := base64.StdEncoding.DecodeString(pubA1)
+ if err != nil {
+ t.Fatalf("decode public key: %v", err)
+ }
+ if len(pubBytes) != 32 {
+ t.Fatalf("public key should decode to 32 bytes, got %d", len(pubBytes))
+ }
+
+ recomputedPub, err := curve25519.X25519(privBytes, curve25519.Basepoint)
+ if err != nil {
+ t.Fatalf("recompute public key: %v", err)
+ }
+ if !bytes.Equal(pubBytes, recomputedPub) {
+ t.Fatalf("public key does not match private key")
+ }
+}
+
+func TestDerivePasswordDeterministicCharsetAndContext(t *testing.T) {
+ master := testMasterKey(t)
+
+ p1 := derivePassword(master, "forgejo")
+ p2 := derivePassword(master, "forgejo")
+ if p1 != p2 {
+ t.Fatalf("password should be deterministic")
+ }
+
+ if len(p1) != passwordLength {
+ t.Fatalf("password length mismatch: got %d want %d", len(p1), passwordLength)
+ }
+
+ for _, ch := range p1 {
+ if !strings.ContainsRune(passwordCharset, ch) {
+ t.Fatalf("password contains invalid character: %q", ch)
+ }
+ }
+
+ pCtxA := derivePasswordWithContext(master, "forgejo", "host:ganymede")
+ pCtxB := derivePasswordWithContext(master, "forgejo", "host:mercury")
+ if pCtxA == pCtxB {
+ t.Fatalf("context should change derived password")
+ }
+
+ pEmptyCtx := derivePasswordWithContext(master, "forgejo", "")
+ if p1 != pEmptyCtx {
+ t.Fatalf("empty context should match default password derivation")
+ }
+}
+
+func TestGenerateMasterKeyBytesLength(t *testing.T) {
+ key := generateMasterKeyBytes()
+ if len(key) != 32 {
+ t.Fatalf("master key should be 32 bytes, got %d", len(key))
+ }
+
+ allZero := true
+ for _, b := range key {
+ if b != 0 {
+ allZero = false
+ break
+ }
+ }
+ if allZero {
+ t.Fatalf("generated master key should not be all zeros")
+ }
+}