203 lines
5.7 KiB
Go
203 lines
5.7 KiB
Go
|
|
package licence
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/ed25519"
|
||
|
|
"encoding/base64"
|
||
|
|
"encoding/binary"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
"unicode/utf8"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
formatVersion = 1
|
||
|
|
certificateDomain = "OPS-LICENCE-ISSUER-CERT/V1"
|
||
|
|
claimsDomain = "OPS-LICENCE/V1"
|
||
|
|
maximumLicenceFileBytes = 64 * 1024
|
||
|
|
)
|
||
|
|
|
||
|
|
var base64URL = base64.RawURLEncoding.Strict()
|
||
|
|
|
||
|
|
func CertificateMessage(c SigningKeyCertificate) ([]byte, error) {
|
||
|
|
if c.FormatVersion != formatVersion {
|
||
|
|
return nil, ErrUnsupportedVersion
|
||
|
|
}
|
||
|
|
|
||
|
|
keyID, err := parseUUID(c.KeyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 凭证密钥 ID 格式错误", ErrInvalidKeyCertificate)
|
||
|
|
}
|
||
|
|
publicKey, err := decodeBase64URL(c.PublicKey, ed25519.PublicKeySize)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 凭证公钥格式错误", ErrInvalidKeyCertificate)
|
||
|
|
}
|
||
|
|
|
||
|
|
message := make([]byte, 0, len(certificateDomain)+len(keyID)+len(publicKey))
|
||
|
|
message = append(message, certificateDomain...)
|
||
|
|
message = append(message, keyID[:]...)
|
||
|
|
message = append(message, publicKey...)
|
||
|
|
return message, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func ClaimsMessage(c Claims) ([]byte, error) {
|
||
|
|
licenceID, err := parseUUID(c.ID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 许可证 ID 格式错误", ErrMalformedFile)
|
||
|
|
}
|
||
|
|
signingKeyID, err := parseUUID(c.SigningKeyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 签发密钥 ID 格式错误", ErrMalformedFile)
|
||
|
|
}
|
||
|
|
if err := validateQuotas(c.Quotas); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
issuedOn, err := encodedDate(c.IssuedOn)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 签发日期格式错误", ErrMalformedFile)
|
||
|
|
}
|
||
|
|
validFrom, err := encodedDate(c.ValidFrom)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 生效日期格式错误", ErrMalformedFile)
|
||
|
|
}
|
||
|
|
expiresOn, err := encodedDate(c.ExpiresOn)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 到期日期格式错误", ErrMalformedFile)
|
||
|
|
}
|
||
|
|
|
||
|
|
message := make([]byte, 0, 256)
|
||
|
|
message = append(message, claimsDomain...)
|
||
|
|
message = append(message, licenceID[:]...)
|
||
|
|
message, err = appendLengthPrefixedString(message, c.PlatformName)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: 平台名称编码错误", ErrMalformedFile)
|
||
|
|
}
|
||
|
|
message, err = appendLengthPrefixedString(message, c.Workspace)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%w: workspace 编码错误", ErrMalformedFile)
|
||
|
|
}
|
||
|
|
message = append(message, issuedOn...)
|
||
|
|
message = append(message, validFrom...)
|
||
|
|
message = append(message, expiresOn...)
|
||
|
|
for _, quota := range quotaValues(c.Quotas) {
|
||
|
|
message = binary.BigEndian.AppendUint64(message, uint64(quota.value))
|
||
|
|
}
|
||
|
|
message = append(message, signingKeyID[:]...)
|
||
|
|
return message, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendLengthPrefixedString(destination []byte, value string) ([]byte, error) {
|
||
|
|
if !utf8.ValidString(value) {
|
||
|
|
return nil, errors.New("字符串不是有效的 UTF-8")
|
||
|
|
}
|
||
|
|
if uint64(len(value)) > math.MaxUint32 {
|
||
|
|
return nil, errors.New("字符串长度超过 uint32")
|
||
|
|
}
|
||
|
|
destination = binary.BigEndian.AppendUint32(destination, uint32(len(value)))
|
||
|
|
destination = append(destination, value...)
|
||
|
|
return destination, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func encodedDate(date Date) ([]byte, error) {
|
||
|
|
if _, err := date.Time(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
value := date.String()
|
||
|
|
if len(value) != len(dateLayout) {
|
||
|
|
return nil, errors.New("日期长度错误")
|
||
|
|
}
|
||
|
|
return []byte(value), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type quotaValue struct {
|
||
|
|
name string
|
||
|
|
value int64
|
||
|
|
}
|
||
|
|
|
||
|
|
func quotaValues(quotas Quotas) [11]quotaValue {
|
||
|
|
return [11]quotaValue{
|
||
|
|
{name: "max_database", value: quotas.MaxDatabase},
|
||
|
|
{name: "max_middleware", value: quotas.MaxMiddleware},
|
||
|
|
{name: "max_network_device", value: quotas.MaxNetworkDevice},
|
||
|
|
{name: "max_security", value: quotas.MaxSecurity},
|
||
|
|
{name: "max_storage", value: quotas.MaxStorage},
|
||
|
|
{name: "max_pc", value: quotas.MaxPC},
|
||
|
|
{name: "max_server", value: quotas.MaxServer},
|
||
|
|
{name: "max_user", value: quotas.MaxUser},
|
||
|
|
{name: "max_role", value: quotas.MaxRole},
|
||
|
|
{name: "max_permission", value: quotas.MaxPermission},
|
||
|
|
{name: "max_menu", value: quotas.MaxMenu},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateQuotas(quotas Quotas) error {
|
||
|
|
for _, quota := range quotaValues(quotas) {
|
||
|
|
if quota.value < 0 {
|
||
|
|
return fmt.Errorf("%w: %s 不能为负数", ErrInvalidQuota, quota.name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func decodeBase64URL(value string, expectedLength int) ([]byte, error) {
|
||
|
|
for index := range value {
|
||
|
|
character := value[index]
|
||
|
|
if (character >= 'A' && character <= 'Z') ||
|
||
|
|
(character >= 'a' && character <= 'z') ||
|
||
|
|
(character >= '0' && character <= '9') ||
|
||
|
|
character == '-' || character == '_' {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
return nil, errors.New("Base64URL 包含非法字符")
|
||
|
|
}
|
||
|
|
decoded, err := base64URL.DecodeString(value)
|
||
|
|
if err != nil {
|
||
|
|
return nil, errors.New("Base64URL 编码错误")
|
||
|
|
}
|
||
|
|
if len(decoded) != expectedLength {
|
||
|
|
return nil, errors.New("解码后长度错误")
|
||
|
|
}
|
||
|
|
return decoded, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseUUID(value string) ([16]byte, error) {
|
||
|
|
var result [16]byte
|
||
|
|
if len(value) != 36 || value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' {
|
||
|
|
return result, errors.New("UUID 必须使用 8-4-4-4-12 格式")
|
||
|
|
}
|
||
|
|
|
||
|
|
byteIndex := 0
|
||
|
|
for index := 0; index < len(value); {
|
||
|
|
if index == 8 || index == 13 || index == 18 || index == 23 {
|
||
|
|
index++
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
high, ok := hexadecimalValue(value[index])
|
||
|
|
if !ok {
|
||
|
|
return result, errors.New("UUID 包含非十六进制字符")
|
||
|
|
}
|
||
|
|
low, ok := hexadecimalValue(value[index+1])
|
||
|
|
if !ok {
|
||
|
|
return result, errors.New("UUID 包含非十六进制字符")
|
||
|
|
}
|
||
|
|
result[byteIndex] = high<<4 | low
|
||
|
|
byteIndex++
|
||
|
|
index += 2
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func hexadecimalValue(value byte) (byte, bool) {
|
||
|
|
switch {
|
||
|
|
case value >= '0' && value <= '9':
|
||
|
|
return value - '0', true
|
||
|
|
case value >= 'a' && value <= 'f':
|
||
|
|
return value - 'a' + 10, true
|
||
|
|
case value >= 'A' && value <= 'F':
|
||
|
|
return value - 'A' + 10, true
|
||
|
|
default:
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
}
|