Go client for Securosys TSB key management and key operation APIs.
The package name is client and the module path is:
github.com/securosys-com/tsb-client-goUse NewTSBClient when you want to configure the client directly:
package main
import (
"log"
tsb "github.com/securosys-com/tsb-client-go"
)
func main() {
client, err := tsb.NewTSBClient("https://tsb.example.com", tsb.AuthStruct{
AppName: "my-application",
AuthType: "TOKEN",
BearerToken: "bearer-token",
ApiKeys: tsb.ApiKeyTypes{
KeyManagementToken: []string{"key-management-api-key"},
KeyOperationToken: []string{"key-operation-api-key"},
ServiceToken: []string{"service-api-key"},
},
})
if err != nil {
log.Fatal(err)
}
_ = client
}Use NewClient when you already have a helpers.SecurosysConfig. Serialized configuration parameters use snake_case:
{
"auth": "TOKEN",
"bearer_token": "bearer-token",
"cert_path": "/path/to/client.crt",
"key_path": "/path/to/client.key",
"rest_api": "https://tsb.example.com",
"app_name": "my-application",
"application_key_pair": "{\"private_key\":\"...\",\"public_key\":\"...\"}",
"api_keys": "{\"key_management_token\":[\"key-management-api-key\"],\"key_operation_token\":[\"key-operation-api-key\"],\"service_token\":[\"service-api-key\"]}"
}The client supports three authorization modes through AuthStruct.AuthType.
NONE sends no bearer token and no client certificate. API keys are still sent when configured.
client, err := tsb.NewTSBClient("https://tsb.example.com", tsb.AuthStruct{
AppName: "my-application",
AuthType: "NONE",
ApiKeys: tsb.ApiKeyTypes{
KeyManagementToken: []string{"key-management-api-key"},
KeyOperationToken: []string{"key-operation-api-key"},
},
})TOKEN sends Authorization: Bearer <token>.
client, err := tsb.NewTSBClient("https://tsb.example.com", tsb.AuthStruct{
AppName: "my-application",
AuthType: "TOKEN",
BearerToken: "bearer-token",
ApiKeys: tsb.ApiKeyTypes{
KeyManagementToken: []string{"key-management-api-key"},
KeyOperationToken: []string{"key-operation-api-key"},
},
})CERT configures mutual TLS. You can provide certificate/key files:
client, err := tsb.NewTSBClient("https://tsb.example.com", tsb.AuthStruct{
AppName: "my-application",
AuthType: "CERT",
CertPath: "/path/to/client.crt",
KeyPath: "/path/to/client.key",
ApiKeys: tsb.ApiKeyTypes{
KeyManagementToken: []string{"key-management-api-key"},
KeyOperationToken: []string{"key-operation-api-key"},
},
})Or PEM strings:
client, err := tsb.NewTSBClient("https://tsb.example.com", tsb.AuthStruct{
AppName: "my-application",
AuthType: "CERT",
CertPEM: certPEM,
KeyPEM: keyPEM,
ApiKeys: tsb.ApiKeyTypes{
KeyManagementToken: []string{"key-management-api-key"},
KeyOperationToken: []string{"key-operation-api-key"},
},
})API keys are optional from the client side. Add them when your TSB tenant or endpoint is configured to require API keys.
When configured, API keys are sent in the X-API-KEY header field.
The client chooses the API key group by operation:
- key management operations use
KeyManagementToken - sign, verify, encrypt, decrypt, wrap, unwrap, block, unblock, certificates, and requests use
KeyOperationToken - random generation uses
ServiceToken
If multiple API keys are configured for a group, the client rolls over to the next one when TSB returns unauthorized error code 631.
The test suite includes live TSB integration tests. Tests that need TSB read configuration from environment variables and skip when required values are missing.
Create a local .env file:
export TSB_URL="https://sbx-rest-api.cloudshsm.com/"
# TOKEN auth. Leave commented when using NONE auth or CERT auth.
# export TSB_BEARER_TOKEN=""
# CERT auth using PEM content. Set both values together.
# export TSB_CERT_PEM=""
# export TSB_KEY_PEM=""
# CERT auth using files. Set both values together.
# export TSB_CERT_PATH=""
# export TSB_KEY_PATH=""
# Optional API keys. Fill these only when your TSB setup requires API keys.
export TSB_SERVICE_TOKEN=""
export TSB_KEY_MANAGEMENT_TOKEN=""
export TSB_KEY_OPERATION_TOKEN=""
export TSB_APPROVER_TOKEN=""
export TSB_APPROVER_KEY_MANAGEMENT_TOKEN=""Load the environment and run tests:
set -a
source .env
set +a
go test -v ./...Run a single test:
go test -v ./... -run TestCreateSignVerifyAllAlgorithmsAndDeleteKeyWithTSBRun the post-quantum key creation test:
go test -v ./... -run TestCreateAndDeletePostQuantumKeysWithTSBAuthentication selection in tests:
- if
TSB_BEARER_TOKENis set, tests useTOKENauth - else if
TSB_CERT_PEMandTSB_KEY_PEMare set, tests useCERTauth from PEM values - else if
TSB_CERT_PATHandTSB_KEY_PATHare set, tests useCERTauth from files - otherwise tests use
NONEauth
The integration tests create temporary keys with labels prefixed by go-client-test- and delete them after each test.
Most examples below create extractable test keys. For production, choose attributes and policy deliberately.
func keyAttributes() map[string]bool {
return map[string]bool{
"decrypt": true,
"encrypt": true,
"extractable": true,
"sign": true,
"unwrap": true,
"verify": true,
"wrap": true,
"destroyable": true,
}
}Supported key attributes:
encrypt: if true, the key can encrypt data. This attribute is only supported for symmetric keys.decrypt: if true, the key can decrypt data.verify: if true, the key can verify signatures. This attribute is only supported for symmetric keys.sign: if true, the key can sign.wrap: if true, the key can wrap another key. This attribute is only supported for symmetric keys.unwrap: if true, the key can unwrap keys.derive: if true, it is possible to derive from this key. Default:false.bip32: if true, key derivation uses BIP32 / SLIP10. This can only be true when the key algorithm isECorEDandderiveis true. Default:false.slip10: if true, key derivation uses SLIP-0010. This can only be true when the key algorithm isECorEDandderiveis true. Default:false.extractable: if true, the key is extractable. This can only be true for keys without smart key attributes. Default:false.modifiable: if true, the key can be modified. This attribute applies only to the key attributes, not to SKA policy. Default:true.destroyable: if true, the key can be deleted. Default:false.sensitive: if true, the key is sensitive. To export a key,sensitivemust be false.copyable: if true, the encrypted key can be stored in external memory. Default:false.
Payloads passed to sign, verify, encrypt, and decrypt are base64 strings.
ctx := context.Background()
password := ""
payload := base64.StdEncoding.EncodeToString([]byte("message to sign"))
label, err := client.CreateOrUpdateKey(
ctx,
"example-ec-sign-key",
password,
keyAttributes(),
"EC",
0,
nil,
"1.2.840.10045.3.1.7", // P-256
false,
)
if err != nil {
log.Fatal(err)
}
defer client.RemoveKey(ctx, label)
signature, status, err := client.Sign(
ctx,
label,
password,
payload,
"UNSPECIFIED",
tsb.SignatureAlgorithmSHA256WithECDSA,
tsb.SignatureTypeRAW,
)
if err != nil {
log.Fatalf("sign failed with status %d: %v", status, err)
}
valid, status, err := client.Verify(
ctx,
label,
password,
payload,
tsb.SignatureAlgorithmSHA256WithECDSA,
signature.Signature,
)
if err != nil {
log.Fatalf("verify failed with status %d: %v", status, err)
}
log.Printf("signature valid: %t", valid)For NONESHA* algorithms, hash the payload before calling Sign and Verify using the matching SHA algorithm. For example, NONESHA256_WITH_RSA expects a SHA-256 digest as the payload. For NONE_WITH_*, pass the digest expected by your TSB/signature configuration.
ctx := context.Background()
password := ""
payload := base64.StdEncoding.EncodeToString([]byte("0123456789abcdef"))
label, err := client.CreateOrUpdateKey(
ctx,
"example-aes-encrypt-key",
password,
keyAttributes(),
"AES",
256,
nil,
"",
false,
)
if err != nil {
log.Fatal(err)
}
defer client.RemoveKey(ctx, label)
encrypted, status, err := client.Encrypt(
ctx,
label,
password,
payload,
tsb.CipherAlgorithmAESGCM,
128,
"",
)
if err != nil {
log.Fatalf("encrypt failed with status %d: %v", status, err)
}
iv := ""
if encrypted.InitializationVector != nil {
iv = *encrypted.InitializationVector
}
decrypted, status, err := client.Decrypt(
ctx,
label,
password,
encrypted.EncryptedPayload,
iv,
tsb.CipherAlgorithmAESGCM,
128,
"",
)
if err != nil {
log.Fatalf("decrypt failed with status %d: %v", status, err)
}
log.Printf("decrypted payload: %s", decrypted.Payload)Use tag length 128 for AES_GCM unless your integration needs a different supported tag length. Use -1 for algorithms where no tag length should be sent.
For RSA_NO_PADDING, decrypt returns the full RSA block. The original plaintext is right-aligned and zero-padded on the left.
ctx := context.Background()
password := ""
wrapKey, err := client.CreateOrUpdateKey(
ctx,
"example-aes-wrap-key",
password,
keyAttributes(),
"AES",
256,
nil,
"",
false,
)
if err != nil {
log.Fatal(err)
}
defer client.RemoveKey(ctx, wrapKey)
keyToWrap, err := client.CreateOrUpdateKey(
ctx,
"example-aes-key-to-wrap",
password,
keyAttributes(),
"AES",
256,
nil,
"",
false,
)
if err != nil {
log.Fatal(err)
}
defer client.RemoveKey(ctx, keyToWrap)
defer client.RemoveKey(ctx, "example-aes-unwrapped-key")
wrapped, status, err := client.Wrap(
wrapKey,
password,
keyToWrap,
password,
tsb.WrapMethodAES,
)
if err != nil {
log.Fatalf("wrap failed with status %d: %v", status, err)
}
status, err = client.UnWrap(
wrapped.WrappedKey,
"example-aes-unwrapped-key",
keyAttributes(),
wrapKey,
password,
tsb.WrapMethodAES,
nil,
)
if err != nil {
log.Fatalf("unwrap failed with status %d: %v", status, err)
}
unwrapped, err := client.GetKey(ctx, "example-aes-unwrapped-key", password)
if err != nil {
log.Fatal(err)
}
log.Printf("unwrapped key algorithm: %s", unwrapped.Algorithm)The unwrap target label must be different from the source key label.
ECEDRSADSABLSAESChaCha20CamelliaTDEA
Post-quantum:
ML-DSA-44ML-DSA-65ML-DSA-87SLH-DSA-SHA2-128sSLH-DSA-SHA2-128fSLH-DSA-SHA2-192sSLH-DSA-SHA2-192fSLH-DSA-SHA2-256sSLH-DSA-SHA2-256fSLH-DSA-SHAKE-128sSLH-DSA-SHAKE-128fSLH-DSA-SHAKE-192sSLH-DSA-SHAKE-192fSLH-DSA-SHAKE-256sSLH-DSA-SHAKE-256fML-KEM-512ML-KEM-768ML-KEM-1024LMSXMSS-SHA256_10_256XMSS-SHAKE256_10_256
DERETHRAW
RSA:
SHA224_WITH_RSA_PSSSHA256_WITH_RSA_PSSSHA384_WITH_RSA_PSSSHA512_WITH_RSA_PSSNONE_WITH_RSANONE_WITH_RSA_PSSSHA224_WITH_RSASHA256_WITH_RSASHA384_WITH_RSASHA512_WITH_RSANONESHA224_WITH_RSANONESHA256_WITH_RSANONESHA384_WITH_RSANONESHA512_WITH_RSANONESHA224_WITH_RSA_PSSNONESHA256_WITH_RSA_PSSNONESHA384_WITH_RSA_PSSNONESHA512_WITH_RSA_PSSSHA1_WITH_RSANONESHA1_WITH_RSASHA1_WITH_RSA_PSS
EC:
NONE_WITH_ECDSASHA1_WITH_ECDSASHA224_WITH_ECDSASHA256_WITH_ECDSADOUBLE_SHA256_WITH_ECDSASHA384_WITH_ECDSASHA512_WITH_ECDSASHA3224_WITH_ECDSASHA3256_WITH_ECDSASHA3384_WITH_ECDSASHA3512_WITH_ECDSASHA256_WITH_ECDSA_DETERMINISTICKECCAK224_WITH_ECDSAKECCAK256_WITH_ECDSAKECCAK384_WITH_ECDSAKECCAK512_WITH_ECDSA
DSA:
NONE_WITH_DSASHA224_WITH_DSASHA256_WITH_DSASHA384_WITH_DSASHA512_WITH_DSASHA1_WITH_DSA
Other:
EDDSABLSLMSXMSS-SHA256_10_256XMSS-SHAKE256_10_256ML_DSAML_DSA_MSLH_DSASHA2_224_WITH_ML_DSASHA2_256_WITH_ML_DSASHA2_384_WITH_ML_DSASHA2_512_WITH_ML_DSASHA3_224_WITH_ML_DSASHA3_256_WITH_ML_DSASHA3_384_WITH_ML_DSASHA3_512_WITH_ML_DSASHAKE_128_WITH_ML_DSASHAKE_256_WITH_ML_DSASHA2_224_WITH_SLH_DSASHA2_256_WITH_SLH_DSASHA2_384_WITH_SLH_DSASHA2_512_WITH_SLH_DSASHA3_224_WITH_SLH_DSASHA3_256_WITH_SLH_DSASHA3_384_WITH_SLH_DSASHA3_512_WITH_SLH_DSASHAKE_128_WITH_SLH_DSASHAKE_256_WITH_SLH_DSA
RSA:
RSA_PADDING_OAEP_WITH_SHA512RSARSA_PADDING_OAEP_WITH_SHA224RSA_PADDING_OAEP_WITH_SHA256RSA_PADDING_OAEP_WITH_SHA1RSA_PADDING_OAEPRSA_PADDING_OAEP_WITH_SHA384RSA_PADDING_PKCSRSA_NO_PADDING
AES:
AES_GCMAES_CTRAES_ECBAES_CBC_NO_PADDINGAES
ChaCha20:
CHACHA20CHACHA20_AEAD
Camellia:
CAMELLIACAMELLIA_CBC_NO_PADDINGCAMELLIA_ECB
TDEA:
TDEA_CBCTDEA_ECBTDEA_CBC_NO_PADDING
AES wrap methods:
AES_WRAPAES_WRAP_DSAAES_WRAP_ECAES_WRAP_EDAES_WRAP_RSAAES_WRAP_BLSAES_WRAP_PADAES_WRAP_PAD_DSAAES_WRAP_PAD_ECAES_WRAP_PAD_EDAES_WRAP_PAD_RSAAES_WRAP_PAD_BLS
RSA wrap methods:
RSA_WRAP_PADRSA_WRAP_OAEP
ML-KEM wrap methods:
ML-KEM-512ML-KEM-768ML-KEM-1024
UNSPECIFIEDISO_20022PDFBTCETH
06496104112120128
This README was created with help from ChatGPT Codex.
This project is licensed under the Apache 2.0 license.