Files
Dušan Borovčanin 61d0427898 NOISSUE - Rename to Magistrala (#3427)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-04-06 15:23:42 +02:00

391 lines
12 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package sdk
import (
"archive/zip"
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/absmach/magistrala/pkg/errors"
"golang.org/x/crypto/ocsp"
)
const (
rsaPrivateKey = "RSA PRIVATE KEY"
ecPrivateKey = "EC PRIVATE KEY"
privateKey = "PRIVATE KEY"
pkcs8PrivateKey = "PKCS8 PRIVATE KEY"
edPrivateKey = "ED25519 PRIVATE KEY"
)
var (
errCreateEntityCSR = errors.New("failed to create entity")
errPrivKeyType = errors.New("unsupported private key type")
errFailedParse = errors.New("failed to parse key PEM")
)
const (
certsEndpoint = "certs"
csrEndpoint = "csrs"
crlEndpoint = "crl"
)
func (sdk mgSDK) IssueCert(ctx context.Context, entityID, ttl string, ipAddrs []string, opts Options, domainID, token string) (Certificate, errors.SDKError) {
type certReq struct {
IpAddrs []string `json:"ip_addresses"`
TTL string `json:"ttl"`
Options Options `json:"options"`
}
r := certReq{
IpAddrs: ipAddrs,
TTL: ttl,
Options: opts,
}
d, err := json.Marshal(r)
if err != nil {
return Certificate{}, errors.NewSDKError(err)
}
url := fmt.Sprintf("%s/%s/%s/issue/%s", sdk.certsURL, domainID, certsEndpoint, entityID)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, d, nil, http.StatusCreated)
if sdkerr != nil {
return Certificate{}, sdkerr
}
var cert Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return Certificate{}, errors.NewSDKError(err)
}
return cert, nil
}
func (sdk mgSDK) ViewCert(ctx context.Context, serialNumber, domainID, token string) (Certificate, errors.SDKError) {
url := fmt.Sprintf("%s/%s/%s/%s", sdk.certsURL, domainID, certsEndpoint, serialNumber)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return Certificate{}, sdkerr
}
var cert Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return Certificate{}, errors.NewSDKError(err)
}
return cert, nil
}
func (sdk mgSDK) RevokeCert(ctx context.Context, serialNumber, domainID, token string) errors.SDKError {
url := fmt.Sprintf("%s/%s/%s/%s/revoke", sdk.certsURL, domainID, certsEndpoint, serialNumber)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) RenewCert(ctx context.Context, serialNumber, domainID, token string) (Certificate, errors.SDKError) {
url := fmt.Sprintf("%s/%s/%s/%s/renew", sdk.certsURL, domainID, certsEndpoint, serialNumber)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return Certificate{}, sdkerr
}
var renewRes struct {
Renewed bool `json:"renewed"`
Certificate Certificate `json:"certificate"`
}
if err := json.Unmarshal(body, &renewRes); err != nil {
return Certificate{}, errors.NewSDKError(err)
}
return renewRes.Certificate, nil
}
func (sdk mgSDK) ListCerts(ctx context.Context, pm PageMetadata, domainID, token string) (CertificatePage, errors.SDKError) {
url, err := sdk.withQueryParams(fmt.Sprintf("%s/%s", sdk.certsURL, domainID), certsEndpoint, pm)
if err != nil {
return CertificatePage{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return CertificatePage{}, sdkerr
}
var cp CertificatePage
if err := json.Unmarshal(body, &cp); err != nil {
return CertificatePage{}, errors.NewSDKError(err)
}
return cp, nil
}
func (sdk mgSDK) DeleteCert(ctx context.Context, entityID, domainID, token string) errors.SDKError {
url := fmt.Sprintf("%s/%s/%s/%s/delete", sdk.certsURL, domainID, certsEndpoint, entityID)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) OCSP(ctx context.Context, serialNumber, cert string) (OCSPResponse, errors.SDKError) {
if serialNumber == "" && cert == "" {
return OCSPResponse{}, errors.NewSDKError(errors.New("either serial number or certificate must be provided"))
}
ocspReq := struct {
SerialNumber string `json:"serial_number,omitempty"`
Certificate string `json:"certificate,omitempty"`
}{}
if serialNumber != "" {
ocspReq.SerialNumber = serialNumber
}
if cert != "" {
ocspReq.Certificate = cert
}
requestBody, err := json.Marshal(ocspReq)
if err != nil {
return OCSPResponse{}, errors.NewSDKError(err)
}
url := fmt.Sprintf("%s/certs/ocsp", sdk.certsURL)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, "", requestBody, nil, http.StatusOK)
if sdkerr != nil {
return OCSPResponse{}, sdkerr
}
ocspResp, err := ocsp.ParseResponse(body, nil)
if err != nil {
return OCSPResponse{}, errors.NewSDKError(fmt.Errorf("failed to parse OCSP response: %w", err))
}
var status CertStatus
switch ocspResp.Status {
case ocsp.Good:
status = CertValid
case ocsp.Revoked:
status = CertRevoked
default:
status = CertUnknown
}
resp := OCSPResponse{
Status: status,
SerialNumber: ocspResp.SerialNumber.String(),
Certificate: body,
}
if ocspResp.RevokedAt != (time.Time{}) {
resp.RevokedAt = &ocspResp.RevokedAt
}
if ocspResp.ProducedAt != (time.Time{}) {
resp.ProducedAt = &ocspResp.ProducedAt
}
if ocspResp.ThisUpdate != (time.Time{}) {
resp.ThisUpdate = &ocspResp.ThisUpdate
}
if ocspResp.NextUpdate != (time.Time{}) {
resp.NextUpdate = &ocspResp.NextUpdate
}
resp.RevocationReason = int(ocspResp.RevocationReason)
return resp, nil
}
func (sdk mgSDK) ViewCA(ctx context.Context) (Certificate, errors.SDKError) {
url := fmt.Sprintf("%s/%s/view-ca", sdk.certsURL, certsEndpoint)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, "", nil, nil, http.StatusOK)
if sdkerr != nil {
return Certificate{}, sdkerr
}
var cert Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return Certificate{}, errors.NewSDKError(err)
}
return cert, nil
}
func (sdk mgSDK) DownloadCA(ctx context.Context) (CertificateBundle, errors.SDKError) {
url := fmt.Sprintf("%s/%s/download-ca", sdk.certsURL, certsEndpoint)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, "", nil, nil, http.StatusOK)
if sdkerr != nil {
return CertificateBundle{}, sdkerr
}
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return CertificateBundle{}, errors.NewSDKError(err)
}
var bundle CertificateBundle
for _, file := range zipReader.File {
fileContent, err := readZipFile(file)
if err != nil {
return CertificateBundle{}, errors.NewSDKError(err)
}
if file.Name == "ca.crt" {
bundle.Certificate = fileContent
}
}
return bundle, nil
}
func (sdk mgSDK) IssueFromCSR(ctx context.Context, entityID, ttl, csr, domainID, token string) (Certificate, errors.SDKError) {
pm := PageMetadata{TTL: ttl}
type csrReq struct {
CSR []byte `json:"csr,omitempty"`
}
r := csrReq{CSR: []byte(csr)}
d, err := json.Marshal(r)
if err != nil {
return Certificate{}, errors.NewSDKError(err)
}
url, err := sdk.withQueryParams(fmt.Sprintf("%s/%s/%s/%s", sdk.certsURL, domainID, certsEndpoint, csrEndpoint), entityID, pm)
if err != nil {
return Certificate{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, d, nil, http.StatusOK)
if sdkerr != nil {
return Certificate{}, sdkerr
}
var cert Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return Certificate{}, errors.NewSDKError(err)
}
return cert, nil
}
func (sdk mgSDK) IssueFromCSRInternal(ctx context.Context, entityID, ttl, csr, token string) (Certificate, errors.SDKError) {
type csrReq struct {
CSR []byte `json:"csr,omitempty"`
}
r := csrReq{CSR: []byte(csr)}
d, err := json.Marshal(r)
if err != nil {
return Certificate{}, errors.NewSDKError(err)
}
pm := PageMetadata{TTL: ttl}
url, err := sdk.withQueryParams(fmt.Sprintf("%s/certs/csrs", sdk.certsURL), entityID, pm)
if err != nil {
return Certificate{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, d, nil, http.StatusOK)
if sdkerr != nil {
return Certificate{}, sdkerr
}
var cert Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return Certificate{}, errors.NewSDKError(err)
}
return cert, nil
}
func (sdk mgSDK) GenerateCRL(ctx context.Context) ([]byte, errors.SDKError) {
url := fmt.Sprintf("%s/certs/%s", sdk.certsURL, crlEndpoint)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, "", nil, nil, http.StatusOK)
if sdkerr != nil {
return nil, sdkerr
}
var crlRes struct {
CRL string `json:"crl"`
}
if err := json.Unmarshal(body, &crlRes); err != nil {
return nil, errors.NewSDKError(err)
}
crlData, err := base64.StdEncoding.DecodeString(crlRes.CRL)
if err != nil {
return nil, errors.NewSDKError(err)
}
return crlData, nil
}
func (sdk mgSDK) RevokeAll(ctx context.Context, entityID, domainID, token string) errors.SDKError {
url := fmt.Sprintf("%s/%s/%s/%s/delete", sdk.certsURL, domainID, certsEndpoint, entityID)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) EntityID(ctx context.Context, serialNumber, domainID, token string) (string, errors.SDKError) {
cert, err := sdk.ViewCert(ctx, serialNumber, domainID, token)
if err != nil {
return "", err
}
return cert.EntityID, nil
}
// CreateCSR creates a Certificate Signing Request from the given metadata and private key.
// The private key may be a PEM-encoded []byte or a crypto.Signer (rsa, ecdsa, ed25519).
func (sdk mgSDK) CreateCSR(ctx context.Context, metadata CSRMetadata, privKey any) (CSR, errors.SDKError) {
template := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: metadata.CommonName,
Organization: metadata.Organization,
OrganizationalUnit: metadata.OrganizationalUnit,
Country: metadata.Country,
Province: metadata.Province,
Locality: metadata.Locality,
StreetAddress: metadata.StreetAddress,
PostalCode: metadata.PostalCode,
},
EmailAddresses: metadata.EmailAddresses,
DNSNames: metadata.DNSNames,
ExtraExtensions: metadata.ExtraExtensions,
}
for _, ip := range metadata.IPAddresses {
if parsed := net.ParseIP(ip); parsed != nil {
template.IPAddresses = append(template.IPAddresses, parsed)
}
}
actualKey := privKey
if keyBytes, ok := privKey.([]byte); ok {
var err error
actualKey, err = extractPrivateKey(keyBytes)
if err != nil {
return CSR{}, errors.NewSDKError(errors.Wrap(errCreateEntityCSR, err))
}
}
var signer crypto.Signer
switch key := actualKey.(type) {
case *rsa.PrivateKey, *ecdsa.PrivateKey:
signer = key.(crypto.Signer)
case ed25519.PrivateKey:
signer = key
default:
return CSR{}, errors.NewSDKError(errors.Wrap(errCreateEntityCSR, errPrivKeyType))
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, signer)
if err != nil {
return CSR{}, errors.NewSDKError(errors.Wrap(errCreateEntityCSR, err))
}
csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
return CSR{CSR: csrPEM}, nil
}
func readZipFile(file *zip.File) ([]byte, error) {
fc, err := file.Open()
if err != nil {
return nil, err
}
defer fc.Close()
return io.ReadAll(fc)
}
func extractPrivateKey(pemKey []byte) (any, error) {
block, _ := pem.Decode(pemKey)
if block == nil {
return nil, errors.New("failed to parse private key PEM")
}
var (
parsedKey any
err error
)
switch block.Type {
case rsaPrivateKey:
parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
case ecPrivateKey:
parsedKey, err = x509.ParseECPrivateKey(block.Bytes)
case privateKey, pkcs8PrivateKey, edPrivateKey:
parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
default:
err = errPrivKeyType
}
if err != nil {
return nil, errFailedParse
}
return parsedKey, nil
}