forked from alterminal/alterminal
176 lines
4.4 KiB
Go
176 lines
4.4 KiB
Go
package jwk
|
||
|
||
import (
|
||
"crypto/rsa"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"math/big"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestNewSigningKey(t *testing.T) {
|
||
k, err := NewSigningKey()
|
||
if err != nil {
|
||
t.Fatal("NewSigningKey: ", err)
|
||
}
|
||
if len(k.Kid) != 43 {
|
||
t.Errorf("Kid 應為 32 bytes SHA-256 的無填充 base64url(43 字元),得到 %d 字元", len(k.Kid))
|
||
}
|
||
if k.Algorithm != AlgRS256 {
|
||
t.Errorf("Algorithm = %q, want %q", k.Algorithm, AlgRS256)
|
||
}
|
||
if !k.Active() {
|
||
t.Error("新產生的金鑰應為使用中(RetiredAt 為 nil)")
|
||
}
|
||
priv, err := k.PrivateKey()
|
||
if err != nil {
|
||
t.Fatal("PrivateKey: ", err)
|
||
}
|
||
if priv.N.BitLen() != rsaKeyBits {
|
||
t.Errorf("金鑰長度 = %d bits, want %d", priv.N.BitLen(), rsaKeyBits)
|
||
}
|
||
if err := priv.Validate(); err != nil {
|
||
t.Error("產生的私鑰未通過自檢: ", err)
|
||
}
|
||
want, err := thumbprint(&priv.PublicKey)
|
||
if err != nil {
|
||
t.Fatal("thumbprint: ", err)
|
||
}
|
||
if k.Kid != want {
|
||
t.Errorf("Kid = %q, want 公鑰 thumbprint %q", k.Kid, want)
|
||
}
|
||
}
|
||
|
||
func TestNewSigningKeyUniqueKid(t *testing.T) {
|
||
a, err := NewSigningKey()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b, err := NewSigningKey()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if a.Kid == b.Kid {
|
||
t.Error("兩把新產生的金鑰不應有相同 Kid")
|
||
}
|
||
}
|
||
|
||
// RFC 7638 §3.1 的測試向量(與 RFC 7517 A.1 同一把 RSA 金鑰)。
|
||
func TestThumbprintRFC7638Vector(t *testing.T) {
|
||
pub := &rsa.PublicKey{
|
||
N: mustBigFromBase64URL(t, "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"),
|
||
E: 65537,
|
||
}
|
||
got, err := thumbprint(pub)
|
||
if err != nil {
|
||
t.Fatal("thumbprint: ", err)
|
||
}
|
||
if want := "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"; got != want {
|
||
t.Errorf("thumbprint = %q, want RFC 7638 §3.1 的 %q", got, want)
|
||
}
|
||
}
|
||
|
||
func TestActive(t *testing.T) {
|
||
k := &SigningKey{}
|
||
if !k.Active() {
|
||
t.Error("RetiredAt 為 nil 時應為使用中")
|
||
}
|
||
now := time.Now()
|
||
k.RetiredAt = &now
|
||
if k.Active() {
|
||
t.Error("RetiredAt 已設定時不應為使用中")
|
||
}
|
||
}
|
||
|
||
func TestPublicJWK(t *testing.T) {
|
||
k, err := NewSigningKey()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
j, err := k.PublicJWK()
|
||
if err != nil {
|
||
t.Fatal("PublicJWK: ", err)
|
||
}
|
||
priv, err := k.PrivateKey()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if j.Kty != "RSA" || j.Use != "sig" || j.Alg != "RS256" {
|
||
t.Errorf("JWK 參數 = kty:%q use:%q alg:%q", j.Kty, j.Use, j.Alg)
|
||
}
|
||
if j.Kid != k.Kid {
|
||
t.Errorf("JWK.Kid = %q, want %q", j.Kid, k.Kid)
|
||
}
|
||
if j.E != "AQAB" {
|
||
t.Errorf("E = %q, want AQAB(65537 的 base64url)", j.E)
|
||
}
|
||
if mustBigFromBase64URL(t, j.N).Cmp(priv.N) != 0 {
|
||
t.Error("N 應等於私鑰的 modulus")
|
||
}
|
||
}
|
||
|
||
func TestPublicJWKJSONShape(t *testing.T) {
|
||
k, err := NewSigningKey()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
j, err := k.PublicJWK()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b, err := json.Marshal(JWKS{Keys: []JWK{*j}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(b)
|
||
if !strings.HasPrefix(s, `{"keys":[{`) || !strings.HasSuffix(s, `}]}`) {
|
||
t.Errorf(`JWKS 應為 {"keys":[…]} 形式,得到 %s`, s)
|
||
}
|
||
var m struct {
|
||
Keys []map[string]any `json:"keys"`
|
||
}
|
||
if err := json.Unmarshal(b, &m); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := m.Keys[0]
|
||
want := []string{"alg", "e", "kid", "kty", "n", "use"}
|
||
if len(got) != len(want) {
|
||
t.Errorf("JWK 應恰含參數 %v,得到 %v", want, got)
|
||
}
|
||
for _, f := range want {
|
||
if _, ok := got[f]; !ok {
|
||
t.Errorf("JWK 缺少參數 %q", f)
|
||
}
|
||
}
|
||
// 私有參數絕不得出現
|
||
for _, p := range []string{`"d":`, `"p":`, `"q":`, `"dp":`, `"dq":`, `"qi":`} {
|
||
if strings.Contains(s, p) {
|
||
t.Errorf("JWKS 不應含私有參數 %s", p)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestPrivateKeyMalformedPEM(t *testing.T) {
|
||
for _, pemStr := range []string{
|
||
"",
|
||
"not a pem",
|
||
"-----BEGIN PRIVATE KEY-----\nYm9ndXMK\n-----END PRIVATE KEY-----",
|
||
} {
|
||
k := &SigningKey{PrivateKeyPEM: pemStr}
|
||
if _, err := k.PrivateKey(); err == nil {
|
||
t.Errorf("格式無效的 PEM %q 不應解析成功", pemStr)
|
||
}
|
||
}
|
||
}
|
||
|
||
func mustBigFromBase64URL(t *testing.T, s string) *big.Int {
|
||
t.Helper()
|
||
b, err := base64.RawURLEncoding.DecodeString(s)
|
||
if err != nil {
|
||
t.Fatalf("decode base64url %q: %v", s, err)
|
||
}
|
||
return new(big.Int).SetBytes(b)
|
||
}
|