mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
61d0427898
Signed-off-by: dusan <borovcanindusan1@gmail.com>
391 lines
12 KiB
Go
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
|
|
}
|