| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package ssh |
| |
| import ( |
| "bytes" |
| "crypto/dsa" |
| "crypto/ecdsa" |
| "crypto/ed25519" |
| "crypto/elliptic" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/sha256" |
| "crypto/x509" |
| "encoding/base64" |
| "encoding/hex" |
| "encoding/pem" |
| "errors" |
| "fmt" |
| "io" |
| "math/big" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "golang.org/x/crypto/ssh/testdata" |
| ) |
| |
| func rawKey(pub PublicKey) interface{} { |
| switch k := pub.(type) { |
| case *rsaPublicKey: |
| return (*rsa.PublicKey)(k) |
| case *dsaPublicKey: |
| return (*dsa.PublicKey)(k) |
| case *ecdsaPublicKey: |
| return (*ecdsa.PublicKey)(k) |
| case ed25519PublicKey: |
| return (ed25519.PublicKey)(k) |
| case *Certificate: |
| return k |
| } |
| panic("unknown key type") |
| } |
| |
| func TestKeyMarshalParse(t *testing.T) { |
| for _, priv := range testSigners { |
| pub := priv.PublicKey() |
| roundtrip, err := ParsePublicKey(pub.Marshal()) |
| if err != nil { |
| t.Errorf("ParsePublicKey(%T): %v", pub, err) |
| } |
| |
| k1 := rawKey(pub) |
| k2 := rawKey(roundtrip) |
| |
| if !reflect.DeepEqual(k1, k2) { |
| t.Errorf("got %#v in roundtrip, want %#v", k2, k1) |
| } |
| } |
| } |
| |
| func TestParsePublicKeyWithSigningAlgoAsKeyFormat(t *testing.T) { |
| key := []byte(`rsa-sha2-256 AAAADHJzYS1zaGEyLTI1NgAAAAMBAAEAAAEBAJ7qMyjLXEJCCJmRknuCLo0uPi5GrPY5pQYr84lhlN8Gor5KVL2LKYCW4e70r5xzj7SrHHSCft1FMlYg1KDO9xrprJh733kQqAPWETmSuH0EfRtGtcH6EarKyVxk6As076/yNiiMKVBtG0RPa1L7FviTfcYK4vnCCVrbv3RmA5CCzuG5BSMbRLxzVb4Ri3p8jhxYT8N4QGe/2yqvJLys5vQ9szpZR3tcFp3DJIVZhBRfR6LnoY23XZniAAMQaUVBX86dXQ++dNwAwZSXSt9Og+AniOCiBYqhNVa5n3DID/H7YtEtG+CbZr3r2KD3fv8AfSLRar4XOp8rsRdD31h/kr8=`) |
| _, _, _, _, err := ParseAuthorizedKey(key) |
| if err == nil { |
| t.Fatal("parsing a public key using a signature algorithm as the key format succeeded unexpectedly") |
| } |
| if !strings.Contains(err.Error(), `signature algorithm "rsa-sha2-256" isn't a key format`) { |
| t.Errorf(`got %v, expected 'signature algorithm "rsa-sha2-256" isn't a key format'`, err) |
| } |
| } |
| |
| func TestUnsupportedCurves(t *testing.T) { |
| raw, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) |
| if err != nil { |
| t.Fatalf("GenerateKey: %v", err) |
| } |
| |
| if _, err = NewSignerFromKey(raw); err == nil || !strings.Contains(err.Error(), "only P-256") { |
| t.Fatalf("NewPrivateKey should not succeed with P-224, got: %v", err) |
| } |
| |
| if _, err = NewPublicKey(&raw.PublicKey); err == nil || !strings.Contains(err.Error(), "only P-256") { |
| t.Fatalf("NewPublicKey should not succeed with P-224, got: %v", err) |
| } |
| } |
| |
| func TestNewPublicKey(t *testing.T) { |
| for _, k := range testSigners { |
| raw := rawKey(k.PublicKey()) |
| // Skip certificates, as NewPublicKey does not support them. |
| if _, ok := raw.(*Certificate); ok { |
| continue |
| } |
| pub, err := NewPublicKey(raw) |
| if err != nil { |
| t.Errorf("NewPublicKey(%#v): %v", raw, err) |
| } |
| if !reflect.DeepEqual(k.PublicKey(), pub) { |
| t.Errorf("NewPublicKey(%#v) = %#v, want %#v", raw, pub, k.PublicKey()) |
| } |
| } |
| } |
| |
| func TestKeySignVerify(t *testing.T) { |
| for _, priv := range testSigners { |
| pub := priv.PublicKey() |
| |
| data := []byte("sign me") |
| sig, err := priv.Sign(rand.Reader, data) |
| if err != nil { |
| t.Fatalf("Sign(%T): %v", priv, err) |
| } |
| |
| if err := pub.Verify(data, sig); err != nil { |
| t.Errorf("publicKey.Verify(%T): %v", priv, err) |
| } |
| sig.Blob[5]++ |
| if err := pub.Verify(data, sig); err == nil { |
| t.Errorf("publicKey.Verify on broken sig did not fail") |
| } |
| } |
| } |
| |
| func TestKeySignWithAlgorithmVerify(t *testing.T) { |
| for k, priv := range testSigners { |
| if algorithmSigner, ok := priv.(MultiAlgorithmSigner); !ok { |
| t.Errorf("Signers %q constructed by ssh package should always implement the MultiAlgorithmSigner interface: %T", k, priv) |
| } else { |
| pub := priv.PublicKey() |
| data := []byte("sign me") |
| |
| signWithAlgTestCase := func(algorithm string, expectedAlg string) { |
| sig, err := algorithmSigner.SignWithAlgorithm(rand.Reader, data, algorithm) |
| if err != nil { |
| t.Fatalf("Sign(%T): %v", priv, err) |
| } |
| if sig.Format != expectedAlg { |
| t.Errorf("signature format did not match requested signature algorithm: %s != %s", sig.Format, expectedAlg) |
| } |
| |
| if err := pub.Verify(data, sig); err != nil { |
| t.Errorf("publicKey.Verify(%T): %v", priv, err) |
| } |
| sig.Blob[5]++ |
| if err := pub.Verify(data, sig); err == nil { |
| t.Errorf("publicKey.Verify on broken sig did not fail") |
| } |
| } |
| |
| // Using the empty string as the algorithm name should result in the same signature format as the algorithm-free Sign method. |
| defaultSig, err := priv.Sign(rand.Reader, data) |
| if err != nil { |
| t.Fatalf("Sign(%T): %v", priv, err) |
| } |
| signWithAlgTestCase("", defaultSig.Format) |
| |
| // RSA keys are the only ones which currently support more than one signing algorithm |
| if pub.Type() == KeyAlgoRSA { |
| for _, algorithm := range []string{KeyAlgoRSA, KeyAlgoRSASHA256, KeyAlgoRSASHA512} { |
| signWithAlgTestCase(algorithm, algorithm) |
| } |
| } |
| } |
| } |
| } |
| |
| func TestKeySignWithShortSignature(t *testing.T) { |
| signer := testSigners["rsa"].(AlgorithmSigner) |
| pub := signer.PublicKey() |
| // Note: data obtained by empirically trying until a result |
| // starting with 0 appeared |
| tests := []struct { |
| algorithm string |
| data []byte |
| }{ |
| { |
| algorithm: KeyAlgoRSA, |
| data: []byte("sign me92"), |
| }, |
| { |
| algorithm: KeyAlgoRSASHA256, |
| data: []byte("sign me294"), |
| }, |
| { |
| algorithm: KeyAlgoRSASHA512, |
| data: []byte("sign me60"), |
| }, |
| } |
| |
| for _, tt := range tests { |
| sig, err := signer.SignWithAlgorithm(rand.Reader, tt.data, tt.algorithm) |
| if err != nil { |
| t.Fatalf("Sign(%T): %v", signer, err) |
| } |
| if sig.Blob[0] != 0 { |
| t.Errorf("%s: Expected signature with a leading 0", tt.algorithm) |
| } |
| sig.Blob = sig.Blob[1:] |
| if err := pub.Verify(tt.data, sig); err != nil { |
| t.Errorf("publicKey.Verify(%s): %v", tt.algorithm, err) |
| } |
| } |
| } |
| |
| func TestParseRSAPrivateKey(t *testing.T) { |
| key := testPrivateKeys["rsa"] |
| |
| rsa, ok := key.(*rsa.PrivateKey) |
| if !ok { |
| t.Fatalf("got %T, want *rsa.PrivateKey", rsa) |
| } |
| |
| if err := rsa.Validate(); err != nil { |
| t.Errorf("Validate: %v", err) |
| } |
| } |
| |
| func TestParseRSAModulusTooLarge(t *testing.T) { |
| rsa16384 := "ssh-rsa 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" |
| _, _, _, _, err := ParseAuthorizedKey([]byte(rsa16384)) |
| |
| if err == nil { |
| t.Fatal("ParseAuthorizedKey accepted a 16384-bit modulus; expected it to be rejected") |
| } |
| |
| expectedError := "rsa modulus too large" |
| if !strings.Contains(err.Error(), expectedError) { |
| t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError) |
| } |
| } |
| |
| func TestParseECPrivateKey(t *testing.T) { |
| key := testPrivateKeys["ecdsa"] |
| |
| ecKey, ok := key.(*ecdsa.PrivateKey) |
| if !ok { |
| t.Fatalf("got %T, want *ecdsa.PrivateKey", ecKey) |
| } |
| |
| if !validateECPublicKey(ecKey.Curve, ecKey.X, ecKey.Y) { |
| t.Fatalf("public key does not validate.") |
| } |
| } |
| |
| func TestParseEncryptedPrivateKeysWithPassphrase(t *testing.T) { |
| data := []byte("sign me") |
| for _, tt := range testdata.PEMEncryptedKeys { |
| t.Run(tt.Name, func(t *testing.T) { |
| _, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte("incorrect")) |
| if err != x509.IncorrectPasswordError { |
| t.Errorf("got %v want IncorrectPasswordError", err) |
| } |
| |
| s, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey)) |
| if err != nil { |
| t.Fatalf("ParsePrivateKeyWithPassphrase returned error: %s", err) |
| } |
| |
| sig, err := s.Sign(rand.Reader, data) |
| if err != nil { |
| t.Fatalf("Signer.Sign: %v", err) |
| } |
| if err := s.PublicKey().Verify(data, sig); err != nil { |
| t.Errorf("Verify failed: %v", err) |
| } |
| |
| _, err = ParsePrivateKey(tt.PEMBytes) |
| if err == nil { |
| t.Fatalf("ParsePrivateKey succeeded, expected an error") |
| } |
| |
| if err, ok := err.(*PassphraseMissingError); !ok { |
| t.Errorf("got error %q, want PassphraseMissingError", err) |
| } else if tt.IncludesPublicKey { |
| if err.PublicKey == nil { |
| t.Fatalf("expected PassphraseMissingError.PublicKey not to be nil") |
| } |
| got, want := err.PublicKey.Marshal(), s.PublicKey().Marshal() |
| if !bytes.Equal(got, want) { |
| t.Errorf("error field %q doesn't match signer public key %q", got, want) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) { |
| for _, tt := range testdata.UnsupportedCipherData { |
| t.Run(tt.Name, func(t *testing.T) { |
| _, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey)) |
| if err == nil { |
| t.Fatalf("expected 'unknown cipher' error for %q, got nil", tt.Name) |
| // If this cipher is now supported, remove it from testdata.UnsupportedCipherData |
| } |
| if !strings.Contains(err.Error(), "unknown cipher") { |
| t.Errorf("wanted 'unknown cipher' error, got %v", err.Error()) |
| } |
| }) |
| } |
| } |
| |
| func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) { |
| pem := testdata.PEMEncryptedKeys[0].PEMBytes |
| for i := 0; i < 4096; i++ { |
| _, err := ParseRawPrivateKeyWithPassphrase(pem, []byte(fmt.Sprintf("%d", i))) |
| if !errors.Is(err, x509.IncorrectPasswordError) { |
| t.Fatalf("expected error: %v, got: %v", x509.IncorrectPasswordError, err) |
| } |
| } |
| } |
| |
| func TestParseDSA(t *testing.T) { |
| // We actually exercise the ParsePrivateKey codepath here, as opposed to |
| // using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go |
| // uses. |
| s, err := ParsePrivateKey(testdata.PEMBytes["dsa"]) |
| if err != nil { |
| t.Fatalf("ParsePrivateKey returned error: %s", err) |
| } |
| |
| data := []byte("sign me") |
| sig, err := s.Sign(rand.Reader, data) |
| if err != nil { |
| t.Fatalf("dsa.Sign: %v", err) |
| } |
| |
| if err := s.PublicKey().Verify(data, sig); err != nil { |
| t.Errorf("Verify failed: %v", err) |
| } |
| } |
| |
| // Tests for authorized_keys parsing. |
| |
| // getTestKey returns a public key, and its base64 encoding. |
| func getTestKey() (PublicKey, string) { |
| k := testPublicKeys["rsa"] |
| |
| b := &bytes.Buffer{} |
| e := base64.NewEncoder(base64.StdEncoding, b) |
| e.Write(k.Marshal()) |
| e.Close() |
| |
| return k, b.String() |
| } |
| |
| func TestMarshalParsePublicKey(t *testing.T) { |
| pub, pubSerialized := getTestKey() |
| line := fmt.Sprintf("%s %s user@host", pub.Type(), pubSerialized) |
| |
| authKeys := MarshalAuthorizedKey(pub) |
| actualFields := strings.Fields(string(authKeys)) |
| if len(actualFields) == 0 { |
| t.Fatalf("failed authKeys: %v", authKeys) |
| } |
| |
| // drop the comment |
| expectedFields := strings.Fields(line)[0:2] |
| |
| if !reflect.DeepEqual(actualFields, expectedFields) { |
| t.Errorf("got %v, expected %v", actualFields, expectedFields) |
| } |
| |
| actPub, _, _, _, err := ParseAuthorizedKey([]byte(line)) |
| if err != nil { |
| t.Fatalf("cannot parse %v: %v", line, err) |
| } |
| if !reflect.DeepEqual(actPub, pub) { |
| t.Errorf("got %v, expected %v", actPub, pub) |
| } |
| } |
| |
| func TestParseDSAHugeQ(t *testing.T) { |
| P := new(big.Int).Lsh(big.NewInt(1), 1023) |
| Q := new(big.Int).Lsh(big.NewInt(1), 20000) // very large |
| // G and Y: Dummy values, just needs to be < P to pass that specific check |
| G := big.NewInt(2) |
| Y := big.NewInt(5) |
| |
| rawKey := struct { |
| P, Q, G, Y *big.Int |
| }{ |
| P: P, |
| Q: Q, |
| G: G, |
| Y: Y, |
| } |
| |
| inputBytes := Marshal(&rawKey) |
| |
| _, _, err := parseDSA(inputBytes) |
| if err == nil { |
| t.Fatal("parseDSA accepted a DSA key with large Q") |
| } |
| |
| expectedError := "ssh: unsupported DSA sub-prime size" |
| if !strings.Contains(err.Error(), expectedError) { |
| t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError) |
| } |
| } |
| |
| func TestParseDSAYOutOfRange(t *testing.T) { |
| // Valid 1024/160 parameters (values don't need to be a real DSA group, |
| // they only need to pass the checkDSAParams bit-length checks and the |
| // G < P / G > 0 checks). |
| P := new(big.Int).Lsh(big.NewInt(1), 1023) |
| P.SetBit(P, 0, 1) // make P odd so it can pass as a prime candidate shape |
| Q := new(big.Int).Lsh(big.NewInt(1), 159) |
| Q.SetBit(Q, 0, 1) |
| G := big.NewInt(2) |
| |
| for _, tc := range []struct { |
| name string |
| Y *big.Int |
| }{ |
| {"Y_zero", big.NewInt(0)}, |
| {"Y_negative", big.NewInt(-1)}, |
| {"Y_equals_P", new(big.Int).Set(P)}, |
| {"Y_greater_than_P", new(big.Int).Add(P, big.NewInt(1))}, |
| {"Y_much_greater_than_P", new(big.Int).Lsh(big.NewInt(1), 20000)}, |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| rawKey := struct { |
| P, Q, G, Y *big.Int |
| }{P: P, Q: Q, G: G, Y: tc.Y} |
| |
| _, _, err := parseDSA(Marshal(&rawKey)) |
| if err == nil { |
| t.Fatalf("parseDSA accepted a DSA key with Y=%s (P=%s)", tc.Y, P) |
| } |
| expectedError := "DSA public value Y out of range" |
| if !strings.Contains(err.Error(), expectedError) { |
| t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError) |
| } |
| }) |
| } |
| } |
| |
| func TestMarshalPrivateKey(t *testing.T) { |
| tests := []struct { |
| name string |
| }{ |
| {"rsa-openssh-format"}, |
| {"ed25519"}, |
| {"p256-openssh-format"}, |
| {"p384-openssh-format"}, |
| {"p521-openssh-format"}, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| expected, ok := testPrivateKeys[tt.name] |
| if !ok { |
| t.Fatalf("cannot find key %s", tt.name) |
| } |
| |
| block, err := MarshalPrivateKey(expected, "test@golang.org") |
| if err != nil { |
| t.Fatalf("cannot marshal %s: %v", tt.name, err) |
| } |
| |
| key, err := ParseRawPrivateKey(pem.EncodeToMemory(block)) |
| if err != nil { |
| t.Fatalf("cannot parse %s: %v", tt.name, err) |
| } |
| |
| if !reflect.DeepEqual(expected, key) { |
| t.Errorf("unexpected marshaled key %s", tt.name) |
| } |
| }) |
| } |
| } |
| |
| func TestMarshalPrivateKeyWithPassphrase(t *testing.T) { |
| tests := []struct { |
| name string |
| }{ |
| {"rsa-openssh-format"}, |
| {"ed25519"}, |
| {"p256-openssh-format"}, |
| {"p384-openssh-format"}, |
| {"p521-openssh-format"}, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| expected, ok := testPrivateKeys[tt.name] |
| if !ok { |
| t.Fatalf("cannot find key %s", tt.name) |
| } |
| |
| block, err := MarshalPrivateKeyWithPassphrase(expected, "test@golang.org", []byte("test-passphrase")) |
| if err != nil { |
| t.Fatalf("cannot marshal %s: %v", tt.name, err) |
| } |
| |
| key, err := ParseRawPrivateKeyWithPassphrase(pem.EncodeToMemory(block), []byte("test-passphrase")) |
| if err != nil { |
| t.Fatalf("cannot parse %s: %v", tt.name, err) |
| } |
| |
| if !reflect.DeepEqual(expected, key) { |
| t.Errorf("unexpected marshaled key %s", tt.name) |
| } |
| }) |
| } |
| } |
| |
| type testAuthResult struct { |
| pubKey PublicKey |
| options []string |
| comments string |
| rest string |
| ok bool |
| } |
| |
| func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []testAuthResult) { |
| rest := authKeys |
| var values []testAuthResult |
| for len(rest) > 0 { |
| var r testAuthResult |
| var err error |
| r.pubKey, r.comments, r.options, rest, err = ParseAuthorizedKey(rest) |
| r.ok = (err == nil) |
| t.Log(err) |
| r.rest = string(rest) |
| values = append(values, r) |
| } |
| |
| if !reflect.DeepEqual(values, expected) { |
| t.Errorf("got %#v, expected %#v", values, expected) |
| } |
| } |
| |
| func TestAuthorizedKeyBasic(t *testing.T) { |
| pub, pubSerialized := getTestKey() |
| line := "ssh-rsa " + pubSerialized + " user@host" |
| testAuthorizedKeys(t, []byte(line), |
| []testAuthResult{ |
| {pub, nil, "user@host", "", true}, |
| }) |
| } |
| |
| func TestAuth(t *testing.T) { |
| pub, pubSerialized := getTestKey() |
| authWithOptions := []string{ |
| `# comments to ignore before any keys...`, |
| ``, |
| `env="HOME=/home/root",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`, |
| `# comments to ignore, along with a blank line`, |
| ``, |
| `env="HOME=/home/root2" ssh-rsa ` + pubSerialized + ` user2@host2`, |
| ``, |
| `# more comments, plus a invalid entry`, |
| `ssh-rsa data-that-will-not-parse user@host3`, |
| } |
| for _, eol := range []string{"\n", "\r\n"} { |
| authOptions := strings.Join(authWithOptions, eol) |
| rest2 := strings.Join(authWithOptions[3:], eol) |
| rest3 := strings.Join(authWithOptions[6:], eol) |
| testAuthorizedKeys(t, []byte(authOptions), []testAuthResult{ |
| {pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true}, |
| {pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true}, |
| {nil, nil, "", "", false}, |
| }) |
| } |
| } |
| |
| func TestAuthWithQuotedSpaceInEnv(t *testing.T) { |
| pub, pubSerialized := getTestKey() |
| authWithQuotedSpaceInEnv := []byte(`env="HOME=/home/root dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`) |
| testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []testAuthResult{ |
| {pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true}, |
| }) |
| } |
| |
| func TestAuthWithQuotedCommaInEnv(t *testing.T) { |
| pub, pubSerialized := getTestKey() |
| authWithQuotedCommaInEnv := []byte(`env="HOME=/home/root,dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`) |
| testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []testAuthResult{ |
| {pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true}, |
| }) |
| } |
| |
| func TestAuthWithQuotedQuoteInEnv(t *testing.T) { |
| pub, pubSerialized := getTestKey() |
| authWithQuotedQuoteInEnv := []byte(`env="HOME=/home/\"root dir",no-port-forwarding` + "\t" + `ssh-rsa` + "\t" + pubSerialized + ` user@host`) |
| authWithDoubleQuotedQuote := []byte(`no-port-forwarding,env="HOME=/home/ \"root dir\"" ssh-rsa ` + pubSerialized + "\t" + `user@host`) |
| testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []testAuthResult{ |
| {pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true}, |
| }) |
| |
| testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []testAuthResult{ |
| {pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true}, |
| }) |
| } |
| |
| func TestAuthWithInvalidSpace(t *testing.T) { |
| _, pubSerialized := getTestKey() |
| authWithInvalidSpace := []byte(`env="HOME=/home/root dir", no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host |
| #more to follow but still no valid keys`) |
| testAuthorizedKeys(t, []byte(authWithInvalidSpace), []testAuthResult{ |
| {nil, nil, "", "", false}, |
| }) |
| } |
| |
| func TestAuthWithMissingQuote(t *testing.T) { |
| pub, pubSerialized := getTestKey() |
| authWithMissingQuote := []byte(`env="HOME=/home/root,no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host |
| env="HOME=/home/root",shared-control ssh-rsa ` + pubSerialized + ` user@host`) |
| |
| testAuthorizedKeys(t, []byte(authWithMissingQuote), []testAuthResult{ |
| {pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true}, |
| }) |
| } |
| |
| func TestInvalidEntry(t *testing.T) { |
| authInvalid := []byte(`ssh-rsa`) |
| _, _, _, _, err := ParseAuthorizedKey(authInvalid) |
| if err == nil { |
| t.Errorf("got valid entry for %q", authInvalid) |
| } |
| } |
| |
| var knownHostsParseTests = []struct { |
| input string |
| err string |
| |
| marker string |
| comment string |
| hosts []string |
| rest string |
| }{ |
| { |
| "", |
| "EOF", |
| |
| "", "", nil, "", |
| }, |
| { |
| "# Just a comment", |
| "EOF", |
| |
| "", "", nil, "", |
| }, |
| { |
| " \t ", |
| "EOF", |
| |
| "", "", nil, "", |
| }, |
| { |
| "localhost ssh-rsa {RSAPUB}", |
| "", |
| |
| "", "", []string{"localhost"}, "", |
| }, |
| { |
| "localhost\tssh-rsa {RSAPUB}", |
| "", |
| |
| "", "", []string{"localhost"}, "", |
| }, |
| { |
| "localhost\tssh-rsa {RSAPUB}\tcomment comment", |
| "", |
| |
| "", "comment comment", []string{"localhost"}, "", |
| }, |
| { |
| "localhost\tssh-rsa {RSAPUB}\tcomment comment\n", |
| "", |
| |
| "", "comment comment", []string{"localhost"}, "", |
| }, |
| { |
| "localhost\tssh-rsa {RSAPUB}\tcomment comment\r\n", |
| "", |
| |
| "", "comment comment", []string{"localhost"}, "", |
| }, |
| { |
| "localhost\tssh-rsa {RSAPUB}\tcomment comment\r\nnext line", |
| "", |
| |
| "", "comment comment", []string{"localhost"}, "next line", |
| }, |
| { |
| "localhost,[host2:123]\tssh-rsa {RSAPUB}\tcomment comment", |
| "", |
| |
| "", "comment comment", []string{"localhost", "[host2:123]"}, "", |
| }, |
| { |
| "@marker \tlocalhost,[host2:123]\tssh-rsa {RSAPUB}", |
| "", |
| |
| "marker", "", []string{"localhost", "[host2:123]"}, "", |
| }, |
| { |
| "@marker \tlocalhost,[host2:123]\tssh-rsa aabbccdd", |
| "short read", |
| |
| "", "", nil, "", |
| }, |
| } |
| |
| func TestKnownHostsParsing(t *testing.T) { |
| rsaPub, rsaPubSerialized := getTestKey() |
| |
| for i, test := range knownHostsParseTests { |
| var expectedKey PublicKey |
| const rsaKeyToken = "{RSAPUB}" |
| |
| input := test.input |
| if strings.Contains(input, rsaKeyToken) { |
| expectedKey = rsaPub |
| input = strings.Replace(test.input, rsaKeyToken, rsaPubSerialized, -1) |
| } |
| |
| marker, hosts, pubKey, comment, rest, err := ParseKnownHosts([]byte(input)) |
| if err != nil { |
| if len(test.err) == 0 { |
| t.Errorf("#%d: unexpectedly failed with %q", i, err) |
| } else if !strings.Contains(err.Error(), test.err) { |
| t.Errorf("#%d: expected error containing %q, but got %q", i, test.err, err) |
| } |
| continue |
| } else if len(test.err) != 0 { |
| t.Errorf("#%d: succeeded but expected error including %q", i, test.err) |
| continue |
| } |
| |
| if !reflect.DeepEqual(expectedKey, pubKey) { |
| t.Errorf("#%d: expected key %#v, but got %#v", i, expectedKey, pubKey) |
| } |
| |
| if marker != test.marker { |
| t.Errorf("#%d: expected marker %q, but got %q", i, test.marker, marker) |
| } |
| |
| if comment != test.comment { |
| t.Errorf("#%d: expected comment %q, but got %q", i, test.comment, comment) |
| } |
| |
| if !reflect.DeepEqual(test.hosts, hosts) { |
| t.Errorf("#%d: expected hosts %#v, but got %#v", i, test.hosts, hosts) |
| } |
| |
| if rest := string(rest); rest != test.rest { |
| t.Errorf("#%d: expected remaining input to be %q, but got %q", i, test.rest, rest) |
| } |
| } |
| } |
| |
| func TestFingerprintLegacyMD5(t *testing.T) { |
| pub, _ := getTestKey() |
| fingerprint := FingerprintLegacyMD5(pub) |
| want := "b7:ef:d3:d5:89:29:52:96:9f:df:47:41:4d:15:37:f4" // ssh-keygen -lf -E md5 rsa |
| if fingerprint != want { |
| t.Errorf("got fingerprint %q want %q", fingerprint, want) |
| } |
| } |
| |
| func TestFingerprintSHA256(t *testing.T) { |
| pub, _ := getTestKey() |
| fingerprint := FingerprintSHA256(pub) |
| want := "SHA256:fi5+D7UmDZDE9Q2sAVvvlpcQSIakN4DERdINgXd2AnE" // ssh-keygen -lf rsa |
| if fingerprint != want { |
| t.Errorf("got fingerprint %q want %q", fingerprint, want) |
| } |
| } |
| |
| func TestInvalidKeys(t *testing.T) { |
| keyTypes := []string{ |
| "RSA PRIVATE KEY", |
| "PRIVATE KEY", |
| "EC PRIVATE KEY", |
| "DSA PRIVATE KEY", |
| "OPENSSH PRIVATE KEY", |
| } |
| |
| for _, keyType := range keyTypes { |
| for _, dataLen := range []int{0, 1, 2, 5, 10, 20} { |
| data := make([]byte, dataLen) |
| if _, err := io.ReadFull(rand.Reader, data); err != nil { |
| t.Fatal(err) |
| } |
| |
| var buf bytes.Buffer |
| pem.Encode(&buf, &pem.Block{ |
| Type: keyType, |
| Bytes: data, |
| }) |
| |
| // This test is just to ensure that the function |
| // doesn't panic so the return value is ignored. |
| ParseRawPrivateKey(buf.Bytes()) |
| } |
| } |
| } |
| |
| func TestSKKeys(t *testing.T) { |
| for _, d := range testdata.SKData { |
| pk, _, _, _, err := ParseAuthorizedKey(d.PubKey) |
| if err != nil { |
| t.Fatalf("parseAuthorizedKey returned error: %v", err) |
| } |
| |
| sigBuf := make([]byte, hex.DecodedLen(len(d.HexSignature))) |
| if _, err := hex.Decode(sigBuf, d.HexSignature); err != nil { |
| t.Fatalf("hex.Decode() failed: %v", err) |
| } |
| |
| dataBuf := make([]byte, hex.DecodedLen(len(d.HexData))) |
| if _, err := hex.Decode(dataBuf, d.HexData); err != nil { |
| t.Fatalf("hex.Decode() failed: %v", err) |
| } |
| |
| sig, _, ok := parseSignature(sigBuf) |
| if !ok { |
| t.Fatalf("parseSignature(%v) failed", sigBuf) |
| } |
| |
| // Test that good data and signature pass verification |
| if err := pk.Verify(dataBuf, sig); err != nil { |
| t.Errorf("%s: PublicKey.Verify(%v, %v) failed: %v", d.Name, dataBuf, sig, err) |
| } |
| |
| // Invalid data being passed in |
| invalidData := []byte("INVALID DATA") |
| if err := pk.Verify(invalidData, sig); err == nil { |
| t.Errorf("%s with invalid data: PublicKey.Verify(%v, %v) passed unexpectedly", d.Name, invalidData, sig) |
| } |
| |
| // Change byte in blob to corrup signature |
| sig.Blob[5] = byte('A') |
| // Corrupted data being passed in |
| if err := pk.Verify(dataBuf, sig); err == nil { |
| t.Errorf("%s with corrupted signature: PublicKey.Verify(%v, %v) passed unexpectedly", d.Name, dataBuf, sig) |
| } |
| } |
| } |
| |
| // skTestHarness builds SK-formatted signatures over a fixed payload |
| // using a caller-supplied signing function, letting tests vary the UP |
| // flag byte without duplicating the wire-format scaffolding. |
| type skTestHarness struct { |
| format string |
| application string |
| data []byte |
| // sign takes the SHA-256 digest of the marshalled SK blob and |
| // returns the value to embed in Signature.Blob. For ECDSA keys the |
| // helper feeds the digest to ecdsa.Sign; for ed25519 the helper |
| // feeds the raw marshalled blob to ed25519.Sign. |
| signDigest func(digest []byte) []byte |
| signBlob func(blob []byte) []byte |
| } |
| |
| func (h skTestHarness) sign(t *testing.T, flags byte) *Signature { |
| t.Helper() |
| hsh := sha256.New() |
| hsh.Write([]byte(h.application)) |
| appDigest := hsh.Sum(nil) |
| |
| hsh.Reset() |
| hsh.Write(h.data) |
| dataDigest := hsh.Sum(nil) |
| |
| var counter uint32 = 1 |
| blob := struct { |
| ApplicationDigest []byte `ssh:"rest"` |
| Flags byte |
| Counter uint32 |
| MessageDigest []byte `ssh:"rest"` |
| }{appDigest, flags, counter, dataDigest} |
| marshalled := Marshal(blob) |
| |
| var sigBlob []byte |
| if h.signDigest != nil { |
| hsh.Reset() |
| hsh.Write(marshalled) |
| sigBlob = h.signDigest(hsh.Sum(nil)) |
| } else { |
| sigBlob = h.signBlob(marshalled) |
| } |
| |
| return &Signature{ |
| Format: h.format, |
| Blob: sigBlob, |
| Rest: Marshal(struct { |
| Flags byte |
| Counter uint32 |
| }{flags, counter}), |
| } |
| } |
| |
| func TestSKUserPresence(t *testing.T) { |
| ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| h := skTestHarness{ |
| format: "sk-ecdsa-sha2-nistp256@openssh.com", |
| application: "ssh:", |
| data: []byte("test data"), |
| signDigest: func(digest []byte) []byte { |
| r, s, err := ecdsa.Sign(rand.Reader, ecKey, digest) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return Marshal(struct{ R, S *big.Int }{r, s}) |
| }, |
| } |
| |
| pk := &skECDSAPublicKey{ |
| application: h.application, |
| PublicKey: ecKey.PublicKey, |
| } |
| |
| // Valid signature with UP=1 should pass. |
| if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil { |
| t.Errorf("Verify failed with UP=1: %v", err) |
| } |
| |
| // Valid signature with UP=0 should fail with the user-presence sentinel. |
| sigNoUP := h.sign(t, 0) |
| if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) { |
| t.Errorf("expected errSKMissingUserPresence, got: %v", err) |
| } |
| |
| // UV set but UP clear must still fail: we only waive UP, never UV-only. |
| if err := pk.Verify(h.data, h.sign(t, 0x04)); !errors.Is(err, errSKMissingUserPresence) { |
| t.Errorf("UV-only (flags=0x04): expected errSKMissingUserPresence, got: %v", err) |
| } |
| |
| // With noTouchRequired, UP=0 passes; UP=1+UV=1 also passes. |
| pk.noTouchRequired = true |
| if err := pk.Verify(h.data, sigNoUP); err != nil { |
| t.Errorf("Verify with noTouchRequired failed: %v", err) |
| } |
| if err := pk.Verify(h.data, h.sign(t, flagUserPresence|0x04)); err != nil { |
| t.Errorf("Verify UP|UV with noTouchRequired failed: %v", err) |
| } |
| } |
| |
| func TestSKKeyWithoutUP(t *testing.T) { |
| ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| edPub, _, err := ed25519.GenerateKey(rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| skEC := &skECDSAPublicKey{PublicKey: ecKey.PublicKey} |
| skED := &skEd25519PublicKey{PublicKey: edPub} |
| certEC := &Certificate{Key: &skECDSAPublicKey{PublicKey: ecKey.PublicKey}} |
| certED := &Certificate{Key: &skEd25519PublicKey{PublicKey: edPub}} |
| rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| t.Fatal(err) |
| } |
| rsaPub, err := NewPublicKey(&rsaKey.PublicKey) |
| if err != nil { |
| t.Fatal(err) |
| } |
| certRSA := &Certificate{Key: rsaPub} |
| |
| // SK raw keys: return a clone with the flag set, original untouched. |
| gotEC := skKeyWithoutUP(skEC) |
| if gotEC == skEC { |
| t.Error("skECDSAPublicKey: expected a clone, got the original pointer") |
| } |
| if !gotEC.(*skECDSAPublicKey).noTouchRequired { |
| t.Error("skECDSAPublicKey clone: noTouchRequired not set") |
| } |
| if skEC.noTouchRequired { |
| t.Error("skECDSAPublicKey: original mutated") |
| } |
| |
| gotED := skKeyWithoutUP(skED) |
| if gotED == skED { |
| t.Error("skEd25519PublicKey: expected a clone, got the original pointer") |
| } |
| if !gotED.(*skEd25519PublicKey).noTouchRequired { |
| t.Error("skEd25519PublicKey clone: noTouchRequired not set") |
| } |
| if skED.noTouchRequired { |
| t.Error("skEd25519PublicKey: original mutated") |
| } |
| |
| // Certificate wrapping SK: return a clone of the cert with a cloned |
| // SK key inside. Neither the original cert nor the original inner |
| // key must be mutated. |
| originalInnerEC := certEC.Key |
| gotCertEC := skKeyWithoutUP(certEC) |
| if gotCertEC == certEC { |
| t.Error("*Certificate(SK ecdsa): expected a clone, got the original pointer") |
| } |
| if got := gotCertEC.(*Certificate).Key.(*skECDSAPublicKey); !got.noTouchRequired { |
| t.Error("*Certificate(SK ecdsa): inner clone missing noTouchRequired") |
| } |
| if certEC.Key != originalInnerEC { |
| t.Error("*Certificate(SK ecdsa): original cert's Key pointer mutated") |
| } |
| if originalInnerEC.(*skECDSAPublicKey).noTouchRequired { |
| t.Error("*Certificate(SK ecdsa): original inner key mutated") |
| } |
| |
| gotCertED := skKeyWithoutUP(certED) |
| if gotCertED == certED { |
| t.Error("*Certificate(SK ed25519): expected a clone, got the original pointer") |
| } |
| if got := gotCertED.(*Certificate).Key.(*skEd25519PublicKey); !got.noTouchRequired { |
| t.Error("*Certificate(SK ed25519): inner clone missing noTouchRequired") |
| } |
| |
| // Non-SK key inside a cert: return original unchanged (nothing to clone). |
| if got := skKeyWithoutUP(certRSA); got != certRSA { |
| t.Error("*Certificate(RSA): expected the original pointer back") |
| } |
| |
| // Plain non-SK key: return original unchanged. |
| if got := skKeyWithoutUP(rsaPub); got != rsaPub { |
| t.Error("rsaPublicKey: expected the original pointer back") |
| } |
| |
| // Pathological: *Certificate whose Key is itself a *Certificate. |
| // The SSH cert format forbids this and parseCert rejects it, but |
| // a Go caller can still construct such a value. skKeyWithoutUP |
| // must not recurse into it (or panic); it returns the input |
| // unchanged. This also defends against a hypothetical cycle built |
| // from hand-constructed Certificate pointers. |
| nestedCert := &Certificate{Key: &Certificate{Key: skEC}} |
| if got := skKeyWithoutUP(nestedCert); got != nestedCert { |
| t.Error("*Certificate wrapping *Certificate: expected the original pointer back") |
| } |
| selfCycle := &Certificate{} |
| selfCycle.Key = selfCycle |
| if got := skKeyWithoutUP(selfCycle); got != selfCycle { |
| t.Error("self-referential *Certificate: expected the original pointer back") |
| } |
| } |
| |
| func TestNoTouchAllowed(t *testing.T) { |
| ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| sk := &skECDSAPublicKey{PublicKey: ecKey.PublicKey} |
| certNoExt := &Certificate{Key: sk} |
| certOptOut := &Certificate{ |
| Key: sk, |
| Permissions: Permissions{Extensions: map[string]string{"no-touch-required": ""}}, |
| } |
| // Non-empty value: OpenSSH writes "" but any value must be accepted |
| // because the check is presence-only. |
| certOptOutNonEmpty := &Certificate{ |
| Key: sk, |
| Permissions: Permissions{Extensions: map[string]string{"no-touch-required": "yes"}}, |
| } |
| // no-touch-required belongs in Extensions, never CriticalOptions. |
| // Putting it in CriticalOptions must NOT be treated as opt-out. |
| certCritOnly := &Certificate{ |
| Key: sk, |
| Permissions: Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}}, |
| } |
| |
| permsEmpty := &Permissions{} |
| permsOptOut := &Permissions{Extensions: map[string]string{"no-touch-required": ""}} |
| permsCritOnly := &Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}} |
| |
| cases := []struct { |
| name string |
| pub PublicKey |
| perms *Permissions |
| want bool |
| }{ |
| {"nil perms, no cert", sk, nil, false}, |
| {"empty perms, no cert", sk, permsEmpty, false}, |
| {"perms opt-out, raw key", sk, permsOptOut, true}, |
| {"nil perms, cert opt-out", certOptOut, nil, true}, |
| {"nil perms, cert opt-out non-empty value", certOptOutNonEmpty, nil, true}, |
| {"nil perms, cert no ext", certNoExt, nil, false}, |
| {"perms opt-out, cert no ext", certNoExt, permsOptOut, true}, |
| {"empty perms, cert opt-out", certOptOut, permsEmpty, true}, |
| // Negative controls: CriticalOptions must not waive UP. |
| {"critical-options only, raw key", sk, permsCritOnly, false}, |
| {"critical-options only, cert", certCritOnly, nil, false}, |
| } |
| for _, tc := range cases { |
| t.Run(tc.name, func(t *testing.T) { |
| if got := noTouchAllowed(tc.pub, tc.perms); got != tc.want { |
| t.Errorf("noTouchAllowed = %v, want %v", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestSKUserPresenceEd25519(t *testing.T) { |
| pub, priv, err := ed25519.GenerateKey(rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| h := skTestHarness{ |
| format: "sk-ssh-ed25519@openssh.com", |
| application: "ssh:", |
| data: []byte("test data"), |
| signBlob: func(blob []byte) []byte { |
| return ed25519.Sign(priv, blob) |
| }, |
| } |
| |
| pk := &skEd25519PublicKey{ |
| application: h.application, |
| PublicKey: pub, |
| } |
| |
| if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil { |
| t.Errorf("Verify failed with UP=1: %v", err) |
| } |
| sigNoUP := h.sign(t, 0) |
| if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) { |
| t.Errorf("expected errSKMissingUserPresence, got: %v", err) |
| } |
| pk.noTouchRequired = true |
| if err := pk.Verify(h.data, sigNoUP); err != nil { |
| t.Errorf("Verify with noTouchRequired failed: %v", err) |
| } |
| } |
| |
| func TestNewSignerWithAlgos(t *testing.T) { |
| algorithSigner, ok := testSigners["rsa"].(AlgorithmSigner) |
| if !ok { |
| t.Fatal("rsa test signer does not implement the AlgorithmSigner interface") |
| } |
| _, err := NewSignerWithAlgorithms(algorithSigner, nil) |
| if err == nil { |
| t.Error("signer with algos created with no algorithms") |
| } |
| |
| _, err = NewSignerWithAlgorithms(algorithSigner, []string{KeyAlgoED25519}) |
| if err == nil { |
| t.Error("signer with algos created with invalid algorithms") |
| } |
| |
| _, err = NewSignerWithAlgorithms(algorithSigner, []string{CertAlgoRSASHA256v01}) |
| if err == nil { |
| t.Error("signer with algos created with certificate algorithms") |
| } |
| |
| mas, err := NewSignerWithAlgorithms(algorithSigner, []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512}) |
| if err != nil { |
| t.Errorf("unable to create signer with valid algorithms: %v", err) |
| } |
| |
| _, err = NewSignerWithAlgorithms(mas, []string{KeyAlgoRSA}) |
| if err == nil { |
| t.Error("signer with algos created with restricted algorithms") |
| } |
| } |
| |
| func TestCryptoPublicKey(t *testing.T) { |
| for _, priv := range testSigners { |
| p1 := priv.PublicKey() |
| key, ok := p1.(CryptoPublicKey) |
| if !ok { |
| continue |
| } |
| p2, err := NewPublicKey(key.CryptoPublicKey()) |
| if err != nil { |
| t.Fatalf("NewPublicKey(CryptoPublicKey) failed for %s, got: %v", p1.Type(), err) |
| } |
| if !reflect.DeepEqual(p1, p2) { |
| t.Errorf("got %#v in NewPublicKey, want %#v", p2, p1) |
| } |
| } |
| for _, d := range testdata.SKData { |
| p1, _, _, _, err := ParseAuthorizedKey(d.PubKey) |
| if err != nil { |
| t.Fatalf("parseAuthorizedKey returned error: %v", err) |
| } |
| k1, ok := p1.(CryptoPublicKey) |
| if !ok { |
| t.Fatalf("%T does not implement CryptoPublicKey", p1) |
| } |
| |
| var p2 PublicKey |
| switch pub := k1.CryptoPublicKey().(type) { |
| case *ecdsa.PublicKey: |
| p2 = &skECDSAPublicKey{ |
| application: "ssh:", |
| PublicKey: *pub, |
| } |
| case ed25519.PublicKey: |
| p2 = &skEd25519PublicKey{ |
| application: "ssh:", |
| PublicKey: pub, |
| } |
| default: |
| t.Fatalf("unexpected type %T from CryptoPublicKey()", pub) |
| } |
| if !reflect.DeepEqual(p1, p2) { |
| t.Errorf("got %#v, want %#v", p2, p1) |
| } |
| } |
| } |
| |
| func TestParseCertWithCertSignatureKey(t *testing.T) { |
| certBytes := []byte(`-----BEGIN SSH CERTIFICATE----- |
| AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIPSp27hvNSB0 |
| IotJnVhjC4zxNgNS8BHlUCxD0VJi4D/eAAAAIIJMi1e5qfx+IFuKD/p/Ssqcb3os |
| CpOw/4wBs1pQ53zwAAAAAAAAAAEAAAACAAAAAAAAABMAAAAPZm9vLmV4YW1wbGUu |
| Y29tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT0AAAAgc3NoLWVkMjU1 |
| MTktY2VydC12MDFAb3BlbnNzaC5jb20AAAAg+sNYhCO35mQT1UBMpmMk8ey+culd |
| IU8vBlPEl4B07swAAAAggiv+RLnboS4znGCVl/n1jDg2uD0h15tW4s/04eS2mLQA |
| AAAAAAAAAQAAAAIAAAAAAAAAEwAAAA9mb28uZXhhbXBsZS5jb20AAAAAAAAAAAAA |
| AAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCV2wETgLKL |
| Kt0bRl3YUnd/ZYSlq0xJMbn4Jj3cdPWykQAAAFMAAAALc3NoLWVkMjU1MTkAAABA |
| WOdbRGEzyRAhiIK227CLUQD5caXYMV8FvSIB7toEE2M/8HnWdG9H3Rsg/v3unruQ |
| JrQldnuPJNe7KOP2+zvUDgAAAFMAAAALc3NoLWVkMjU1MTkAAABAm3bIPp85ZpIe |
| D+izJcUqlcAOri7HO8bULFNHT6LVegvB06xQ5TLwMlrxWUF4cafl1tSe8JQck4a6 |
| cLYUOHfQDw== |
| -----END SSH CERTIFICATE----- |
| `) |
| block, _ := pem.Decode(certBytes) |
| if block == nil { |
| t.Fatal("invalid test certificate") |
| } |
| |
| if _, err := ParsePublicKey(block.Bytes); err == nil { |
| t.Fatal("parsing an SSH certificate using another certificate as signature key succeeded; expected failure") |
| } |
| } |