package oidcauth import ( "context" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "testing" "time" "github.com/coreos/go-oidc" "github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2/jwt" ) func setupForJWT(t *testing.T, authType int, f func(c *Config)) (*Authenticator, string) { t.Helper() config := &Config{ Type: TypeJWT, JWTSupportedAlgs: []string{oidc.ES256}, ClaimMappings: map[string]string{ "first_name": "name", "/org/primary": "primary_org", "/nested/Size": "size", "Age": "age", "Admin": "is_admin", "/nested/division": "division", "/nested/remote": "is_remote", }, ListClaimMappings: map[string]string{ "https://go-sso/groups": "groups", }, } var issuer string switch authType { case authOIDCDiscovery: srv := oidcauthtest.Start(t) config.OIDCDiscoveryURL = srv.Addr() config.OIDCDiscoveryCACert = srv.CACert() issuer = config.OIDCDiscoveryURL // TODO(sso): is this a bug in vault? // config.BoundIssuer = issuer case authStaticKeys: pubKey, _ := oidcauthtest.SigningKeys() config.BoundIssuer = "https://legit.issuer.internal/" config.JWTValidationPubKeys = []string{pubKey} issuer = config.BoundIssuer case authJWKS: srv := oidcauthtest.Start(t) config.JWKSURL = srv.Addr() + "/certs" config.JWKSCACert = srv.CACert() issuer = "https://legit.issuer.internal/" // TODO(sso): is this a bug in vault? // config.BoundIssuer = issuer default: require.Fail(t, "inappropriate authType: %d", authType) } if f != nil { f(config) } require.NoError(t, config.Validate()) oa, err := New(config, hclog.NewNullLogger()) require.NoError(t, err) t.Cleanup(oa.Stop) return oa, issuer } func TestJWT_OIDC_Functions_Fail(t *testing.T) { t.Run("static", func(t *testing.T) { testJWT_OIDC_Functions_Fail(t, authStaticKeys) }) t.Run("JWKS", func(t *testing.T) { testJWT_OIDC_Functions_Fail(t, authJWKS) }) t.Run("oidc discovery", func(t *testing.T) { testJWT_OIDC_Functions_Fail(t, authOIDCDiscovery) }) } func testJWT_OIDC_Functions_Fail(t *testing.T, authType int) { t.Helper() t.Run("GetAuthCodeURL", func(t *testing.T) { oa, _ := setupForJWT(t, authType, nil) _, err := oa.GetAuthCodeURL( context.Background(), "https://example.com", map[string]string{"foo": "bar"}, ) requireErrorContains(t, err, `GetAuthCodeURL is incompatible with type "jwt"`) }) t.Run("ClaimsFromAuthCode", func(t *testing.T) { oa, _ := setupForJWT(t, authType, nil) _, _, err := oa.ClaimsFromAuthCode( context.Background(), "abc", "def", ) requireErrorContains(t, err, `ClaimsFromAuthCode is incompatible with type "jwt"`) }) } func TestJWT_ClaimsFromJWT(t *testing.T) { t.Run("static", func(t *testing.T) { testJWT_ClaimsFromJWT(t, authStaticKeys) }) t.Run("JWKS", func(t *testing.T) { testJWT_ClaimsFromJWT(t, authJWKS) }) t.Run("oidc discovery", func(t *testing.T) { // TODO(sso): the vault versions of these tests did not run oidc-discovery testJWT_ClaimsFromJWT(t, authOIDCDiscovery) }) } func testJWT_ClaimsFromJWT(t *testing.T, authType int) { t.Helper() t.Run("missing audience", func(t *testing.T) { if authType == authOIDCDiscovery { // TODO(sso): why isn't this strict? t.Skip("why?") return } oa, issuer := setupForJWT(t, authType, nil) cl := jwt.Claims{ Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", Issuer: issuer, NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), Audience: jwt.Audience{"https://go-sso.test"}, Expiry: jwt.NewNumericDate(time.Now().Add(5 * time.Second)), } privateCl := struct { User string `json:"https://go-sso/user"` Groups []string `json:"https://go-sso/groups"` }{ "jeff", []string{"foo", "bar"}, } jwtData, err := oidcauthtest.SignJWT("", cl, privateCl) require.NoError(t, err) _, err = oa.ClaimsFromJWT(context.Background(), jwtData) requireErrorContains(t, err, "audience claim found in JWT but no audiences are bound") }) t.Run("valid inputs", func(t *testing.T) { oa, issuer := setupForJWT(t, authType, func(c *Config) { c.BoundAudiences = []string{ "https://go-sso.test", "another_audience", } }) cl := jwt.Claims{ Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", Issuer: issuer, Audience: jwt.Audience{"https://go-sso.test"}, NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), Expiry: jwt.NewNumericDate(time.Now().Add(5 * time.Second)), } type orgs struct { Primary string `json:"primary"` } type nested struct { Division int64 `json:"division"` Remote bool `json:"remote"` Size string `json:"Size"` } privateCl := struct { User string `json:"https://go-sso/user"` Groups []string `json:"https://go-sso/groups"` FirstName string `json:"first_name"` Org orgs `json:"org"` Color string `json:"color"` Age int64 `json:"Age"` Admin bool `json:"Admin"` Nested nested `json:"nested"` }{ User: "jeff", Groups: []string{"foo", "bar"}, FirstName: "jeff2", Org: orgs{"engineering"}, Color: "green", Age: 85, Admin: true, Nested: nested{ Division: 3, Remote: true, Size: "medium", }, } jwtData, err := oidcauthtest.SignJWT("", cl, privateCl) require.NoError(t, err) claims, err := oa.ClaimsFromJWT(context.Background(), jwtData) require.NoError(t, err) expectedClaims := &Claims{ Values: map[string]string{ "name": "jeff2", "primary_org": "engineering", "size": "medium", "age": "85", "is_admin": "true", "division": "3", "is_remote": "true", }, Lists: map[string][]string{ "groups": []string{"foo", "bar"}, }, } require.Equal(t, expectedClaims, claims) }) t.Run("unusable claims", func(t *testing.T) { oa, issuer := setupForJWT(t, authType, func(c *Config) { c.BoundAudiences = []string{ "https://go-sso.test", "another_audience", } }) cl := jwt.Claims{ Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", Issuer: issuer, Audience: jwt.Audience{"https://go-sso.test"}, NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), Expiry: jwt.NewNumericDate(time.Now().Add(5 * time.Second)), } type orgs struct { Primary string `json:"primary"` } type nested struct { Division int64 `json:"division"` Remote bool `json:"remote"` Size []string `json:"Size"` } privateCl := struct { User string `json:"https://go-sso/user"` Groups []string `json:"https://go-sso/groups"` FirstName string `json:"first_name"` Org orgs `json:"org"` Color string `json:"color"` Age int64 `json:"Age"` Admin bool `json:"Admin"` Nested nested `json:"nested"` }{ User: "jeff", Groups: []string{"foo", "bar"}, FirstName: "jeff2", Org: orgs{"engineering"}, Color: "green", Age: 85, Admin: true, Nested: nested{ Division: 3, Remote: true, Size: []string{"medium"}, }, } jwtData, err := oidcauthtest.SignJWT("", cl, privateCl) require.NoError(t, err) _, err = oa.ClaimsFromJWT(context.Background(), jwtData) requireErrorContains(t, err, "error converting claim '/nested/Size' to string from unknown type []interface {}") }) t.Run("bad signature", func(t *testing.T) { oa, issuer := setupForJWT(t, authType, func(c *Config) { c.BoundAudiences = []string{ "https://go-sso.test", "another_audience", } }) cl := jwt.Claims{ Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", Issuer: issuer, Audience: jwt.Audience{"https://go-sso.test"}, NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), Expiry: jwt.NewNumericDate(time.Now().Add(5 * time.Second)), } privateCl := struct { User string `json:"https://go-sso/user"` Groups []string `json:"https://go-sso/groups"` }{ "jeff", []string{"foo", "bar"}, } jwtData, err := oidcauthtest.SignJWT(badPrivKey, cl, privateCl) require.NoError(t, err) _, err = oa.ClaimsFromJWT(context.Background(), jwtData) switch authType { case authOIDCDiscovery, authJWKS: requireErrorContains(t, err, "failed to verify id token signature") case authStaticKeys: requireErrorContains(t, err, "no known key successfully validated the token signature") default: require.Fail(t, "unexpected type: %d", authType) } }) t.Run("bad issuer", func(t *testing.T) { oa, _ := setupForJWT(t, authType, func(c *Config) { c.BoundAudiences = []string{ "https://go-sso.test", "another_audience", } }) cl := jwt.Claims{ Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", Issuer: "https://not.real.issuer.internal/", Audience: jwt.Audience{"https://go-sso.test"}, NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), Expiry: jwt.NewNumericDate(time.Now().Add(5 * time.Second)), } privateCl := struct { User string `json:"https://go-sso/user"` Groups []string `json:"https://go-sso/groups"` }{ "jeff", []string{"foo", "bar"}, } jwtData, err := oidcauthtest.SignJWT("", cl, privateCl) require.NoError(t, err) claims, err := oa.ClaimsFromJWT(context.Background(), jwtData) switch authType { case authOIDCDiscovery: requireErrorContains(t, err, "error validating signature: oidc: id token issued by a different provider") case authStaticKeys: requireErrorContains(t, err, "validation failed, invalid issuer claim (iss)") case authJWKS: // requireErrorContains(t, err, "validation failed, invalid issuer claim (iss)") // TODO(sso) The original vault test doesn't care about bound issuer. require.NoError(t, err) expectedClaims := &Claims{ Values: map[string]string{}, Lists: map[string][]string{ "groups": []string{"foo", "bar"}, }, } require.Equal(t, expectedClaims, claims) default: require.Fail(t, "unexpected type: %d", authType) } }) t.Run("bad audience", func(t *testing.T) { oa, issuer := setupForJWT(t, authType, func(c *Config) { c.BoundAudiences = []string{ "https://go-sso.test", "another_audience", } }) cl := jwt.Claims{ Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", Issuer: issuer, NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), Audience: jwt.Audience{"https://fault.plugin.auth.jwt.test"}, Expiry: jwt.NewNumericDate(time.Now().Add(5 * time.Second)), } privateCl := struct { User string `json:"https://go-sso/user"` Groups []string `json:"https://go-sso/groups"` }{ "jeff", []string{"foo", "bar"}, } jwtData, err := oidcauthtest.SignJWT("", cl, privateCl) require.NoError(t, err) _, err = oa.ClaimsFromJWT(context.Background(), jwtData) requireErrorContains(t, err, "error validating claims: aud claim does not match any bound audience") }) } func TestJWT_ClaimsFromJWT_ExpiryClaims(t *testing.T) { t.Run("static", func(t *testing.T) { t.Parallel() testJWT_ClaimsFromJWT_ExpiryClaims(t, authStaticKeys) }) t.Run("JWKS", func(t *testing.T) { t.Parallel() testJWT_ClaimsFromJWT_ExpiryClaims(t, authJWKS) }) // TODO(sso): the vault versions of these tests did not run oidc-discovery // t.Run("oidc discovery", func(t *testing.T) { // t.Parallel() // testJWT_ClaimsFromJWT_ExpiryClaims(t, authOIDCDiscovery) // }) } func testJWT_ClaimsFromJWT_ExpiryClaims(t *testing.T, authType int) { t.Helper() tests := map[string]struct { Valid bool IssuedAt time.Time NotBefore time.Time Expiration time.Time DefaultLeeway int ExpLeeway int }{ // iat, auto clock_skew_leeway (60s), auto expiration leeway (150s) "auto expire leeway using iat with auto clock_skew_leeway": {true, time.Now().Add(-205 * time.Second), time.Time{}, time.Time{}, 0, 0}, "expired auto expire leeway using iat with auto clock_skew_leeway": {false, time.Now().Add(-215 * time.Second), time.Time{}, time.Time{}, 0, 0}, // iat, clock_skew_leeway (10s), auto expiration leeway (150s) "auto expire leeway using iat with custom clock_skew_leeway": {true, time.Now().Add(-150 * time.Second), time.Time{}, time.Time{}, 10, 0}, "expired auto expire leeway using iat with custom clock_skew_leeway": {false, time.Now().Add(-165 * time.Second), time.Time{}, time.Time{}, 10, 0}, // iat, no clock_skew_leeway (0s), auto expiration leeway (150s) "auto expire leeway using iat with no clock_skew_leeway": {true, time.Now().Add(-145 * time.Second), time.Time{}, time.Time{}, -1, 0}, "expired auto expire leeway using iat with no clock_skew_leeway": {false, time.Now().Add(-155 * time.Second), time.Time{}, time.Time{}, -1, 0}, // nbf, auto clock_skew_leeway (60s), auto expiration leeway (150s) "auto expire leeway using nbf with auto clock_skew_leeway": {true, time.Time{}, time.Now().Add(-205 * time.Second), time.Time{}, 0, 0}, "expired auto expire leeway using nbf with auto clock_skew_leeway": {false, time.Time{}, time.Now().Add(-215 * time.Second), time.Time{}, 0, 0}, // nbf, clock_skew_leeway (10s), auto expiration leeway (150s) "auto expire leeway using nbf with custom clock_skew_leeway": {true, time.Time{}, time.Now().Add(-145 * time.Second), time.Time{}, 10, 0}, "expired auto expire leeway using nbf with custom clock_skew_leeway": {false, time.Time{}, time.Now().Add(-165 * time.Second), time.Time{}, 10, 0}, // nbf, no clock_skew_leeway (0s), auto expiration leeway (150s) "auto expire leeway using nbf with no clock_skew_leeway": {true, time.Time{}, time.Now().Add(-145 * time.Second), time.Time{}, -1, 0}, "expired auto expire leeway using nbf with no clock_skew_leeway": {false, time.Time{}, time.Now().Add(-155 * time.Second), time.Time{}, -1, 0}, // iat, auto clock_skew_leeway (60s), custom expiration leeway (10s) "custom expire leeway using iat with clock_skew_leeway": {true, time.Now().Add(-65 * time.Second), time.Time{}, time.Time{}, 0, 10}, "expired custom expire leeway using iat with clock_skew_leeway": {false, time.Now().Add(-75 * time.Second), time.Time{}, time.Time{}, 0, 10}, // iat, clock_skew_leeway (10s), custom expiration leeway (10s) "custom expire leeway using iat with clock_skew_leeway with default leeway": {true, time.Now().Add(-5 * time.Second), time.Time{}, time.Time{}, 10, 10}, "expired custom expire leeway using iat with clock_skew_leeway with default leeway": {false, time.Now().Add(-25 * time.Second), time.Time{}, time.Time{}, 10, 10}, // iat, clock_skew_leeway (10s), no expiration leeway (10s) "no expire leeway using iat with clock_skew_leeway": {true, time.Now().Add(-5 * time.Second), time.Time{}, time.Time{}, 10, -1}, "expired no expire leeway using iat with clock_skew_leeway": {false, time.Now().Add(-15 * time.Second), time.Time{}, time.Time{}, 10, -1}, // nbf, default clock_skew_leeway (60s), custom expiration leeway (10s) "custom expire leeway using nbf with clock_skew_leeway": {true, time.Time{}, time.Now().Add(-65 * time.Second), time.Time{}, 0, 10}, "expired custom expire leeway using nbf with clock_skew_leeway": {false, time.Time{}, time.Now().Add(-75 * time.Second), time.Time{}, 0, 10}, // nbf, clock_skew_leeway (10s), custom expiration leeway (0s) "custom expire leeway using nbf with clock_skew_leeway with default leeway": {true, time.Time{}, time.Now().Add(-5 * time.Second), time.Time{}, 10, 10}, "expired custom expire leeway using nbf with clock_skew_leeway with default leeway": {false, time.Time{}, time.Now().Add(-25 * time.Second), time.Time{}, 10, 10}, // nbf, clock_skew_leeway (10s), no expiration leeway (0s) "no expire leeway using nbf with clock_skew_leeway with default leeway": {true, time.Time{}, time.Now().Add(-5 * time.Second), time.Time{}, 10, -1}, "no expire leeway using nbf with clock_skew_leeway with default leeway and nbf": {true, time.Time{}, time.Now().Add(-5 * time.Second), time.Time{}, 10, -100}, "expired no expire leeway using nbf with clock_skew_leeway": {false, time.Time{}, time.Now().Add(-15 * time.Second), time.Time{}, 10, -1}, "expired no expire leeway using nbf with clock_skew_leeway with default leeway and nbf": {false, time.Time{}, time.Now().Add(-15 * time.Second), time.Time{}, 10, -100}, } for name, tt := range tests { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() oa, issuer := setupForJWT(t, authType, func(c *Config) { c.BoundAudiences = []string{ "https://go-sso.test", "another_audience", } c.ClockSkewLeeway = time.Duration(tt.DefaultLeeway) * time.Second c.ExpirationLeeway = time.Duration(tt.ExpLeeway) * time.Second c.NotBeforeLeeway = 0 }) jwtData := setupLogin(t, tt.IssuedAt, tt.Expiration, tt.NotBefore, issuer) _, err := oa.ClaimsFromJWT(context.Background(), jwtData) if tt.Valid { require.NoError(t, err) } else { require.Error(t, err) } }) } } func TestJWT_ClaimsFromJWT_NotBeforeClaims(t *testing.T) { t.Run("static", func(t *testing.T) { t.Parallel() testJWT_ClaimsFromJWT_NotBeforeClaims(t, authStaticKeys) }) t.Run("JWKS", func(t *testing.T) { t.Parallel() testJWT_ClaimsFromJWT_NotBeforeClaims(t, authJWKS) }) // TODO(sso): the vault versions of these tests did not run oidc-discovery // t.Run("oidc discovery", func(t *testing.T) { // t.Parallel() // testJWT_ClaimsFromJWT_NotBeforeClaims(t, authOIDCDiscovery) // }) } func testJWT_ClaimsFromJWT_NotBeforeClaims(t *testing.T, authType int) { t.Helper() tests := map[string]struct { Valid bool IssuedAt time.Time NotBefore time.Time Expiration time.Time DefaultLeeway int NBFLeeway int }{ // iat, auto clock_skew_leeway (60s), no nbf leeway (0) "no nbf leeway using iat with auto clock_skew_leeway": {true, time.Now().Add(55 * time.Second), time.Time{}, time.Now(), 0, -1}, "not yet valid no nbf leeway using iat with auto clock_skew_leeway": {false, time.Now().Add(65 * time.Second), time.Time{}, time.Now(), 0, -1}, // iat, clock_skew_leeway (10s), no nbf leeway (0s) "no nbf leeway using iat with custom clock_skew_leeway": {true, time.Now().Add(5 * time.Second), time.Time{}, time.Time{}, 10, -1}, "not yet valid no nbf leeway using iat with custom clock_skew_leeway": {false, time.Now().Add(15 * time.Second), time.Time{}, time.Time{}, 10, -1}, // iat, no clock_skew_leeway (0s), nbf leeway (5s) "nbf leeway using iat with no clock_skew_leeway": {true, time.Now(), time.Time{}, time.Time{}, -1, 5}, "not yet valid nbf leeway using iat with no clock_skew_leeway": {false, time.Now().Add(6 * time.Second), time.Time{}, time.Time{}, -1, 5}, // exp, auto clock_skew_leeway (60s), auto nbf leeway (150s) "auto nbf leeway using exp with auto clock_skew_leeway": {true, time.Time{}, time.Time{}, time.Now().Add(205 * time.Second), 0, 0}, "not yet valid auto nbf leeway using exp with auto clock_skew_leeway": {false, time.Time{}, time.Time{}, time.Now().Add(215 * time.Second), 0, 0}, // exp, clock_skew_leeway (10s), auto nbf leeway (150s) "auto nbf leeway using exp with custom clock_skew_leeway": {true, time.Time{}, time.Time{}, time.Now().Add(150 * time.Second), 10, 0}, "not yet valid auto nbf leeway using exp with custom clock_skew_leeway": {false, time.Time{}, time.Time{}, time.Now().Add(165 * time.Second), 10, 0}, // exp, no clock_skew_leeway (0s), auto nbf leeway (150s) "auto nbf leeway using exp with no clock_skew_leeway": {true, time.Time{}, time.Time{}, time.Now().Add(145 * time.Second), -1, 0}, "not yet valid auto nbf leeway using exp with no clock_skew_leeway": {false, time.Time{}, time.Time{}, time.Now().Add(152 * time.Second), -1, 0}, // exp, auto clock_skew_leeway (60s), custom nbf leeway (10s) "custom nbf leeway using exp with auto clock_skew_leeway": {true, time.Time{}, time.Time{}, time.Now().Add(65 * time.Second), 0, 10}, "not yet valid custom nbf leeway using exp with auto clock_skew_leeway": {false, time.Time{}, time.Time{}, time.Now().Add(75 * time.Second), 0, 10}, // exp, clock_skew_leeway (10s), custom nbf leeway (10s) "custom nbf leeway using exp with custom clock_skew_leeway": {true, time.Time{}, time.Time{}, time.Now().Add(15 * time.Second), 10, 10}, "not yet valid custom nbf leeway using exp with custom clock_skew_leeway": {false, time.Time{}, time.Time{}, time.Now().Add(25 * time.Second), 10, 10}, // exp, no clock_skew_leeway (0s), custom nbf leeway (5s) "custom nbf leeway using exp with no clock_skew_leeway": {true, time.Time{}, time.Time{}, time.Now().Add(3 * time.Second), -1, 5}, "custom nbf leeway using exp with no clock_skew_leeway with default leeway": {true, time.Time{}, time.Time{}, time.Now().Add(3 * time.Second), -100, 5}, "not yet valid custom nbf leeway using exp with no clock_skew_leeway": {false, time.Time{}, time.Time{}, time.Now().Add(7 * time.Second), -1, 5}, "not yet valid custom nbf leeway using exp with no clock_skew_leeway with default leeway": {false, time.Time{}, time.Time{}, time.Now().Add(7 * time.Second), -100, 5}, } for name, tt := range tests { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() oa, issuer := setupForJWT(t, authType, func(c *Config) { c.BoundAudiences = []string{ "https://go-sso.test", "another_audience", } c.ClockSkewLeeway = time.Duration(tt.DefaultLeeway) * time.Second c.ExpirationLeeway = 0 c.NotBeforeLeeway = time.Duration(tt.NBFLeeway) * time.Second }) jwtData := setupLogin(t, tt.IssuedAt, tt.Expiration, tt.NotBefore, issuer) _, err := oa.ClaimsFromJWT(context.Background(), jwtData) if tt.Valid { require.NoError(t, err) } else { require.Error(t, err) } }) } } func setupLogin(t *testing.T, iat, exp, nbf time.Time, issuer string) string { cl := jwt.Claims{ Audience: jwt.Audience{"https://go-sso.test"}, Issuer: issuer, Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", IssuedAt: jwt.NewNumericDate(iat), Expiry: jwt.NewNumericDate(exp), NotBefore: jwt.NewNumericDate(nbf), } privateCl := struct { User string `json:"https://go-sso/user"` Groups []string `json:"https://go-sso/groups"` Color string `json:"color"` }{ "foobar", []string{"foo", "bar"}, "green", } jwtData, err := oidcauthtest.SignJWT("", cl, privateCl) require.NoError(t, err) return jwtData } func TestParsePublicKeyPEM(t *testing.T) { getPublicPEM := func(t *testing.T, pub interface{}) string { derBytes, err := x509.MarshalPKIXPublicKey(pub) require.NoError(t, err) pemBlock := &pem.Block{ Type: "PUBLIC KEY", Bytes: derBytes, } return string(pem.EncodeToMemory(pemBlock)) } t.Run("rsa", func(t *testing.T) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) pub := privateKey.Public() pubPEM := getPublicPEM(t, pub) got, err := parsePublicKeyPEM([]byte(pubPEM)) require.NoError(t, err) require.Equal(t, pub, got) }) t.Run("ecdsa", func(t *testing.T) { privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) pub := privateKey.Public() pubPEM := getPublicPEM(t, pub) got, err := parsePublicKeyPEM([]byte(pubPEM)) require.NoError(t, err) require.Equal(t, pub, got) }) t.Run("ed25519", func(t *testing.T) { pub, _, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) pubPEM := getPublicPEM(t, pub) got, err := parsePublicKeyPEM([]byte(pubPEM)) require.NoError(t, err) require.Equal(t, pub, got) }) } const ( badPrivKey string = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEILTAHJm+clBKYCrRDc74Pt7uF7kH+2x2TdL5cH23FEcsoAoGCCqGSM49 AwEHoUQDQgAE+C3CyjVWdeYtIqgluFJlwZmoonphsQbj9Nfo5wrEutv+3RTFnDQh vttUajcFAcl4beR+jHFYC00vSO4i5jZ64g== -----END EC PRIVATE KEY-----` )